cm-tarnation
Version:
An alternative parser for CodeMirror 6
349 lines • 12.2 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/. */
import { Matched } from "../matched";
import { RegExpMatcher } from "../matchers/regexp";
import { Rule } from "./rule";
/** A {@link Rule} subclass that uses *other* {@link Rule}s to chain together matches. */
export class Chain extends Rule {
/**
* @param repo - The {@link Repository} to add this rule to.
* @param rule - The rule definition.
*/
constructor(repo, rule) {
super(repo, rule);
this.chain = rule.chain.map(item => parseChainRule(repo, item));
if (rule.skip) {
this.skip = new RegExpMatcher(rule.skip, repo.ignoreCase, repo.variables);
}
this.context = new ChainContext(this.chain, this.skip);
}
/**
* @param state - The current {@link GrammarState}.
* @param str - The string to match.
* @param pos - The position to start matching at.
*/
exec(str, pos, state) {
this.context.reset(state, str, pos);
while (!this.context.done)
step(this.context);
const finished = this.context.finish();
if (!finished)
return null;
return new Matched(state, this.node, this.context.total, pos, finished);
}
}
/**
* Step function for a running chain match. A step may or may not advance
* the chain - this is simply repeated as many times as needed.
*/
function step(ctx) {
ctx.skip();
step: switch (ctx.current[1]) {
case Quantifier.ONE: {
const result = ctx.current[0].match(ctx.state, ctx.str, ctx.pos);
if (result)
ctx.advance(result);
else
ctx.fail();
break;
}
case Quantifier.OPTIONAL: {
const result = ctx.current[0].match(ctx.state, ctx.str, ctx.pos);
if (result)
ctx.add(result);
ctx.advance();
break;
}
case Quantifier.ZERO_OR_MORE: {
take(ctx, ctx.current[0]);
ctx.advance();
break;
}
case Quantifier.ONE_OR_MORE: {
const advanced = take(ctx, ctx.current[0]);
if (advanced)
ctx.advance();
else
ctx.fail();
break;
}
case Quantifier.ALTERNATIVES: {
const rules = ctx.current[0];
let advanced = false;
let couldFail = false;
for (let i = 0; i < rules.length; i++) {
const type = rules[i][1];
if (type === Quantifier.ONE || type === Quantifier.ONE_OR_MORE) {
couldFail = true;
}
switch (type) {
case Quantifier.ONE:
case Quantifier.OPTIONAL: {
const result = rules[i][0].match(ctx.state, ctx.str, ctx.pos);
if (result) {
ctx.add(result);
advanced = true;
}
break;
}
case Quantifier.ZERO_OR_MORE:
case Quantifier.ONE_OR_MORE: {
advanced = take(ctx, rules[i][0]);
break;
}
}
// leave for loop if we advanced
if (advanced)
break;
}
if (!advanced && couldFail)
ctx.fail();
else
ctx.advance();
break;
}
case Quantifier.REPEATING_ZERO_OR_MORE: {
if (ctx.advanced && ctx.nextMatches()) {
ctx.advance();
break;
}
if (ctx.advanced === null)
ctx.advanced = false;
const rules = ctx.current[0];
for (let i = 0; i < rules.length; i++) {
const result = rules[i].match(ctx.state, ctx.str, ctx.pos);
if (result) {
ctx.add(result);
ctx.advanced = true;
break step;
}
}
ctx.advance();
ctx.advanced = null;
break;
}
case Quantifier.REPEATING_ONE_OR_MORE: {
if (ctx.advanced && ctx.nextMatches()) {
ctx.advance();
break;
}
if (ctx.advanced === null)
ctx.advanced = false;
const rules = ctx.current[0];
for (let i = 0; i < rules.length; i++) {
const result = rules[i].match(ctx.state, ctx.str, ctx.pos);
if (result) {
ctx.add(result);
ctx.advanced = true;
break step;
}
}
if (!ctx.advanced)
ctx.fail();
else
ctx.advance();
ctx.advanced = null;
break;
}
}
}
/** Utility function for running a rule as many times as possible. */
function take(ctx, rule) {
let advanced = false;
let result;
while ((result = rule.match(ctx.state, ctx.str, ctx.pos))) {
ctx.add(result);
ctx.skip();
advanced = true;
}
return advanced;
}
/** Types of quantifier for a chain rule. */
var Quantifier;
(function (Quantifier) {
/** No suffix. */
Quantifier[Quantifier["ONE"] = 0] = "ONE";
/** `?` suffix. */
Quantifier[Quantifier["OPTIONAL"] = 1] = "OPTIONAL";
/** `*` suffix. */
Quantifier[Quantifier["ZERO_OR_MORE"] = 2] = "ZERO_OR_MORE";
/** `+` suffix. */
Quantifier[Quantifier["ONE_OR_MORE"] = 3] = "ONE_OR_MORE";
/** Chain rule strings separated by `|` pipes. */
Quantifier[Quantifier["ALTERNATIVES"] = 4] = "ALTERNATIVES";
/** Rule names separated by `|*` pipes. */
Quantifier[Quantifier["REPEATING_ZERO_OR_MORE"] = 5] = "REPEATING_ZERO_OR_MORE";
/** Rule names separated by `|+` pipes. */
Quantifier[Quantifier["REPEATING_ONE_OR_MORE"] = 6] = "REPEATING_ONE_OR_MORE";
})(Quantifier || (Quantifier = {}));
/** Class used for tracking the state of a in progress chain match. */
class ChainContext {
constructor(rules, skip) {
this.rules = rules;
this.total = "";
this.index = 0;
this.failed = false;
this.advanced = null;
this.results = null;
if (skip)
this.skipMatcher = skip;
}
/** True if the running match has finished. */
get done() {
return this.index >= this.rules.length;
}
/** Gets the current rule, based on the current index. */
get current() {
if (this.done)
throw new Error("Cannot get current rule when done");
return this.rules[this.index];
}
/** Adds a {@link Matched} to the result list. */
add(result) {
if (!this.results)
this.results = [];
this.results.push(result);
this.total += result.total;
this.pos += result.length;
}
/** Sets the match to have failed. */
fail() {
this.failed = true;
this.index = this.rules.length;
}
/**
* Advances to the next rule.
*
* @param result - A result to add to the results list, if desired.
*/
advance(result) {
if (result)
this.add(result);
this.index++;
}
/**
* Returns `null` if the match failed, otherwise a list of {@link Matched}
* objects will be returned.
*/
finish() {
return this.failed ? null : this.results;
}
/**
* Checks to see if the next rule would match. Used for leaving
* `REPEATING` quantifier rules early.
*/
nextMatches() {
if (this.done)
throw new Error("Cannot get next rule when done");
const rule = this.rules[this.index + 1];
const state = this.state.clone();
switch (rule[1]) {
case Quantifier.ONE:
case Quantifier.OPTIONAL:
case Quantifier.ZERO_OR_MORE:
case Quantifier.ONE_OR_MORE: {
const result = rule[0].match(state, this.str, this.pos);
if (result)
return true;
break;
}
case Quantifier.ALTERNATIVES: {
for (let i = 0; i < rule[0].length; i++) {
for (let j = 0; j < rule[0][i].length; j++) {
const result = rule[0][i][0].match(state, this.str, this.pos);
if (result)
return true;
}
}
break;
}
case Quantifier.REPEATING_ZERO_OR_MORE:
case Quantifier.REPEATING_ONE_OR_MORE: {
for (let i = 0; i < rule[0].length; i++) {
const result = rule[0][i].match(state, this.str, this.pos);
if (result)
return true;
}
}
}
return false;
}
/** Greedy consumes any characters matched by the `skip` pattern. */
skip() {
if (!this.skipMatcher)
return;
let result;
while ((result = this.skipMatcher.match(this.str, this.pos))) {
this.pos += result.length;
this.total += result.total;
}
}
/** Resets the current state with the new match arguments. */
reset(state, str, pos) {
this.state = state;
this.str = str;
this.pos = pos;
this.total = "";
this.index = 0;
this.failed = false;
this.advanced = null;
this.results = null;
}
}
/**
* Parses a chain rule string, and returns the rule(s) it specifies and
* what type of quantifier it uses.
*/
function parseChainRule(repo, str) {
const repeatAlternatives = /\|[*+]/.test(str);
const normalAlternatives = /\|(?![*+])/.test(str);
if (repeatAlternatives && normalAlternatives) {
throw new Error("Cannot mix |* (or |+) and |");
}
if (!repeatAlternatives && !normalAlternatives) {
let type = Quantifier.ONE;
// prettier-ignore
switch (str[str.length - 1]) {
case "?":
type = Quantifier.OPTIONAL;
break;
case "*":
type = Quantifier.ZERO_OR_MORE;
break;
case "+":
type = Quantifier.ONE_OR_MORE;
break;
}
if (type !== Quantifier.ONE) {
str = str.slice(0, str.length - 1);
}
const rule = repo.get(str);
if (!(rule instanceof Rule))
throw new Error(`Rule "${str}" not found`);
return [rule, type];
}
// normal alternatives
else if (normalAlternatives) {
const rules = str.split(/\s*\|\s*/).map(item => parseChainRule(repo, item));
return [rules, Quantifier.ALTERNATIVES];
}
// repeating alternatives
else if (repeatAlternatives) {
const zeroOrMore = /\|\*/.test(str);
const oneOrMore = /\|\+/.test(str);
if (zeroOrMore && oneOrMore) {
throw new Error("Cannot have repeating alternatives with both * and +");
}
const rules = str.split(/\s*\|[*+]?\s*/).map(item => repo.get(item));
if (rules.some(rule => !(rule instanceof Rule))) {
throw new Error(`Rule "${str}" not found`);
}
return [
rules,
zeroOrMore ? Quantifier.REPEATING_ZERO_OR_MORE : Quantifier.REPEATING_ONE_OR_MORE
];
}
throw new Error("Unreachable");
}
//# sourceMappingURL=chain.js.map