@godxjp/markdown 33.17.0 → 33.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@ pnpm add @godxjp/markdown # + `mermaid` (optional) to draw ```mermaid fen
8
8
  ````
9
9
 
10
10
  ```tsx
11
+ import "@godxjp/markdown/katex.css"; // once, in the app entry: KaTeX's stylesheet + fonts
11
12
  import { Prose } from "@godxjp/ui/data-display";
12
13
  import { Markdown } from "@godxjp/markdown";
13
14
 
@@ -22,6 +23,29 @@ GFM — tables, task lists (read-only), fenced code with `language-*` classes, a
22
23
  strikethrough — and an `id` on every heading (GitHub slugs, de-duplicated). The output is the same
23
24
  as `react-markdown` + `remark-gfm` + `rehype-sanitize`, which is what the apps rendered before.
24
25
 
26
+ **Math** — `$…$` and `$$…$$` inline, a `$$` block or a ` ```math ` fence for display — is
27
+ drawn by KaTeX at parse time (no DOM: the same HTML in a browser, on a server, in a Worker), as
28
+ HTML plus MathML, so a screen reader reads the formula. The content is literal TeX: `\,` keeps its
29
+ backslash. A single `$` follows Pandoc's rule, so prices stay prose: `$5 and $10` and `$5-$10` are
30
+ text (the opening `$` is followed by a non-space, the closing one preceded by a non-space and not
31
+ by `\`, and not followed by a digit). Write `\$` for a literal dollar anywhere. A formula that does
32
+ not parse shows its source in `.katex-error`.
33
+
34
+ **The KaTeX stylesheet is the host's to load**, once: `import "@godxjp/markdown/katex.css"`. It
35
+ `@import`s `katex/dist/katex.min.css` from the KaTeX this package depends on (so the CSS always
36
+ matches the renderer; any bundler that resolves CSS `@import` from `node_modules` — Vite, webpack,
37
+ esbuild — copies the fonts) and lets a wide display formula scroll instead of widening the page.
38
+ Without it, formulas render as unstyled text plus MathML.
39
+
40
+ **Syntax colour** — a fence in a known language is tokenised at parse time and every token is a
41
+ `<span data-code-token="…">`, with the kit's names (`comment`, `keyword`, `string`,
42
+ `string-expression`, `function`, `constant`, `parameter`, `punctuation`, `link`, `inserted`,
43
+ `deleted`, `changed`). `Prose` colours them from the `--code-block-token-*` knobs, in light and dark —
44
+ this package ships no code palette. Languages: bash, shell, css, diff, dockerfile, go, ini/toml,
45
+ java, javascript, json, markdown, php, python, rust, sql, typescript, xml/html, yaml (with their
46
+ aliases: `ts`, `js`, `jsx`, `tsx`, `sh`, `zsh`, `yml`, `html`, `py`, `rs`, …). Any other language,
47
+ a fence without one, ` ```mermaid ` and ` ```math ` stay plain text.
48
+
25
49
  Every table renders inside a scroll box (`.ui-prose-table-scroll`, styled by `Prose`), keyboard-
26
50
  focusable only while it overflows, so a wide table scrolls instead of widening the page.
27
51
 
@@ -31,6 +55,15 @@ focusable only while it overflows, so a wide table scrolls instead of widening t
31
55
  - **One schema** (`markdownSchema`): links allow `http`, `https`, `mailto` and relative URLs;
32
56
  images allow `http`, `https` and relative URLs. `javascript:`, `data:` and every other scheme are
33
57
  removed — including a scheme split by tabs or newlines. SVG is never inlined from a URL.
58
+ - **KaTeX runs with `trust: false`, `strict: "error"` and `maxSize: 10`**, before the sanitiser,
59
+ and the schema allows exactly what KaTeX emits: MathML elements, KaTeX's own class names,
60
+ geometry-only inline styles (lengths, colours, keywords — no `url()`, no parentheses at all) and
61
+ `svg` / `path` / `line` for radicals and stretchy arrows (no `href`, no `<image>`). `\href`,
62
+ `\url`, `\includegraphics`, `\htmlClass`, `\htmlId`, `\htmlStyle` and `\htmlData` render as their
63
+ own source text. A non-standard construct still draws (rehype-katex retries leniently, `trust` is
64
+ never relaxed); a user-sized box (`\rule{9999em}{9999em}`) is capped at 10em.
65
+ - **Code tokens** are `span`s whose only attribute is `data-code-token`, pinned to the twelve names;
66
+ a body cannot mint one (raw HTML is dropped).
34
67
  - **Mermaid is a picture only after its SVG passes `checkMermaidSvg`**: no `<script>`,
35
68
  `<foreignObject>`, `<a>`, `<image>`, event handlers, non-fragment `href`, `url()` that leaves the
36
69
  document, or `@import`. A diagram that fails, or a host without `mermaid`, shows the code.
@@ -46,6 +79,7 @@ focusable only while it overflows, so a wide table scrolls instead of widening t
46
79
  | `components` | Element overrides. Mermaid fences are drawn before a host `pre` is asked, so a host code renderer keeps Mermaid. |
47
80
  | `rehypePlugins` | Run after the sanitiser — presentation only. |
48
81
  | `mermaid={false}` | Keep ```mermaid fences as code (or hand them to the host `pre`). |
82
+ | `highlight={false}` | Leave fenced code as plain text — a host `pre` that runs its own highlighter. A host `pre` otherwise receives the tokenised `<code>`. |
49
83
  | `allowedElements` / `unwrapDisallowed` | Narrow a body (an activity feed: `strong`, `em`, `del`, `code`). Only removes. |
50
84
  | `shortCellLength` | A table cell holding one short token (no whitespace, ≤ 24 characters by default) gets `data-short`; `Prose` keeps it on one line. `false` turns it off. |
51
85
 
@@ -55,7 +89,7 @@ so a narrow table scrolls instead of squashing them.
55
89
 
56
90
  ## Grammar v1 and the codec (`@godxjp/markdown/codec`)
57
91
 
58
- The renderer (`RENDERER_VERSION` 5), the codec and `@godxjp/block-editor` read one syntax, from one
92
+ The renderer (`RENDERER_VERSION` 6), the codec and `@godxjp/block-editor` read one syntax, from one
59
93
  set of parsers, so a body means the same thing in the editor, on a server and on the page.
60
94
 
61
95
  ### Syntax
@@ -91,6 +125,10 @@ set of parsers, so a body means the same thing in the editor, on a server and on
91
125
 
92
126
  - **Wikilinks and embeds, verbatim** — `[[target]]`, `[[target#heading]]`, `[[target|label]]`,
93
127
  `![[embed]]` (a block when alone on its line; the inner text is kept verbatim — `![[B#見出し]]`, `![[img.png|300]]`). Marks around them survive (`**[[x]]**`). Inside a table cell write `[[x\|label]]`.
128
+ - **Math** — `$x$`, `$$x$$`, a `$$` block, a ` ```math ` fence (Pandoc's single-dollar rule,
129
+ above). The codec keeps math verbatim as a raw inline / raw block (a fence is an ordinary
130
+ `codeBlock{language: "math"}`), so the block editor needs no math node. Because `$` is now
131
+ special, a literal `$x$` in text is written `\$x$`; a lone `$` (a price) is never escaped.
94
132
  - **`asset:` images** — `![alt](asset:id)` round-trips as written (the renderer resolves it through
95
133
  `resolveUrl`).
96
134
  - **Raw blocks** — anything the document does not model (raw HTML, footnote definitions, link
@@ -102,7 +140,7 @@ set of parsers, so a body means the same thing in the editor, on a server and on
102
140
  ```ts
103
141
  import { CODEC_ID, normalize, parse, serialize } from "@godxjp/markdown/codec";
104
142
 
105
- CODEC_ID; // "kit-md@1" — bumped whenever serialize would write a body differently
143
+ CODEC_ID; // "kit-md@2" — bumped whenever serialize would write a body differently (@2: math)
106
144
  const doc = parse(markdown); // plain JSON, ProseMirror/Tiptap shape
107
145
  const md = serialize(doc); // canonical Markdown
108
146
  normalize(markdown) === serialize(parse(markdown)); // what the editor saves
@@ -64,7 +64,7 @@ declare function serialize(doc: Doc | DocNode): string;
64
64
  * Identifies the serializer's output. Bumped whenever `serialize` would write a stored body
65
65
  * differently, so a host can tell which normalization a saved `normalized_body` came from.
66
66
  */
67
- declare const CODEC_ID = "kit-md@1";
67
+ declare const CODEC_ID = "kit-md@2";
68
68
  /** `serialize(parse(markdown))` — the canonical form of a body, exactly what the editor saves. */
69
69
  declare function normalize(markdown: string): string;
70
70
 
@@ -74,16 +74,57 @@ import { directive } from "micromark-extension-directive";
74
74
  import { gfm } from "micromark-extension-gfm";
75
75
  import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
76
76
  import { frontmatter } from "micromark-extension-frontmatter";
77
+ import { math } from "micromark-extension-math";
77
78
  import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
79
+ import { mathFromMarkdown, mathToMarkdown } from "mdast-util-math";
78
80
  import { directiveFromMarkdown } from "mdast-util-directive";
79
81
  import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
80
82
  function micromarkExtensions() {
81
83
  const [container] = directive().flow[58];
82
84
  const containerOnly = { flow: { 58: container } };
83
- return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
85
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), dollarMath(), containerOnly];
86
+ }
87
+ var DOLLAR = 36;
88
+ var isSpace = (code) => code === null || code === void 0 || code < 0 || code === 32;
89
+ var isDigit = (code) => code != null && code >= 48 && code <= 57;
90
+ var BACKSLASH = 92;
91
+ function dollarMath() {
92
+ const extension = math();
93
+ const text2 = extension.text[DOLLAR];
94
+ const tokenize = function(effects, ok, nok) {
95
+ const codes = [];
96
+ let done = false;
97
+ const finish = (code) => {
98
+ done = true;
99
+ const single = codes[1] !== DOLLAR;
100
+ const after = codes[codes.length - 1];
101
+ const before = codes[codes.length - 3];
102
+ const refused = isSpace(codes[1]) || isSpace(before) || before === BACKSLASH || isDigit(after);
103
+ if (single && refused) return nok(code);
104
+ return ok(code);
105
+ };
106
+ const watch = (state) => (code) => {
107
+ if (done) return state(code);
108
+ codes.push(code);
109
+ const next = state(code);
110
+ return next && !done ? watch(next) : next;
111
+ };
112
+ return watch(
113
+ text2.tokenize.call(this, effects, finish, (code) => {
114
+ done = true;
115
+ return nok(code);
116
+ })
117
+ );
118
+ };
119
+ return { ...extension, text: { [DOLLAR]: { ...text2, tokenize } } };
84
120
  }
