whatwg-url
Advanced tools
Comparing version 0.1.0 to 0.2.0
611
lib/url.js
@@ -5,2 +5,5 @@ "use strict"; | ||
const Guid = require("Guid"); | ||
const tr46 = require("tr46"); | ||
/*jshint unused: false */ | ||
@@ -71,3 +74,3 @@ | ||
function isASCIIHex(c) { | ||
return (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); | ||
return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); | ||
} | ||
@@ -138,9 +141,126 @@ | ||
//TODO: Finish up | ||
function parseIPv6(input) { | ||
if (!net.isIPv6(input)) { | ||
const ip = [0, 0, 0, 0, 0, 0, 0, 0]; | ||
let piecePtr = 0; | ||
let compressPtr = null; | ||
let pointer = 0; | ||
input = punycode.ucs2.decode(input); | ||
if (at(input, pointer) === ":") { | ||
if (at(input, pointer + 1) !== ":") { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
pointer += 2; | ||
++piecePtr; | ||
compressPtr = piecePtr; | ||
} | ||
let ipv4 = false; | ||
Main: | ||
while (pointer < input.length) { | ||
if (piecePtr === 8) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
if (at(input, pointer) === ":") { | ||
if (compressPtr !== null) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
++pointer; | ||
++piecePtr; | ||
compressPtr = piecePtr; | ||
continue; | ||
} | ||
let value = 0; | ||
let length = 0; | ||
while (length < 4 && isASCIIHex(input[pointer])) { | ||
value = value * 0x10 + parseInt(at(input, pointer), 16); | ||
++pointer; | ||
++length; | ||
} | ||
switch (at(input, pointer)) { | ||
case ".": | ||
if (length === 0) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
pointer -= length; | ||
ipv4 = true; | ||
break Main; | ||
case ":": | ||
++pointer; | ||
if (input[pointer] === undefined) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
break; | ||
case undefined: | ||
break; | ||
default: | ||
throw new TypeError("Invalid Host"); | ||
} | ||
ip[piecePtr] = value; | ||
++piecePtr; | ||
} | ||
if (ipv4 && piecePtr > 6) { | ||
throw new TypeError("Invalid Host"); | ||
} else if (input[pointer] !== undefined) { | ||
let dotsSeen = 0; | ||
while (input[pointer] !== undefined) { | ||
let value = null; | ||
if (!isASCIIDigit(input[pointer])) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
while (isASCIIDigit(input[pointer])) { | ||
const number = parseInt(at(input, pointer), 10); | ||
if (value === null) { | ||
value = number; | ||
} else if (value === 0) { | ||
throw new TypeError("Invalid Host"); | ||
} else { | ||
value = value * 10 + number; | ||
} | ||
++pointer; | ||
if (value > 255) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
} | ||
if (dotsSeen < 3 && at(input, pointer) !== ".") { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
ip[piecePtr] = ip[piecePtr] * 0x100 + value; | ||
if (dotsSeen === 1 || dotsSeen === 3) { | ||
++piecePtr; | ||
} | ||
++pointer; | ||
if (dotsSeen === 3 && input[pointer] !== undefined) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
++dotsSeen; | ||
} | ||
} | ||
return "[" + input + "]"; | ||
if (compressPtr !== null) { | ||
let swaps = piecePtr - compressPtr; | ||
piecePtr = 7; | ||
while (piecePtr !== 0 && swaps > 0) { | ||
const temp = ip[compressPtr + swaps - 1]; // piece | ||
ip[compressPtr + swaps - 1] = ip[piecePtr]; | ||
ip[piecePtr] = temp; | ||
--piecePtr; | ||
--swaps; | ||
} | ||
} else if (piecePtr !== 8) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
return ip; | ||
} | ||
@@ -158,3 +278,3 @@ | ||
return parseIPv6(input.substr(1, input.length - 2)); | ||
return serializeHost(parseIPv6(input.substr(1, input.length - 2))); | ||
} | ||
@@ -169,3 +289,7 @@ | ||
let asciiDomain = punycode.toASCII(domain.toLowerCase()); | ||
const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.TRANSITIONAL, false); | ||
if (asciiDomain === null) { | ||
throw new TypeError("Invalid Host"); | ||
} | ||
if (asciiDomain.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1) { | ||
@@ -175,6 +299,34 @@ throw new TypeError("Invalid Host"); | ||
return isUnicode ? punycode.toUnicode(asciiDomain) : asciiDomain; | ||
return isUnicode ? tr46.toUnicode(asciiDomain, false).domain : asciiDomain; | ||
} | ||
//TODO: Finish up | ||
function findLongestZeroSequence(arr) { | ||
let maxIdx = null; | ||
let maxLen = 1; // only find elements > 1 | ||
let currStart = null; | ||
let currLen = 0; | ||
for (var i = 0; i < arr.length; ++i) { | ||
if (arr[i] !== 0) { | ||
if (currLen > maxLen) { | ||
maxIdx = currStart; | ||
maxLen = currLen; | ||
} | ||
currStart = null; | ||
currLen = 0; | ||
} else { | ||
if (currStart === null) { | ||
currStart = i; | ||
} | ||
++currLen; | ||
} | ||
} | ||
return { | ||
idx: maxIdx, | ||
len: maxLen | ||
}; | ||
} | ||
function serializeHost(host) { | ||
@@ -185,2 +337,29 @@ if (host === null) { | ||
// IPv6 serializer | ||
if (host instanceof Array) { | ||
let output = ""; | ||
const seqResult = findLongestZeroSequence(host); | ||
const compressPtr = seqResult.idx; | ||
for (var i = 0; i < host.length; ++i) { | ||
if (compressPtr === i) { | ||
if (i === 0) { | ||
output += "::"; | ||
} else { | ||
output += ":"; | ||
} | ||
i += seqResult.len - 1; | ||
continue; | ||
} | ||
output += host[i].toString(16); | ||
if (i !== host.length - 1) { | ||
output += ":"; | ||
} | ||
} | ||
return "[" + output + "]"; | ||
} | ||
return host; | ||
@@ -655,2 +834,17 @@ } | ||
function serializeOrigin(tuple) { | ||
if (tuple.scheme === undefined || tuple.host === undefined || tuple.port === undefined) { | ||
return "null"; | ||
} | ||
let result = tuple.scheme + "://"; | ||
result += tr46.toUnicode(tuple.host, false).domain; | ||
if (relativeSchemas[tuple.scheme] && tuple.port !== relativeSchemas[tuple.scheme]) { | ||
result += ":" + tuple.port; | ||
} | ||
return result; | ||
} | ||
function mixin(src, target) { | ||
@@ -672,219 +866,258 @@ const props = Object.getOwnPropertyNames(src); | ||
const getBaseSymbol = Symbol("getTheBase"); | ||
const baseSymbol = Symbol("base"); | ||
const isURLSymbol = Symbol("isURL"); | ||
const guidSymbol = Symbol("guid"); | ||
const URLUtils = {}; | ||
function setTheInput(obj, input, url) { | ||
if (url) { | ||
obj[urlSymbol] = url; | ||
obj[inputSymbol] = input; | ||
} else { | ||
obj[urlSymbol] = null; | ||
if (input === null) { | ||
obj[inputSymbol] = ""; | ||
} else { | ||
obj[inputSymbol] = input; | ||
Object.defineProperties(URLUtils, { | ||
href: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return this[inputSymbol]; | ||
} | ||
try { | ||
obj[urlSymbol] = new URLStateMachine(input, obj[baseSymbol]); | ||
} catch (e) {} | ||
} | ||
} | ||
return serializeURL(this[urlSymbol].url); | ||
}, | ||
set: function (val) { | ||
let input = String(this[inputSymbol]); | ||
let url = null; | ||
const query = obj[urlSymbol] !== null && obj[urlSymbol].url.query !== null ? obj[urlSymbol].url.query : ""; | ||
// TODO: Update URLSearchParams | ||
} | ||
if (this[isURLSymbol]) { | ||
// SPEC: says to use "get the base" algorithm, | ||
// but the base might've already been provided by the constructor. | ||
// Clarify! | ||
const parsedURL = new URLStateMachine(this[inputSymbol], this[getBaseSymbol]()); | ||
input = ""; | ||
url = parsedURL; | ||
} | ||
const URLUtils = { | ||
get href() { | ||
if (this[urlSymbol] === null) { | ||
return this[inputSymbol]; | ||
} | ||
// set the input algorithm | ||
if (url !== null) { | ||
this[urlSymbol] = url; | ||
this[inputSymbol] = input; | ||
} else { | ||
if (input === null) { | ||
this[inputSymbol] = ""; | ||
} else { | ||
this[inputSymbol] = input; | ||
return serializeURL(this[urlSymbol].url); | ||
}, | ||
set href(val) { | ||
let input = String(val); | ||
// TODO: query encoding | ||
this[urlSymbol] = new URLStateMachine(this[inputSymbol], this[getBaseSymbol]()); | ||
} | ||
} | ||
if (this[isURLSymbol]) { | ||
// SPEC: says to use "get the base" algorithm, | ||
// but the base might've already been provided by the constructor. | ||
// Clarify! | ||
const parsedURL = new URLStateMachine(input, this[baseSymbol]); | ||
input = ""; | ||
setTheInput(this, "", parsedURL); | ||
} else { | ||
setTheInput(this, input); | ||
// TODO: Run pre-update steps | ||
} | ||
}, | ||
const query = this[urlSymbol] !== null && this[urlSymbol].url.query !== null ? this[urlSymbol].url.query : ""; | ||
// TODO: Update URLSearchParams | ||
get origin() { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
const url = this[urlSymbol].url; | ||
switch (url.scheme) { | ||
case "blob": | ||
try { | ||
return module.exports.getURL()(url.scheme_data).origin; | ||
} catch (e) { | ||
if (this[guidSymbol] === undefined) { | ||
this[guidSymbol] = Guid.raw(); | ||
} | ||
return this[guidSymbol]; | ||
} | ||
break; | ||
case "ftp": | ||
case "gopher": | ||
case "http": | ||
case "https": | ||
case "ws": | ||
case "wss": | ||
return serializeOrigin({ | ||
scheme: url.scheme, | ||
host: url.host, | ||
port: url.port === "" ? relativeSchemas[url.scheme] : url.port | ||
}); | ||
case "file": | ||
// spec says "exercise to the reader", chrome says "file://" | ||
return "file://"; | ||
default: | ||
if (this[guidSymbol] === undefined) { | ||
this[guidSymbol] = Guid.raw(); | ||
} | ||
return this[guidSymbol]; | ||
} | ||
}, | ||
// TODO: origin | ||
protocol: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return ":"; | ||
} | ||
if (this === undefined) {console.log("this");} | ||
if (this[urlSymbol] === undefined) {console.log("url");} | ||
return this[urlSymbol].url.scheme + ":"; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val + ":", null, null, this[urlSymbol].url, STATES.SCHEME_START); | ||
get protocol() { | ||
if (this[urlSymbol] === null) { | ||
return ":"; | ||
} | ||
return this[urlSymbol].url.scheme + ":"; | ||
}, | ||
set protocol(val) { | ||
if (this[urlSymbol] === null) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val + ":", null, null, this[urlSymbol].url, STATES.SCHEME_START); | ||
}, | ||
username: { | ||
get: function () { | ||
return this[urlSymbol] === null ? "" : this[urlSymbol].url.username; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
get username() { | ||
return this[urlSymbol] === null ? "" : this[urlSymbol].url.username; | ||
}, | ||
set username(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol].url.username = ""; | ||
const decoded = punycode.ucs2.decode(val); | ||
for (let i = 0; i < decoded.length; ++i) { | ||
this[urlSymbol].url.username += usernameEncode(decoded[i]); | ||
} | ||
this[urlSymbol].url.username = ""; | ||
const decoded = punycode.ucs2.decode(val); | ||
for (let i = 0; i < decoded.length; ++i) { | ||
this[urlSymbol].url.username += usernameEncode(decoded[i]); | ||
} | ||
}, | ||
password: { | ||
get: function () { | ||
return this[urlSymbol] === null || this[urlSymbol].url.password === null ? "" : this[urlSymbol].url.password; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
get password() { | ||
return this[urlSymbol] === null || this[urlSymbol].url.password === null ? "" : this[urlSymbol].url.password; | ||
}, | ||
set password(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol].url.password = ""; | ||
const decoded = punycode.ucs2.decode(val); | ||
for (let i = 0; i < decoded.length; ++i) { | ||
this[urlSymbol].url.password += passwordEncode(decoded[i]); | ||
} | ||
this[urlSymbol].url.password = ""; | ||
const decoded = punycode.ucs2.decode(val); | ||
for (let i = 0; i < decoded.length; ++i) { | ||
this[urlSymbol].url.password += passwordEncode(decoded[i]); | ||
} | ||
}, | ||
host: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return serializeHost(this[urlSymbol].url.host) + | ||
(this[urlSymbol].url.port === "" ? "" : ":" + this[urlSymbol].url.port); | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST); | ||
get host() { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return serializeHost(this[urlSymbol].url.host) + | ||
(this[urlSymbol].url.port === "" ? "" : ":" + this[urlSymbol].url.port); | ||
}, | ||
set host(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST); | ||
}, | ||
hostname: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return serializeHost(this[urlSymbol].url.host); | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST_NAME); | ||
get hostname() { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return serializeHost(this[urlSymbol].url.host); | ||
}, | ||
set hostname(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST_NAME); | ||
}, | ||
port: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return this[urlSymbol].url.port; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative || this[urlSymbol].url.scheme === "file") { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.PORT); | ||
get port() { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
return this[urlSymbol].url.port; | ||
}, | ||
set port(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative || this[urlSymbol].url.scheme === "file") { | ||
return; | ||
} | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.PORT); | ||
}, | ||
pathname: { | ||
get: function () { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
if (!this[urlSymbol].url.isRelative) { | ||
return this[urlSymbol].url.scheme_data; | ||
} | ||
get pathname() { | ||
if (this[urlSymbol] === null) { | ||
return ""; | ||
} | ||
if (!this[urlSymbol].url.isRelative) { | ||
return this[urlSymbol].url.scheme_data; | ||
} | ||
return "/" + this[urlSymbol].url.path.join("/"); | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol].url.path = []; | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.RELATIVE_PATH_START); | ||
return "/" + this[urlSymbol].url.path.join("/"); | ||
}, | ||
set pathname(val) { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.isRelative) { | ||
return; | ||
} | ||
this[urlSymbol].url.path = []; | ||
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.RELATIVE_PATH_START); | ||
}, | ||
search: { | ||
get: function () { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.query) { | ||
return ""; | ||
} | ||
get search() { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.query) { | ||
return ""; | ||
} | ||
return "?" + this[urlSymbol].url.query; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null) { | ||
return; | ||
} | ||
if (val === "") { | ||
this[urlSymbol].url.query = null; | ||
// TODO: empty query object | ||
return; | ||
} | ||
return "?" + this[urlSymbol].url.query; | ||
}, | ||
set search(val) { | ||
if (this[urlSymbol] === null) { | ||
return; | ||
} | ||
if (val === "") { | ||
this[urlSymbol].url.query = null; | ||
// TODO: empty query object | ||
return; | ||
} | ||
const input = val[0] === "?" ? val.substr(1) : val; | ||
this[urlSymbol].url.query = ""; | ||
const input = val[0] === "?" ? val.substr(1) : val; | ||
this[urlSymbol].url.query = ""; | ||
// TODO: Add query encoding | ||
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.QUERY); | ||
// TODO: Add query encoding | ||
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.QUERY); | ||
// TODO: Update query object | ||
// TODO: Update query object | ||
}, | ||
get hash() { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.fragment) { | ||
return ""; | ||
} | ||
return "#" + this[urlSymbol].url.fragment; | ||
}, | ||
set hash(val) { | ||
if (this[urlSymbol] === null || this[urlSymbol].url.scheme === "javascript") { | ||
return; | ||
} | ||
if (val === "") { | ||
this[urlSymbol].url.fragment = null; | ||
return; | ||
} | ||
hash: { | ||
get: function () { | ||
if (this[urlSymbol] === null || !this[urlSymbol].url.fragment) { | ||
return ""; | ||
} | ||
const input = val[0] === "#" ? val.substr(1) : val; | ||
this[urlSymbol].url.fragment = ""; | ||
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.FRAGMENT); | ||
} | ||
}; | ||
return "#" + this[urlSymbol].url.fragment; | ||
}, | ||
set: function (val) { | ||
if (this[urlSymbol] === null || this[urlSymbol].url.scheme === "javascript") { | ||
return; | ||
} | ||
if (val === "") { | ||
this[urlSymbol].url.fragment = null; | ||
return; | ||
} | ||
function urlToASCII(domain) { | ||
try { | ||
const asciiDomain = parseHost(domain); | ||
return asciiDomain; | ||
} catch (e) { | ||
return ""; | ||
} | ||
} | ||
const input = val[0] === "#" ? val.substr(1) : val; | ||
this[urlSymbol].url.fragment = ""; | ||
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.FRAGMENT); | ||
} | ||
function urlToUnicode(domain) { | ||
try { | ||
const unicodeDomain = parseHost(domain, true); | ||
return unicodeDomain; | ||
} catch (e) { | ||
return ""; | ||
} | ||
}); | ||
} | ||
@@ -904,11 +1137,11 @@ function init(url, base) { | ||
parsedBase = new URLStateMachine(base); | ||
this[baseSymbol] = parsedBase.url; | ||
} | ||
this[urlSymbol] = new URLStateMachine(url, parsedBase.url); | ||
this[inputSymbol] = ""; | ||
const parsedURL = new URLStateMachine(url, parsedBase ? parsedBase.url : undefined); | ||
setTheInput(this, "", parsedURL); | ||
} | ||
module.exports.getURL = function (getBase) { | ||
module.exports.getURL = function () { | ||
function URL() { | ||
this[getBaseSymbol] = getBase; | ||
this[isURLSymbol] = true; | ||
@@ -919,4 +1152,14 @@ init.apply(this, arguments); | ||
mixin(URLUtils, URL.prototype); | ||
URL.toASCII = urlToASCII; | ||
URL.toUnicode = urlToUnicode; | ||
return URL; | ||
}; | ||
module.exports.addToObject = function (obj, base) { | ||
obj[isURLSymbol] = false; | ||
obj[baseSymbol] = new URLStateMachine(base).url; | ||
setTheInput(obj, null, null); | ||
mixin(URLUtils, obj); | ||
}; |
{ | ||
"name": "whatwg-url", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "An implementation of the WHATWG URL algorithm", | ||
@@ -10,3 +10,8 @@ "main": "lib/url.js", | ||
}, | ||
"dependencies": { | ||
"guid": "0.0.12", | ||
"tr46": "~0.0.1" | ||
}, | ||
"devDependencies": { | ||
"istanbul": "~0.3.14", | ||
"jscs": "^1.13.0", | ||
@@ -18,2 +23,3 @@ "jshint": "^2.7.0", | ||
"scripts": { | ||
"coverage": "istanbul cover node_modules/mocha/bin/_mocha", | ||
"lint": "jscs lib/ test/ scripts/ && jshint lib/ test/ scripts/", | ||
@@ -20,0 +26,0 @@ "pretest": "iojs scripts/get-latest-platform-tests.js", |
295376
15
1401
2
5
+ Addedguid@0.0.12
+ Addedtr46@~0.0.1
+ Addedguid@0.0.12(transitive)
+ Addedtr46@0.0.3(transitive)