UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

349 lines 12.2 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/. */ 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