UNPKG

@jinntec/jinn-codemirror

Version:

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

211 lines (194 loc) 7.46 kB
import { EditorSelection, Extension } from "@codemirror/state"; import { Tree, TreeCursor } from "@lezer/common"; import { EditorView, keymap, KeyBinding, Command } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; import { Diagnostic, linter, lintGutter, Action } from "@codemirror/lint"; import { snippet } from "@codemirror/autocomplete"; import { leidenPlus2epiDoc, debugLeidenTree } from "./import/leiden+2xml"; import { EditorConfig, EditorCommands, snippetCommand, wrapCommand } from "./config"; import { leiden } from "./language"; import { xml2leidenPlus } from "./import/xml2leiden+"; import { JinnCodemirror } from "./jinn-codemirror"; const fixAbbrevAction = (node:TreeCursor):Action => { return { name: "Fix", apply: (view:EditorView, from: number, to:number) => { const word = view.state.wordAt(from); if (word) { from = word.from; } const content = view.state.doc.sliceString(from, to); const tx = view.state.update({ changes: {from: from, to: to, insert: `(${content.toString()})`} }); view.dispatch(tx); } } }; const leidenParseLinter = (editor: JinnCodemirror) => (view: EditorView): Diagnostic[] => { function emitEvent(valid: boolean) { editor.valid = valid; editor.dispatchEvent(new CustomEvent(valid ? 'valid' : 'invalid', { detail: diagnostics, composed: true, bubbles: true })); } const diagnostics:Diagnostic[] = []; const tree = syntaxTree(view.state); let abbrevs = 0; let hasInnerAbbrev = false; tree.iterate({ enter: (node:TreeCursor) => { if (node.name === 'Abbrev') { abbrevs += 1; } else if (node.type.isError) { diagnostics.push({ message: 'Syntaxfehler', severity: 'error', from: node.from, to: node.to }); } }, leave: (node:TreeCursor) => { if (node.name === 'Abbrev') { if (abbrevs === 2) { hasInnerAbbrev = true; } else if (!hasInnerAbbrev) { diagnostics.push({ message: 'Invalid abbreviation. Abbreviations must\nuse double parenthesis, e.g. "(C(aesar))"', severity: 'error', from: node.from, to: node.to, actions: [ fixAbbrevAction(node) ] }); } abbrevs -= 1; if (abbrevs === 0) { hasInnerAbbrev = false; } } } }); emitEvent(diagnostics.length === 0); return diagnostics; } export const toggleUnclearCommand:Command = (editor) => { editor.dispatch(editor.state.changeByRange(range => { const content = editor.state.doc.slice(range.from, range.to).toString(); let newContent = ''; for (let i = 0; i < content.length; i++) { if (i + 1 < content.length && content.charCodeAt(i + 1) === 0x323) { newContent += content.charAt(i); i++; } else { newContent += `${content.charAt(i)}\u0323`; } } return { changes: [{from: range.from, to: range.to, insert: newContent}], range: EditorSelection.range(range.from, range.from + newContent.length) }; })); return true; } export const fixNewlinesCommand: Command = (editor) => { const content = editor.state.doc.toString(); let fixed; if (content.indexOf('/') !== -1) { const lines = content.split(/(?<!\/)\/\/?/); const split = lines.map((line, idx) => { if (/^\s+/.test(line) || idx === 0) { return `${idx + 1}. ${line.replace(/^\s+/, '')}`; } return `${idx + 1}.- ${line.replace(/^\s+/, '')}`; }); fixed = split.join('\n'); } else { let matchCount = 0; fixed = content.replace(/^(?!\d+\.)/gm, () => { matchCount += 1; return `${matchCount}. `; }); } editor.dispatch({ changes: [{from: 0, to: editor.state.doc.length, insert: fixed}] }); return true; } export const expansionCommand: Command = (editor) => { editor.state.selection.ranges.forEach((range) => { const content = editor.state.doc.slice(range.from, range.to).toString(); let abbrev = '', expan = ''; if (content.length > 0) { abbrev = content.charAt(0); expan = content.substring(1); } const template = `(\${1:${abbrev}}(\${2:${expan}}))\${3}`; const snip = snippet(template); snip(editor, {label: ''}, range.from, range.to); }); return true; } const commands:EditorCommands = { expan: expansionCommand, div: wrapCommand('<=\n', '\n=>'), fragment: snippetCommand('<D=.${1:1}.fragment<=\n${2:_}\n=>=D>${3}'), part: snippetCommand('<D=.${1:A}.part<=\n${2:_}\n=>=D>${3}'), recto: wrapCommand('<D=.r<=\n', '\n=>=D>'), verso: wrapCommand('<D=.v<=\n', '\n=>=D>'), erasure: wrapCommand('〚', '〛'), foreign: snippetCommand('~|${1:_}|~${2:gr}${3}'), unclear: toggleUnclearCommand, fixNewlines: fixNewlinesCommand, snippet: { create: (template:string) => snippetCommand(template) } }; const leidenKeymap: readonly KeyBinding[] = [ { key: "Ctrl-Shift-e", mac: "Cmd-Shift-e", run: commands.expan }, { key: "Ctrl-Shift-a", mac: "Cmd-Shift-a", run: commands.div }, { key: "Ctrl-Shift-f", mac: "Cmd-Shift-f", run: commands.fragment }, { key: "Ctrl-Shift-p", mac: "Cmd-Shift-p", run: commands.part }, { key: "Ctrl-Shift-r", mac: "Cmd-Shift-r", run: commands.recto }, { key: "Ctrl-Shift-v", mac: "Cmd-Shift-v", run: commands.verso }, { key: "Ctrl-Shift-d", mac: "Cmd-Shift-d", run: commands.erasure }, { key: "Ctrl-Shift-u", mac: "Cmd-Shift-u", run: commands.unclear }, { key: "Ctrl-Shift-f", mac: "Cmd-Shift-f", run: commands.foreign } ]; export class LeidenConfig extends EditorConfig { constructor(editor:JinnCodemirror, toolbar: HTMLElement[]) { super(editor, toolbar, commands); } async getExtensions(): Promise<Extension[]> { return [ leiden(), linter(leidenParseLinter(this.editor)), keymap.of(leidenKeymap), lintGutter() ]; } getCommands():EditorCommands { return commands; } onUpdate(tree: Tree, content: string): string { // debugLeidenTree(content, tree); return leidenPlus2epiDoc(content, tree); } serialize(): Element | string | null { return this.editor.content; } setFromValue(value: Element | NodeListOf<ChildNode> | string | null|undefined): string { if (!value) { return '' } if (value instanceof NodeList) { const result:string[] = []; value.forEach((node) => result.push(xml2leidenPlus(node))); return result.join(''); } if (value instanceof Element) { return xml2leidenPlus(value); } return super.setFromValue(value); } }