<|start_filename|>test.js<|end_filename|>
"use strict";
const customizeURL = require("incomplete-url");
const {describe, it} = require("mocha");
const {expect} = require("chai");
const relateURL = require("./");
const tests = require("./helpers/tests.json");
const {URL} = require("universal-url");



const combinations = (options, type) =>
{
	const _URL = type==="common_deep" ? URL : customizeURL({ urlExclusions:["searchParams"] }).IncompleteURL;
	const outputName = outputKey(options.output);
	//let skipped = 0;

	if (type !== "common_deep")
	{
		// Instances of `IncompleteURL` cannot be cloned normally
		options = { ...options, clone:false };
	}

	it(`supports ${tests.length} different url combinations`, function()
	{
		this.timeout(10000);  // for the shim

		for (let i=0; i<tests.length; i++)
		{
			const relativeURL = tests[i].related[type][outputName];

			//if (relativeURL === null) { skipped++; continue }

			const base = new _URL( tests[i].base );
			const url  = new _URL( tests[i].url, base );
			expect( relateURL(url,base,options) ).to.equal(relativeURL);

			//console.log(i, type)
			//console.log(tests[i].url)
			//console.log(tests[i].base)
			//console.log("=======")
		}

		//if (skipped > 0) console.log(`${skipped} skipped`);
	});
};



const options = overrides =>
({
	output: relateURL.SHORTEST,

	// minurl options
	clone: false,
	defaultPorts: {},
	indexFilenames: [],
	plusQueries: false,
	queryNames: [],
	removeAuth: false,
	removeDefaultPort: false,
	removeEmptyHash: false,
	removeEmptyQueries: false,
	removeEmptyQueryNames: false,
	removeEmptyQueryValues: false,
	removeEmptySegmentNames: false,
	removeHash: false,
	removeIndexFilename: false,
	removeQueryNames: false,
	removeQueryOddities: false,
	removeRootTrailingSlash: false,
	removeTrailingSlash: false,
	removeWWW: false,
	sortQueries: false,
	stringify: true,  // special
	...overrides
});



const outputKey = value => Object.keys(relateURL).find(key => relateURL[key] === value);



it(`has "common" options profile publicly available`, () =>
{
	expect( relateURL.COMMON_PROFILE ).to.be.an("object");

	const originalValue = relateURL.COMMON_PROFILE;

	expect(() => relateURL.COMMON_PROFILE.defaultPorts = "changed").to.throw(Error);
	expect(() => relateURL.COMMON_PROFILE.nonExistent = "new").to.throw(Error);
	expect(relateURL.COMMON_PROFILE).to.deep.equal(originalValue);

	expect(() => relateURL.COMMON_PROFILE = "changed").to.throw(Error);
	expect(relateURL.COMMON_PROFILE).to.equal(originalValue);
});



it(`has "careful" options profile publicly available`, () =>
{
	expect( relateURL.CAREFUL_PROFILE ).to.be.an("object");

	const originalValue = relateURL.CAREFUL_PROFILE;

	expect(() => relateURL.CAREFUL_PROFILE.defaultPorts = "changed").to.throw(Error);
	expect(() => relateURL.CAREFUL_PROFILE.nonExistent = "new").to.throw(Error);
	expect(relateURL.CAREFUL_PROFILE).to.deep.equal(originalValue);

	expect(() => relateURL.CAREFUL_PROFILE = "changed").to.throw(Error);
	expect(relateURL.CAREFUL_PROFILE).to.equal(originalValue);
});



it("has output levels available", () =>
{
	expect(relateURL).to.contain.all.keys(["PROTOCOL_RELATIVE", "SHORTEST"]);

	const originalValue = relateURL.SHORTEST;

	expect(() => relateURL.SHORTEST = "changed").to.throw(Error);
	expect(relateURL.SHORTEST).to.equal(originalValue);
});



it("accepts URL input", () =>
{
	const opts = options();
	const url  = new URL("http://domain.com/dir1/dir2/index.html");
	const base = new URL("http://domain.com/dir1/dir3/index.html");
	expect( relateURL(url,base,opts) ).to.equal("../dir2/index.html");
});



