// // Copyright 2023 DXOS.org // import { completionKeymap } from '@codemirror/autocomplete'; import { defaultKeymap, indentWithTab } from '@codemirror/commands'; import { jsonLanguage } from '@codemirror/lang-json'; import { insertNewlineContinueMarkupCommand, markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { type LanguageDescription, foldNodeProp, syntaxHighlighting } from '@codemirror/language'; import { languages } from '@codemirror/language-data'; import { type Extension, Prec } from '@codemirror/state'; import { keymap } from '@codemirror/view'; import { type MarkdownConfig } from '@lezer/markdown'; import { isTruthy } from '@dxos/util'; import { mermaidHighlighting, mermaidLanguageDescription } from '../mermaid/index.ts'; import { markdownHighlightStyle, markdownTagsExtensions } from './highlight.ts'; export type MarkdownBundleOptions = { /** Additional fenced-code languages prepended to the standard language-data list. */ codeLanguages?: LanguageDescription[]; extensions?: MarkdownConfig[]; indentWithTab?: boolean; setextHeading?: boolean; }; /** * Creates markdown extensions. * To be used in conjunction with createBasicExtensions. * * Refs: * https://codemirror.net/docs/community * https://codemirror.net/docs/ref/#codemirror.basicSetup */ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): Extension[] => { return [ Prec.high(keymap.of([{ key: 'Enter', run: insertNewlineContinueMarkupCommand({ nonTightLists: false }) }])), // Main extension. // https://github.com/codemirror/lang-markdown // https://codemirror.net/5/mode/markdown/index.html (demo). markdown({ // GRM by default (vs strict CommonMark): // Table, TaskList, Strikethrough, and Autolink. // NOTE: This extends the parser; it doesn't affect rendering. // https://github.github.com/gfm // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown base: markdownLanguage, // Languages for syntax highlighting fenced code blocks. // Caller-supplied languages are checked first so they can override defaults. // Mermaid is registered here rather than by plugin-mermaid: `codeLanguages` is read once when // the parser is built, so an extension contributed later cannot add a fenced-code language. defaultCodeLanguage: jsonLanguage, codeLanguages: [...(options.codeLanguages ?? []), mermaidLanguageDescription, ...languages], // Don't complete HTML tags. completeHTMLTags: false, // Parser extensions. extensions: [ // GFM provided by default. markdownTagsExtensions, ...(options.extensions ?? defaultExtensions()), // Disable folding for fenced code blocks by overriding foldNodeProp. // Note: returning null from foldService does not prevent syntaxFolding fallback, // so we must override the node prop directly on the FencedCode node type. noFencedCodeFolding, ], }), // Custom styles. // Fenced mermaid uses custom tags, so it needs its own style alongside markdown's. mermaidHighlighting(), syntaxHighlighting(markdownHighlightStyle()), keymap.of( [ // https://codemirror.net/docs/ref/#commands.indentWithTab options.indentWithTab !== false && indentWithTab, // https://codemirror.net/docs/ref/#commands.defaultKeymap ...defaultKeymap, // TODO(burdon): Remove? ...completionKeymap, ].filter(isTruthy), ), ]; }; /** * Disables folding for fenced code blocks. * * foldService cannot block folding because returning null just defers to the next service, * and CodeMirror always falls back to syntaxFolding (which reads foldNodeProp). * The only reliable fix is to override foldNodeProp on FencedCode to return null. */ const noFencedCodeFolding: MarkdownConfig = { props: [ foldNodeProp.add({ FencedCode: () => null, }), ], }; /** * Default customizations. * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts */ export const defaultExtensions = (): MarkdownConfig[] => [noSetExtHeading, noHtml]; /** * Remove SetextHeading (e.g., headings created from "---"). */ const noSetExtHeading: MarkdownConfig = { remove: ['SetextHeading'], }; /** * Remove HTML and XML parsing. */ const noHtml: MarkdownConfig = { // remove: ['HTMLBlock', 'HTMLTag'], };