/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/web/01_urlpattern.js
489 строк
14 KB
Kenta Moriuchi
chore: make prefer-primordials lint an internal plugin (#35978)
23 июл 2026, 11:53
Не верифицирован
23 июл 2026, 11:53
5b55d08
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // @ts-check /// <reference path="../../core/internal.d.ts" /> /// <reference path="../../core/lib.deno_core.d.ts" /> /// <reference path="../webidl/internal.d.ts" /> /// <reference path="./internal.d.ts" /> /// <reference path="../../cli/tsc/dts/lib.deno_url.d.ts" /> (function () { const { core, primordials } = __bootstrap; const { op_urlpattern_parse, op_urlpattern_process_match_input, } = core.ops; const { ArrayPrototypePush, MathRandom, ObjectAssign, ObjectCreate, ObjectPrototypeIsPrototypeOf, RegExpPrototypeExec, RegExpPrototypeTest, SafeMap, SafeRegExp, StringPrototypeSlice, Symbol, SymbolFor, TypeError, Uint32Array, } = primordials; const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js"); // Lazy-load createFilteredInspectProxy from console let _createFilteredInspectProxy; function getCreateFilteredInspectProxy() { if (!_createFilteredInspectProxy) { _createFilteredInspectProxy = core.loadExtScript( "ext:deno_web/01_console.js", ).createFilteredInspectProxy; } return _createFilteredInspectProxy; } const _components = Symbol("components"); const urlPatternSettings = { groupStringFallback: false }; /** * @typedef Components * @property {Component} protocol * @property {Component} username * @property {Component} password * @property {Component} hostname * @property {Component} port * @property {Component} pathname * @property {Component} search * @property {Component} hash */ const COMPONENTS_KEYS = [ "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash", ]; /** * @typedef Component * @property {string} patternString * @property {RegExp} regexp * @property {string[]} groupNameList */ /** * This implements a least-recently-used cache that has a pseudo-"young * generation" by using sampling. The idea is that we want to keep the most * recently used items in the cache, but we don't want to pay the cost of * updating the cache on every access. This relies on the fact that the data * we're caching is not uniformly distributed, and that the most recently used * items are more likely to be used again soon (long tail distribution). * * The LRU cache is implemented as a Map, with the key being the cache key and * the value being the cache value. When an item is accessed, it is moved to the * end of the Map. When an item is inserted, if the Map is at capacity, the * first item in the Map is deleted. Because maps iterate using insertion order, * this means that the oldest item is always the first. * * The sampling is implemented by using a random number generator to decide * whether to update the cache on each access. This means that the cache will * not be updated on every access, but will be updated on a random subset of * accesses. * * @template K * @template V */ class SampledLRUCache { /** @type {SafeMap<K, V>} */ #map = new SafeMap(); #capacity = 0; #sampleRate = 0.1; /** @type {K} */ #lastUsedKey = undefined; /** @type {V} */ #lastUsedValue = undefined; /** @param {number} capacity */ constructor(capacity) { this.#capacity = capacity; } /** * @param {K} key * @param {(key: K) => V} factory * @return {V} */ getOrInsert(key, factory) { if (this.#lastUsedKey === key) return this.#lastUsedValue; const value = this.#map.get(key); if (value !== undefined) { if (MathRandom() < this.#sampleRate) { // put the item into the map this.#map.delete(key); this.#map.set(key, value); } this.#lastUsedKey = key; this.#lastUsedValue = value; return value; } else { // value doesn't exist yet, create const value = factory(key); if (MathRandom() < this.#sampleRate) { // if the map is at capacity, delete the oldest (first) element if (this.#map.size > this.#capacity) { // deno-lint-ignore deno-internal/prefer-primordials this.#map.delete(this.#map.keys().next().value); } // insert the new value this.#map.set(key, value); } this.#lastUsedKey = key; this.#lastUsedValue = value; return value; } } } const matchInputCache = new SampledLRUCache(4096); /** * Shared buffer for receiving URL component offsets from the * op_urlpattern_process_match_input op. * * Layout: buf[0..8] = cumulative start offsets, buf[8] = total length. * Component i's value = concat.slice(buf[i], buf[i+1]). */ const matchBuf = new Uint32Array(9); /** * Calls the op and extracts the 8 component values from the concatenated * string + offset buffer. Returns a flat 8-element string array, or null. * @param {string | object} input * @param {string | undefined} baseURL * @returns {string[] | null} */ function processMatchInput(input, baseURL) { const concat = op_urlpattern_process_match_input( input, baseURL ?? null, matchBuf, ); if (concat === null) return null; return [ StringPrototypeSlice(concat, matchBuf[0], matchBuf[1]), StringPrototypeSlice(concat, matchBuf[1], matchBuf[2]), StringPrototypeSlice(concat, matchBuf[2], matchBuf[3]), StringPrototypeSlice(concat, matchBuf[3], matchBuf[4]), StringPrototypeSlice(concat, matchBuf[4], matchBuf[5]), StringPrototypeSlice(concat, matchBuf[5], matchBuf[6]), StringPrototypeSlice(concat, matchBuf[6], matchBuf[7]), StringPrototypeSlice(concat, matchBuf[7], matchBuf[8]), ]; } /** * Cache-compatible factory: calls processMatchInput with no baseURL. * @param {string | object} input * @returns {string[] | null} */ function processMatchInputCached(input) { return processMatchInput(input, undefined); } const _hasRegExpGroups = Symbol("[[hasRegExpGroups]]"); class URLPattern { /** @type {Component[]} */ [_components]; [_hasRegExpGroups]; #reusedResult; /** * @param {URLPatternInput} input * @param {string} [baseURLOrOptions] * @param {string} [maybeOptions] */ constructor( input, baseURLOrOptions = undefined, maybeOptions = undefined, ) { this[webidl.brand] = webidl.brand; const prefix = "Failed to construct 'URLPattern'"; let baseURL; let options; if (webidl.type(baseURLOrOptions) === "String") { webidl.requiredArguments(arguments.length, 1, prefix); input = webidl.converters.URLPatternInput(input, prefix, "Argument 1"); baseURL = webidl.converters.USVString( baseURLOrOptions, prefix, "Argument 2", ); options = webidl.converters.URLPatternOptions( maybeOptions !== undefined ? maybeOptions : { __proto: null }, prefix, "Argument 3", ); } else { if (input !== undefined) { input = webidl.converters.URLPatternInput(input, prefix, "Argument 1"); } else { input = { __proto__: null }; } options = webidl.converters.URLPatternOptions( baseURLOrOptions, prefix, "Argument 2", ); } const parsed = op_urlpattern_parse(input, baseURL, options); this[_hasRegExpGroups] = parsed.hasRegexpGroups; const flags = options.ignoreCase ? "ui" : "u"; const components = [ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, ]; for (let i = 0; i < 8; ++i) { const key = COMPONENTS_KEYS[i]; const c = parsed[key]; try { c.regexp = new SafeRegExp(c.regexpString, flags); } catch (e) { throw new TypeError(`${prefix}: ${key} is invalid; ${e.message}`); } components[i] = c; } this[_components] = components; } get protocol() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][0].patternString; } get username() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][1].patternString; } get password() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][2].patternString; } get hostname() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][3].patternString; } get port() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][4].patternString; } get pathname() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][5].patternString; } get search() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][6].patternString; } get hash() { webidl.assertBranded(this, URLPatternPrototype); return this[_components][7].patternString; } get hasRegExpGroups() { webidl.assertBranded(this, URLPatternPrototype); return this[_hasRegExpGroups]; } /** * @param {URLPatternInput} input * @param {string} [baseURL] * @returns {boolean} */ test(input, baseURL = undefined) { webidl.assertBranded(this, URLPatternPrototype); const prefix = "Failed to execute 'test' on 'URLPattern'"; webidl.requiredArguments(arguments.length, 1, prefix); input = webidl.converters.URLPatternInput(input, prefix, "Argument 1"); if (baseURL !== undefined) { baseURL = webidl.converters.USVString(baseURL, prefix, "Argument 2"); } const values = baseURL === undefined ? matchInputCache.getOrInsert(input, processMatchInputCached) : processMatchInput(input, baseURL); if (values === null) return false; const components = this[_components]; for (let i = 0; i < 8; ++i) { const component = components[i]; switch (component.regexpString) { case "^$": if (values[i] !== "") return false; break; case "^(.*)$": break; default: { if (!RegExpPrototypeTest(component.regexp, values[i])) return false; } } } return true; } /** * @param {URLPatternInput} input * @param {string} [baseURL] * @returns {URLPatternResult | null} */ exec(input, baseURL = undefined) { webidl.assertBranded(this, URLPatternPrototype); const prefix = "Failed to execute 'exec' on 'URLPattern'"; webidl.requiredArguments(arguments.length, 1, prefix); input = webidl.converters.URLPatternInput(input, prefix, "Argument 1"); if (baseURL !== undefined) { baseURL = webidl.converters.USVString(baseURL, prefix, "Argument 2"); } const values = baseURL === undefined ? matchInputCache.getOrInsert(input, processMatchInputCached) : processMatchInput(input, baseURL); if (values === null) { return null; } // globalThis.allocAttempt++; this.#reusedResult ??= { inputs: [undefined] }; const result = this.#reusedResult; // We don't construct the `inputs` until after the matching is done under // the assumption that most patterns do not match. const components = this[_components]; for (let i = 0; i < 8; ++i) { const key = COMPONENTS_KEYS[i]; /** @type {Component} */ const component = components[i]; const res = result[key] ??= { input: values[i], groups: component.regexpString === "^(.*)$" ? { "0": values[i] } : {}, }; switch (component.regexpString) { case "^$": if (values[i] !== "") return null; break; case "^(.*)$": res.groups["0"] = values[i]; break; default: { const match = RegExpPrototypeExec(component.regexp, values[i]); if (match === null) return null; const groupList = component.groupNameList; const groups = res.groups; for (let j = 0; j < groupList.length; ++j) { // TODO(lucacasonato): this is vulnerable to override mistake if (urlPatternSettings.groupStringFallback) { groups[groupList[j]] = match[j + 1] ?? ""; } else { groups[groupList[j]] = match[j + 1]; } } break; } } res.input = values[i]; } // Reconstruct inputs from the original arguments (the op no longer // returns them -- they were a pass-through of the caller's arguments). result.inputs[0] = typeof input === "string" ? input : ObjectAssign(ObjectCreate(null), input); if (baseURL !== undefined) ArrayPrototypePush(result.inputs, baseURL); this.#reusedResult = undefined; return result; } [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { return inspect( getCreateFilteredInspectProxy()({ object: this, evaluate: ObjectPrototypeIsPrototypeOf(URLPatternPrototype, this), keys: [ "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash", "hasRegExpGroups", ], }), inspectOptions, ); } } webidl.configureInterface(URLPattern); const URLPatternPrototype = URLPattern.prototype; webidl.converters.URLPatternInit = webidl .createDictionaryConverter("URLPatternInit", [ { key: "protocol", converter: webidl.converters.USVString }, { key: "username", converter: webidl.converters.USVString }, { key: "password", converter: webidl.converters.USVString }, { key: "hostname", converter: webidl.converters.USVString }, { key: "port", converter: webidl.converters.USVString }, { key: "pathname", converter: webidl.converters.USVString }, { key: "search", converter: webidl.converters.USVString }, { key: "hash", converter: webidl.converters.USVString }, { key: "baseURL", converter: webidl.converters.USVString }, ]); webidl.converters["URLPatternInput"] = (V, prefix, context, opts) => { // Union for (URLPatternInit or USVString) if (typeof V == "object") { return webidl.converters.URLPatternInit(V, prefix, context, opts); } return webidl.converters.USVString(V, prefix, context, opts); }; webidl.converters.URLPatternOptions = webidl .createDictionaryConverter("URLPatternOptions", [ { key: "ignoreCase", converter: webidl.converters.boolean, defaultValue: false, }, ]); return { URLPattern, urlPatternSettings }; })();