cm-tarnation
Version:
An alternative parser for CodeMirror 6
210 lines • 8.63 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 { 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