cm-tarnation
Version:
An alternative parser for CodeMirror 6
182 lines • 5.81 kB
JavaScript
/* 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