UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

200 lines 7.58 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 { continuedIndent, delimitedIndent, flatIndent, foldInside, foldNodeProp, indentNodeProp } from "@codemirror/language"; import { NodeProp, NodeType } from "@lezer/common"; import { styleTags, tags } from "@lezer/highlight"; import { createID, EmbeddedParserProp, re } from "./../util"; export const NodeTypeProp = new NodeProp(); /** Effectively a light wrapper around a CodeMirror `NodeType`. */ export class Node { /** @param id - The ID to assign to this node. */ constructor(id, { type, emit, tag, openedBy, closedBy, group, nest, fold, indent, autocomplete }) { if (!type) { if (!autocomplete || typeof autocomplete === "boolean") { throw new Error("Node name/type is required"); } type = createID(autocomplete); } if (emit === false) throw new Error("Node cannot be emitted"); this.id = id; this.name = type; if (autocomplete) { if (typeof autocomplete === "boolean") autocomplete = type; this.autocomplete = autocomplete; } if (typeof emit !== "string") emit = type; const props = []; props.push(NodeTypeProp.add({ [emit]: this })); // prettier-ignore { if (tag) props.push(styleTags(parseTag(emit, tag))); if (nest) props.push(EmbeddedParserProp.add({ [emit]: nest })); if (openedBy) props.push(NodeProp.openedBy.add({ [emit]: [openedBy].flat() })); if (closedBy) props.push(NodeProp.closedBy.add({ [emit]: [closedBy].flat() })); if (group) props.push(NodeProp.group.add({ [emit]: [group].flat() })); if (fold) props.push(foldNodeProp.add({ [emit]: parseFold(fold) })); if (indent) props.push(indentNodeProp.add({ [emit]: parseIndent(indent) })); } this.type = NodeType.define({ id, name: emit, props }); } /** Special `Node` used for when a rule doesn't emit anything. */ static None = new Node(-1, { type: "_none", emit: "None" }); } /** * 1. Tag modifier text * 2. Tag function name * 3. Tag function argument * 4. Tag name, no function */ const PARSE_TAG_REGEX = /^(?:\((\S*?)\))?(?:\s+|^)(?:(?:(\S+?)\((\S+)\))|(\S+))$/; /** * Parses a tag string, and converts it into an object that can be fed into * CodeMirror's `styleTags` function. * * Examples: * * ```text * tag * func(tag) * (!) tag * (!) func(tag) * (...) tag * (...) func(tag) * (parent/) tag * (parent/) func(tag) * (grandparent/parent) tag * (grandparent/parent) func(tag) * ``` */ function parseTag(node, str) { const [, modifier, func, arg, last] = PARSE_TAG_REGEX.exec(str); if (last && !(last in tags)) throw new Error(`Unknown tag: ${last}`); if (func && !(func in tags)) throw new Error(`Unknown tag function: ${func}`); if (arg && !(arg in tags)) throw new Error(`Unknown tag argument: ${arg}`); let name = arg ? arg : last; let prefix = ""; let suffix = ""; // @ts-ignore TS doesn't realize I've checked for this let tag = tags[name]; // @ts-ignore ditto if (func) tag = tags[func](tag); if (modifier) { if (modifier.endsWith("...")) suffix = "/..."; if (modifier.endsWith("!")) suffix = "!"; if (modifier.endsWith("/")) prefix = modifier; // check for parents else { const split = modifier.split("/"); const last = split[split.length - 1]; if (last === "..." || last === "!") split.pop(); if (split.length) prefix = `${split.join("/")}/`; } } // e.g. foo/... or foo/bar/... etc. const style = `${prefix}${node}${suffix}`; return { [style]: tag }; } /** * `offset(n n)`, `offset(-2 -5)`, `offset(+1 2)`, `offset(0 0)`, etc. * * 1. Left offset * 2. Right offset */ const PARSE_OFFSET_FOLD_REGEX = /^offset\(([+-]?\d+),\s+([+-]?\d+)\)$/; /** Parses a `fold` string, returning a CodeMirror `foldNodeProp` compatible function. */ function parseFold(fold) { // prettier-ignore switch (fold) { // folds entire node case true: return node => ({ from: node.from, to: node.to }); // folds between two delimiters, which are the first and last child case "inside": return foldInside; // folds everything past the first-ish line case "past_first_line": return (node, state) => ({ from: Math.min(node.from + 20, state.doc.lineAt(node.from).to), to: node.to - 1 }); // like the "true" case, except with an offset // (or the fold string is invalid) default: { if (fold.startsWith("offset")) { const match = PARSE_OFFSET_FOLD_REGEX.exec(fold); if (!match) throw new Error("Invalid fold offset"); const left = parseInt(match[1], 10); const right = parseInt(match[2], 10); return node => ({ from: node.from + left, to: node.to + right }); } else { throw new Error(`Unknown fold option: ${fold}`); } } } } /** 1. Closing */ const PARSE_DELIMITED_INDENT_REGEX = /^delimited\((.+?)\)$/; /** 1. Except Regex */ const PARSE_CONTINUED_INDENT_REGEX = /^continued(?:\((.+?)\))?$/; /** 1. Units */ const PARSE_ADD_INDENT_REGEX = /^add\(([+-]?\d+)\)$/; /** 1. Units */ const PARSE_SET_INDENT_REGEX = /^set\(([+-]?\d+)\)$/; /** Parses an indent string, returning a `indentNodeProp` compatible function. */ function parseIndent(indent) { if (indent === "flat") return flatIndent; if (indent === "continued") return continuedIndent(); if (indent.startsWith("delimited")) { const match = PARSE_DELIMITED_INDENT_REGEX.exec(indent); if (!match) throw new Error("Invalid delimited indent"); const [, closing] = match; return delimitedIndent({ closing }); } if (indent.startsWith("continued")) { const match = PARSE_CONTINUED_INDENT_REGEX.exec(indent); if (!match) throw new Error("Invalid continued indent"); const except = re(match[1]); if (!except) throw new Error("Invalid continued indent except regex"); return continuedIndent({ except }); } if (indent.startsWith("add")) { const match = PARSE_ADD_INDENT_REGEX.exec(indent); if (!match) throw new Error("Invalid add indent"); const units = parseInt(match[1], 10); return cx => cx.baseIndent + cx.unit * units; } if (indent.startsWith("set")) { const match = PARSE_SET_INDENT_REGEX.exec(indent); if (!match) throw new Error("Invalid set indent"); const units = parseInt(match[1], 10); return () => units; } throw new Error(`Unknown indent option: ${indent}`); } //# sourceMappingURL=node.js.map