UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

182 lines 5.81 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/. */ /** Internal state for a {@link Grammar}. */ export class GrammarState { variables; context; stack; last; /** * @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(variables, context = {}, stack = new GrammarStack(), last) { this.variables = variables; this.context = context; this.stack = stack; this.last = last; } /** * 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, value) { 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) { return this.context[key] ?? null; } /** * Expands any substitutions found in the given string. * * @param str - The string to expand. */ sub(str) { 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) { 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 { /** @param stack - The stack array to use. Will be shallow cloned. */ constructor(stack) { 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, rules, end) { 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) { 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) { 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, b) { // 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, b) { 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; } //# sourceMappingURL=state.js.map