diff --git a/packages/common/_markdown/commands.ts b/packages/common/_markdown/commands.ts deleted file mode 100644 index 82b7023c..00000000 --- a/packages/common/_markdown/commands.ts +++ /dev/null @@ -1,364 +0,0 @@ -// Changes made to this file: -// - ignore language facet stuff, always assume markdown -import { - ChangeSpec, - EditorSelection, - StateCommand, - SyntaxNode, - syntaxTree, - Text, - Tree, -} from "../../../dep_common.ts"; -import { markdownLanguage } from "./markdown.ts"; - -function nodeStart(node: SyntaxNode, doc: Text) { - return doc.sliceString(node.from, node.from + 50); -} - -class Context { - constructor( - readonly node: SyntaxNode, - readonly from: number, - readonly to: number, - readonly spaceBefore: string, - readonly spaceAfter: string, - readonly type: string, - readonly item: SyntaxNode | null, - ) {} - - blank(trailing: boolean = true) { - let result = this.spaceBefore; - if (this.node.name == "Blockquote") result += ">"; - else { - for ( - let i = this.to - this.from - result.length - this.spaceAfter.length; - i > 0; - i-- - ) { - result += " "; - } - } - return result + (trailing ? this.spaceAfter : ""); - } - - marker(doc: Text, add: number) { - let number = this.node.name == "OrderedList" - ? String(+itemNumber(this.item!, doc)[2] + add) - : ""; - return this.spaceBefore + number + this.type + this.spaceAfter; - } -} - -function getContext(node: SyntaxNode, line: string, doc: Text) { - let nodes = []; - for ( - let cur: SyntaxNode | null = node; - cur && cur.name != "Document"; - cur = cur.parent - ) { - if (cur.name == "ListItem" || cur.name == "Blockquote") nodes.push(cur); - } - let context = [], - pos = 0; - for (let i = nodes.length - 1; i >= 0; i--) { - let node = nodes[i], - match, - start = pos; - if ( - node.name == "Blockquote" && - (match = /^[ \t]*>( ?)/.exec(line.slice(pos))) - ) { - pos += match[0].length; - context.push(new Context(node, start, pos, "", match[1], ">", null)); - } else if ( - node.name == "ListItem" && - node.parent!.name == "OrderedList" && - (match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(nodeStart(node, doc))) - ) { - let after = match[3], - len = match[0].length; - if (after.length >= 4) { - after = after.slice(0, after.length - 4); - len -= 4; - } - pos += len; - context.push( - new Context(node.parent!, start, pos, match[1], after, match[2], node), - ); - } else if ( - node.name == "ListItem" && - node.parent!.name == "BulletList" && - (match = /^([ \t]*)([-+*])([ \t]{1,4}\[[ xX]\])?([ \t]+)/.exec( - nodeStart(node, doc), - )) - ) { - let after = match[4], - len = match[0].length; - if (after.length > 4) { - after = after.slice(0, after.length - 4); - len -= 4; - } - let type = match[2]; - if (match[3]) type += match[3].replace(/[xX]/, " "); - pos += len; - context.push( - new Context(node.parent!, start, pos, match[1], after, type, node), - ); - } - } - return context; -} - -function itemNumber(item: SyntaxNode, doc: Text) { - return /^(\s*)(\d+)(?=[.)])/.exec( - doc.sliceString(item.from, item.from + 10), - )!; -} - -function renumberList( - after: SyntaxNode, - doc: Text, - changes: ChangeSpec[], - offset = 0, -) { - for (let prev = -1, node = after;;) { - if (node.name == "ListItem") { - let m = itemNumber(node, doc); - let number = +m[2]; - if (prev >= 0) { - if (number != prev + 1) return; - changes.push({ - from: node.from + m[1].length, - to: node.from + m[0].length, - insert: String(prev + 2 + offset), - }); - } - prev = number; - } - let next = node.nextSibling; - if (!next) break; - node = next; - } -} - -/// This command, when invoked in Markdown context with cursor -/// selection(s), will create a new line with the markup for -/// blockquotes and lists that were active on the old line. If the -/// cursor was directly after the end of the markup for the old line, -/// trailing whitespace and list markers are removed from that line. -/// -/// The command does nothing in non-Markdown context, so it should -/// not be used as the only binding for Enter (even in a Markdown -/// document, HTML and code regions might use a different language). -export const insertNewlineContinueMarkup: StateCommand = ({ - state, - dispatch, -}) => { - let tree = syntaxTree(state), - { doc } = state; - let dont = null, - changes = state.changeByRange((range) => { - if (!range.empty) { - // TODO: Hack due to languagefacet stuff not working - // || !markdownLanguage.isActiveAt(state, range.from)) - return (dont = { range }); - } - let pos = range.from, - line = doc.lineAt(pos); - let context = getContext(tree.resolveInner(pos, -1), line.text, doc); - while ( - context.length && - context[context.length - 1].from > pos - line.from - ) { - context.pop(); - } - if (!context.length) return (dont = { range }); - let inner = context[context.length - 1]; - if (inner.to - inner.spaceAfter.length > pos - line.from) { - return (dont = { range }); - } - - let emptyLine = pos >= inner.to - inner.spaceAfter.length && - !/\S/.test(line.text.slice(inner.to)); - // Empty line in list - if (inner.item && emptyLine) { - // First list item or blank line before: delete a level of markup - if ( - inner.node.firstChild!.to >= pos || - (line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text)) - ) { - let next = context.length > 1 ? context[context.length - 2] : null; - let delTo, - insert = ""; - if (next && next.item) { - // Re-add marker for the list at the next level - delTo = line.from + next.from; - insert = next.marker(doc, 1); - } else { - delTo = line.from + (next ? next.to : 0); - } - let changes: ChangeSpec[] = [{ from: delTo, to: pos, insert }]; - if (inner.node.name == "OrderedList") { - renumberList(inner.item!, doc, changes, -2); - } - if (next && next.node.name == "OrderedList") { - renumberList(next.item!, doc, changes); - } - return { - range: EditorSelection.cursor(delTo + insert.length), - changes, - }; - } else { - // Move this line down - let insert = ""; - for (let i = 0, e = context.length - 2; i <= e; i++) { - insert += context[i].blank(i < e); - } - insert += state.lineBreak; - return { - range: EditorSelection.cursor(pos + insert.length), - changes: { from: line.from, insert }, - }; - } - } - - if (inner.node.name == "Blockquote" && emptyLine && line.from) { - let prevLine = doc.lineAt(line.from - 1), - quoted = />\s*$/.exec(prevLine.text); - // Two aligned empty quoted lines in a row - if (quoted && quoted.index == inner.from) { - let changes = state.changes([ - { from: prevLine.from + quoted.index, to: prevLine.to }, - { from: line.from + inner.from, to: line.to }, - ]); - return { range: range.map(changes), changes }; - } - } - - let changes: ChangeSpec[] = []; - if (inner.node.name == "OrderedList") { - renumberList(inner.item!, doc, changes); - } - let insert = state.lineBreak; - let continued = inner.item && inner.item.from < line.from; - // If not dedented - if ( - !continued || - /^[\s\d.)\-+*>]*/.exec(line.text)![0].length >= inner.to - ) { - for (let i = 0, e = context.length - 1; i <= e; i++) { - insert += i == e && !continued - ? context[i].marker(doc, 1) - : context[i].blank(); - } - } - let from = pos; - while ( - from > line.from && - /\s/.test(line.text.charAt(from - line.from - 1)) - ) { - from--; - } - changes.push({ from, to: pos, insert }); - return { range: EditorSelection.cursor(from + insert.length), changes }; - }); - if (dont) return false; - dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" })); - return true; -}; - -function isMark(node: SyntaxNode) { - return node.name == "QuoteMark" || node.name == "ListMark"; -} - -function contextNodeForDelete(tree: Tree, pos: number) { - let node = tree.resolveInner(pos, -1), - scan = pos; - if (isMark(node)) { - scan = node.from; - node = node.parent!; - } - for (let prev; (prev = node.childBefore(scan));) { - if (isMark(prev)) { - scan = prev.from; - } else if (prev.name == "OrderedList" || prev.name == "BulletList") { - node = prev.lastChild!; - scan = node.to; - } else { - break; - } - } - return node; -} - -/// This command will, when invoked in a Markdown context with the -/// cursor directly after list or blockquote markup, delete one level -/// of markup. When the markup is for a list, it will be replaced by -/// spaces on the first invocation (a further invocation will delete -/// the spaces), to make it easy to continue a list. -/// -/// When not after Markdown block markup, this command will return -/// false, so it is intended to be bound alongside other deletion -/// commands, with a higher precedence than the more generic commands. -export const deleteMarkupBackward: StateCommand = ({ state, dispatch }) => { - let tree = syntaxTree(state); - let dont = null, - changes = state.changeByRange((range) => { - let pos = range.from, - { doc } = state; - if (range.empty && markdownLanguage.isActiveAt(state, range.from)) { - let line = doc.lineAt(pos); - let context = getContext( - contextNodeForDelete(tree, pos), - line.text, - doc, - ); - if (context.length) { - let inner = context[context.length - 1]; - let spaceEnd = inner.to - inner.spaceAfter.length + - (inner.spaceAfter ? 1 : 0); - // Delete extra trailing space after markup - if ( - pos - line.from > spaceEnd && - !/\S/.test(line.text.slice(spaceEnd, pos - line.from)) - ) { - return { - range: EditorSelection.cursor(line.from + spaceEnd), - changes: { from: line.from + spaceEnd, to: pos }, - }; - } - if (pos - line.from == spaceEnd) { - let start = line.from + inner.from; - // Replace a list item marker with blank space - if ( - inner.item && - inner.node.from < inner.item.from && - /\S/.test(line.text.slice(inner.from, inner.to)) - ) { - return { - range, - changes: { - from: start, - to: line.from + inner.to, - insert: inner.blank(), - }, - }; - } - // Delete one level of indentation - if (start < pos) { - return { - range: EditorSelection.cursor(start), - changes: { from: start, to: pos }, - }; - } - } - } - } - return (dont = { range }); - }); - if (dont) return false; - dispatch( - state.update(changes, { scrollIntoView: true, userEvent: "delete" }), - ); - return true; -}; diff --git a/packages/common/_markdown/index.ts b/packages/common/_markdown/index.ts deleted file mode 100644 index 471c4dcc..00000000 --- a/packages/common/_markdown/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Local changes made to this file: -// * Disable HTML tags - -import { Prec } from "@codemirror/state"; -import { KeyBinding, keymap } from "../../../dep_common.ts"; -import { - Language, - LanguageDescription, - LanguageSupport, -} from "../../../dep_common.ts"; -import { - MarkdownExtension, - MarkdownParser, - parseCode, -} from "../../../dep_common.ts"; -// import { html } from "@codemirror/lang-html"; -import { - commonmarkLanguage, - getCodeParser, - markdownLanguage, - mkLang, -} from "./markdown.ts"; -import { - deleteMarkupBackward, - insertNewlineContinueMarkup, -} from "./commands.ts"; -export { - commonmarkLanguage, - deleteMarkupBackward, - insertNewlineContinueMarkup, - markdownLanguage, -}; - -/// A small keymap with Markdown-specific bindings. Binds Enter to -/// [`insertNewlineContinueMarkup`](#lang-markdown.insertNewlineContinueMarkup) -/// and Backspace to -/// [`deleteMarkupBackward`](#lang-markdown.deleteMarkupBackward). -export const markdownKeymap: readonly KeyBinding[] = [ - { key: "Enter", run: insertNewlineContinueMarkup }, - { key: "Backspace", run: deleteMarkupBackward }, -]; - -// const htmlNoMatch = html({ matchClosingTags: false }); - -/// Markdown language support. -export function markdown( - config: { - /// When given, this language will be used by default to parse code - /// blocks. - defaultCodeLanguage?: Language | LanguageSupport; - /// A source of language support for highlighting fenced code - /// blocks. When it is an array, the parser will use - /// [`LanguageDescription.matchLanguageName`](#language.LanguageDescription^matchLanguageName) - /// with the fenced code info to find a matching language. When it - /// is a function, will be called with the info string and may - /// return a language or `LanguageDescription` object. - codeLanguages?: - | readonly LanguageDescription[] - | ((info: string) => Language | LanguageDescription | null); - /// Set this to false to disable installation of the Markdown - /// [keymap](#lang-markdown.markdownKeymap). - addKeymap?: boolean; - /// Markdown parser - /// [extensions](https://github.com/lezer-parser/markdown#user-content-markdownextension) - /// to add to the parser. - extensions?: MarkdownExtension; - /// The base language to use. Defaults to - /// [`commonmarkLanguage`](#lang-markdown.commonmarkLanguage). - base?: Language; - } = {}, -) { - let { - codeLanguages, - defaultCodeLanguage, - addKeymap = true, - base: { parser } = commonmarkLanguage, - } = config; - if (!(parser instanceof MarkdownParser)) { - throw new RangeError( - "Base parser provided to `markdown` should be a Markdown parser", - ); - } - let extensions = config.extensions ? [config.extensions] : []; - // let support = [htmlNoMatch.support], - let support = [], - defaultCode; - if (defaultCodeLanguage instanceof LanguageSupport) { - support.push(defaultCodeLanguage.support); - defaultCode = defaultCodeLanguage.language; - } else if (defaultCodeLanguage) { - defaultCode = defaultCodeLanguage; - } - let codeParser = codeLanguages || defaultCode - ? getCodeParser(codeLanguages, defaultCode) - : undefined; - extensions.push( - parseCode({ codeParser }), //, htmlParser: htmlNoMatch.language.parser }) - ); - if (addKeymap) support.push(Prec.high(keymap.of(markdownKeymap))); - return new LanguageSupport(mkLang(parser.configure(extensions)), support); -} diff --git a/packages/common/_markdown/markdown.ts b/packages/common/_markdown/markdown.ts deleted file mode 100644 index 5c247889..00000000 --- a/packages/common/_markdown/markdown.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - defineLanguageFacet, - foldNodeProp, - indentNodeProp, - Language, - languageDataProp, - LanguageDescription, - ParseContext, -} from "../../../dep_common.ts"; -import { - baseParser, - Emoji, - GFM, - MarkdownParser, - Subscript, - Superscript, -} from "../../../dep_common.ts"; - -const data = defineLanguageFacet({ block: { open: "" } }); - -export const commonmark = baseParser.configure({ - props: [ - foldNodeProp.add((type) => { - if (!type.is("Block") || type.is("Document")) return undefined; - return (tree, state) => ({ - from: state.doc.lineAt(tree.from).to, - to: tree.to, - }); - }), - indentNodeProp.add({ - Document: () => null, - }), - languageDataProp.add({ - Document: data, - }), - ], -}); - -export function mkLang(parser: MarkdownParser) { - return new Language(data, parser); -} - -/// Language support for strict CommonMark. -export const commonmarkLanguage = mkLang(commonmark); - -const extended = commonmark.configure([GFM, Subscript, Superscript, Emoji]); - -/// Language support for [GFM](https://github.github.com/gfm/) plus -/// subscript, superscript, and emoji syntax. -export const markdownLanguage = mkLang(extended); - -export function getCodeParser( - languages: - | readonly LanguageDescription[] - | ((info: string) => Language | LanguageDescription | null) - | undefined, - defaultLanguage?: Language, -) { - return (info: string) => { - if (info && languages) { - let found = null; - if (typeof languages == "function") found = languages(info); - else found = LanguageDescription.matchLanguageName(languages, info, true); - if (found instanceof LanguageDescription) { - return found.support - ? found.support.language.parser - : ParseContext.getSkippingParser(found.load()); - } else if (found) return found.parser; - } - return defaultLanguage ? defaultLanguage.parser : null; - }; -}