@seed-ship/mcp-ui-solid 6.22.0 → 6.23.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/CHANGELOG.md +94 -0
- package/README.md +102 -0
- package/dist/components/UIResourceRenderer.cjs +15 -5
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +2 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +15 -5
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/context/MCPUIConfigContext.cjs.map +1 -1
- package/dist/context/MCPUIConfigContext.d.ts +57 -28
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
- package/dist/context/MCPUIConfigContext.js.map +1 -1
- package/dist/context/MCPUIMathContext.cjs +19 -0
- package/dist/context/MCPUIMathContext.cjs.map +1 -0
- package/dist/context/MCPUIMathContext.d.ts +18 -0
- package/dist/context/MCPUIMathContext.d.ts.map +1 -0
- package/dist/context/MCPUIMathContext.js +19 -0
- package/dist/context/MCPUIMathContext.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/katex.cjs +23 -0
- package/dist/plugins/katex.cjs.map +1 -0
- package/dist/plugins/katex.d.cts +7 -0
- package/dist/plugins/katex.d.ts +7 -0
- package/dist/plugins/katex.d.ts.map +1 -0
- package/dist/plugins/katex.js +23 -0
- package/dist/plugins/katex.js.map +1 -0
- package/dist/utils/iframe-coep.cjs.map +1 -1
- package/dist/utils/iframe-coep.d.ts +7 -6
- package/dist/utils/iframe-coep.d.ts.map +1 -1
- package/dist/utils/iframe-coep.js.map +1 -1
- package/dist/utils/math-markdown.cjs +240 -0
- package/dist/utils/math-markdown.cjs.map +1 -0
- package/dist/utils/math-markdown.d.ts +16 -0
- package/dist/utils/math-markdown.d.ts.map +1 -0
- package/dist/utils/math-markdown.js +240 -0
- package/dist/utils/math-markdown.js.map +1 -0
- package/package.json +22 -3
- package/src/components/UIResourceRenderer.tsx +24 -3
- package/src/context/MCPUIConfigContext.tsx +57 -28
- package/src/context/MCPUIMathContext.tsx +32 -0
- package/src/index.ts +4 -0
- package/src/plugins/katex.ts +25 -0
- package/src/utils/iframe-coep.ts +7 -6
- package/src/utils/math-markdown.ts +222 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iframe-coep.js","sources":["../../src/utils/iframe-coep.ts"],"sourcesContent":["/**\n * COEP helpers for the two iframes the library renders\n * (`IframeRenderer` in `UIResourceRenderer.tsx`, and the YouTube / Vimeo\n * embed in `VideoRenderer.tsx`).\n *\n * @since v6.21.0\n *\n * A host that serves `Cross-Origin-Embedder-Policy: credentialless` makes the\n * browser refuse any cross-origin iframe whose own document sends no COEP\n * header. The HTML attribute `credentialless` on the `<iframe>` lifts that,\n * at the price of the embedded document losing access to the cookies and\n * storage of its own origin — see `MCPUIConfigContext` for the policy.\n */\n\nimport { createSignal, onMount, type Accessor } from 'solid-js'\nimport { isTrustedIframeDomain } from '../services/validation'\nimport type { MCPUIConfig } from '../context/MCPUIConfigContext'\n\n/**\n * Does this iframe get the boolean `credentialless` attribute?\n *\n * `'auto'` (the default) says yes only when the embed has nothing to lose:\n * the host is not trusted AND the iframe already runs without cookies because\n * its `sandbox` has no `allow-same-origin` (`sandboxedWithoutSameOrigin`).\n * The video embed carries no sandbox, so in `'auto'` it keeps its provider\n * cookies and a COEP host must opt in with `'always'`.\n *\n * An unparsable URL is not trusted, so it gets the attribute.\n *\n * Takes a RESOLVED config
|
|
1
|
+
{"version":3,"file":"iframe-coep.js","sources":["../../src/utils/iframe-coep.ts"],"sourcesContent":["/**\n * COEP helpers for the two iframes the library renders\n * (`IframeRenderer` in `UIResourceRenderer.tsx`, and the YouTube / Vimeo\n * embed in `VideoRenderer.tsx`).\n *\n * @since v6.21.0\n *\n * A host that serves `Cross-Origin-Embedder-Policy: credentialless` makes the\n * browser refuse any cross-origin iframe whose own document sends no COEP\n * header. The HTML attribute `credentialless` on the `<iframe>` lifts that,\n * at the price of the embedded document losing access to the cookies and\n * storage of its own origin — see `MCPUIConfigContext` for the policy.\n */\n\nimport { createSignal, onMount, type Accessor } from 'solid-js'\nimport { isTrustedIframeDomain } from '../services/validation'\nimport type { MCPUIConfig } from '../context/MCPUIConfigContext'\n\n/**\n * Does this iframe get the boolean `credentialless` attribute?\n *\n * `'auto'` (the default) says yes only when the embed has nothing to lose:\n * the host is not trusted AND the iframe already runs without cookies because\n * its `sandbox` has no `allow-same-origin` (`sandboxedWithoutSameOrigin`).\n * The video embed carries no sandbox, so in `'auto'` it keeps its provider\n * cookies and a COEP host must opt in with `'always'`.\n *\n * An unparsable URL is not trusted, so it gets the attribute.\n *\n * Takes a RESOLVED config — `MCPUIConfig`, which is what `useMCPUIConfig()`\n * and `DEFAULT_MCPUI_CONFIG` hand out, every key present. The authored view is\n * `MCPUIConfigInput`, whose fields are optional so that a new key stays\n * backward compatible; accepting one of those here would mean re-implementing\n * the defaults next to their source, and silently drifting from them.\n */\nexport function shouldSetCredentialless(\n url: string,\n config: MCPUIConfig,\n options: { sandboxedWithoutSameOrigin: boolean }\n): boolean {\n const mode = config.iframeCredentialless\n if (mode === 'always') return true\n if (mode === 'never') return false\n if (!options.sandboxedWithoutSameOrigin) return false\n return !isTrustedIframeDomain(url, {\n customTrustedDomains: config.customTrustedIframeDomains,\n })\n}\n\n/**\n * `window.crossOriginIsolated`, read **after mount** into a signal.\n *\n * Reading it during render would make the server emit markup the client\n * cannot reproduce; the signal starts at `false` (the SSR value) and flips\n * once mounted, so hydration always matches.\n */\nexport function createCrossOriginIsolated(): Accessor<boolean> {\n const [isolated, setIsolated] = createSignal(false)\n onMount(() => {\n setIsolated(typeof window !== 'undefined' && window.crossOriginIsolated === true)\n })\n return isolated\n}\n\n/**\n * Does the \"open in a new tab\" link show under an embed?\n *\n * `'auto'` shows it only on a cross-origin-isolated page — exactly where COEP\n * can block the frame, and where the blocking is silent (a refused\n * cross-origin document fires no `error` event the parent can observe).\n *\n * Takes a resolved config, for the reason given on `shouldSetCredentialless`.\n */\nexport function shouldShowIframeFallbackLink(\n config: MCPUIConfig,\n crossOriginIsolated: boolean\n): boolean {\n const mode = config.iframeFallbackLink\n if (mode === 'always') return true\n if (mode === 'never') return false\n return crossOriginIsolated\n}\n"],"names":[],"mappings":";;AAmCO,SAAS,wBACd,KACA,QACA,SACS;AACT,QAAM,OAAO,OAAO;AACpB,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,QAAS,QAAO;AAC7B,MAAI,CAAC,QAAQ,2BAA4B,QAAO;AAChD,SAAO,CAAC,sBAAsB,KAAK;AAAA,IACjC,sBAAsB,OAAO;AAAA,EAAA,CAC9B;AACH;AASO,SAAS,4BAA+C;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAI,aAAa,KAAK;AAClD,UAAQ,MAAM;AACZ,gBAAY,OAAO,WAAW,eAAe,OAAO,wBAAwB,IAAI;AAAA,EAClF,CAAC;AACD,SAAO;AACT;AAWO,SAAS,6BACd,QACA,qBACS;AACT,QAAM,OAAO,OAAO;AACpB,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,QAAS,QAAO;AAC7B,SAAO;AACT;"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const marked = require("marked");
|
|
4
|
+
const escapeHtml = require("./escape-html.cjs");
|
|
5
|
+
const sanitizeHtml = require("./sanitize-html.cjs");
|
|
6
|
+
const MATH_MARKDOWN_LIMITS = {
|
|
7
|
+
sourceLength: 1e5,
|
|
8
|
+
expressionLength: 4096,
|
|
9
|
+
expressionCount: 128
|
|
10
|
+
};
|
|
11
|
+
const MATH_TAGS = [
|
|
12
|
+
"math",
|
|
13
|
+
"semantics",
|
|
14
|
+
"mrow",
|
|
15
|
+
"mi",
|
|
16
|
+
"mo",
|
|
17
|
+
"mn",
|
|
18
|
+
"mtext",
|
|
19
|
+
"mspace",
|
|
20
|
+
"ms",
|
|
21
|
+
"msup",
|
|
22
|
+
"msub",
|
|
23
|
+
"msubsup",
|
|
24
|
+
"mfrac",
|
|
25
|
+
"msqrt",
|
|
26
|
+
"mroot",
|
|
27
|
+
"mtable",
|
|
28
|
+
"mtr",
|
|
29
|
+
"mtd",
|
|
30
|
+
"mover",
|
|
31
|
+
"munder",
|
|
32
|
+
"munderover",
|
|
33
|
+
"mpadded",
|
|
34
|
+
"mphantom",
|
|
35
|
+
"mstyle",
|
|
36
|
+
"menclose"
|
|
37
|
+
];
|
|
38
|
+
const MATH_ATTRS = [
|
|
39
|
+
"xmlns",
|
|
40
|
+
"display",
|
|
41
|
+
"class",
|
|
42
|
+
"mathvariant",
|
|
43
|
+
"stretchy",
|
|
44
|
+
"fence",
|
|
45
|
+
"separator",
|
|
46
|
+
"lspace",
|
|
47
|
+
"rspace",
|
|
48
|
+
"accent",
|
|
49
|
+
"accentunder",
|
|
50
|
+
"columnalign",
|
|
51
|
+
"rowalign",
|
|
52
|
+
"columnspacing",
|
|
53
|
+
"rowspacing",
|
|
54
|
+
"columnlines",
|
|
55
|
+
"rowlines",
|
|
56
|
+
"frame",
|
|
57
|
+
"notation",
|
|
58
|
+
"width",
|
|
59
|
+
"height",
|
|
60
|
+
"depth",
|
|
61
|
+
"voffset",
|
|
62
|
+
"scriptlevel",
|
|
63
|
+
"displaystyle"
|
|
64
|
+
];
|
|
65
|
+
const STRICT_MATHML_CONFIG = {
|
|
66
|
+
ALLOWED_TAGS: [...MATH_TAGS, "span"],
|
|
67
|
+
ALLOWED_ATTR: [...MATH_ATTRS],
|
|
68
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style", "a"],
|
|
69
|
+
// DOMPurify normally keeps the text of forbidden tags. TeX annotations can
|
|
70
|
+
// be large and duplicate the accessible MathML, so remove their subtree.
|
|
71
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
72
|
+
FORBID_ATTR: ["style"]
|
|
73
|
+
};
|
|
74
|
+
const CELL_MARKDOWN_TAGS = [
|
|
75
|
+
"a",
|
|
76
|
+
"strong",
|
|
77
|
+
"em",
|
|
78
|
+
"b",
|
|
79
|
+
"i",
|
|
80
|
+
"code",
|
|
81
|
+
"span",
|
|
82
|
+
"br",
|
|
83
|
+
"button",
|
|
84
|
+
"svg",
|
|
85
|
+
"path",
|
|
86
|
+
"p",
|
|
87
|
+
"ul",
|
|
88
|
+
"ol",
|
|
89
|
+
"li",
|
|
90
|
+
"pre",
|
|
91
|
+
"blockquote",
|
|
92
|
+
"h1",
|
|
93
|
+
"h2",
|
|
94
|
+
"h3",
|
|
95
|
+
"h4",
|
|
96
|
+
"h5",
|
|
97
|
+
"h6",
|
|
98
|
+
...MATH_TAGS
|
|
99
|
+
];
|
|
100
|
+
const FINAL_PROFILES = {
|
|
101
|
+
prose: {
|
|
102
|
+
ADD_TAGS: [...MATH_TAGS],
|
|
103
|
+
ADD_ATTR: [...MATH_ATTRS, "target", "rel"],
|
|
104
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style", "form", "textarea", "select", "button", "iframe", "object", "embed"],
|
|
105
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
106
|
+
FORBID_ATTR: ["style"]
|
|
107
|
+
},
|
|
108
|
+
cellMarkdown: {
|
|
109
|
+
ALLOWED_TAGS: CELL_MARKDOWN_TAGS,
|
|
110
|
+
ALLOWED_ATTR: [
|
|
111
|
+
"href",
|
|
112
|
+
"target",
|
|
113
|
+
"rel",
|
|
114
|
+
"class",
|
|
115
|
+
"data-citation-page",
|
|
116
|
+
"data-citation-source",
|
|
117
|
+
"data-citation-doc",
|
|
118
|
+
"data-citation-verified",
|
|
119
|
+
"title",
|
|
120
|
+
"fill",
|
|
121
|
+
"stroke",
|
|
122
|
+
"viewBox",
|
|
123
|
+
"stroke-linecap",
|
|
124
|
+
"stroke-linejoin",
|
|
125
|
+
"stroke-width",
|
|
126
|
+
"d",
|
|
127
|
+
...MATH_ATTRS
|
|
128
|
+
],
|
|
129
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style"],
|
|
130
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
131
|
+
FORBID_ATTR: ["style"]
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
function isCurrencyLike(raw, tex, following) {
|
|
135
|
+
var _a, _b, _c;
|
|
136
|
+
if (/^\$\s|\s\$$/.test(raw)) return true;
|
|
137
|
+
if (/^\d+(?:[.,]\d+)?-\s*$/.test(tex)) return true;
|
|
138
|
+
if (/^\d/.test(tex) && /^\d/.test(following)) return true;
|
|
139
|
+
const amount = (_a = /^\d+(?:[.,]\d{1,2})?(?=$|[\s,;:!?()[\]+*/-])/.exec(tex)) == null ? void 0 : _a[0];
|
|
140
|
+
if (!amount) return false;
|
|
141
|
+
const trailing = tex.slice(amount.length);
|
|
142
|
+
if (!trailing) return false;
|
|
143
|
+
const hasUnmatchedOpeningParen = (((_b = trailing.match(/\(/g)) == null ? void 0 : _b.length) ?? 0) > (((_c = trailing.match(/\)/g)) == null ? void 0 : _c.length) ?? 0);
|
|
144
|
+
const containsTexCommand = /\\[A-Za-z]+/.test(trailing);
|
|
145
|
+
if (hasUnmatchedOpeningParen && !containsTexCommand) return true;
|
|
146
|
+
const closerLooksLikeLaterOpener = /^(?![\s,.;:!?)}\]])(?:\\[^\n]|[^\\\n$])+\$(?!\$)/.test(following);
|
|
147
|
+
return closerLooksLikeLaterOpener;
|
|
148
|
+
}
|
|
149
|
+
const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"';
|
|
150
|
+
function cellLinkExtension() {
|
|
151
|
+
return {
|
|
152
|
+
renderer: {
|
|
153
|
+
link(token) {
|
|
154
|
+
const anchor = marked.Renderer.prototype.link.call(this, token);
|
|
155
|
+
return anchor.replace("<a ", `<a ${CELL_LINK_ATTRIBUTES} `);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function mathExtension(renderer) {
|
|
161
|
+
let count = 0;
|
|
162
|
+
const token = (raw, tex, displayMode) => ({
|
|
163
|
+
type: displayMode ? "mcpDisplayMath" : "mcpInlineMath",
|
|
164
|
+
raw,
|
|
165
|
+
tex,
|
|
166
|
+
displayMode,
|
|
167
|
+
render: ++count <= MATH_MARKDOWN_LIMITS.expressionCount && tex.length <= MATH_MARKDOWN_LIMITS.expressionLength
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
extensions: [
|
|
171
|
+
{
|
|
172
|
+
name: "mcpDisplayMath",
|
|
173
|
+
level: "block",
|
|
174
|
+
start(src) {
|
|
175
|
+
const index = src.indexOf("$$");
|
|
176
|
+
return index < 0 ? void 0 : index;
|
|
177
|
+
},
|
|
178
|
+
tokenizer(src) {
|
|
179
|
+
const match = /^\$\$\s*\n?([\s\S]+?)\n?\s*\$\$(?:\n|$)/.exec(src);
|
|
180
|
+
if (!match) return void 0;
|
|
181
|
+
return token(match[0], match[1], true);
|
|
182
|
+
},
|
|
183
|
+
renderer(tokenValue) {
|
|
184
|
+
return renderToken(tokenValue, renderer);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "mcpInlineMath",
|
|
189
|
+
level: "inline",
|
|
190
|
+
start(src) {
|
|
191
|
+
const index = src.indexOf("$");
|
|
192
|
+
return index < 0 ? void 0 : index;
|
|
193
|
+
},
|
|
194
|
+
tokenizer(src) {
|
|
195
|
+
const match = /^\$(?!\$)((?:\\[^\n]|[^\\\n$])+?)\$(?!\$)/.exec(src);
|
|
196
|
+
if (!match || isCurrencyLike(match[0], match[1], src.slice(match[0].length))) return void 0;
|
|
197
|
+
return token(match[0], match[1], false);
|
|
198
|
+
},
|
|
199
|
+
renderer(tokenValue) {
|
|
200
|
+
return renderToken(tokenValue, renderer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function renderToken(token, renderer) {
|
|
207
|
+
if (!token.render) return escapeHtml.escapeHtml(token.raw);
|
|
208
|
+
try {
|
|
209
|
+
const rendered = renderer(token.tex, { displayMode: token.displayMode });
|
|
210
|
+
if (!rendered) return escapeHtml.escapeHtml(token.raw);
|
|
211
|
+
return sanitizeHtml.sanitizeHtml(rendered, STRICT_MATHML_CONFIG);
|
|
212
|
+
} catch {
|
|
213
|
+
return escapeHtml.escapeHtml(token.raw);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function parseMathMarkdown(source, renderer, profile, transformText) {
|
|
217
|
+
const input = source === null || source === void 0 ? "" : String(source);
|
|
218
|
+
if (!sanitizeHtml.canSanitizeHtml()) return escapeHtml.escapeHtml(input);
|
|
219
|
+
if (input.length > MATH_MARKDOWN_LIMITS.sourceLength) return escapeHtml.escapeHtml(input);
|
|
220
|
+
const textRenderer = transformText ? {
|
|
221
|
+
renderer: {
|
|
222
|
+
text(token) {
|
|
223
|
+
if ("tokens" in token && token.tokens) {
|
|
224
|
+
return this.parser.parseInline(token.tokens);
|
|
225
|
+
}
|
|
226
|
+
const safeText = marked.Renderer.prototype.text.call(this, token);
|
|
227
|
+
return transformText(safeText);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} : void 0;
|
|
231
|
+
const extensions = [mathExtension(renderer)];
|
|
232
|
+
if (profile === "cellMarkdown") extensions.push(cellLinkExtension());
|
|
233
|
+
if (textRenderer) extensions.push(textRenderer);
|
|
234
|
+
const parser = new marked.Marked({ gfm: true }, ...extensions);
|
|
235
|
+
const parsed = parser.parse(input, { async: false });
|
|
236
|
+
return sanitizeHtml.sanitizeHtml(parsed, FINAL_PROFILES[profile]);
|
|
237
|
+
}
|
|
238
|
+
exports.MATH_MARKDOWN_LIMITS = MATH_MARKDOWN_LIMITS;
|
|
239
|
+
exports.parseMathMarkdown = parseMathMarkdown;
|
|
240
|
+
//# sourceMappingURL=math-markdown.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"math-markdown.cjs","sources":["../../src/utils/math-markdown.ts"],"sourcesContent":["import type { Config } from 'dompurify'\nimport { Marked, Renderer, type MarkedExtension, type Tokens } from 'marked'\nimport type { MathRenderer } from '../context/MCPUIMathContext'\nimport { escapeHtml } from './escape-html'\nimport { canSanitizeHtml, sanitizeHtml } from './sanitize-html'\n\nexport type MathMarkdownProfile = 'prose' | 'cellMarkdown'\nexport type MarkdownTextTransform = (escapedText: string) => string\n\nexport const MATH_MARKDOWN_LIMITS = {\n sourceLength: 100_000,\n expressionLength: 4_096,\n expressionCount: 128,\n} as const\n\nconst MATH_TAGS = [\n 'math', 'semantics', 'mrow', 'mi', 'mo', 'mn', 'mtext', 'mspace', 'ms',\n 'msup', 'msub', 'msubsup', 'mfrac', 'msqrt', 'mroot', 'mtable', 'mtr',\n 'mtd', 'mover', 'munder', 'munderover', 'mpadded', 'mphantom', 'mstyle', 'menclose',\n] as const\n\nconst MATH_ATTRS = [\n 'xmlns', 'display', 'class', 'mathvariant', 'stretchy', 'fence', 'separator',\n 'lspace', 'rspace', 'accent', 'accentunder', 'columnalign', 'rowalign',\n 'columnspacing', 'rowspacing', 'columnlines', 'rowlines', 'frame', 'notation',\n 'width', 'height', 'depth', 'voffset', 'scriptlevel', 'displaystyle',\n] as const\n\nconst STRICT_MATHML_CONFIG = {\n ALLOWED_TAGS: [...MATH_TAGS, 'span'],\n ALLOWED_ATTR: [...MATH_ATTRS],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'a'],\n // DOMPurify normally keeps the text of forbidden tags. TeX annotations can\n // be large and duplicate the accessible MathML, so remove their subtree.\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n} as Config\n\nconst CELL_MARKDOWN_TAGS = [\n 'a', 'strong', 'em', 'b', 'i', 'code', 'span', 'br', 'button', 'svg', 'path',\n 'p', 'ul', 'ol', 'li', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n ...MATH_TAGS,\n]\n\nconst FINAL_PROFILES: Record<MathMarkdownProfile, Config> = {\n prose: {\n ADD_TAGS: [...MATH_TAGS],\n ADD_ATTR: [...MATH_ATTRS, 'target', 'rel'],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'form', 'textarea', 'select', 'button', 'iframe', 'object', 'embed'],\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n } as Config,\n cellMarkdown: {\n ALLOWED_TAGS: CELL_MARKDOWN_TAGS,\n ALLOWED_ATTR: [\n 'href', 'target', 'rel', 'class', 'data-citation-page', 'data-citation-source',\n 'data-citation-doc', 'data-citation-verified', 'title', 'fill', 'stroke',\n 'viewBox', 'stroke-linecap', 'stroke-linejoin', 'stroke-width', 'd',\n ...MATH_ATTRS,\n ],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style'],\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n } as Config,\n}\n\ninterface MathToken extends Tokens.Generic {\n type: 'mcpDisplayMath' | 'mcpInlineMath'\n raw: string\n tex: string\n displayMode: boolean\n render: boolean\n}\n\nfunction isCurrencyLike(raw: string, tex: string, following: string): boolean {\n if (/^\\$\\s|\\s\\$$/.test(raw)) return true\n if (/^\\d+(?:[.,]\\d+)?-\\s*$/.test(tex)) return true\n if (/^\\d/.test(tex) && /^\\d/.test(following)) return true\n\n // A currency opener can otherwise pair with the opening delimiter of a\n // later expression (\"$5 then ($x$)\"). Reinterpret that first candidate\n // only when it starts with a bounded numeric amount and its apparent closer\n // is immediately followed by a complete later inline body. This deliberately\n // leaves inherently ambiguous standalone forms ($5$, $2 + 3$, $2\\pi$) as\n // math and does not depend on the prose between the two delimiters.\n const amount = /^\\d+(?:[.,]\\d{1,2})?(?=$|[\\s,;:!?()[\\]+*/-])/.exec(tex)?.[0]\n if (!amount) return false\n\n // A standalone numeric token is valid math even when followed by prose or\n // another expression (\"$5$, then $x$\"). Reassignment requires content\n // beyond the amount itself.\n const trailing = tex.slice(amount.length)\n if (!trailing) return false\n\n // During streaming, the later formula may not have closed yet. An unmatched\n // opening parenthesis before its dollar is sufficient structural evidence\n // that the numeric dollar was currency; otherwise a permissive renderer can\n // briefly flash \"5 then (\" as math.\n const hasUnmatchedOpeningParen = (trailing.match(/\\(/g)?.length ?? 0) > (trailing.match(/\\)/g)?.length ?? 0)\n const containsTexCommand = /\\\\[A-Za-z]+/.test(trailing)\n if (hasUnmatchedOpeningParen && !containsTexCommand) return true\n\n const closerLooksLikeLaterOpener = /^(?![\\s,.;:!?)}\\]])(?:\\\\[^\\n]|[^\\\\\\n$])+\\$(?!\\$)/.test(following)\n return closerLooksLikeLaterOpener\n}\n\nconst CELL_LINK_ATTRIBUTES = 'target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-blue-600 dark:text-blue-400 hover:underline\"'\n\nfunction cellLinkExtension(): MarkedExtension {\n return {\n renderer: {\n link(this: Renderer, token: Tokens.Link): string {\n // Marked owns escaping of the destination, title and nested label\n // tokens. Only inject fixed attributes into its already-safe anchor.\n const anchor = Renderer.prototype.link.call(this, token) as string\n return anchor.replace('<a ', `<a ${CELL_LINK_ATTRIBUTES} `)\n },\n },\n }\n}\n\nfunction mathExtension(renderer: MathRenderer): MarkedExtension {\n let count = 0\n const token = (raw: string, tex: string, displayMode: boolean): MathToken => ({\n type: displayMode ? 'mcpDisplayMath' : 'mcpInlineMath',\n raw,\n tex,\n displayMode,\n render: ++count <= MATH_MARKDOWN_LIMITS.expressionCount && tex.length <= MATH_MARKDOWN_LIMITS.expressionLength,\n })\n\n return {\n extensions: [\n {\n name: 'mcpDisplayMath',\n level: 'block',\n start(src) {\n const index = src.indexOf('$$')\n return index < 0 ? undefined : index\n },\n tokenizer(src) {\n const match = /^\\$\\$\\s*\\n?([\\s\\S]+?)\\n?\\s*\\$\\$(?:\\n|$)/.exec(src)\n if (!match) return undefined\n return token(match[0], match[1], true)\n },\n renderer(tokenValue) {\n return renderToken(tokenValue as MathToken, renderer)\n },\n },\n {\n name: 'mcpInlineMath',\n level: 'inline',\n start(src) {\n const index = src.indexOf('$')\n return index < 0 ? undefined : index\n },\n tokenizer(src) {\n const match = /^\\$(?!\\$)((?:\\\\[^\\n]|[^\\\\\\n$])+?)\\$(?!\\$)/.exec(src)\n if (!match || isCurrencyLike(match[0], match[1], src.slice(match[0].length))) return undefined\n return token(match[0], match[1], false)\n },\n renderer(tokenValue) {\n return renderToken(tokenValue as MathToken, renderer)\n },\n },\n ],\n }\n}\n\nfunction renderToken(token: MathToken, renderer: MathRenderer): string {\n if (!token.render) return escapeHtml(token.raw)\n try {\n const rendered = renderer(token.tex, { displayMode: token.displayMode })\n if (!rendered) return escapeHtml(token.raw)\n return sanitizeHtml(rendered, STRICT_MATHML_CONFIG)\n } catch {\n return escapeHtml(token.raw)\n }\n}\n\n/**\n * Parse Markdown with isolated, bounded math support and sanitize the complete\n * result for its final sink. The optional transform sees escaped ordinary text\n * tokens only. Math, code, raw HTML and link destinations never pass through\n * it, so citation syntax cannot corrupt TeX optional arguments or attributes.\n */\nexport function parseMathMarkdown(\n source: string,\n renderer: MathRenderer,\n profile: MathMarkdownProfile,\n transformText?: MarkdownTextTransform,\n): string {\n const input = source === null || source === undefined ? '' : String(source)\n if (!canSanitizeHtml()) return escapeHtml(input)\n if (input.length > MATH_MARKDOWN_LIMITS.sourceLength) return escapeHtml(input)\n\n const textRenderer = transformText\n ? {\n renderer: {\n text(this: Renderer, token: Tokens.Text | Tokens.Escape): string {\n // Block text tokens (notably list-item text) can own already\n // tokenized inline children. Recurse first so math and code keep\n // their own renderers; leaf text callbacks run as parsing descends.\n if ('tokens' in token && token.tokens) {\n return this.parser.parseInline(token.tokens)\n }\n // Delegate entity handling and nested token rendering to Marked's\n // own renderer. `escapeHtml` is deliberately not equivalent: it\n // turns source `&` into visible `&` by double-escaping it.\n const safeText = Renderer.prototype.text.call(this, token)\n return transformText(safeText as string)\n },\n },\n }\n : undefined\n const extensions = [mathExtension(renderer)]\n if (profile === 'cellMarkdown') extensions.push(cellLinkExtension())\n if (textRenderer) extensions.push(textRenderer)\n const parser = new Marked({ gfm: true }, ...extensions)\n const parsed = parser.parse(input, { async: false }) as string\n return sanitizeHtml(parsed, FINAL_PROFILES[profile])\n}\n"],"names":["Renderer","escapeHtml","sanitizeHtml","canSanitizeHtml","Marked"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MathRenderer } from '../context/MCPUIMathContext';
|
|
2
|
+
export type MathMarkdownProfile = 'prose' | 'cellMarkdown';
|
|
3
|
+
export type MarkdownTextTransform = (escapedText: string) => string;
|
|
4
|
+
export declare const MATH_MARKDOWN_LIMITS: {
|
|
5
|
+
readonly sourceLength: 100000;
|
|
6
|
+
readonly expressionLength: 4096;
|
|
7
|
+
readonly expressionCount: 128;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Parse Markdown with isolated, bounded math support and sanitize the complete
|
|
11
|
+
* result for its final sink. The optional transform sees escaped ordinary text
|
|
12
|
+
* tokens only. Math, code, raw HTML and link destinations never pass through
|
|
13
|
+
* it, so citation syntax cannot corrupt TeX optional arguments or attributes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseMathMarkdown(source: string, renderer: MathRenderer, profile: MathMarkdownProfile, transformText?: MarkdownTextTransform): string;
|
|
16
|
+
//# sourceMappingURL=math-markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"math-markdown.d.ts","sourceRoot":"","sources":["../../src/utils/math-markdown.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAI/D,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,cAAc,CAAA;AAC1D,MAAM,MAAM,qBAAqB,GAAG,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAA;AAEnE,eAAO,MAAM,oBAAoB;;;;CAIvB,CAAA;AAuKV;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,mBAAmB,EAC5B,aAAa,CAAC,EAAE,qBAAqB,GACpC,MAAM,CA8BR"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Marked, Renderer } from "marked";
|
|
2
|
+
import { escapeHtml } from "./escape-html.js";
|
|
3
|
+
import { canSanitizeHtml, sanitizeHtml } from "./sanitize-html.js";
|
|
4
|
+
const MATH_MARKDOWN_LIMITS = {
|
|
5
|
+
sourceLength: 1e5,
|
|
6
|
+
expressionLength: 4096,
|
|
7
|
+
expressionCount: 128
|
|
8
|
+
};
|
|
9
|
+
const MATH_TAGS = [
|
|
10
|
+
"math",
|
|
11
|
+
"semantics",
|
|
12
|
+
"mrow",
|
|
13
|
+
"mi",
|
|
14
|
+
"mo",
|
|
15
|
+
"mn",
|
|
16
|
+
"mtext",
|
|
17
|
+
"mspace",
|
|
18
|
+
"ms",
|
|
19
|
+
"msup",
|
|
20
|
+
"msub",
|
|
21
|
+
"msubsup",
|
|
22
|
+
"mfrac",
|
|
23
|
+
"msqrt",
|
|
24
|
+
"mroot",
|
|
25
|
+
"mtable",
|
|
26
|
+
"mtr",
|
|
27
|
+
"mtd",
|
|
28
|
+
"mover",
|
|
29
|
+
"munder",
|
|
30
|
+
"munderover",
|
|
31
|
+
"mpadded",
|
|
32
|
+
"mphantom",
|
|
33
|
+
"mstyle",
|
|
34
|
+
"menclose"
|
|
35
|
+
];
|
|
36
|
+
const MATH_ATTRS = [
|
|
37
|
+
"xmlns",
|
|
38
|
+
"display",
|
|
39
|
+
"class",
|
|
40
|
+
"mathvariant",
|
|
41
|
+
"stretchy",
|
|
42
|
+
"fence",
|
|
43
|
+
"separator",
|
|
44
|
+
"lspace",
|
|
45
|
+
"rspace",
|
|
46
|
+
"accent",
|
|
47
|
+
"accentunder",
|
|
48
|
+
"columnalign",
|
|
49
|
+
"rowalign",
|
|
50
|
+
"columnspacing",
|
|
51
|
+
"rowspacing",
|
|
52
|
+
"columnlines",
|
|
53
|
+
"rowlines",
|
|
54
|
+
"frame",
|
|
55
|
+
"notation",
|
|
56
|
+
"width",
|
|
57
|
+
"height",
|
|
58
|
+
"depth",
|
|
59
|
+
"voffset",
|
|
60
|
+
"scriptlevel",
|
|
61
|
+
"displaystyle"
|
|
62
|
+
];
|
|
63
|
+
const STRICT_MATHML_CONFIG = {
|
|
64
|
+
ALLOWED_TAGS: [...MATH_TAGS, "span"],
|
|
65
|
+
ALLOWED_ATTR: [...MATH_ATTRS],
|
|
66
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style", "a"],
|
|
67
|
+
// DOMPurify normally keeps the text of forbidden tags. TeX annotations can
|
|
68
|
+
// be large and duplicate the accessible MathML, so remove their subtree.
|
|
69
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
70
|
+
FORBID_ATTR: ["style"]
|
|
71
|
+
};
|
|
72
|
+
const CELL_MARKDOWN_TAGS = [
|
|
73
|
+
"a",
|
|
74
|
+
"strong",
|
|
75
|
+
"em",
|
|
76
|
+
"b",
|
|
77
|
+
"i",
|
|
78
|
+
"code",
|
|
79
|
+
"span",
|
|
80
|
+
"br",
|
|
81
|
+
"button",
|
|
82
|
+
"svg",
|
|
83
|
+
"path",
|
|
84
|
+
"p",
|
|
85
|
+
"ul",
|
|
86
|
+
"ol",
|
|
87
|
+
"li",
|
|
88
|
+
"pre",
|
|
89
|
+
"blockquote",
|
|
90
|
+
"h1",
|
|
91
|
+
"h2",
|
|
92
|
+
"h3",
|
|
93
|
+
"h4",
|
|
94
|
+
"h5",
|
|
95
|
+
"h6",
|
|
96
|
+
...MATH_TAGS
|
|
97
|
+
];
|
|
98
|
+
const FINAL_PROFILES = {
|
|
99
|
+
prose: {
|
|
100
|
+
ADD_TAGS: [...MATH_TAGS],
|
|
101
|
+
ADD_ATTR: [...MATH_ATTRS, "target", "rel"],
|
|
102
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style", "form", "textarea", "select", "button", "iframe", "object", "embed"],
|
|
103
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
104
|
+
FORBID_ATTR: ["style"]
|
|
105
|
+
},
|
|
106
|
+
cellMarkdown: {
|
|
107
|
+
ALLOWED_TAGS: CELL_MARKDOWN_TAGS,
|
|
108
|
+
ALLOWED_ATTR: [
|
|
109
|
+
"href",
|
|
110
|
+
"target",
|
|
111
|
+
"rel",
|
|
112
|
+
"class",
|
|
113
|
+
"data-citation-page",
|
|
114
|
+
"data-citation-source",
|
|
115
|
+
"data-citation-doc",
|
|
116
|
+
"data-citation-verified",
|
|
117
|
+
"title",
|
|
118
|
+
"fill",
|
|
119
|
+
"stroke",
|
|
120
|
+
"viewBox",
|
|
121
|
+
"stroke-linecap",
|
|
122
|
+
"stroke-linejoin",
|
|
123
|
+
"stroke-width",
|
|
124
|
+
"d",
|
|
125
|
+
...MATH_ATTRS
|
|
126
|
+
],
|
|
127
|
+
FORBID_TAGS: ["annotation", "annotation-xml", "style"],
|
|
128
|
+
ADD_FORBID_CONTENTS: ["annotation", "annotation-xml"],
|
|
129
|
+
FORBID_ATTR: ["style"]
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function isCurrencyLike(raw, tex, following) {
|
|
133
|
+
var _a, _b, _c;
|
|
134
|
+
if (/^\$\s|\s\$$/.test(raw)) return true;
|
|
135
|
+
if (/^\d+(?:[.,]\d+)?-\s*$/.test(tex)) return true;
|
|
136
|
+
if (/^\d/.test(tex) && /^\d/.test(following)) return true;
|
|
137
|
+
const amount = (_a = /^\d+(?:[.,]\d{1,2})?(?=$|[\s,;:!?()[\]+*/-])/.exec(tex)) == null ? void 0 : _a[0];
|
|
138
|
+
if (!amount) return false;
|
|
139
|
+
const trailing = tex.slice(amount.length);
|
|
140
|
+
if (!trailing) return false;
|
|
141
|
+
const hasUnmatchedOpeningParen = (((_b = trailing.match(/\(/g)) == null ? void 0 : _b.length) ?? 0) > (((_c = trailing.match(/\)/g)) == null ? void 0 : _c.length) ?? 0);
|
|
142
|
+
const containsTexCommand = /\\[A-Za-z]+/.test(trailing);
|
|
143
|
+
if (hasUnmatchedOpeningParen && !containsTexCommand) return true;
|
|
144
|
+
const closerLooksLikeLaterOpener = /^(?![\s,.;:!?)}\]])(?:\\[^\n]|[^\\\n$])+\$(?!\$)/.test(following);
|
|
145
|
+
return closerLooksLikeLaterOpener;
|
|
146
|
+
}
|
|
147
|
+
const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"';
|
|
148
|
+
function cellLinkExtension() {
|
|
149
|
+
return {
|
|
150
|
+
renderer: {
|
|
151
|
+
link(token) {
|
|
152
|
+
const anchor = Renderer.prototype.link.call(this, token);
|
|
153
|
+
return anchor.replace("<a ", `<a ${CELL_LINK_ATTRIBUTES} `);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function mathExtension(renderer) {
|
|
159
|
+
let count = 0;
|
|
160
|
+
const token = (raw, tex, displayMode) => ({
|
|
161
|
+
type: displayMode ? "mcpDisplayMath" : "mcpInlineMath",
|
|
162
|
+
raw,
|
|
163
|
+
tex,
|
|
164
|
+
displayMode,
|
|
165
|
+
render: ++count <= MATH_MARKDOWN_LIMITS.expressionCount && tex.length <= MATH_MARKDOWN_LIMITS.expressionLength
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
extensions: [
|
|
169
|
+
{
|
|
170
|
+
name: "mcpDisplayMath",
|
|
171
|
+
level: "block",
|
|
172
|
+
start(src) {
|
|
173
|
+
const index = src.indexOf("$$");
|
|
174
|
+
return index < 0 ? void 0 : index;
|
|
175
|
+
},
|
|
176
|
+
tokenizer(src) {
|
|
177
|
+
const match = /^\$\$\s*\n?([\s\S]+?)\n?\s*\$\$(?:\n|$)/.exec(src);
|
|
178
|
+
if (!match) return void 0;
|
|
179
|
+
return token(match[0], match[1], true);
|
|
180
|
+
},
|
|
181
|
+
renderer(tokenValue) {
|
|
182
|
+
return renderToken(tokenValue, renderer);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "mcpInlineMath",
|
|
187
|
+
level: "inline",
|
|
188
|
+
start(src) {
|
|
189
|
+
const index = src.indexOf("$");
|
|
190
|
+
return index < 0 ? void 0 : index;
|
|
191
|
+
},
|
|
192
|
+
tokenizer(src) {
|
|
193
|
+
const match = /^\$(?!\$)((?:\\[^\n]|[^\\\n$])+?)\$(?!\$)/.exec(src);
|
|
194
|
+
if (!match || isCurrencyLike(match[0], match[1], src.slice(match[0].length))) return void 0;
|
|
195
|
+
return token(match[0], match[1], false);
|
|
196
|
+
},
|
|
197
|
+
renderer(tokenValue) {
|
|
198
|
+
return renderToken(tokenValue, renderer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function renderToken(token, renderer) {
|
|
205
|
+
if (!token.render) return escapeHtml(token.raw);
|
|
206
|
+
try {
|
|
207
|
+
const rendered = renderer(token.tex, { displayMode: token.displayMode });
|
|
208
|
+
if (!rendered) return escapeHtml(token.raw);
|
|
209
|
+
return sanitizeHtml(rendered, STRICT_MATHML_CONFIG);
|
|
210
|
+
} catch {
|
|
211
|
+
return escapeHtml(token.raw);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function parseMathMarkdown(source, renderer, profile, transformText) {
|
|
215
|
+
const input = source === null || source === void 0 ? "" : String(source);
|
|
216
|
+
if (!canSanitizeHtml()) return escapeHtml(input);
|
|
217
|
+
if (input.length > MATH_MARKDOWN_LIMITS.sourceLength) return escapeHtml(input);
|
|
218
|
+
const textRenderer = transformText ? {
|
|
219
|
+
renderer: {
|
|
220
|
+
text(token) {
|
|
221
|
+
if ("tokens" in token && token.tokens) {
|
|
222
|
+
return this.parser.parseInline(token.tokens);
|
|
223
|
+
}
|
|
224
|
+
const safeText = Renderer.prototype.text.call(this, token);
|
|
225
|
+
return transformText(safeText);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} : void 0;
|
|
229
|
+
const extensions = [mathExtension(renderer)];
|
|
230
|
+
if (profile === "cellMarkdown") extensions.push(cellLinkExtension());
|
|
231
|
+
if (textRenderer) extensions.push(textRenderer);
|
|
232
|
+
const parser = new Marked({ gfm: true }, ...extensions);
|
|
233
|
+
const parsed = parser.parse(input, { async: false });
|
|
234
|
+
return sanitizeHtml(parsed, FINAL_PROFILES[profile]);
|
|
235
|
+
}
|
|
236
|
+
export {
|
|
237
|
+
MATH_MARKDOWN_LIMITS,
|
|
238
|
+
parseMathMarkdown
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=math-markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"math-markdown.js","sources":["../../src/utils/math-markdown.ts"],"sourcesContent":["import type { Config } from 'dompurify'\nimport { Marked, Renderer, type MarkedExtension, type Tokens } from 'marked'\nimport type { MathRenderer } from '../context/MCPUIMathContext'\nimport { escapeHtml } from './escape-html'\nimport { canSanitizeHtml, sanitizeHtml } from './sanitize-html'\n\nexport type MathMarkdownProfile = 'prose' | 'cellMarkdown'\nexport type MarkdownTextTransform = (escapedText: string) => string\n\nexport const MATH_MARKDOWN_LIMITS = {\n sourceLength: 100_000,\n expressionLength: 4_096,\n expressionCount: 128,\n} as const\n\nconst MATH_TAGS = [\n 'math', 'semantics', 'mrow', 'mi', 'mo', 'mn', 'mtext', 'mspace', 'ms',\n 'msup', 'msub', 'msubsup', 'mfrac', 'msqrt', 'mroot', 'mtable', 'mtr',\n 'mtd', 'mover', 'munder', 'munderover', 'mpadded', 'mphantom', 'mstyle', 'menclose',\n] as const\n\nconst MATH_ATTRS = [\n 'xmlns', 'display', 'class', 'mathvariant', 'stretchy', 'fence', 'separator',\n 'lspace', 'rspace', 'accent', 'accentunder', 'columnalign', 'rowalign',\n 'columnspacing', 'rowspacing', 'columnlines', 'rowlines', 'frame', 'notation',\n 'width', 'height', 'depth', 'voffset', 'scriptlevel', 'displaystyle',\n] as const\n\nconst STRICT_MATHML_CONFIG = {\n ALLOWED_TAGS: [...MATH_TAGS, 'span'],\n ALLOWED_ATTR: [...MATH_ATTRS],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'a'],\n // DOMPurify normally keeps the text of forbidden tags. TeX annotations can\n // be large and duplicate the accessible MathML, so remove their subtree.\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n} as Config\n\nconst CELL_MARKDOWN_TAGS = [\n 'a', 'strong', 'em', 'b', 'i', 'code', 'span', 'br', 'button', 'svg', 'path',\n 'p', 'ul', 'ol', 'li', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n ...MATH_TAGS,\n]\n\nconst FINAL_PROFILES: Record<MathMarkdownProfile, Config> = {\n prose: {\n ADD_TAGS: [...MATH_TAGS],\n ADD_ATTR: [...MATH_ATTRS, 'target', 'rel'],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'form', 'textarea', 'select', 'button', 'iframe', 'object', 'embed'],\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n } as Config,\n cellMarkdown: {\n ALLOWED_TAGS: CELL_MARKDOWN_TAGS,\n ALLOWED_ATTR: [\n 'href', 'target', 'rel', 'class', 'data-citation-page', 'data-citation-source',\n 'data-citation-doc', 'data-citation-verified', 'title', 'fill', 'stroke',\n 'viewBox', 'stroke-linecap', 'stroke-linejoin', 'stroke-width', 'd',\n ...MATH_ATTRS,\n ],\n FORBID_TAGS: ['annotation', 'annotation-xml', 'style'],\n ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],\n FORBID_ATTR: ['style'],\n } as Config,\n}\n\ninterface MathToken extends Tokens.Generic {\n type: 'mcpDisplayMath' | 'mcpInlineMath'\n raw: string\n tex: string\n displayMode: boolean\n render: boolean\n}\n\nfunction isCurrencyLike(raw: string, tex: string, following: string): boolean {\n if (/^\\$\\s|\\s\\$$/.test(raw)) return true\n if (/^\\d+(?:[.,]\\d+)?-\\s*$/.test(tex)) return true\n if (/^\\d/.test(tex) && /^\\d/.test(following)) return true\n\n // A currency opener can otherwise pair with the opening delimiter of a\n // later expression (\"$5 then ($x$)\"). Reinterpret that first candidate\n // only when it starts with a bounded numeric amount and its apparent closer\n // is immediately followed by a complete later inline body. This deliberately\n // leaves inherently ambiguous standalone forms ($5$, $2 + 3$, $2\\pi$) as\n // math and does not depend on the prose between the two delimiters.\n const amount = /^\\d+(?:[.,]\\d{1,2})?(?=$|[\\s,;:!?()[\\]+*/-])/.exec(tex)?.[0]\n if (!amount) return false\n\n // A standalone numeric token is valid math even when followed by prose or\n // another expression (\"$5$, then $x$\"). Reassignment requires content\n // beyond the amount itself.\n const trailing = tex.slice(amount.length)\n if (!trailing) return false\n\n // During streaming, the later formula may not have closed yet. An unmatched\n // opening parenthesis before its dollar is sufficient structural evidence\n // that the numeric dollar was currency; otherwise a permissive renderer can\n // briefly flash \"5 then (\" as math.\n const hasUnmatchedOpeningParen = (trailing.match(/\\(/g)?.length ?? 0) > (trailing.match(/\\)/g)?.length ?? 0)\n const containsTexCommand = /\\\\[A-Za-z]+/.test(trailing)\n if (hasUnmatchedOpeningParen && !containsTexCommand) return true\n\n const closerLooksLikeLaterOpener = /^(?![\\s,.;:!?)}\\]])(?:\\\\[^\\n]|[^\\\\\\n$])+\\$(?!\\$)/.test(following)\n return closerLooksLikeLaterOpener\n}\n\nconst CELL_LINK_ATTRIBUTES = 'target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-blue-600 dark:text-blue-400 hover:underline\"'\n\nfunction cellLinkExtension(): MarkedExtension {\n return {\n renderer: {\n link(this: Renderer, token: Tokens.Link): string {\n // Marked owns escaping of the destination, title and nested label\n // tokens. Only inject fixed attributes into its already-safe anchor.\n const anchor = Renderer.prototype.link.call(this, token) as string\n return anchor.replace('<a ', `<a ${CELL_LINK_ATTRIBUTES} `)\n },\n },\n }\n}\n\nfunction mathExtension(renderer: MathRenderer): MarkedExtension {\n let count = 0\n const token = (raw: string, tex: string, displayMode: boolean): MathToken => ({\n type: displayMode ? 'mcpDisplayMath' : 'mcpInlineMath',\n raw,\n tex,\n displayMode,\n render: ++count <= MATH_MARKDOWN_LIMITS.expressionCount && tex.length <= MATH_MARKDOWN_LIMITS.expressionLength,\n })\n\n return {\n extensions: [\n {\n name: 'mcpDisplayMath',\n level: 'block',\n start(src) {\n const index = src.indexOf('$$')\n return index < 0 ? undefined : index\n },\n tokenizer(src) {\n const match = /^\\$\\$\\s*\\n?([\\s\\S]+?)\\n?\\s*\\$\\$(?:\\n|$)/.exec(src)\n if (!match) return undefined\n return token(match[0], match[1], true)\n },\n renderer(tokenValue) {\n return renderToken(tokenValue as MathToken, renderer)\n },\n },\n {\n name: 'mcpInlineMath',\n level: 'inline',\n start(src) {\n const index = src.indexOf('$')\n return index < 0 ? undefined : index\n },\n tokenizer(src) {\n const match = /^\\$(?!\\$)((?:\\\\[^\\n]|[^\\\\\\n$])+?)\\$(?!\\$)/.exec(src)\n if (!match || isCurrencyLike(match[0], match[1], src.slice(match[0].length))) return undefined\n return token(match[0], match[1], false)\n },\n renderer(tokenValue) {\n return renderToken(tokenValue as MathToken, renderer)\n },\n },\n ],\n }\n}\n\nfunction renderToken(token: MathToken, renderer: MathRenderer): string {\n if (!token.render) return escapeHtml(token.raw)\n try {\n const rendered = renderer(token.tex, { displayMode: token.displayMode })\n if (!rendered) return escapeHtml(token.raw)\n return sanitizeHtml(rendered, STRICT_MATHML_CONFIG)\n } catch {\n return escapeHtml(token.raw)\n }\n}\n\n/**\n * Parse Markdown with isolated, bounded math support and sanitize the complete\n * result for its final sink. The optional transform sees escaped ordinary text\n * tokens only. Math, code, raw HTML and link destinations never pass through\n * it, so citation syntax cannot corrupt TeX optional arguments or attributes.\n */\nexport function parseMathMarkdown(\n source: string,\n renderer: MathRenderer,\n profile: MathMarkdownProfile,\n transformText?: MarkdownTextTransform,\n): string {\n const input = source === null || source === undefined ? '' : String(source)\n if (!canSanitizeHtml()) return escapeHtml(input)\n if (input.length > MATH_MARKDOWN_LIMITS.sourceLength) return escapeHtml(input)\n\n const textRenderer = transformText\n ? {\n renderer: {\n text(this: Renderer, token: Tokens.Text | Tokens.Escape): string {\n // Block text tokens (notably list-item text) can own already\n // tokenized inline children. Recurse first so math and code keep\n // their own renderers; leaf text callbacks run as parsing descends.\n if ('tokens' in token && token.tokens) {\n return this.parser.parseInline(token.tokens)\n }\n // Delegate entity handling and nested token rendering to Marked's\n // own renderer. `escapeHtml` is deliberately not equivalent: it\n // turns source `&` into visible `&` by double-escaping it.\n const safeText = Renderer.prototype.text.call(this, token)\n return transformText(safeText as string)\n },\n },\n }\n : undefined\n const extensions = [mathExtension(renderer)]\n if (profile === 'cellMarkdown') extensions.push(cellLinkExtension())\n if (textRenderer) extensions.push(textRenderer)\n const parser = new Marked({ gfm: true }, ...extensions)\n const parsed = parser.parse(input, { async: false }) as string\n return sanitizeHtml(parsed, FINAL_PROFILES[profile])\n}\n"],"names":[],"mappings":"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"}
|