urlsearchparams
Version:
implementaion of URLSearchParams at WHATWG Living Standard https://url.spec.whatwg.org/
490 lines (400 loc) • 12.4 kB
text/typescript
/// <reference path="types/webidl.d.ts" />
/// <reference path="types/utf8-encoding.d.ts" />
// polyfill for String.fromCodePoint
declare var String: {
new (value?: any): String;
(value?: any): string;
prototype: String;
fromCharCode(...codes: number[]): string;
/**
* Pollyfill of String.fromCodePoint
*/
fromCodePoint(...codePoints: number[]): string;
};
// for dynamic require
declare var require: any;
// import only type info
import te = require("utf8-encoding");
var TextEncoder, TextDecoder;
if (typeof window === "undefined") { // in node.js
var TextEncoding: typeof te = require("utf8-encoding");
TextEncoder = TextEncoding.TextEncoder;
TextDecoder = TextEncoding.TextDecoder;
}
// save platform implementation if exists
var nativeURLSearchParams;
if (typeof URLSearchParams !== "undefined") {
nativeURLSearchParams = URLSearchParams;
}
module URLSearchParams {
var encoder = new TextEncoder("utf-8");
var decoder = new TextDecoder();
function encode(s: string): Uint8Array {
"use strict";
return encoder.encode(s);
}
function decode(bytes: Uint8Array): string {
"use strict";
return decoder.decode(bytes);
}
function copy<T>(obj: T): T {
"use strict";
return JSON.parse(JSON.stringify(obj));
}
/**
* TODO: separate percentEncoder to module
*/
// https://url.spec.whatwg.org/#percent-encode
function percentEncode(byt: number): string {
"use strict";
return "%" + byt.toString(16).toUpperCase();
}
// utility not in spec
export function percentEncoder(str: string): string {
"use strict";
var encoded = encode(str);
var result = "";
for (var i = 0; i < encoded.length; i ++) {
result += percentEncode(encoded[i]);
}
return result;
}
// TODO: better using []number ?
// https://url.spec.whatwg.org/#percent-decode
function percentDecode(input: Uint8Array): Uint8Array {
"use strict";
// step 1
var output: number[] = [];
// step 2
for (var i = 0; i < input.length; i ++) {
var byt = input[i];
// step 2-1
if (byt !== 37) {
output.push(byt);
continue;
}
// step 2-2
if (byt === 37) {
// has more 2 byte
if (i + 2 < input.length) {
var b1 = input[i+1];
var b2 = input[i+2];
var range1 = (0x30 <= b1 && b1 <= 0x39) ||
(0x41 <= b1 && b1 <= 0x46) ||
(0x61 <= b1 && b1 <= 0x66);
var range2 = (0x30 <= b2 && b2 <= 0x39) ||
(0x41 <= b2 && b2 <= 0x46) ||
(0x61 <= b2 && b2 <= 0x66);
if (!range1 && !range2) {
output.push(byt);
continue;
}
}
}
// step 2-3
// step 2-3-1
var u1 = input[i+1];
var u2 = input[i+2];
var hex = decode(new Uint8Array([u1, u2]));
var bytePoint = parseInt(hex, 16);
// step 2-3-2
output.push(bytePoint);
// step 2-3-3
i = i + 2;
continue;
}
// step 3
return new Uint8Array(output);
}
// utility not in spec
export function percentDecoder(str: string): string {
"use strict";
var encoded = encode(str);
return decode(percentDecode(encoded));
}
// https://url.spec.whatwg.org/#concept-urlencoded-parser
function URLEncodedParse(input: Uint8Array, encodingOverride?: string, useCharset?: boolean, isIndex?: boolean): IPair[] {
"use strict";
// step 1
if (encodingOverride === undefined) {
encodingOverride = "utf-8";
}
// step 2
if (encodingOverride !== "utf-8") {
// omit byte range checking (=<0x7F)
throw new Error("unsupported encoding");
}
// step 3
var array = Array.prototype.slice.call(input);
var sequences = [];
while (true) {
var i = array.indexOf(38); // &
if (i < 0) {
sequences.push(array);
break;
}
sequences.push(array.splice(0, i));
array.shift();
}
// step 4
if (isIndex === true) {
if (sequences[0].indexOf(61) === -1) { // =
sequences[0].unshift(61);
}
}
// step 5, 6
var pairs: IPair[] = sequences.map((bytes: number[]): IPair => {
// step 6-1
if (bytes.length === 0) return;
// step 6-2
var name, value;
var i = bytes.indexOf(61);
if (i > 0) { // =
name = bytes.splice(0, i);
bytes.shift();
value = bytes;
}
// step 6-3
else {
name = bytes;
value = [];
}
// step 4
name.map((e: number) => {
if (e === 43) { // +
e = 0x20;
}
return e;
});
// step 5
if (useCharset && name === "_charset_") {
throw new Error("unsupported flug '_charset_'");
}
// step 8 parsent decode
name = decode(percentDecode(new Uint8Array(name)));
value = decode(percentDecode(new Uint8Array(value)));
return { name: name, value: value };
});
return pairs;
}
// https://url.spec.whatwg.org/#concept-urlencoded-serializer
function URLEncodedSerialize(pairs: IPair[], encodingOverride?: string): string {
"use strict";
// step 1
if (encodingOverride === undefined) {
encodingOverride = "utf-8";
}
// this imeplementation support only utf-8
if (encodingOverride !== "utf-8") {
throw new Error("unsupported encoding");
}
// step 2
var output = "";
// step 3
pairs.forEach((pair: IPair, index: number) => {
// step 3-1
var outputPair = copy(pair);
// step 3-2
var encodedName = encode(outputPair.name);
var encodedValue = encode(outputPair.value);
// step 3-3
outputPair.name = URLEncodedByteSerialize(encodedName);
outputPair.value = URLEncodedByteSerialize(encodedValue);
// step 3-4
if (index !== 0) {
output += "&";
}
// step 3-5
output += `${outputPair.name}=${outputPair.value}`;
});
// step 4
return output;
}
// https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer
function URLEncodedByteSerialize(input: Uint8Array): string {
"use strict";
// step 1
var output = "";
// step 2
for (var i = 0; i < input.length; i++) {
var byt = input[i];
if (byt === 0x20) {
output += "+"; // 0x2B
continue;
}
if ([0x2A, 0x2D, 0x2E].indexOf(byt) !== -1) {
output += String.fromCodePoint(byt);
continue;
}
if (0x30 <= byt && byt <= 0x39) {
output += String.fromCodePoint(byt);
continue;
}
if (0x41 <= byt && byt <= 0x5A) {
output += String.fromCodePoint(byt);
continue;
}
if (byt === 0x5F) {
output += String.fromCodePoint(byt);
continue;
}
if (0x61 <= byt && byt <= 0x7A) {
output += String.fromCodePoint(byt);
continue;
}
// otherwise
output += percentEncode(byt);
}
// step 3
return output;
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
// [Constructor(optional (USVString or URLSearchParams) init = ""), Exposed=(Window,Worker)]
// interface URLSearchParams {
// void append(USVString name, USVString value);
// void delete(USVString name);
// USVString? get(USVString name);
// sequence<USVString> getAll(USVString name);
// boolean has(USVString name);
// void set(USVString name, USVString value);
// iterable<USVString, USVString>;
// stringifier;
// };
interface IURLSearchParams {
append(name: USVString, value: USVString): void;
delete(name: USVString): void;
get(name: USVString): USVString;
getAll(name: USVString): USVString[];
has(name: USVString): boolean;
set(name: USVString, value: USVString): void;
toString(): string; // stringifier;
// iterable<USVString, USVString>;
};
interface IPair {
name: USVString;
value: USVString;
}
export class URLSearchParams implements IURLSearchParams {
// https://url.spec.whatwg.org/#concept-urlsearchparams-list
private list: IPair[] = [];
// https://url.spec.whatwg.org/#concept-urlsearchparams-url-object
private urlObject: URL[] = [];
// https://url.spec.whatwg.org/#concept-urlsearchparams-new
constructor(init?: USVString);
constructor(init?: URLSearchParams);
constructor(init?: any) {
// step 1
var query = this;
// step 2
if (typeof init === "string") {
query.list = this.parse(init);
}
// step 3
if (URLSearchParams.prototype.isPrototypeOf(init)) {
query.list = copy(init.list);
}
// step 4
return query;
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-append
append(name: USVString, value: USVString): void {
if (name === undefined || value === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.append.");
}
// step 1
this.list.push({ name: name, value: value });
// step 2
this.update();
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-delete
delete(name: USVString): void {
if (name === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.delete.");
}
// step 1
this.list = this.list.filter((pair: IPair) => pair.name !== name);
// step 2
this.update();
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-get
get(name: USVString): USVString {
if (name === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.get.");
}
return this.getAll(name).shift() || null;
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-getall
getAll(name: USVString): USVString[] {
if (name === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.getAll.");
}
return this.list.reduce((acc: USVString[], pair: IPair) => {
if (pair.name === name) {
acc.push(pair.value);
}
return acc;
}, []);
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-set
set(name: USVString, value: USVString): void { // LABEL: performance
if (name === undefined || value === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.set.");
}
// if exists, this appended will remove in filter.
this.list.push({ name: name, value: value });
// update all pair
this.list = this.list.map((pair: IPair) => {
if (pair.name === name) {
pair.value = value;
}
return pair;
})
// filter duplicates
.filter(function(pair: IPair) {
if (pair.name === name) {
if (this.emitted) {
// current pair is duplicate
return false;
} else {
// first pair of key
this.emitted = true;
return true;
}
}
// other pair
return true;
}, { emitted: false });
// step 3
this.update();
}
// https://url.spec.whatwg.org/#dom-urlsearchparams-has
has(name: USVString): boolean {
if (name === undefined) {
throw new TypeError("Not enough arguments to URLSearchParams.has.");
}
return this.list.some((pair: IPair) => pair.name === name);
}
// https://url.spec.whatwg.org/#concept-urlencoded-string-parser
private parse(input: USVString): IPair[] {
return URLEncodedParse(encode(input));
}
// https://url.spec.whatwg.org/#concept-urlsearchparams-update
private update(): void {
// step 1
this.urlObject.forEach((url) => {
// TODO: add query to url
// url.query = this.serialize(this.list);
// step 2
// this.urlObject.preupdate();
});
}
toString(): string {
return URLEncodedSerialize(this.list);
}
}
}
// export to
// - window in browser
// - module.exports in node.js
// if platform has implements, use that.
this.URLSearchParams = nativeURLSearchParams || URLSearchParams.URLSearchParams;