Skip to content
Source
Sign inSign up
Open History to see the last commit

markdownMath.ts

86 lines3.0 KB

1/// <reference types="mdast-util-to-hast" />
2import type { Root } from "mdast";
3import remarkMath from "remark-math";
4import remarkParse from "remark-parse";
5import remarkGfm from "remark-gfm";
6import { unified } from "unified";
7​
8const mathParser = unified()
9 .use(remarkParse)
10 .use(remarkGfm)
11 .use(remarkMath, { singleDollarTextMath: true })
12 .freeze();
13​
14export function parseMarkdownWithMath(source: string) {
15 return mathParser.parse(source);
17​
18/** Closed, parser-owned formulas with authored source for editors. */
19export function markdownMathRanges(source: string, tree?: Root) {
20 const ranges: Array<{
21 start: number;
22 end: number;
23 source: string;
24 tex: string;
25 display: boolean;
26 inTable: boolean;
27 }> = [];
28 if (!mayContainMath(source)) return ranges;
29 const visit = (node: Root | Root["children"][number], inTable = false) => {
30 const table = inTable || node.type === "tableCell";
31 if (
32 node.type === "math" ||
33 node.type === "inlineMath" ||
34 (node.type === "code" && node.lang === "math")
35 ) {
36 const start = node.position?.start.offset;
37 const end = node.position?.end.offset;
38 if (start === undefined || end === undefined) return;
39 const authored = source.slice(start, end);
40 if (node.type === "code") {
41 const open = /^[ \t]*(`{3,}|~{3,})/.exec(authored)?.[1];
42 const close = /\n[ \t>]*(`{3,}|~{3,})[ \t\r]*$/.exec(authored)?.[1];
43 if (!open || !close || open[0] !== close[0] || close.length < open.length) return;
44 }
45 const display = node.type !== "inlineMath" || /^(?:\$\$|\\\[)/.test(authored);
46 ranges.push({ start, end, source: authored, tex: node.value, display, inTable: table });
47 } else if ("children" in node) node.children.forEach((child) => visit(child, table));
48 };
49 visit(tree ?? parseMarkdownWithMath(source));
50 return ranges;
52​
53/** Messages without these characters skip math parsing entirely. */
54function mayContainMath(text: string): boolean {
55 return (
56 mayContainDollarMath(text) ||
57 text.includes("\\(") ||
58 text.includes("\\[") ||
59 /(?:`{3,}|~{3,})[ \t]*math\b/.test(text)
60 );
62​
63/**
64 * A `$` formula needs a closing `$` (or the second `$` of `$$`) after another `$` on its line and
65 * not right after a space (micromark reads a tab as virtual spaces, which still close). So `$skill`
66 * chips and prices skip the parser.
67 */
68function mayContainDollarMath(text: string): boolean {
69 let opened = false;
70 for (let index = 0; index < text.length; index += 1) {
71 const char = text[index];
72 if (char === "\n" || char === "\r") {
73 opened = false;
74 } else if (char === "$") {
75 if (opened && text[index - 1] !== " ") return true;
76 opened = true;
77 }
78 }
79 return false;
81​
82export function mathKind(className: string | undefined): "inline" | "display" | null {
83 if (!className?.includes("language-math")) return null;
84 if (className.includes("math-display")) return "display";
85 return className.includes("math-inline") ? "inline" : null;