it("rejects non-URL input", () =>
{
	const opts = options();
	let base,url;

	url  = "http://domain.com/dir1/dir2/index.html";
	base = "http://domain.com/dir1/dir3/index.html";
	expect(() => relateURL(url,base,opts)).to.throw(TypeError);

	url  = new URL("http://domain.com/dir1/dir2/index.html");
	base =         "http://domain.com/dir1/dir3/index.html";
	expect(() => relateURL(url,base,opts)).to.throw(TypeError);

	url  =         "http://domain.com/dir1/dir2/index.html";
	base = new URL("http://domain.com/dir1/dir3/index.html");
	expect(() => relateURL(url,base,opts)).to.throw(TypeError);
});



describe("options", () =>
{
	it("clone = false", () =>
	{
		const opts = options({ removeHash:true });
		const url  = new URL("http://domain.com/path#hash");
		const base = new URL("http://domain.com/");

		relateURL(url, base, opts);

		expect(url.hash).to.be.empty;
	});



	it("clone = true", () =>
	{
		const opts = options({ clone:true, removeHash:true });
		const url  = new URL("http://domain.com/path#hash");
		const base = new URL("http://domain.com/");

		relateURL(url, base, opts);

		expect(url.hash).to.not.be.empty;
	});



	it("removeAuth = false", () =>
	{
		const opts = options();
		let base, url;

		url  = new URL("http://user:pass@domain.com/");
		base = new URL("http://domain.com/");
		expect( relateURL(url,base,opts) ).to.equal("//user:pass@domain.com/");

		url  = new URL("http://domain.com/");
		base = new URL("http://user:pass@domain.com/");
		expect( relateURL(url,base,opts) ).to.equal("//domain.com/");
	});



	it("removeAuth = true", () =>
	{
		const opts = options({ removeAuth:true });
		let base, url;

		url  = new URL("http://user:pass@domain.com/");
		base = new URL("http://domain.com/");
		expect( relateURL(url,base,opts) ).to.equal("");

		url  = new URL("http://domain.com/");
		base = new URL("http://user:pass@domain.com/");
		expect( relateURL(url,base,opts) ).to.equal("");
	});



	it("removeEmptyQueryNames = true, removeEmptyQueryValues = true, removeQueryNames = true", () =>
	{
		const opts = options({ queryNames:["var1"], removeEmptyQueryNames:true, removeEmptyQueryValues:true, removeQueryNames:true });
		const url  = new URL("http://domain.com/?var1=value&var2=&=value#hash");
		const base = new URL("http://domain.com/#hash");
		expect( relateURL(url,base,opts) ).to.equal("#hash");
	});



	it("stringify = false", () =>
	{
		const opts = options({ stringify:false });
		const url  = new URL("http://domain.com/path");
		const base = new URL("http://domain.com/");
		expect( relateURL(url,base,opts) ).to.be.an.instanceOf(URL);
	});



	describe("in common profile; output = PROTOCOL_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.PROTOCOL_RELATIVE }), "common_deep" );
	});



	describe("in common profile; output = ROOT_PATH_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.ROOT_PATH_RELATIVE }), "common_deep" );
	});



	describe("in common profile; output = PATH_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.PATH_RELATIVE }), "common_deep" );
	});



	describe("in common profile; output = SHORTEST", () =>
	{
		combinations(relateURL.COMMON_PROFILE, "common_deep");
	});



	// Simulate an incomplete `URL` implementation that's missing `URLSearchParams`
	describe("in common profile; output = PROTOCOL_RELATIVE, removeEmptyQueries = false, sortQueries = false", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.PROTOCOL_RELATIVE, removeEmptyQueries:false, sortQueries:false }), "common_shallow" );
	});



	// Simulate an incomplete `URL` implementation that's missing `URLSearchParams`
	describe("in common profile; output = ROOT_PATH_RELATIVE, removeEmptyQueries = false, sortQueries = false", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.ROOT_PATH_RELATIVE, removeEmptyQueries:false, sortQueries:false }), "common_shallow" );
	});



	// Simulate an incomplete `URL` implementation that's missing `URLSearchParams`
	describe("in common profile; output = PATH_RELATIVE, removeEmptyQueries = false, sortQueries = false", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, output:relateURL.PATH_RELATIVE, removeEmptyQueries:false, sortQueries:false }), "common_shallow" );
	});



	// Simulate an incomplete `URL` implementation that's missing `URLSearchParams`
	describe("in common profile; output = SHORTEST, removeEmptyQueries = false, sortQueries = false", () =>
	{
		combinations( options({ ...relateURL.COMMON_PROFILE, removeEmptyQueries:false, sortQueries:false }), "common_shallow", "careful" );
	});



	describe("in careful profile; output = PROTOCOL_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.CAREFUL_PROFILE, output:relateURL.PROTOCOL_RELATIVE }), "careful" );
	});



	describe("in careful profile; output = ROOT_PATH_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.CAREFUL_PROFILE, output:relateURL.ROOT_PATH_RELATIVE }), "careful" );
	});



	describe("in careful profile; output = PATH_RELATIVE", () =>
	{
		combinations( options({ ...relateURL.CAREFUL_PROFILE, output:relateURL.PATH_RELATIVE }), "careful" );
	});



	describe("in careful profile; output = SHORTEST", () =>
	{
		combinations(relateURL.CAREFUL_PROFILE, "careful");
	});
});


