cm-tarnation
Version:
An alternative parser for CodeMirror 6
112 lines (98 loc) • 3.53 kB
text/typescript
/* 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
}