X-Git-Url: https://git.ayoreis.com/relax.git/blobdiff_plain/7281d7636cdd2dd9740915b0451afe7120925ec4..83524f5609ef938630538f11371a2d0a4b88035f:/relax/url-pattern-pattern-string-parser.ts?ds=inline diff --git a/relax/url-pattern-pattern-string-parser.ts b/relax/url-pattern-pattern-string-parser.ts index aced26d..662625c 100644 --- a/relax/url-pattern-pattern-string-parser.ts +++ b/relax/url-pattern-pattern-string-parser.ts @@ -17,7 +17,7 @@ const ASCII_CODE_POINT = /^\p{ASCII}$/u; /** https://urlpattern.spec.whatwg.org/#token */ class Token { /** https://urlpattern.spec.whatwg.org/#token-type */ - type: Type = "invalid-char"; + type: TokenType = "invalid-char"; /** https://urlpattern.spec.whatwg.org/#token-index */ index = 0; /** https://urlpattern.spec.whatwg.org/#token-value */ @@ -25,7 +25,7 @@ class Token { } /** https://urlpattern.spec.whatwg.org/#token-type */ -type Type = +type TokenType = | "open" | "close" | "regexp" @@ -43,7 +43,7 @@ type TokenizePolicy = "strict" | "lenient"; /** https://urlpattern.spec.whatwg.org/#tokenizer */ class Tokenizer { /** https://urlpattern.spec.whatwg.org/#tokenizer-input */ - input!: string[]; + input: string[] = []; /** https://urlpattern.spec.whatwg.org/#tokenizer-policy */ policy: TokenizePolicy = "strict"; /** https://urlpattern.spec.whatwg.org/#tokenizer-token-list */ @@ -69,7 +69,7 @@ class Tokenizer { /** https://urlpattern.spec.whatwg.org/#add-a-token */ add_a_token( - type: Type, + type: TokenType, next_position: number, value_position: number, value_length: number, @@ -87,7 +87,7 @@ class Tokenizer { /** https://urlpattern.spec.whatwg.org/#add-a-token-with-default-length */ add_a_token_with_default_length( - type: Type, + type: TokenType, next_position: number, value_position: number, ) { @@ -96,7 +96,7 @@ class Tokenizer { } /** https://urlpattern.spec.whatwg.org/#add-a-token-with-default-position-and-length */ - add_a_token_with_default_position_and_length(type: Type) { + add_a_token_with_default_position_and_length(type: TokenType) { this.add_a_token_with_default_length(type, this.next_index, this.index); } @@ -301,3 +301,315 @@ function is_a_valid_name_code_point(code_point: string, first: boolean) { if (first) return IDENTIFIER_START.test(code_point); return IDENTIFIER_PART.test(code_point); } + +/** https://urlpattern.spec.whatwg.org/#part */ +export class Part { + /** https://urlpattern.spec.whatwg.org/#part-type */ + readonly type; + /** https://urlpattern.spec.whatwg.org/#part-value */ + readonly value; + /** https://urlpattern.spec.whatwg.org/#part-modifier */ + readonly modifier; + /** https://urlpattern.spec.whatwg.org/#part-name */ + readonly name; + /** https://urlpattern.spec.whatwg.org/#part-prefix */ + readonly prefix; + /** https://urlpattern.spec.whatwg.org/#part-suffix */ + readonly suffix; + + constructor( + type: PartType, + value: string, + modifier: PartModifier, + name = "", + prefix = "", + suffix = "", + ) { + this.type = type; + this.value = value; + this.modifier = modifier; + this.name = name; + this.prefix = prefix; + this.suffix = suffix; + } +} + +/** https://urlpattern.spec.whatwg.org/#part-type */ +type PartType = "fixed-text" | "regexp" | "segment-wildcard" | "full-wildcard"; +/** https://urlpattern.spec.whatwg.org/#part-modifier */ +type PartModifier = "none" | "optional" | "zero-or-more" | "one-or-more"; + +/** https://urlpattern.spec.whatwg.org/#options */ +class Options { + /** https://urlpattern.spec.whatwg.org/#options-delimiter-code-point */ + readonly delimiter_code_point; + /** https://urlpattern.spec.whatwg.org/#options-prefix-code-point */ + readonly prefix_code_point; + /** https://urlpattern.spec.whatwg.org/#options-ignore-case */ + readonly ignore_case; + + constructor( + delimiter_code_point: string, + prefix_code_point: string, + ignore_case = false, + ) { + this.delimiter_code_point = delimiter_code_point; + this.prefix_code_point = prefix_code_point; + this.ignore_case = ignore_case; + } +} + +/** https://urlpattern.spec.whatwg.org/#default-options */ +export const DEFAULT_OPTIONS = new Options("", ""); +/** https://urlpattern.spec.whatwg.org/#hostname-options */ +export const HOSTNAME_OPTIONS = new Options(".", ""); +/** https://urlpattern.spec.whatwg.org/#pathname-options */ +export const PATHNAME_OPTIONS = new Options("/", "/"); + +/** https://urlpattern.spec.whatwg.org/#encoding-callback */ +type EncodingCallback = (input: string) => string; + +/** https://urlpattern.spec.whatwg.org/#pattern-parser */ +class PatternParser { + /** https://urlpattern.spec.whatwg.org/#pattern-parser-token-list */ + token_list: Token[] = []; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-encoding-callback */ + readonly encoding_callback; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-segment-wildcard-regexp */ + readonly segment_wildcard_regexp; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-part-list */ + readonly part_list: Part[] = []; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-pending-fixed-value */ + pending_fixed_value = ""; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-index */ + index = 0; + /** https://urlpattern.spec.whatwg.org/#pattern-parser-next-numeric-name */ + next_numeric_name = 0; + + constructor( + encoding_callback: EncodingCallback, + segment_wildcard_regexp: string, + ) { + this.encoding_callback = encoding_callback; + this.segment_wildcard_regexp = segment_wildcard_regexp; + } + + /** https://urlpattern.spec.whatwg.org/#try-to-consume-a-token */ + try_to_consume_a_token(type: TokenType) { + const next_token = this.token_list[this.index]!; + if (next_token.type !== type) return null; + this.index++; + return next_token; + } + + /** https://urlpattern.spec.whatwg.org/#try-to-consume-a-modifier-token */ + try_to_consume_a_modifier_token() { + let token = this.try_to_consume_a_token("other-modifier"); + if (token !== null) return token; + token = this.try_to_consume_a_token("asterisk"); + return token; + } + + /** https://urlpattern.spec.whatwg.org/#try-to-consume-a-regexp-or-wildcard-token */ + try_to_consume_a_regexp_or_wildcard_token(name_token: Token | null) { + let token = this.try_to_consume_a_token("regexp"); + + if (name_token === null && token === null) { + token = this.try_to_consume_a_token("asterisk"); + } + + return token; + } + + /** https://urlpattern.spec.whatwg.org/#consume-a-required-token */ + consume_a_required_token(type: TokenType) { + const result = this.try_to_consume_a_token(type); + if (result === null) throw new TypeError(); + return result; + } + + /** https://urlpattern.spec.whatwg.org/#consume-text */ + consume_text() { + let result = ""; + + while (true) { + let token = this.try_to_consume_a_token("char"); + token ??= this.try_to_consume_a_token("escaped-char"); + if (token === null) break; + result += token.value; + } + + return result; + } + + /** https://urlpattern.spec.whatwg.org/#maybe-add-a-part-from-the-pending-fixed-value */ + maybe_add_a_part_from_the_pending_fixed_value() { + if (this.pending_fixed_value === "") return; + const encoded_value = this.encoding_callback(this.pending_fixed_value); + this.pending_fixed_value = ""; + const part = new Part("fixed-text", encoded_value, "none"); + this.part_list.push(part); + } + + /** https://urlpattern.spec.whatwg.org/#add-a-part */ + add_a_part( + prefix: string, + name_token: Token | null, + regexp_or_wildcard_token: Token | null, + suffix: string, + modifier_token: Token | null, + ) { + let modifier: PartModifier = "none"; + if (modifier_token?.value === "?") modifier = "optional"; + else if (modifier_token?.value === "*") modifier = "zero-or-more"; + else if (modifier_token?.value === "+") modifier = "one-or-more"; + + if ( + name_token === null && regexp_or_wildcard_token === null && + modifier === "none" + ) { + this.pending_fixed_value += prefix; + return; + } + + this.maybe_add_a_part_from_the_pending_fixed_value(); + + if (name_token === null && regexp_or_wildcard_token === null) { + if (prefix === "") return; + const encoded_value = this.encoding_callback(prefix); + const part = new Part("fixed-text", encoded_value, modifier); + this.part_list.push(part); + return; + } + + let regexp_value = ""; + + if (regexp_or_wildcard_token === null) { + regexp_value = this.segment_wildcard_regexp; + } else if (regexp_or_wildcard_token.type === "asterisk") { + regexp_value = FULL_WILDCARD_REGEXP_VALUE; + } else { + regexp_value = regexp_or_wildcard_token.value; + } + + let type: PartType = "regexp"; + + if (regexp_value === this.segment_wildcard_regexp) { + type = "segment-wildcard"; + regexp_value = ""; + } else if (regexp_value === FULL_WILDCARD_REGEXP_VALUE) { + type = "full-wildcard"; + regexp_value = ""; + } + + let name = ""; + + if (name_token !== null) { + name = name_token.value; + } else if (regexp_or_wildcard_token !== null) { + name = String(this.next_numeric_name); + this.next_numeric_name++; + } + + if (this.is_a_duplicate_name(name)) throw new TypeError(); + + const encoded_prefix = this.encoding_callback(prefix); + const encoded_suffix = this.encoding_callback(suffix); + const part = new Part( + type, + regexp_value, + modifier, + name, + encoded_prefix, + encoded_suffix, + ); + this.part_list.push(part); + } + + /** https://urlpattern.spec.whatwg.org/#is-a-duplicate-name */ + is_a_duplicate_name(name: string) { + return this.part_list.some((part) => part.name === name); + } +} + +/** https://urlpattern.spec.whatwg.org/#parse-a-pattern-string */ +export function parse_a_pattern_string( + input: string, + options: Options, + encoding_callback: EncodingCallback, +) { + const parser = new PatternParser( + encoding_callback, + generate_a_segment_wildcard_regexp(options), + ); + parser.token_list = tokenize(input, "strict"); + + while (parser.index < parser.token_list.length) { + const char_token = parser.try_to_consume_a_token("char"); + let name_token = parser.try_to_consume_a_token("name"); + let regexp_or_wildcard_token = parser + .try_to_consume_a_regexp_or_wildcard_token(name_token); + + if (name_token !== null || regexp_or_wildcard_token !== null) { + let prefix = ""; + if (char_token !== null) prefix = char_token.value; + + if (prefix !== "" && prefix !== options.prefix_code_point) { + parser.pending_fixed_value += prefix; + prefix = ""; + } + + parser.maybe_add_a_part_from_the_pending_fixed_value(); + const modifier_token = parser.try_to_consume_a_modifier_token(); + parser.add_a_part( + prefix, + name_token, + regexp_or_wildcard_token, + "", + modifier_token, + ); + continue; + } + + let fixed_token = char_token; + fixed_token ??= parser.try_to_consume_a_token("escaped-char"); + + if (fixed_token !== null) { + parser.pending_fixed_value += fixed_token.value; + continue; + } + + const open_token = parser.try_to_consume_a_token("open"); + + if (open_token !== null) { + const prefix = parser.consume_text(); + name_token = parser.try_to_consume_a_token("name"); + regexp_or_wildcard_token = parser + .try_to_consume_a_regexp_or_wildcard_token(name_token); + const suffix = parser.consume_text(); + parser.consume_a_required_token("close"); + const modifier_token = parser.try_to_consume_a_modifier_token(); + parser.add_a_part( + prefix, + name_token, + regexp_or_wildcard_token, + suffix, + modifier_token, + ); + continue; + } + + parser.maybe_add_a_part_from_the_pending_fixed_value(); + parser.consume_a_required_token("end"); + } + + return parser.part_list; +} + +/** https://urlpattern.spec.whatwg.org/#full-wildcard-regexp-value */ +const FULL_WILDCARD_REGEXP_VALUE = ".*"; + +/** https://urlpattern.spec.whatwg.org/#generate-a-segment-wildcard-regexp */ +function generate_a_segment_wildcard_regexp(options: Options) { + return `[^${RegExp.escape(options.delimiter_code_point)}]+?`; +}