UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

112 lines (98 loc) 3.53 kB
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import type { Matcher, VariableTable } from "../../types" import { pointsMatch, toPoints } from "../../util" /** * Matcher that takes in a list of strings, and matches each one against an * input to see if any match. */ export class LookupMatcher implements Matcher { /** True if matching is case-insensitive. */ private declare ignoreCase: boolean /** A list of number arrays, each one being a codepoint representation of a word. */ private declare entries: number[][] /** A mapping between the `entries` list and lengths of their source word. */ private declare lengths: Map<number[], number> /** The longest entry in `entries`, by its original string length. */ private declare max: number /** * @param src - The source list of strings. * @param ignoreCase - If `true`, matches will be case-insensitive. * @param variables - A variable table to use when expanding variables. */ constructor(src: string[], ignoreCase?: boolean, variables?: VariableTable) { // longest first const sorted = [...src].sort((a, b) => b.length - a.length) this.max = sorted[0].length this.ignoreCase = Boolean(ignoreCase) this.lengths = new Map() this.entries = sorted.map(entry => { if (variables) entry = expandVariables(entry, variables) if (ignoreCase) entry = entry.toLowerCase() const points = toPoints(entry) this.lengths.set(points, entry.length) return points }) } /** * Internal method which returns the length of a match against a string, * if one was found. * * @param str - The string to match. * @param pos - The position to start matching at. */ private exec(str: string, pos: number) { const slice = str.slice(pos, pos + this.max) const against = toPoints(this.ignoreCase ? slice.toLowerCase() : slice) for (let i = 0; i < this.entries.length; i++) { const points = this.entries[i] if (pointsMatch(points, against, 0)) return this.lengths.get(points)! } return null } /** * Tests to see if a string is in this list. * * @param str - The string to match. * @param pos - The position to start matching at. */ test(str: string, pos: number) { return this.exec(str, pos) !== null } /** * Returns the results of attempting to match a string against this list. * * @param str - The string to match. * @param pos - The position to start matching at. */ match(str: string, pos: number) { const result = this.exec(str, pos) if (!result) return null return { total: str.slice(pos, pos + result), captures: null, length: result } } } /** * Expands the variables in a pattern source using a variable table. * * @param src - The source to expand. * @param variables - The variable table to use. */ function expandVariables(src: string, variables: VariableTable) { let depth = 0 while (/@\w/.test(src) && depth < 5) { depth++ src = src.replace(/@(\w+)/g, (_, ident: string) => { const value = variables[ident] if (typeof value !== "string") { throw new Error(`Variable ${ident} is not a string`) } return value }) } return src }