OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / node_modules / bare-url / lib / url-search-params.js

277 lines · javascript ref:
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)
}