<|start_filename|>helpers/merge.js<|end_filename|>
"use strict";
// @todo try https://npmjs.com/json-preserve-indent ?
const newData = require("./tests.json");
const orgData = require("./tests-bak.json");

for (let i=0; i<newData.length; i++)
{
  const newDatum = newData[i];

  for (let j=0; j<orgData.length; j++)
  {
    const orgDatum = orgData[j];

    if (orgDatum.base!==newDatum.base || orgDatum.url!==newDatum.url)
    {
      continue;
    }

    newDatum.related = orgDatum.related;

    break;
  }
}

require("fs").writeFileSync(
  `${__dirname}/tests.json`,
  JSON.stringify(newData, null, "\t") + "\n"
);


<|start_filename|>index.js<|end_filename|>
"use strict";
const deepFreeze = require("deep-freeze-node");
const evaluateValue = require("evaluate-value");
const isURL = require("isurl");
const minURL = require("minurl");
const URLRelation = require("url-relation");



const PROTOCOL_RELATIVE  = Symbol("protocol_relative");
const ROOT_PATH_RELATIVE = Symbol("root_path_relative");
const PATH_RELATIVE      = Symbol("path_relative");
const SHORTEST           = Symbol("shortest");

const CAREFUL_PROFILE = { ...minURL.CAREFUL_PROFILE, output:SHORTEST };
const COMMON_PROFILE  = { ...minURL.COMMON_PROFILE,  output:SHORTEST };

const SEGMENT_PATTERN = /\/|[^\/]+/g;



const relateSegments = (baseSegments, urlSegments) =>
{
	let parentIndex = -1;
	let related = true;
	let slashes = 0;

	// Find parents
	const relatedSegments = baseSegments.reduce((result, baseSegment, i) =>
	{
		// Record groups of repeating slashes
		if (baseSegment === "/")
		{
			slashes++;
		}
		else
		{
			slashes = 0;
		}

		if (related)
		{
			if (urlSegments[i] !== baseSegment)
			{
				related = false;
			}
			else
			{
				// The last related segment
				parentIndex = i;
			}
		}

		if (!related)
		{
			// If a segment, infixed repeating slashes, or a prefixed/leading repeating slash
			if (slashes===0 || slashes>1 || i===0)
			{
				// Up one level
				result.push("..", "/");
			}
		}

		return result;
	}, []);

	// If relation starts at a path beginning with "//"
	if (relatedSegments.length===0 && urlSegments[parentIndex+1]==="/")
	{
		relatedSegments.push(".", "/");
	}

	// Add children -- starting after last related segment
	relatedSegments.push( ...urlSegments.slice(parentIndex+1) );

	return relatedSegments;
};



const relateToProtocol = (url, options) => minURL(url, options).slice(url.protocol.length);



const relateToRootPath = (url, options) =>
{
	// @todo does this support mailto? unlikely scenario, but complete
	const pattern = new RegExp(`^${url.protocol}\/?\/?${url.username}:?${url.password}@?${url.hostname}:?${url.port}`);

	const stringifiedURL = minURL(url,
	{
		clone: false,
		plusQueries: false,
		removeAuth: false,
		removeDefaultPort: false,
		removeEmptyHash: false,
		removeEmptyQueries: false,
		removeEmptyQueryNames: false,
		removeEmptyQueryValues: false,
		removeEmptySegmentNames: false,
		removeHash: false,
		removeIndexFilename: false,
		removeQueryNames: false,
		removeQueryOddities: false,
		removeRootTrailingSlash: options.removeRootTrailingSlash,
		removeTrailingSlash: options.removeTrailingSlash,
		removeWWW: false,
		sortQueries: false,
		stringify: true
	});

	// Remove everything before the path
	const truncated = stringifiedURL.replace(pattern, "");

	if (truncated === "")
	{
		return "/";
	}
	else if (truncated.startsWith("//"))
	{
		// Avoid complication with protocol-relative URLs
		return relateToProtocol(url, options);
	}
	else
	{
		return truncated;
	}
};



