char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport URLPattern extension to typescript5553f00

main
6.3 KiB198 linesraw
1import { registerUncloneable } from "ext:web/serialization.js";
2import {
3  ObjectCreate,
4  ObjectDefineProperty,
5  RegExp,
6  RegExpPrototypeExec,
7  TypeError,
8  WeakMap,
9  WeakMapPrototypeGet,
10  WeakMapPrototypeSet,
11} from "ext:core/primordials";
12import { isObject, usv } from "ext:web/idl.js";
13import { installInterface } from "ext:web/interface.js";
14import { op_urlpattern_match_input, op_urlpattern_parse } from "ext:core/ops";
15import type { NativeComponent, PatternComponent, PatternInit, PatternInput } from "ext:core/ops";
16
17interface Component {
18  pattern: string;
19  regexp: RegExp;
20  names: string[];
21}
22interface PatternState {
23  components: Component[];
24  hasRegExpGroups: boolean;
25}
26type PatternResult = Record<PatternComponent, {
27  groups: Record<string, string | undefined>;
28  input: string;
29}> & {
30  inputs: [PatternInput] | [PatternInput, string];
31};
32
33const patterns = new WeakMap<object, PatternState>();
34registerUncloneable(patterns);
35// Order matches the native parser's component array; dictionaries convert in lexicographic order.
36const components: PatternComponent[] = [
37  "protocol",
38  "username",
39  "password",
40  "hostname",
41  "port",
42  "pathname",
43  "search",
44  "hash",
45];
46const members: (PatternComponent | "baseURL")[] = [
47  "baseURL",
48  "hash",
49  "hostname",
50  "password",
51  "pathname",
52  "port",
53  "protocol",
54  "search",
55  "username",
56];
57
58function convertInput(value: unknown): PatternInput {
59  if (value !== null && value !== undefined && !isObject(value)) {
60    return usv(value);
61  }
62  const input: PatternInit = ObjectCreate(null);
63  if (value !== null && value !== undefined) {
64    for (let index = 0; index < members.length; index++) {
65      const name = members[index];
66      const member = value[name];
67      if (member !== undefined) input[name] = usv(member);
68    }
69  }
70  return input;
71}
72function state(receiver: unknown): PatternState {
73  const value = WeakMapPrototypeGet(patterns, receiver);
74  if (!value) throw new TypeError("Illegal invocation");
75  return value;
76}
77function define<T extends object, K extends keyof T>(object: T, name: K, value: NoInfer<T[K]>): void {
78  ObjectDefineProperty(object, name, {
79    value,
80    writable: true,
81    enumerable: true,
82    configurable: true,
83  });
84}
85function match(receiver: unknown, input: unknown, base: unknown, exec: false): boolean;
86function match(receiver: unknown, input: unknown, base: unknown, exec: true): PatternResult | null;
87function match(receiver: unknown, rawInput: unknown, rawBase: unknown, exec: boolean): PatternResult | null | boolean {
88  const data = state(receiver);
89  let input = convertInput(rawInput);
90  const base = rawBase === undefined ? undefined : usv(rawBase);
91  const parsed = op_urlpattern_match_input([input, base ?? null]);
92  if (parsed[1] !== null) throw new TypeError(parsed[1]);
93  const values = parsed[0];
94  if (values === null) return exec ? null : false;
95  const result: Partial<PatternResult> | null = exec
96    ? {
97      hash: undefined,
98      hostname: undefined,
99      inputs: undefined,
100      password: undefined,
101      pathname: undefined,
102      port: undefined,
103      protocol: undefined,
104      search: undefined,
105      username: undefined,
106    }
107    : null;
108  for (let index = 0; index < components.length; index++) {
109    const name = components[index];
110    const component = data.components[index];
111    const captures = RegExpPrototypeExec(component.regexp, values[name]);
112    if (captures === null) return exec ? null : false;
113    if (exec) {
114      const groups: Record<string, string | undefined> = {};
115      for (let group = 0; group < component.names.length; group++) {
116        define(groups, component.names[group], captures[group + 1]);
117      }
118      define(result!, name, { groups, input: values[name] });
119    }
120  }
121  if (!exec) return true;
122  if (typeof input !== "string") {
123    const dictionary: PatternInit = {};
124    for (let index = 0; index < members.length; index++) {
125      const name = members[index];
126      if (input[name] !== undefined) define(dictionary, name, input[name]);
127    }
128    input = dictionary;
129  }
130  result!.inputs = base === undefined ? [input] : [input, base];
131  // Every component was defined above; partial results never escape.
132  return result as PatternResult;
133}
134
135interface URLPattern extends Readonly<Record<PatternComponent, string>> {}
136
137class URLPattern {
138  constructor(
139    rawInput: unknown = undefined,
140    baseOrOptions: unknown = undefined,
141    options: unknown = undefined,
142  ) {
143    const withBase = arguments.length >= 3 ||
144      (baseOrOptions !== null && baseOrOptions !== undefined &&
145        !isObject(baseOrOptions));
146    const input = convertInput(rawInput);
147    const base = withBase ? usv(baseOrOptions) : null;
148    options = withBase ? options : baseOrOptions;
149    if (options !== null && options !== undefined && !isObject(options)) {
150      throw new TypeError("Invalid URLPattern options");
151    }
152    const ignoreCase = !!options?.ignoreCase;
153    const parsed = op_urlpattern_parse([input, base, ignoreCase]);
154    if (parsed[1] !== null) throw new TypeError(parsed[1]);
155    // Reuse the native array: new indexed writes could hit inherited setters.
156    const compiled: (NativeComponent | Component)[] = parsed[0][0];
157    for (let index = 0; index < compiled.length; index++) {
158      const component = compiled[index] as NativeComponent;
159      compiled[index] = {
160        pattern: component[0],
161        regexp: new RegExp(component[1], component[3]),
162        names: component[2],
163      };
164    }
165    WeakMapPrototypeSet(patterns, this, {
166      components: compiled as Component[],
167      hasRegExpGroups: parsed[0][1],
168    });
169  }
170  test(input: unknown = undefined, base: unknown = undefined): boolean {
171    return match(this, input, base, false);
172  }
173  exec(input: unknown = undefined, base: unknown = undefined): PatternResult | null {
174    return match(this, input, base, true);
175  }
176  get hasRegExpGroups(): boolean {
177    return state(this).hasRegExpGroups;
178  }
179}
180for (let index = 0; index < components.length; index++) {
181  const name = components[index];
182  const getter = {
183    get(this: unknown): string {
184      return state(this).components[index].pattern;
185    },
186  }.get;
187  ObjectDefineProperty(getter, "name", {
188    value: `get ${name}`,
189    configurable: true,
190  });
191  ObjectDefineProperty(URLPattern.prototype, name, {
192    get: getter,
193    enumerable: true,
194    configurable: true,
195  });
196}
197installInterface("URLPattern", URLPattern);
198export { URLPattern };