import { readParam, writeParam } from "./param.js"
import { setURLPathname } from "./url.js"
export let setGeoParamsURL
export let setGeoParamsBeforeSetHook
/**
* Geo URI Parameters as defined in RFC 5870
*
* Intended to be similar to {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams|URLSearchParams},
* with an {@link https://url.spec.whatwg.org/#concept-urlsearchparams-url-object|associated URL object}.
*
* The following methods of `URLSearchParams` are not implemented in `GeoParams` because they only make sense for repeated parameters:
* - `append`
* - `getAll`
*
* `crs` and `u` parameters can't be repeated according to the {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.3|URI Scheme Syntax}.
* It's not so clear about other parameters
* because RFC 5870 talks about sets of parameters and parameter names in the {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.4.4|URI Comparison section}.
* `GeoParams` assumes that parameters can't be repeated.
* Practically it doesn't matter much because the only commonly used parameter should be `u`.
*/
export class GeoParams {
#p
#url
#beforeSetHook
// see https://github.com/nodejs/node/blob/0c6e16bc849450a450a9d2dbfbf6244c04f90642/lib/internal/url.js#L319 for a similar approach
static {
setGeoParamsURL = (obj, url) => {
obj.#url = url
}
setGeoParamsBeforeSetHook = (obj, beforeSetHook) => {
obj.#beforeSetHook = beforeSetHook
}
}
/**
* Create a new GeoParams object
*
* `options` is one of:
* - undefined to construct empty geo parameters
* - string to construct geo parameters according to geo URI parameters syntax,
* which is a `p` rule in {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.3|URI Scheme Syntax}
* except without the leading `;` separator
* - array of name-value string pairs
* - record of string keys and string values
* @param {string|string[][]|Object.<string, string>} [options]
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams|MDN} for the similar URLSearchParams constructor
*/
constructor(options) {
if (options == null) {
this.#p = ""
} else if (typeof options == "string") {
this.#p = options
} else {
const iterable = options?.[Symbol.iterator] ? options : Object.entries(options)
const kvs = []
for (const kv of iterable) {
if (kv.length != 2) {
throw new TypeError(`GeoParams constructor: Expected 2 items in pair but got ${kv.length}`)
}
this.#setKvs(kvs, ...kv)
}
this.#writeCoordsAndKvs(null, kvs)
}
}
/**
* Total number of parameter entries
* @type {number}
* @readonly
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/size|MDN} for the similar property of URLSearchParams
*/
get size() {
const [, kvs] = this.#readCoordsAndKvs()
return kvs.length
}
/**
* Get the value associated to the given parameter
* @param {string} name
* @returns {string|null} - parameter value or null for a missing parameter
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/get|MDN} for the similar method of URLSearchParams
*/
get(name) {
const [, kvs] = this.#readCoordsAndKvs()
for (const [k, v] of kvs) {
if (k.toLowerCase() == name.toLowerCase()) {
return v
}
}
return null
}
/**
* Set the value associated with a given parameter to the given value
* @param {string} name
* @param {string} value
* @returns {void}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/set|MDN} for the similar method of URLSearchParams
*/
set(name, value) {
if (this.#beforeSetHook) {
this.#beforeSetHook(name, value)
}
const [coords, kvs] = this.#readCoordsAndKvs()
this.#setKvs(kvs, name, value)
this.#writeCoordsAndKvs(coords, kvs)
}
/**
* Delete the specified parameter
*
* Delete a parameter with the given name.
* If `value` is specified, delete only if the parameter has this value.
* @param {string} name
* @param {string} [value] - optional value to check
* @returns {void}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/delete|MDN} for the similar method of URLSearchParams
*/
delete(name, value) {
const [coords, kvs] = this.#readCoordsAndKvs()
const lcName = name.toLowerCase()
for (const [i, [k, v]] of kvs.entries()) {
const lcK = k.toLowerCase()
if (lcK != lcName) continue
if (value != null) {
if (v != value) continue
}
kvs.splice(i, 1)
break
}
this.#writeCoordsAndKvs(coords, kvs)
}
/**
* Indicate whether the specified parameter is present
*
* Check if a parameter with the given name is present.
* If `value` is specified, also check if the parameter has this value.
* @param {string} name
* @param {string} [value] - optional value to check
* @returns {boolean}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/has|MDN} for the similar method of URLSearchParams
*/
has(name, value) {
const [, kvs] = this.#readCoordsAndKvs()
for (const [k, v] of kvs) {
if (k.toLowerCase() == name.toLowerCase()) {
if (value != null) {
return (v ?? "") == value
} else {
return true
}
}
}
return false
}
/**
* Get an iterable for all name-value pairs of parameters
* @returns {Iterator.<string[]>}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#urlsearchparamssymbol.iterator|MDN} for the similar method of URLSearchParams
*/
[Symbol.iterator]() {
return this.entries()
}
/**
* Get an iterable for all name-value pairs of parameters
*
* Same as iterating through a geo params object directly.
* @returns {Iterable.<string[]>}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|MDN} for the similar method of URLSearchParams
*/
*entries() {
const [, kvs] = this.#readCoordsAndKvs()
for (const kv of kvs) {
yield kv
}
}
/**
* Get an iterable for all names of parameters
* @returns {Iterable.<string>}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/keys|MDN} for the similar method of URLSearchParams
*/
*keys() {
const [, kvs] = this.#readCoordsAndKvs()
for (const [k] of kvs) {
yield k
}
}
/**
* Get an iterable for all values of parameters
* @returns {Iterable.<string>}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/values|MDN} for the similar method of URLSearchParams
*/
*values() {
const [, kvs] = this.#readCoordsAndKvs()
for (const [, v] of kvs) {
yield v
}
}
/**
* Convert the parameters to a string
*
* The string is a semicolon-separated list of geo parameters.
* This is the part of RFC 5870 geo URI that comes after the coordinates.
* The `;` that separates the coordinates and the parameters is not included in the string.
*
* Each geo parameter is represented by:
* - `name=value`
* - `name` without both `value` and `=` if the value is an empty string
* (a "flag" type parameter discussed in the parameter registry section of {@link https://datatracker.ietf.org/doc/html/rfc5870#section-4|RFC 5870})
* @returns {string}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString|MDN} for the similar method of URLSearchParams
*/
toString() {
if (this.#url) {
[, paramsString] = this.#url.pathname.split(/;(.*)/)
return paramsString
} else {
return this.#p
}
}
/**
* Callback function type to be used with {@link GeoParams#forEach}
* @callback forEachCallback
* @param {string} value - parameter value
* @param {string} name - parameter name
* @param {GeoParams} geoParams - the `GeoParams` object the `forEach()` was called upon
*/
/**
* Iterate through all parameters via a callback function
* @param {forEachCallback} callback - function to execute on each element
* @param {*} [thisArg] - value to use as `this` when executing `callback`
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/forEach|MDN} for the similar method of URLSearchParams
*/
forEach(callback, thisArg) {
for (const [k, v] of this) {
callback.call(thisArg, v, k, this)
}
}
#readCoordsAndKvs() {
if (this.#url) {
const [coords, ...params] = this.#url.pathname.split(";")
return [coords, params.map(readParam)]
} else if (this.#p == "") {
return [null, []]
} else {
const params = this.#p.split(";")
return [null, params.map(readParam)]
}
}
#setKvs(kvs, name, value) {
const lcName = name.toLowerCase()
let hasCrs = false
for (const [i, [k]] of kvs.entries()) {
const lcK = k.toLowerCase()
if (i == 0) {
hasCrs = lcK == "crs"
}
if (lcK == lcName) {
kvs[i] = [name, value]
return
}
}
if (lcName == "u" && hasCrs) {
kvs.splice(1, 0, [name, value])
} else if (lcName == "crs" || lcName == "u") {
kvs.unshift([name, value])
} else {
kvs.push([name, value])
}
}
#writeCoordsAndKvs(coords, kvs) {
const params = kvs.map(([k, v]) => writeParam(k, v))
if (this.#url) {
setURLPathname(this.#url, [coords, ...params].join(";"))
} else {
this.#p = params.join(";")
}
}
}