const relateURL = (url, base, options) =>
{
	if (!isURL.lenient(url) || !isURL.lenient(base))
	{
		throw new TypeError("Invalid URL");
	}

	options = { ...COMMON_PROFILE, ...options };

	const ignoreAuth = evaluateValue(options.removeAuth, url);

	const relation = new URLRelation(url, base,
	{
		components:              ignoreAuth ? [URLRelation.AUTH] : undefined,
		defaultPorts:            options.defaultPorts,
		ignoreDefaultPort:       relationOption(options.removeDefaultPort),
		ignoreEmptyQueries:      relationOption(options.removeEmptyQueries),
		ignoreEmptySegmentNames: relationOption(options.removeEmptySegmentNames),
		ignoreIndexFilename:     relationOption(options.removeIndexFilename),
		ignoreQueryNames:        relationOption(options.removeQueryNames),
		ignoreQueryOrder:        relationOption(options.sortQueries),
		ignoreWWW:               relationOption(options.removeWWW),
		indexFilenames:          options.indexFilenames,
		queryNames:              options.queryNames
	});

	if (relation.upTo(URLRelation.PATH))
	{
		// Avoid turning a hash-relative URL into ""
		options.removeEmptyHash = false;
	}

	if (!relation.upTo(URLRelation.PROTOCOL))
	{
		return minURL(url, options);
	}
	else if (!relation.upTo(URLRelation.HOST) || options.output===PROTOCOL_RELATIVE)
	{
		return relateToProtocol(url, options);
	}

	if (options.clone)
	{
		base = new URL(base);
		url = new URL(url);

		// Don't let `minURL` clone anything, since it's called multiple times
		options.clone = false;
	}

	// Stringify'ing would require a reparse
	const noStringify = { ...options, stringify:false };

	url = minURL(url, noStringify);

	if (!options.stringify)
	{
		// This isn't recommended, but still possible
		return url;
	}
	else if (options.output === ROOT_PATH_RELATIVE)
	{
		return relateToRootPath(url, options);
	}

	// NOTE :: https://github.com/whatwg/url/issues/221
	const urlHash = url.href.endsWith("#") ? "#" : url.hash;

	if (relation.upTo(URLRelation.SEARCH))
	{
		return urlHash;
	}
	else if (relation.upTo(URLRelation.FILENAME))
	{
		// @todo this is the second time this will be ran -- first time within `minURL()`
		if (!evaluateValue(options.sortQueries, url))
		{
			base = minURL(base, noStringify);

			// Avoid similar queries minifying to the same, but not truncating because
			// they were seen as unrelated due to a shallow scan
			if (url.search === base.search)
			{
				return urlHash;
			}
		}

		if (url.search === "")
		{
			return "." + urlHash;
		}

		return url.search + urlHash;
	}

	const baseSegments = splitPathname(base.pathname);
	const urlSegments = splitPathname(url.pathname);
	let baseFilename = baseSegments[baseSegments.length - 1];
	let urlFilename = urlSegments[urlSegments.length - 1];

	if (urlFilename===undefined || urlFilename==="/")
	{
		urlFilename = "";
	}
	else
	{
		// Remove filename
		urlSegments.pop();
	}

	if (baseFilename===undefined || baseFilename==="/")
	{
		baseFilename = "";
	}
	else
	{
		// Remove filename
		baseSegments.pop();
	}

	if (relation.upTo(URLRelation.SEGMENTS))
	{
		if (urlFilename==="" && baseFilename!=="")
		{
			urlFilename = ".";
		}

		return urlFilename + url.search + urlHash;
	}

	const relatedSegments = relateSegments(baseSegments, urlSegments);

	if (urlFilename==="" && relatedSegments[relatedSegments.length-2]==="..")
	{
		relatedSegments.pop();
	}

	const pathRelative = relatedSegments.join("") + urlFilename + url.search + urlHash;

	if (options.output === PATH_RELATIVE)
	{
		return pathRelative;
	}

	const rootPathRelative = relateToRootPath(url, options);

	// Return shortest -- if same, root-path/protocol-relative takes priority as it's more direct
	return rootPathRelative.length <= pathRelative.length ? rootPathRelative : pathRelative;
};