85
121
  function fromMarkdownExtensions() {
86
- return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
122
+ return [
123
+ gfmFromMarkdown(),
124
+ directiveFromMarkdown(),
125
+ frontmatterFromMarkdown(["yaml"]),
126
+ mathFromMarkdown()
127
+ ];
87
128
  }
88
129
  var WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1F64F}\u{1F900}-\u{1F9FF}\u{20000}-\u{3FFFD}]/u;
89
130
  function displayWidth(value) {
@@ -97,6 +138,9 @@ function toMarkdownGfm() {
97
138
  function toMarkdownFrontmatter() {
98
139
  return frontmatterToMarkdown(["yaml"]);
99
140
  }
141
+ function toMarkdownMath() {
142
+ return mathToMarkdown();
143
+ }
100
144
 
101
145
  // src/codec/options.ts
102
146
  function containerDirective(node, _, state, info) {
@@ -136,6 +180,7 @@ function toMarkdownOptions() {
136
180
  extensions: [
137
181
  toMarkdownGfm(),
138
182
  toMarkdownFrontmatter(),
183
+ toMarkdownMath(),
139
184
  { handlers: { containerDirective } }
140
185
  ]
141
186
  };
@@ -187,9 +232,22 @@ function withMarks(node, marks) {
187
232
  function leafText(value, marks) {
188
233
  return marks.length ? { type: "text", text: value, marks } : { type: "text", text: value };
189
234
  }
235
+ function inlineMathSource(value, after) {
236
+ let size = /^\s|[\s\\]$/.test(value) || /^\d/.test(after) ? 2 : 1;
237
+ const runs = new Set([...value.matchAll(/\$+/g)].map((run) => run[0].length));
238
+ while (runs.has(size)) size++;
239
+ const fence = "$".repeat(size);
240
+ const pad = /^\$|\$$/.test(value) || /^\s[^]*\s$/.test(value) ? " " : "";
241
+ return `${fence}${pad}${value}${pad}${fence}`;
242
+ }
190
243
  function inline(nodes, marks = []) {
191
- return nodes.flatMap((node) => {
244
+ return nodes.flatMap((node, index) => {
192
245
  switch (node.type) {
246
+ case "inlineMath": {
247
+ const next = nodes[index + 1];
248
+ const source = inlineMathSource(node.value, next?.type === "text" ? next.value : "");
249
+ return [withMarks({ type: "rawInline", attrs: { source } }, marks)];
250
+ }
193
251
  case "text":
194
252
  return text(node.value, marks);
195
253
  case "emphasis":
@@ -562,19 +620,33 @@ function decodeAttentionReferences(markdown) {
562
620
  }
563
621
  return out;
564
622
  }
623
+ function decodeDollarEscapes(markdown) {
624
+ if (!markdown.includes("\\$")) return markdown;
625
+ const target = JSON.stringify(parse(markdown));
626
+ const all = markdown.replace(/\\\$/g, "$");
627
+ if (JSON.stringify(parse(all)) === target) return all;
628
+ let out = markdown;
629
+ for (const escape of [...markdown.matchAll(/\\\$/g)].reverse()) {
630
+ const at = escape.index;
631
+ const candidate = out.slice(0, at) + out.slice(at + 1);
632
+ if (JSON.stringify(parse(candidate)) === target) out = candidate;
633
+ }
634
+ return out;
635
+ }
565
636
  function serialize(doc) {
566
637
  const root = { type: "root", children: flow(doc.content ?? []) };
567
638
  const markdown = toMarkdown2(root, toMarkdownOptions());
568
639
  if (markdown.trim() === "") return "";
569
640
  const tidy = markdown.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\n+$/, "\n");
570
- return decodeAttentionReferences(tidy);
641
+ return decodeDollarEscapes(decodeAttentionReferences(tidy));
571
642
  }
572
643
 
573
644
  // src/codec/outline.ts
574
645
  import GithubSlugger from "github-slugger";
575
646
  import { visit } from "unist-util-visit";
576
647
  function textOf(node) {
577
- if (node.type === "text" || node.type === "inlineCode" || node.type === "html") return node.value;
648
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "inlineMath" || node.type === "html")
649
+ return node.value;
578
650
  if (node.type === "image" || node.type === "imageReference") return "";
579
651
  if ("children" in node) return node.children.map(textOf).join("");
580
652
  return "";
@@ -594,7 +666,7 @@ function frontmatterOf(markdown) {
594
666
  }
595
667
 
596
668
  // src/codec/index.ts
597
- var CODEC_ID = "kit-md@1";
669
+ var CODEC_ID = "kit-md@2";
598
670
  function normalize(markdown) {
599
671
  return serialize(parse(markdown));
600
672
  }
package/dist/index.d.ts CHANGED
@@ -37,6 +37,9 @@ declare function rehypeHeadingIds(options?: {
37
37
  * - `protocols`: links and images allow only `http`, `https`, `mailto` (links) and relative URLs.
38
38
  * `javascript:`, `data:`, `vbscript:` and every other scheme are dropped.
39
39
  * - Task-list checkboxes stay `disabled` (the default): ticking one would be a write to the body.
40
+ * - KaTeX's output (math.ts `katexSchema`: MathML, its own classes, geometry-only inline styles,
41
+ * `svg` paths for radicals and stretchy arrows) and the code tokens (`data-code-token`, the kit's
42
+ * twelve names) — both produced by the pipeline before this schema judges them (renderer v6).
40
43
  *
41
44
  * Hosts extend it with `extendSchema`, never by editing a copy.
42
45
  */
@@ -77,7 +80,7 @@ type PluggableList = NonNullable<Options$1["remarkPlugins"]>;
77
80
  * differently — a schema change, a new default plugin — not on every release.
78
81
  */
79
82
  declare const MARKDOWN_FORMAT = "md";
80
- declare const RENDERER_VERSION = 5;
83
+ declare const RENDERER_VERSION = 6;
81
84
  type MarkdownProps = {
82
85
  /** The Markdown source. */
83
86
  children: string;
@@ -111,6 +114,12 @@ type MarkdownProps = {
111
114
  * `pre` (or plain code) as well (gh#1116).
112
115
  */
113
116
  mermaid?: boolean;
117
+ /**
118
+ * Tokenise fenced code in a known language as `<span data-code-token="keyword">` … (default
119
+ * true), which `Prose` colours. `false` leaves the code as plain text — a host `pre` that runs
120
+ * its own highlighter.
121
+ */
122
+ highlight?: boolean;
114
123
  /**
115
124
  * NARROW the elements a body may produce — an activity feed that shows only inline formatting.
116
125
  * react-markdown's own options, applied after the sanitiser; they can only remove, never allow
@@ -149,7 +158,7 @@ type MarkdownProps = {
149
158
  };
150
159
  /**
151
160
  * THE ONE RENDERER (gh#1108). GFM — tables, task lists, fenced code, autolinks, footnotes,
152
- * strikethrough — plus heading anchors, sanitised by `markdownSchema`, with ```mermaid drawn only
161
+ * strikethrough — plus heading anchors, KaTeX math, highlighted code, sanitised by `markdownSchema`, with ```mermaid drawn only
153
162
  * through the SVG gate. Raw HTML in the source is never parsed into elements (no `rehype-raw`): it
154
163
  * stays visible text. Output matches `react-markdown` + `remark-gfm` + `rehype-sanitize`, which is
155
164
  * what godx-task and godx-content rendered before, so the swap is drop-in.
@@ -157,7 +166,7 @@ type MarkdownProps = {
157
166
  * Renders bare elements; the host wraps it (`<Prose>` from `@godxjp/ui/data-display`), so this
158
167
  * package carries no UI-kit dependency.
159
168
  */
160
- declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, externalLinks, origin, }: MarkdownProps): React.JSX.Element;
169
+ declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, highlight, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, externalLinks, origin, }: MarkdownProps): React.JSX.Element;
161
170
 
162
171
  type MermaidSvgCheck = {
163
172
  ok: true;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as React3 from "react";
5
5
  import ReactMarkdown from "react-markdown";
6
6
  import rehypeSanitize from "rehype-sanitize";
7
7
  import remarkGfm from "remark-gfm";
8
- import { visit as visit2 } from "unist-util-visit";
8
+ import { visit as visit3 } from "unist-util-visit";
9
9
 
10
10
  // src/headings.ts
11
11
  import GithubSlugger from "github-slugger";
@@ -32,6 +32,210 @@ function rehypeHeadingIds(options = {}) {
32
32
  };
33
33
  }
34
34
 
35
+ // src/highlight.ts
36
+ import { createLowlight } from "lowlight";
37
+ import bash from "highlight.js/lib/languages/bash";
38
+ import css from "highlight.js/lib/languages/css";
39
+ import diff from "highlight.js/lib/languages/diff";
40
+ import dockerfile from "highlight.js/lib/languages/dockerfile";
41
+ import go from "highlight.js/lib/languages/go";
42
+ import ini from "highlight.js/lib/languages/ini";
43
+ import java from "highlight.js/lib/languages/java";
44
+ import javascript from "highlight.js/lib/languages/javascript";
45
+ import json from "highlight.js/lib/languages/json";
46
+ import markdown from "highlight.js/lib/languages/markdown";
47
+ import php from "highlight.js/lib/languages/php";
48
+ import python from "highlight.js/lib/languages/python";
49
+ import rust from "highlight.js/lib/languages/rust";
50
+ import shell from "highlight.js/lib/languages/shell";
51
+ import sql from "highlight.js/lib/languages/sql";
52
+ import typescript from "highlight.js/lib/languages/typescript";
53
+ import xml from "highlight.js/lib/languages/xml";
54
+ import yaml from "highlight.js/lib/languages/yaml";
55
+ import { visit as visit2 } from "unist-util-visit";
56
+ var lowlight = createLowlight({
57
+ bash,
58
+ css,
59
+ diff,
60
+ dockerfile,
61
+ go,
62
+ ini,
63
+ java,
64
+ javascript,
65
+ json,
66
+ markdown,
67
+ php,
68
+ python,
69
+ rust,
70
+ shell,
71
+ sql,
72
+ typescript,
73
+ xml,
74
+ yaml
75
+ });
76
+ var CODE_TOKENS = [
77
+ "comment",
78
+ "keyword",
79
+ "string",
80
+ "string-expression",
81
+ "function",
82
+ "constant",
83
+ "parameter",
84
+ "punctuation",
85
+ "link",
86
+ "inserted",
87
+ "deleted",
88
+ "changed"
89
+ ];
90
+ var SCOPES = {
91
+ comment: "comment",
92
+ quote: "comment",
93
+ doctag: "keyword",
94
+ keyword: "keyword",
95
+ built_in: "keyword",
96
+ type: "keyword",
97
+ "selector-tag": "keyword",
98
+ tag: "punctuation",
99
+ name: "keyword",
100
+ section: "keyword",
101
+ string: "string",
102
+ char: "string",
103
+ regexp: "string-expression",
104
+ subst: "string-expression",
105
+ "template-variable": "string-expression",
106
+ "title.function": "function",
107
+ title: "function",
108
+ "title.class": "constant",
109
+ number: "constant",
110
+ literal: "constant",
111
+ symbol: "constant",
112
+ "variable.constant": "constant",
113
+ "variable.language": "constant",
114
+ "selector-id": "constant",
115
+ "selector-class": "constant",
116
+ bullet: "constant",
117
+ params: "parameter",
118
+ attr: "parameter",
119
+ attribute: "parameter",
120
+ property: "parameter",
121
+ "template-tag": "parameter",
122
+ punctuation: "punctuation",
123
+ operator: "punctuation",
124
+ link: "link",
125
+ addition: "inserted",
126
+ deletion: "deleted"
127
+ };
128
+ function tokenOf(className) {
129
+ if (!Array.isArray(className) || !String(className[0]).startsWith("hljs-")) return void 0;
130
+ const parts = className.map(
131
+ (name, i) => i === 0 ? String(name).slice(5) : String(name).replace(/_+$/, "")
132
+ );
133
+ for (let n = parts.length; n > 0; n--) {
134
+ const token = SCOPES[parts.slice(0, n).join(".")];
135
+ if (token) return token;
136
+ }
137
+ return void 0;
138
+ }
139
+ function retag(nodes) {
140
+ return nodes.flatMap((node) => {
141
+ if (node.type !== "element") return [node];
142
+ const children = retag(node.children);
143
+ const token = tokenOf(node.properties?.className);
144
+ return token ? [{ type: "element", tagName: "span", properties: { dataCodeToken: token }, children }] : children;
145
+ });
146
+ }
147
+ var SKIP = /* @__PURE__ */ new Set(["mermaid", "math"]);
148
+ function rehypeHighlight() {
149
+ return (tree) => {
150
+ visit2(tree, "element", (node, _index, parent) => {
151
+ if (node.tagName !== "code" || parent?.type !== "element" || parent.tagName !== "pre") return;
152
+ const classes = node.properties?.className ?? [];
153
+ const language = classes.find((name) => name.startsWith("language-"))?.slice(9);
154
+ if (!language || SKIP.has(language) || !lowlight.registered(language)) return;
155
+ if (!node.children.every((child) => child.type === "text")) return;
156
+ const text = node.children.map((child) => child.type === "text" ? child.value : "").join("");
157
+ node.children = retag(lowlight.highlight(language, text).children);
158
+ });
159
+ };
160
+ }
161
+
162
+ // src/math.ts
163
+ import rehypeKatex from "rehype-katex";
164
+ var KATEX_OPTIONS = {
165
+ trust: false,
166
+ strict: "error",
167
+ maxSize: 10,
168
+ output: "htmlAndMathml"
169
+ };
170
+ function rehypeMath() {
171
+ return rehypeKatex(KATEX_OPTIONS);
172
+ }
173
+ var KATEX_CLASSES = `accent accent-body accent-full amsrm angl anglpad arraycolsep base boldsymbol
174
+ boxpad brace-center brace-left brace-right cancel-lap cancel-pad cd-arrow-pad cd-label-left
175
+ cd-label-right cd-vert-arrow cjk_fallback clap col-align-c col-align-l col-align-r colorbox com
176
+ delim-size1 delim-size4 delimcenter delimsizing eqn-num fbox fcolorbox fix fleqn fontsize-ensurer
177
+ frac-line halfarrow-left halfarrow-right hbox hdashline hide-tail hline inner katex katex-display
178
+ katex-error katex-html katex-mathml large-op leqno llap mainrm mathbb mathbf mathboldfrak mathboldsf
179
+ mathcal mathfrak mathit mathitsf mathnormal mathrm mathscr mathsf mathsfit mathtt mbin mclose mfrac
180
+ minner mml-eqn-num mop mopen mord mover mpunct mrel mspace msupsub mtable mtight mtr-glue mult
181
+ munder newline nobreak nulldelimiter op-limits op-symbol overlay overline overline-line pstrut
182
+ reset-size1 reset-size10 reset-size11 reset-size2 reset-size3 reset-size4 reset-size5 reset-size6
183
+ reset-size7 reset-size8 reset-size9 rlap root rule size1 size10 size11 size2 size3 size4 size5
184
+ size6 size7 size8 size9 sizing small-op smash sout sqrt stretchy strut svg-align tag text textbb
185
+ textbf textboldfrak textboldsf textfrak textit textitsf textrm textscr textsf texttt thinbox
186
+ underline underline-line vbox vertical-separator vlist vlist-r vlist-s vlist-t vlist-t2 x-arrow
187
+ x-arrow-pad`.split(/\s+/);
188
+ var KATEX_STYLE = /^(?:\s*(?:background-color|border(?:-(?:top|right|bottom|left))?(?:-(?:width|style|color))?|bottom|color|height|left|margin(?:-(?:top|right|bottom|left))?|max-width|min-width|padding(?:-(?:top|right|bottom|left))?|position|right|top|vertical-align|width)\s*:\s*[-#.%\w ]+;?)*\s*$/i;
189
+ var MATHML_TAGS = `math semantics annotation mrow mi mn mo ms mtext mspace mpadded mphantom mstyle
190
+ menclose mfrac msqrt mroot msub msup msubsup munder mover munderover mtable mtr mtd mlabeledtr`.split(
191
+ /\s+/
192
+ );
193
+ var katexSchema = {
194
+ tagNames: [...MATHML_TAGS, "svg", "path", "line"],
195
+ attributes: {
196
+ span: [
197
+ ["className", ...KATEX_CLASSES],
198
+ ["style", KATEX_STYLE],
199
+ ["ariaHidden", "true"]
200
+ ],
201
+ math: [
202
+ ["xmlns", "http://www.w3.org/1998/Math/MathML"],
203
+ ["display", "block", "inline"]
204
+ ],
205
+ annotation: [["encoding", "application/x-tex"]],
206
+ mi: ["mathvariant"],
207
+ mo: ["stretchy", "fence", "separator", "minsize", "maxsize", "lspace", "rspace", "accent"],
208
+ mfrac: ["linethickness"],
209
+ mtable: ["rowspacing", "rowlines", "columnalign", "columnspacing", "columnlines", "width"],
210
+ mtd: ["columnalign"],
211
+ mstyle: ["scriptlevel", "displaystyle", "mathcolor", "mathsize", "mathbackground"],
212
+ mover: ["accent"],
213
+ munder: ["accentunder"],
214
+ munderover: ["accent", "accentunder"],
215
+ mpadded: [
216
+ "width",
217
+ "height",
218
+ "depth",
219
+ "lspace",
220
+ "voffset",
221
+ "mathbackground",
222
+ ["style", KATEX_STYLE]
223
+ ],
224
+ mspace: ["width", "height", "depth", "mathbackground"],
225
+ menclose: ["notation", "mathbackground"],
226
+ svg: [
227
+ ["xmlns", "http://www.w3.org/2000/svg"],
228
+ "width",
229
+ "height",
230
+ "viewBox",
231
+ "preserveAspectRatio",
232
+ ["style", KATEX_STYLE]
233
+ ],
234
+ path: ["d"],
235
+ line: ["x1", "y1", "x2", "y2", "strokeWidth"]
236
+ }
237
+ };
238
+
35
239
  // src/mermaid.tsx
36
240
  import * as React from "react";
37
241
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -90,8 +294,8 @@ function checkMermaidSvg(svg) {
90
294
  const name = element2.localName.toLowerCase();
91
295
  if (!ALLOWED_ELEMENTS.has(name)) return { ok: false, reason: `element ${name}` };
92
296
  if (name === "style") {
93
- const css = element2.textContent ?? "";
94
- if (CSS_HAZARD.test(css) || !urlsAreLocal(css)) return { ok: false, reason: "style" };
297
+ const css2 = element2.textContent ?? "";
298
+ if (CSS_HAZARD.test(css2) || !urlsAreLocal(css2)) return { ok: false, reason: "style" };
95
299
  }
96
300
  for (const attribute of Array.from(element2.attributes)) {
97
301
  const attr = attribute.localName.toLowerCase();
@@ -107,20 +311,26 @@ function checkMermaidSvg(svg) {
107
311
  return { ok: true, svg: root };
108
312
  }
109
313
  var loading = null;
110
- function loadMermaid() {
111
- loading ??= import("mermaid").then((module) => {
112
- const api = module.default;
314
+ var initializedTheme = null;
315
+ async function loadMermaid() {
316
+ loading ??= import("mermaid").then((module) => module.default).catch(() => null);
317
+ const api = await loading;
318
+ const theme = document.documentElement.dataset.theme === "dark" ? "dark" : "default";
319
+ if (api && theme !== initializedTheme) {
113
320
  api.initialize({
114
321
  startOnLoad: false,
115
322
  securityLevel: "strict",
116
323
  // SVG text labels, not HTML in <foreignObject> — which the gate rejects outright.
117
324
  htmlLabels: false,
118
325
  flowchart: { htmlLabels: false },
119
- theme: document.documentElement.dataset.theme === "dark" ? "dark" : "default"
326
+ // Journey wraps its labels in <switch><foreignObject> by default ("element switch" at the
327
+ // gate, so every journey fell back to code); tspan placement draws them as SVG text.
328
+ journey: { textPlacement: "tspan" },
329
+ theme
120
330
  });
121
- return api;
122
- }).catch(() => null);
123
- return loading;
331
+ initializedTheme = theme;
332
+ }
333
+ return api;
124
334
  }
125
335
  function MermaidCode({ source }) {
126
336
  return /* @__PURE__ */ jsx("pre", { "data-mermaid": "code", children: /* @__PURE__ */ jsx("code", { className: "language-mermaid", children: source }) });
@@ -188,6 +398,7 @@ function TableScroll({ children }) {
188
398
  import { defaultSchema } from "rehype-sanitize";
189
399
  var markdownSchema = {
190
400
  ...defaultSchema,
401
+ tagNames: [...defaultSchema.tagNames ?? [], ...katexSchema.tagNames],
191
402
  clobberPrefix: "",
192
403
  protocols: {
193
404
  ...defaultSchema.protocols,
@@ -197,6 +408,8 @@ var markdownSchema = {
197
408
  },
198
409
  attributes: {
199
410
  ...defaultSchema.attributes,
411
+ ...katexSchema.attributes,
412
+ span: [...katexSchema.attributes.span, ["dataCodeToken", ...CODE_TOKENS]],
200
413
  // Heading anchors (rehype-headings, below) and the code fence language.
201
414
  h1: [...defaultSchema.attributes?.h1 ?? [], "id"],
202
415
  h2: [...defaultSchema.attributes?.h2 ?? [], "id"],
@@ -340,16 +553,57 @@ import { directive } from "micromark-extension-directive";
340
553
  import { gfm } from "micromark-extension-gfm";
341
554
  import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
342
555
  import { frontmatter } from "micromark-extension-frontmatter";
556
+ import { math } from "micromark-extension-math";
343
557
  import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
558
+ import { mathFromMarkdown, mathToMarkdown } from "mdast-util-math";
344
559
  import { directiveFromMarkdown } from "mdast-util-directive";
345
560
  import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
346
561
  function micromarkExtensions() {
347
562
  const [container] = directive().flow[58];
348
563
  const containerOnly = { flow: { 58: container } };
349
- return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
564
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), dollarMath(), containerOnly];
565
+ }
566
+ var DOLLAR = 36;
567
+ var isSpace = (code) => code === null || code === void 0 || code < 0 || code === 32;
568
+ var isDigit = (code) => code != null && code >= 48 && code <= 57;
569
+ var BACKSLASH = 92;
570
+ function dollarMath() {
571
+ const extension = math();
572
+ const text = extension.text[DOLLAR];
573
+ const tokenize = function(effects, ok, nok) {
574
+ const codes = [];
575
+ let done = false;
576
+ const finish = (code) => {
577
+ done = true;
578
+ const single = codes[1] !== DOLLAR;
579
+ const after = codes[codes.length - 1];
580
+ const before = codes[codes.length - 3];
581
+ const refused = isSpace(codes[1]) || isSpace(before) || before === BACKSLASH || isDigit(after);
582
+ if (single && refused) return nok(code);
583
+ return ok(code);
584
+ };
585
+ const watch = (state) => (code) => {
586
+ if (done) return state(code);
587
+ codes.push(code);
588
+ const next = state(code);
589
+ return next && !done ? watch(next) : next;
590
+ };
591
+ return watch(
592
+ text.tokenize.call(this, effects, finish, (code) => {
593
+ done = true;
594
+ return nok(code);
595
+ })
596
+ );
597
+ };
598
+ return { ...extension, text: { [DOLLAR]: { ...text, tokenize } } };
350
599
  }
351
600
  function fromMarkdownExtensions() {
352
- return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
601
+ return [
602
+ gfmFromMarkdown(),
603
+ directiveFromMarkdown(),
604
+ frontmatterFromMarkdown(["yaml"]),
605
+ mathFromMarkdown()
606
+ ];
353
607
  }
354
608
 
355
609
  // src/directives.ts
@@ -432,7 +686,7 @@ function directive2(node, nested) {
432
686
  // src/markdown.tsx
433
687
  import { jsx as jsx3 } from "react/jsx-runtime";
434
688
  var MARKDOWN_FORMAT = "md";
435
- var RENDERER_VERSION = 5;
689
+ var RENDERER_VERSION = 6;
436
690
  var URL_ATTRIBUTES = {
437
691
  a: "href",
438
692
  img: "src",
@@ -442,7 +696,7 @@ var URL_ATTRIBUTES = {
442
696
  function rehypeResolveUrls(options) {
443
697
  return (tree) => {
444
698
  if (!options.resolve) return;
445
- visit2(tree, "element", (node) => {
699
+ visit3(tree, "element", (node) => {
446
700
  const key = URL_ATTRIBUTES[node.tagName];
447
701
  const value = key ? node.properties?.[key] : void 0;
448
702
  if (typeof value !== "string") return;
@@ -455,7 +709,7 @@ var graphemes = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Se
455
709
  function textOf2(node) {
456
710
  let text = "";
457
711
  let hasImage = false;
458
- visit2(node, (child) => {
712
+ visit3(node, (child) => {
459
713
  if (child.type === "text") text += child.value;
460
714
  else if (child.type === "element" && child.tagName === "img") hasImage = true;
461
715
  });
@@ -465,7 +719,7 @@ function rehypeShortCells(options) {
465
719
  return (tree) => {
466
720
  const { max } = options;
467
721
  if (max === false || max <= 0) return;
468
- visit2(tree, "element", (node) => {
722
+ visit3(tree, "element", (node) => {
469
723
  if (node.tagName !== "td" && node.tagName !== "th") return;
470
724
  const { text, hasImage } = textOf2(node);
471
725
  const token = text.trim();
@@ -479,7 +733,7 @@ function rehypeExternalLinks(options) {
479
733
  return (tree) => {
480
734
  if (options.policy !== "new-tab") return;
481
735
  const origin = options.origin ?? globalThis.location?.origin;
482
- visit2(tree, "element", (node) => {
736
+ visit3(tree, "element", (node) => {
483
737
  if (node.tagName !== "a") return;
484
738
  const href = node.properties?.href;
485
739
  if (typeof href !== "string" || !/^https?:\/\//i.test(href)) return;
@@ -515,6 +769,7 @@ function Markdown({
515
769
  resolveUrl,
516
770
  headingId,
517
771
  mermaid = true,
772
+ highlight = true,
518
773
  allowedElements,
519
774
  disallowedElements,
520
775
  allowElement,
@@ -548,6 +803,9 @@ function Markdown({
548
803
  rehypePlugins: [
549
804
  [rehypeResolveUrls, { resolve: resolveUrl }],
550
805
  [rehypeHeadingIds, { resolve: headingId }],
806
+ // Both BEFORE the sanitiser, which judges their output (schema.ts).
807
+ rehypeMath,
808
+ ...highlight ? [rehypeHighlight] : [],
551
809
  [rehypeSanitize, sanitizeSchema],
552
810
  [rehypeShortCells, { max: shortCellLength }],
553
811
  [rehypeExternalLinks, { policy: externalLinks, origin }],
@@ -589,9 +847,9 @@ var MARKDOWN_FIXTURES = [
589
847
  html: '<ul class="contains-task-list">\n<li class="task-list-item"><input type="checkbox" disabled="" checked=""/> done</li>\n<li class="task-list-item"><input type="checkbox" disabled=""/> todo</li>\n</ul>'
590
848
  },
591
849
  {
592
- name: "fenced code keeps its language class",
850
+ name: "fenced code keeps its language class, tokenised (renderer v6)",
593
851
  markdown: "```ts\nconst a = 1;\n```",
594
- html: '<pre><code class="language-ts">const a = 1;\n</code></pre>'
852
+ html: '<pre><code class="language-ts"><span data-code-token="keyword">const</span> a = <span data-code-token="constant">1</span>;\n</code></pre>'
595
853
  },
596
854
  {
597
855
  name: "links and autolinks",
@@ -691,6 +949,61 @@ var MARKDOWN_FIXTURES = [
691
949
  markdown: "---\ntitle: Spec\ntags: [a]\n---\n\n# Heading\n\nBody",
692
950
  html: '<h1 id="heading">Heading</h1>\n<p>Body</p>',
693
951
  forbidden: ["title: Spec", "<hr"]
952
+ },
953
+ // Renderer v6: math (KaTeX, before the sanitiser) and syntax colour (`data-code-token`).
954
+ {
955
+ name: "inline and display math are drawn by KaTeX; `\\,` keeps its backslash",
956
+ markdown: "Inline $a\\,b$ and display:\n\n$$\nx^2\n$$",
957
+ html: '<p>Inline <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mtext>\u2009</mtext><mi>b</mi></mrow><annotation encoding="application/x-tex">a\\,b</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord mathnormal">b</span></span></span></span> and display:</p>\n<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msup><mi>x</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">x^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8641em"></span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em"><span style="top:-3.113em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>'
958
+ },
959
+ {
960
+ name: "a ```math fence is display math",
961
+ markdown: "```math\n\\frac{1}{2}\n```",
962
+ html: '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow><annotation encoding="application/x-tex">\\frac{1}{2}\n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.0074em;vertical-align:-0.686em"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em"><span style="top:-2.314em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">2</span></span></span><span style="top:-3.23em"><span class="pstrut" style="height:3em"></span><span class="frac-line" style="border-bottom-width:0.04em"></span></span><span style="top:-3.677em"><span class="pstrut" style="height:3em"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">\u200B</span></span><span class="vlist-r"><span class="vlist" style="height:0.686em"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span>'
963
+ },
964
+ {
965
+ name: "prices stay text: a single `$` follows Pandoc's rule",
966
+ markdown: "$5 and $10, $5-$10, \\$x\\$",
967
+ html: "<p>$5 and $10, $5-$10, $x$</p>",
968
+ forbidden: ["katex"]
969
+ },
970
+ {
971
+ name: "a formula that does not parse shows its source",
972
+ markdown: "$\\frac{1$",
973
+ html: '<p><span class="katex-error" title="ParseError: KaTeX parse error: Unexpected end of input in a macro argument, expected &#x27;}&#x27; at end of input: \\frac{1" style="color:#cc0000">\\frac{1</span></p>'
974
+ },
975
+ {
976
+ name: "KaTeX \\href, \\url and \\includegraphics stay text (trust: false)",
977
+ hostile: true,
978
+ markdown: "$\\href{javascript:alert(1)}{a}$ $\\url{https://evil.example}$ $\\includegraphics{https://evil.example/i.png}$",
979
+ html: '<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\href</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\href{javascript:alert(1)}{a}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\href</span></span></span></span></span> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\url</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\url{https://evil.example}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\url</span></span></span></span></span> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\includegraphics</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\includegraphics{https://evil.example/i.png}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\includegraphics</span></span></span></span></span></p>',
980
+ forbidden: ["<a ", "href=", "<img", "src="]
981
+ },
982
+ {
983
+ name: "KaTeX \\htmlClass, \\htmlId, \\htmlStyle and \\htmlData stay text",
984
+ hostile: true,
985
+ markdown: "$\\htmlClass{ui-pwn}{a}$ $\\htmlId{pwn}{b}$ $\\htmlStyle{background:url(https://evil.example)}{c}$ $\\htmlData{pwn=1}{d}$",
986
+ html: '<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\htmlClass</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\htmlClass{ui-pwn}{a}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\htmlClass</span></span></span></span></span> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\htmlId</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\htmlId{pwn}{b}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\htmlId</span></span></span></span></span> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\htmlStyle</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\htmlStyle{background:url(https://evil.example)}{c}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\htmlStyle</span></span></span></span></span> <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mstyle mathcolor="#cc0000"><mtext>\\htmlData</mtext></mstyle></mrow><annotation encoding="application/x-tex">\\htmlData{pwn=1}{d}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord text" style="color:#cc0000"><span class="mord" style="color:#cc0000">\\htmlData</span></span></span></span></span></p>',
987
+ forbidden: ['class="ui-pwn"', 'id="pwn"', "data-pwn", 'style="background']
988
+ },
989
+ {
990
+ name: "a user-sized KaTeX box is capped at 10em",
991
+ hostile: true,
992
+ markdown: "$\\rule{9999em}{9999em}$",
993
+ html: '<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mpadded height="0em" voffset="0em"><mspace mathbackground="black" width="10em" height="10em"></mspace></mpadded></mrow><annotation encoding="application/x-tex">\\rule{9999em}{9999em}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:10em"></span><span class="mord rule" style="border-right-width:10em;border-top-width:10em;bottom:0em"></span></span></span></span></p>',
994
+ forbidden: ["width:9999em", 'width="9999em"']
995
+ },
996
+ {
997
+ name: "HTML in a highlighted fence stays text",
998
+ hostile: true,
999
+ markdown: "```html\n<script>alert(1)</script>\n```",
1000
+ html: '<pre><code class="language-html"><span data-code-token="punctuation">&lt;<span data-code-token="keyword">script</span>&gt;</span>alert(1)<span data-code-token="punctuation">&lt;/<span data-code-token="keyword">script</span>&gt;</span>\n</code></pre>',
1001
+ forbidden: ["<script"]
1002
+ },
1003
+ {
1004
+ name: "a fence in an unknown language stays plain",
1005
+ markdown: "```unknown\nlet a = 1\n```",
1006
+ html: '<pre><code class="language-unknown">let a = 1\n</code></pre>'
694
1007
  }
695
1008
  ];
696
1009
  export {
package/katex.css ADDED
@@ -0,0 +1,11 @@
1
+ /*
2
+ * KaTeX's stylesheet and fonts, at the version this package renders with (renderer v6).
3
+ * `import "@godxjp/markdown/katex.css";` once in the app — math draws unstyled without it.
4
+ */
5
+ @import "katex/dist/katex.min.css";
6
+
7
+ /* A display formula wider than the column scrolls, as a wide table does, instead of widening the page. */
8
+ .katex-display {
9
+ overflow-x: auto;
10
+ overflow-y: hidden;
11
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/markdown",
3
- "version": "33.17.0",
4
- "description": "The one Markdown renderer for GoDX apps: GFM + heading anchors, one sanitiser schema, a shared fixture corpus, and Mermaid drawn only after its SVG passes a fail-closed gate.",
3
+ "version": "33.18.0",
4
+ "description": "The one Markdown renderer for GoDX apps: GFM + heading anchors, KaTeX math, highlighted code, one sanitiser schema, a shared fixture corpus, and Mermaid drawn only after its SVG passes a fail-closed gate.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -15,11 +15,15 @@
15
15
  "types": "./dist/codec/index.d.ts",
16
16
  "import": "./dist/codec/index.js"
17
17
  },
18
+ "./katex.css": "./katex.css",
18
19
  "./package.json": "./package.json"
19
20
  },
20
- "sideEffects": false,
21
+ "sideEffects": [
22
+ "*.css"
23
+ ],
21
24
  "files": [
22
25
  "dist",
26
+ "katex.css",
23
27
  "README.md"
24
28
  ],
25
29
  "publishConfig": {
@@ -34,17 +38,23 @@
34
38
  "license": "Apache-2.0",
35
39
  "dependencies": {
36
40
  "github-slugger": "^2.0.0",
41
+ "highlight.js": "~11.11.1",
42
+ "katex": "^0.16.47",
43
+ "lowlight": "^3.3.0",
37
44
  "mdast-util-directive": "^3.1.1",
38
45
  "mdast-util-from-markdown": "^2.1.0",
39
46
  "mdast-util-frontmatter": "^2.0.1",
40
47
  "mdast-util-gfm": "^3.1.0",
48
+ "mdast-util-math": "^3.0.0",
41
49
  "mdast-util-to-markdown": "^2.2.0",
42
50
  "micromark-extension-cjk-friendly": "^2.0.2",
43
51
  "micromark-extension-directive": "^4.0.0",
44
52
  "micromark-extension-frontmatter": "^2.0.0",
45
53
  "micromark-extension-gfm": "^3.0.0",
54
+ "micromark-extension-math": "^3.1.0",
46
55
  "micromark-util-types": "^2.0.2",
47
56
  "react-markdown": "^10.1.0",
57
+ "rehype-katex": "^7.0.1",
48
58
  "rehype-sanitize": "^6.0.0",
49
59
  "remark-gfm": "^4.0.1",
50
60
  "unist-util-visit": "^5.1.0"
@@ -63,6 +73,8 @@
63
73
  "gfm",
64
74
  "sanitize",
65
75
  "mermaid",
76
+ "katex",
77
+ "highlight",
66
78
  "godxjp"
67
79
  ]
68
80
  }