const kind = Symbol.for('bare.url.search-params.kind') // The URL each instance writes back to, if any. Kept module private so that the // setter it drives cannot be pointed at an arbitrary object. const urls = new WeakMap() class URLSearchParams { static get [kind]() { return 0 // Compatibility version } // https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams constructor(init, url = null) { this._params = null if (url) urls.set(this, url) if (typeof init === 'string') { this._parse(init) } else { this._params = new Map() if (init) { for (const [name, value] of typeof init[Symbol.iterator] === 'function' ? init : Object.entries(init)) { this.append(name, value) } } } } get [kind]() { return URLSearchParams[kind] } // https://url.spec.whatwg.org/#dom-urlsearchparams-size get size() { let size = 0 for (const values of this._params.values()) size += values.length return size } // https://url.spec.whatwg.org/#dom-urlsearchparams-append append(name, value = null) { if (value === null) return let list = this._params.get(name) if (list === undefined) { list = [] this._params.set(name, list) } list.push(value) this._update() } // https://url.spec.whatwg.org/#dom-urlsearchparams-delete delete(name, value = null) { if (value === null) this._params.delete(name) else { let list = this._params.get(name) if (list === undefined) return list = list.filter((found) => found !== value) if (list.length === 0) this._params.delete(name) else this._params.set(name, list) } this._update() } // https://url.spec.whatwg.org/#dom-urlsearchparams-get get(name) { const list = this._params.get(name) if (list === undefined) return null return list[0] } // https://url.spec.whatwg.org/#dom-urlsearchparams-getall getAll(name) { const list = this._params.get(name) if (list === undefined) return [] return Array.from(list) } // https://url.spec.whatwg.org/#dom-urlsearchparams-has has(name, value = null) { const list = this._params.get(name) if (list === undefined) return false if (value === null) return true return list.includes(value) } // https://url.spec.whatwg.org/#dom-urlsearchparams-set set(name, value = null) { if (value === null) this._params.delete(name) else this._params.set(name, [value]) this._update() } toString() { return this._serialize() } toJSON() { return [...this] } *[Symbol.iterator]() { for (const [name, values] of this._params) { for (const value of values) yield [name, value] } } [Symbol.for('bare.inspect')]() { const object = { __proto__: { constructor: URLSearchParams } } for (const [name, values] of this._params) { if (values.length === 1) object[name] = values[0] else object[name] = values } return object } // https://url.spec.whatwg.org/#concept-urlsearchparams-update _update() { const url = urls.get(this) if (url === undefined) return url.search = this._serialize() } // https://url.spec.whatwg.org/#concept-urlencoded-parser _parse(input) { const params = new Map() this._params = params const len = input.length let start = input.charCodeAt(0) === 0x3f /* ? */ ? 1 : 0 while (start < len) { let end = input.indexOf('&', start) if (end === -1) end = len if (end !== start) { let split = input.indexOf('=', start) if (split === -1 || split > end) split = end const name = decode(input.slice(start, split)) const value = split === end ? '' : decode(input.slice(split + 1, end)) const list = params.get(name) if (list === undefined) params.set(name, [value]) else list.push(value) } start = end + 1 } } // https://url.spec.whatwg.org/#concept-urlencoded-serializer _serialize() { let result = '' let separator = '' for (const [name, values] of this._params) { // The name is shared by all of its values, so it is only encoded once. const encoded = encode(String(name)) for (const value of values) { result += separator + encoded + '=' + encode(String(value)) separator = '&' } } return result } } module.exports = exports = URLSearchParams exports.isURLSearchParams = function isURLSearchParams(value) { if (value instanceof URLSearchParams) return true return typeof value === 'object' && value !== null && value[kind] === URLSearchParams[kind] } // https://url.spec.whatwg.org/#concept-urlencoded-byte-serializer // // Everything except ASCII alphanumerics and `*-._` is percent-encoded, and a // space becomes `+` rather than `%20`. Names and values usually consist entirely // of characters that are left alone, so a component is tested as a whole before // any of it is rewritten. const unencoded = /^[\w*.-]*$/ // As above, but allowing spaces, which need nothing more than turning into `+`. // Spaces are by far the most common reason for a component to need rewriting at // all, so recognizing them here spares those components a trip through // encodeURIComponent(). const spaced = /^[\w*. -]*$/ // The characters that encodeURIComponent() leaves alone but the urlencoded byte // serializer does not. Two patterns are needed because a global regular // expression cannot be shared between test() and replace(), as test() advances // its lastIndex. const extra = /[!'()~]/ const extraAll = /[!'()~]/g const escapes = { '!': '%21', "'": '%27', '(': '%28', ')': '%29', '~': '%7E' } // A surrogate pair, or a surrogate that is not part of one and so encodes no // scalar value. Pairs are matched first so that only the capture group can hold // a lone surrogate; a lookbehind would say this more directly but isn't // supported by every engine. const lone = /[\ud800-\udbff][\udc00-\udfff]|([\ud800-\udbff]|[\udc00-\udfff])/g function encode(component) { if (unencoded.test(component)) return component if (spaced.test(component)) return component.replaceAll(' ', '+') let encoded try { encoded = encodeURIComponent(component) } catch { // encodeURIComponent() rejects lone surrogates, whereas the UTF-8 encoder the // serializer is defined in terms of replaces them with U+FFFD. encoded = encodeURIComponent( component.replace(lone, (match, single) => (single === undefined ? match : '\ufffd')) ) } if (encoded.includes('%20')) encoded = encoded.replaceAll('%20', '+') if (extra.test(encoded)) encoded = encoded.replace(extraAll, (match) => escapes[match]) return encoded } // https://url.spec.whatwg.org/#percent-decode-after-encoding function decode(component) { if (component.indexOf('+') !== -1) component = component.replaceAll('+', ' ') if (component.indexOf('%') === -1) return component return decodeURIComponent(component) }