const relationOption = option =>
{
	return (url1, url2) => evaluateValue(option,url1) && evaluateValue(option,url2);
};



const splitPathname = pathname =>
{
	// Remove leading slash, which will always exist
	pathname = pathname.substr(1);

	// Split by and include slashes
	// Simply splitting produced issues with trailing "//" and joining
	let output = pathname.match(SEGMENT_PATTERN);

	if (output === null)
	{
		output = [];
	}

	return output;
};



Object.assign
(
	relateURL,
	{
		CAREFUL_PROFILE,
		COMMON_PROFILE,

		PROTOCOL_RELATIVE,
		ROOT_PATH_RELATIVE,
		PATH_RELATIVE,
		SHORTEST
	}
);



module.exports = deepFreeze(relateURL);


<|start_filename|>helpers/generate.js<|end_filename|>
"use strict";
const {writeFileSync} = require("fs");

const urls =
[
	// Parts removing from right to left
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1=",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../",
	"http://user:pass@www.domain.com:80/test//tes.ter/./",
	"http://user:pass@www.domain.com:80/test//tes.ter/",
	"http://user:pass@www.domain.com:80/test//",
	"http://user:pass@www.domain.com:80/test/",
	"http://user:pass@www.domain.com:80/",
	"http://user:pass@www.domain.com:80",
	"http://user:pass@www.domain.com/",
	"http://user:pass@www.domain.com",
	"http://user:pass@www.domain./",
	"http://user:pass@www.domain.",
	"http://user:pass@www.domain/",
	"http://user:pass@www.domain",
	"http://user:pass@www./",
	"http://user:pass@www.",
	"http://user:pass@www/",
	"http://user:pass@www",

	// Parts changing from left to right
	"https://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user2:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass2@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user2:pass2@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://:@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www2.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain2.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com2:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:81/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test2//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test/tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter2/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index2.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va+r1= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r12= +dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1=++dir&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir2&var2=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var22=text&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text2&var3#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var32#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor2",

	// Edge cases
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3&=#anchor",
	"http://user:pass@www.domain.com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var2=text2&var3#anchor",
	"http://user:pass@www.<EMAIL>:80/test//tes.ter/./../index.html?var2=text&va r1= +dir&var3&var2=text2#anchor",
	"http://user:pass@www.<EMAIL>:80//",
	"http://user:pass@www.domain.com:80///test/",
	"http://user:pass@com:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@localhost:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"http://user:pass@127.0.0.1:80/test//tes.ter/./../index.html?va r1= +dir&var2=text&var3#anchor",
	"other://domain/"
];



const generate = () => saveFile(`${__dirname}/tests.json`);



const generateData = () =>
{
	const output = [];

	for (let i=0; i<urls.length; i++)
	{
		for (let j=0; j<urls.length; j++)
		{
			output.push(
			{
				url:  urls[i],
				base: urls[j],
				related:
				{
					careful:
					{
						// Placeholder values for manual editing
						PROTOCOL_RELATIVE: null,
						ROOT_PATH_RELATIVE: null,
						PATH_RELATIVE: null,
						SHORTEST: null
					},
					common_deep:
					{
						// Placeholder values for manual editing
						PROTOCOL_RELATIVE: null,
						ROOT_PATH_RELATIVE: null,
						PATH_RELATIVE: null,
						SHORTEST: null
					},
					common_shallow:
					{
						// Placeholder values for manual editing
						PROTOCOL_RELATIVE: null,
						ROOT_PATH_RELATIVE: null,
						PATH_RELATIVE: null,
						SHORTEST: null
					}
				}
			});
		}
	}

	return output;
};



// Extra line break for unix/git
const generateString = () => JSON.stringify(generateData(), null, "\t") + "\n";



const saveFile = location =>
{
	writeFileSync(location, generateString());

	console.log(`Written to: ${location}`);
};



generate();
