UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

210 lines 8.63 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 { createID, createLookbehind, re } from "../../util"; import { Matched } from "../matched"; import { RegExpMatcher } from "../matchers/regexp"; import { Node } from "../node"; /** * A {@link Node} with some sort of associated pattern. Patterns exist as * subclasses of this class. */ export class Rule { /** * @param repo - The {@link Repository} to add this rule to. * @param rule - The rule definition. */ constructor(repo, rule) { let type = rule.type ?? createID(); let emit = (rule.type && rule.emit !== false) || rule.autocomplete; this.name = type; this.node = !emit ? Node.None : new Node(repo.id(), rule); if (rule.captures) { this.captures = []; for (const key in rule.captures) { const value = rule.captures[key]; const idx = parseInt(key, 10); // conditional if ("matches" in value) this.captures[idx] = captureFunction(repo, value); // node or reused else this.captures[idx] = repo.add(value); } } if (rule.lookbehind) { const negative = rule.lookbehind[0] === "!"; const regexp = re(rule.lookbehind); if (!regexp) throw new Error("Invalid lookbehind regexp"); this.lookbehind = createLookbehind(regexp, negative); } if (rule.lookahead) { this.lookahead = new RegExpMatcher(rule.lookahead, repo.ignoreCase, repo.variables); } if (rule.context) { if (!Array.isArray(rule.context)) { this.contextSetters = [contextSetter(rule.context)]; } else { this.contextSetters = rule.context.map(contextSetter); } } if (rule.contextImmediate) this.contextImmediate = true; if (rule.rematch) this.rematch = true; } /** * @param state - The current {@link GrammarState}. * @param str - The string to match. * @param pos - The position to start matching at. */ match(state, str, pos) { if (this.lookbehind && !this.lookbehind(str, pos)) return null; if (this.contextSetters && this.contextImmediate) { for (let i = 0; i < this.contextSetters.length; i++) { this.contextSetters[i](state); } } let matched = this.exec(str, pos, state); if (matched) { if (this.lookahead && !this.lookahead.test(str, pos + matched.length)) { return null; } // if rematch, we just return basically "success!", // and let the tokenizer move on if (this.rematch) return new Matched(state, this.node, "", pos); // returned a MatchOutput, which we need to turn into a Matched if (!(matched instanceof Matched)) { const output = matched; matched = new Matched(state, this.node, matched.total, pos); state.last = output; if (this.captures) { if (!output.captures) throw new Error("Output has no captures when it should"); const captures = []; let capturePos = pos; for (let i = 0; i < this.captures.length; i++) { const val = this.captures[i]; const capture = output.captures[i]; let node = Node.None; if (val) { const resolved = typeof val === "function" ? val(state, capture) : val; if (resolved === false) return null; if (resolved === true) node = Node.None; else node = resolved; } captures.push(new Matched(state, node, capture, capturePos)); capturePos += capture.length; } matched.captures = captures; } } // returned a full Matched, which we may need to wrap else { state.last = matched.output(); if (this.captures) { for (let i = 0; i < this.captures.length; i++) { const val = this.captures[i]; const capture = matched?.captures?.[i]; if (!capture) throw new Error("Missing capture"); let node = null; if (val) { const resolved = typeof val === "function" ? val(state, capture.total) : val; if (resolved === false) return null; if (resolved === true) continue; else node = resolved; } if (node) matched.captures[i] = capture.wrap(node); } } } if (this.contextSetters && !this.contextImmediate) { for (let i = 0; i < this.contextSetters.length; i++) { this.contextSetters[i](state); } } } return matched; } } /** Creates a context setter from its definition. */ function contextSetter(setter) { return (state) => { // check if and match conditions // if only "if", check if that string isn't empty // if only "matches", check against the entire match // if both, check "if" against "matches" if (setter.if || setter.matches) { const against = setter.if ? state.sub(setter.if) : state.last?.total; if (typeof against !== "string") return; if (!setter.matches && !setter.if) return; if (setter.matches) { const matches = state.sub(setter.matches); if (matches instanceof RegExp) { if (!matches.test(against)) return; } else if (typeof matches === "string") { if (matches !== against) return; } else { return; } } } // if we're here, we can set context state.set(setter.set, setter.to); }; } /** Creates a `CaptureFunction` from a capture condition definition. */ function captureFunction(repo, cond) { // TODO: fix lower casing matching here const matcher = cond.matches.startsWith("/") ? re(cond.matches) : cond.matches; if (!matcher) throw new Error("Invalid match condition"); let nodeThen = null; let nodeElse = null; if (cond.then) nodeThen = repo.add(cond.then); if (cond.else) nodeElse = repo.add(cond.else); return (state, capture) => { const matches = typeof matcher === "string" ? state.sub(matcher) : matcher; if (typeof matches !== "string" && !(matches instanceof RegExp)) { throw new Error("Invalid match condition"); } const against = cond.if ? state.sub(cond.if) : capture; if (typeof against !== "string") throw new Error("Invalid capture condition"); let passed = false; if (matches instanceof RegExp) { if (matches.test(against)) passed = true; } else if (typeof matches === "string") { if (matches === against) passed = true; } if (passed && nodeThen) return nodeThen; if (!passed && nodeElse) return nodeElse; return passed; }; } //# sourceMappingURL=rule.js.map