cm-tarnation
Version:
An alternative parser for CodeMirror 6
192 lines (169 loc) • 5.64 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 { GrammarStackElement, MatchOutput, VariableTable } from "../types"
import type { Node } from "./node"
import type { Rule } from "./rules/rule"
import type { State } from "./rules/state"
/** Internal state for a {@link Grammar}. */
export class GrammarState {
/**
* @param variables - The variables to use when substituting.
* @param context - The current context table.
* @param stack - The current {@link GrammarStack}.
* @param last - The last {@link MatchOutput} that was matched.
*/
constructor(
public variables: VariableTable,
public context: Record<string, string> = {},
public stack: GrammarStack = new GrammarStack(),
public last?: MatchOutput
) {}
/**
* Sets a key in the context table.
*
* @param key - The key to set.
* @param value - The value to set. If `null`, the key will be removed.
*/
set(key: string, value: string | null) {
if (value === null) {
this.context = { ...this.context }
delete this.context[key]
} else {
const subbed = this.sub(value)
if (typeof subbed !== "string") throw new Error("Invalid context value")
this.context = { ...this.context, [key]: subbed }
}
}
/**
* Gets a key from the context table.
*
* @param key - The key to get.
*/
get(key: string): string | null {
return this.context[key] ?? null
}
/**
* Expands any substitutions found in the given string.
*
* @param str - The string to expand.
*/
sub(str: string) {
if (str[0] !== "$") return str
// variable substitution
if (str.startsWith("$var:")) {
const [, name] = str.split(":")
return this.variables[name]
}
// context substitution
else if (str.startsWith("$ctx:")) {
const [, name] = str.split(":")
return this.context[name]
}
// match/capture substition
else if (this.last?.captures) {
const [, index] = str.split("$")
return this.last.captures[parseInt(index, 10)]
}
throw new Error("Couldn't resolve substitute")
}
/**
* Returns if another {@link GrammarState} is effectively equivalent to this one.
*
* @param other - The other {@link GrammarState} to compare to.
*/
equals(other: GrammarState) {
if (this.variables !== other.variables) return false
if (!contextEquivalent(this.context, other.context)) return false
if (!this.stack.equals(other.stack)) return false
return true
}
/** Returns a new clone of this state, including its stack. */
clone() {
return new GrammarState(this.variables, this.context, this.stack.clone(), this.last)
}
}
/** A stack of {@link GrammarStackElement}s used by a {@link Grammar}. */
export class GrammarStack {
/** The current stack. */
declare readonly stack: GrammarStackElement[]
/** @param stack - The stack array to use. Will be shallow cloned. */
constructor(stack?: GrammarStackElement[]) {
this.stack = stack?.slice() ?? []
}
/**
* Pushes a new {@link GrammarStackElement}.
*
* @param node - The parent {@link Node}.
* @param rules - The rules to loop parsing with.
* @param end - A specific {@link Rule} that, when matched, should pop
* this element off.
*/
push(node: Node, rules: (Rule | State)[], end: Rule | State) {
this.stack.push({ node, rules, end })
}
/** Pops the last element on the stack. */
pop() {
if (this.stack.length === 0) throw new Error("Grammar stack underflow")
return this.stack.pop()
}
/**
* Remove every element at or beyond the index given.
*
* @param idx - The index to remove elements at or beyond.
*/
close(idx: number) {
this.stack.splice(idx)
}
/**
* Returns if another {@link GrammarStack} is effectively equivalent to this one.
*
* @param other - The other {@link GrammarStack} to compare to.
*/
equals(other: GrammarStack) {
if (this === other) return true
if (this.length !== other.stack.length) return false
for (let i = 0; i < this.stack.length; i++) {
if (!stackElementEquivalent(this.stack[i], other.stack[i])) return false
}
return true
}
/** The number of elements on the stack. */
get length() {
return this.stack.length
}
/** The last parent {@link Node}. */
get node() {
return this.stack[this.stack.length - 1].node
}
/** The last list of rules. */
get rules() {
return this.stack[this.stack.length - 1].rules
}
/** The last end rule. */
get end() {
return this.stack[this.stack.length - 1].end
}
/** Returns a new clone of this stack. */
clone() {
return new GrammarStack(this.stack)
}
}
function stackElementEquivalent(a: GrammarStackElement, b: GrammarStackElement) {
// do quick checks first
if (a.node !== b.node || a.end !== b.end || a.rules.length !== b.rules.length) {
return false
}
for (let i = 0; i < a.rules.length; i++) {
if (a.rules[i] !== b.rules[i]) return false
}
return true
}
function contextEquivalent(a: Record<string, string>, b: Record<string, string>) {
if (a === b) return true
if (Object.keys(a).length !== Object.keys(b).length) return false
for (const key in a) {
if (a[key] !== b[key]) return false
}
return true
}