ソースを参照

Add support for rendering math in Markdown with MathJax

GitOrigin-RevId: dd1c1c72237a5364c642e46c66e08ea7a6977662
Domagoj Kriskovic 3 ヶ月 前
親
コミット
63a605484c

+ 2 - 1
services/web/frontend/js/features/source-editor/languages/markdown/index.ts

@@ -2,6 +2,7 @@ import { markdown as markdownLanguage } from '@codemirror/lang-markdown'
 import { shortcuts } from './shortcuts'
 import { languages } from '../index'
 import { Strikethrough, Table } from '@lezer/markdown'
+import { Math } from './math'
 import {
   HighlightStyle,
   LanguageSupport,
@@ -12,7 +13,7 @@ import { tags } from '@lezer/highlight'
 export const markdown = () => {
   const { language, support } = markdownLanguage({
     codeLanguages: languages,
-    extensions: [Strikethrough, Table],
+    extensions: [Strikethrough, Table, Math],
   })
 
   return new LanguageSupport(language, [

+ 57 - 0
services/web/frontend/js/features/source-editor/languages/markdown/math.ts

@@ -0,0 +1,57 @@
+import { InlineContext, MarkdownConfig, InlineParser } from '@lezer/markdown'
+
+const DOLLAR = 36 /* '$' */
+
+// Whitespace (including the start/end of the inline section, where `char`
+// returns -1).
+const isSpace = (code: number) =>
+  code === -1 || code === 32 || code === 9 || code === 10 || code === 13
+
+const isDigit = (code: number) => code >= 48 && code <= 57
+
+const InlineMathDelim = { resolve: 'InlineMath', mark: 'InlineMathMark' }
+const BlockMathDelim = { resolve: 'BlockMath', mark: 'BlockMathMark' }
+
+// Inline parsing runs over the whole paragraph, so `$$…$$` handles multi-line
+// equations as long as there is no blank line inside them.
+const blockMathParser: InlineParser = {
+  name: 'BlockMath',
+  parse(cx: InlineContext, next: number, pos: number) {
+    if (next !== DOLLAR || cx.char(pos + 1) !== DOLLAR) {
+      return -1
+    }
+    return cx.addDelimiter(BlockMathDelim, pos, pos + 2, true, true)
+  },
+  // Run before the default `Emphasis` parser, and (via `InlineMath`'s `after`)
+  // before the single-dollar inline parser, so `$$` is matched as a display
+  // delimiter rather than two empty inline ones.
+  before: 'Emphasis',
+}
+
+const inlineMathParser: InlineParser = {
+  name: 'InlineMath',
+  parse(cx: InlineContext, next: number, pos: number) {
+    if (next !== DOLLAR) {
+      return -1
+    }
+    const before = cx.char(pos - 1)
+    const after = cx.char(pos + 1)
+    const canOpen = !isSpace(after)
+    const canClose = !isSpace(before) && !isDigit(after)
+    if (!canOpen && !canClose) {
+      return -1
+    }
+    return cx.addDelimiter(InlineMathDelim, pos, pos + 1, canOpen, canClose)
+  },
+  // Run after `BlockMath` so `$$` is tried as a display delimiter first, but
+  // still before `Emphasis`. The default `Escape` parser consumes `\$` first.
+  after: 'BlockMath',
+}
+
+export const Math: MarkdownConfig = {
+  // No `style` is set on these nodes: math is rendered by the visual editor
+  // (which slices the raw source), so the grammar change stays inert for
+  // source-mode syntax highlighting and non-experiment users.
+  defineNodes: ['InlineMath', 'InlineMathMark', 'BlockMath', 'BlockMathMark'],
+  parseInline: [blockMathParser, inlineMathParser],
+}