UNPKG

@jinntec/jinn-codemirror

Version:

Source code editor component based on codemirror with language support for XML and Leiden+

236 lines (235 loc) 7.09 kB
import { basicSetup } from "codemirror"; import { EditorView, placeholder, showPanel } from "@codemirror/view"; import { ViewPlugin, keymap } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; import { EditorSelection } from "@codemirror/state"; import { indentWithTab } from "@codemirror/commands"; import { snippet } from "@codemirror/autocomplete"; import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { materialDark, materialLight } from "@uiw/codemirror-theme-material"; import { solarizedDark, solarizedLight } from "@uiw/codemirror-theme-solarized"; function theme(name) { switch (name) { case "dark": return oneDarkTheme; case "material-dark": return materialDark; case "material-light": return materialLight; case "solarized-dark": return solarizedDark; case "solarized-light": return solarizedLight; default: return null; } } var SourceType = /* @__PURE__ */ ((SourceType2) => { SourceType2["xml"] = "xml"; SourceType2["html"] = "html"; SourceType2["leiden_plus"] = "leiden_plus"; SourceType2["edcs"] = "edcs"; SourceType2["phi"] = "phi"; SourceType2["default"] = "default"; SourceType2["xquery"] = "xquery"; SourceType2["css"] = "css"; SourceType2["tex"] = "tex"; SourceType2["markdown"] = "markdown"; SourceType2["json"] = "json"; return SourceType2; })(SourceType || {}); ; class ParametrizedCommand { } const wrapCommand = (start, end) => (editor) => { editor.dispatch(editor.state.changeByRange((range) => { return { changes: [{ from: range.from, insert: start }, { from: range.to, insert: end }], range: EditorSelection.range(range.from + start.length, range.to + start.length) }; })); return true; }; const insertCommand = (insert) => (editor) => { editor.dispatch(editor.state.changeByRange((range) => { return { changes: [{ from: range.from, insert }], range: EditorSelection.range(range.from, range.from) }; })); return true; }; const snippetCommand = (template) => (editor) => { template = template.replace(/\$\|([^|]+)\|/g, "${$1}"); editor.state.selection.ranges.forEach((range) => { const content = editor.state.doc.slice(range.from, range.to); if (content.length > 0) { template = template.replace(/\${(?:\d+:)?_}/, content.toString()); } else { template = template.replace(/\${(\d+:)?_}/, "${$1}"); } const snip = snippet(template); snip(editor, { label: "" }, range.from, range.to); }); return true; }; function initCommand(cmdName, cmd, control) { if (cmd.create) { const paramsAttr = control.dataset.params; if (paramsAttr) { let params; try { params = JSON.parse(paramsAttr); } catch (e) { params = [paramsAttr]; } if (Array.isArray(params) && params.length === cmd.create.length) { return cmd.create.apply(null, params); } else { console.error("<jinn-codemirror> Expected %d arguments for command %s", cmd.create.length, cmdName); return null; } } } return cmd; } const defaultCommands = { snippet: { create: (template) => snippetCommand(template) } }; class EditorConfig { constructor(editor, toolbar = [], commands = defaultCommands) { this.threshold = 300; this._status = null; this.editor = editor; this.commands = commands; this.keymap = []; this.namespace = null; if (toolbar) { toolbar.forEach((control) => { const cmdName = control.dataset.command; const cmd = commands[cmdName]; if (cmd) { const shortcut = control.getAttribute("data-key"); if (shortcut && shortcut.length > 0) { const command = initCommand(cmdName, cmd, control); if (command) { const binding = { key: shortcut, run: command }; this.keymap.push(binding); } } } }); } } async getConfig() { const self = this; let runningUpdate = null; const updateListener = ViewPlugin.fromClass(class { update(update) { if (update.docChanged) { if (runningUpdate) { clearTimeout(runningUpdate); } runningUpdate = setTimeout(() => { const tree = syntaxTree(update.state); const lines = update.state.doc.toJSON(); const content = self.onUpdate(tree, lines.join("\n")); try { const serialized = self.serialize(); if (serialized != null) { self.editor._value = serialized; self.editor.emitUpdateEvent(content); } } catch (e) { } }, self.threshold); } } }); const createStatusPanel = (view) => { this._status = document.createElement("div"); this._status.className = "status"; this._status.part = "status"; return { dom: this._status }; }; const customExtensions = await this.getExtensions(this.editor); const extensions = [ basicSetup, EditorView.lineWrapping, keymap.of([indentWithTab, ...this.keymap]), placeholder(this.editor.placeholder), ...customExtensions, updateListener, showPanel.of(createStatusPanel) ]; if (this.editor && this.editor.theme) { const extTheme = theme(this.editor.theme); if (extTheme) { extensions.push(extTheme); } else { console.error("<jinn-codemirror> Unknown theme: %s", this.editor.theme); } } return { extensions }; } getCommands() { return this.commands; } onUpdate(tree, content) { return content; } /** * Strips default namespace declarations (xmlns="...") from serialized XML string * Only removes namespaces that match this.namespace */ stripDefaultNamespaces(xmlString) { if (!this.namespace) { return xmlString; } const escapedNamespace = this.namespace.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`\\s+xmlns\\s*=\\s*["']${escapedNamespace}["']`, "g"); return xmlString.replace(regex, ""); } setFromValue(value) { if (!value) { return ""; } if (value instanceof Node || value instanceof NodeList) { const serializer = new XMLSerializer(); if (value instanceof NodeList) { const buf = []; for (let i = 0; i < value.length; i++) { buf.push(this.stripDefaultNamespaces(serializer.serializeToString(value[i]))); } return buf.join(""); } return this.stripDefaultNamespaces(serializer.serializeToString(value)); } if (typeof value === "string") { return value; } return JSON.stringify(value); } set status(msg) { if (this._status) { this._status.innerHTML = msg; } } } export { EditorConfig, ParametrizedCommand, SourceType, defaultCommands, initCommand, insertCommand, snippetCommand, wrapCommand };