@seed-ship/mcp-ui-solid 6.21.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 +173 -0
- package/README.md +102 -0
- package/dist/components/ChartJSRenderer.cjs +244 -68
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts +9 -0
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +246 -70
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +9 -1
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +9 -1
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/LightboxOverlay.cjs +139 -16
- package/dist/components/LightboxOverlay.cjs.map +1 -1
- package/dist/components/LightboxOverlay.d.ts +6 -0
- package/dist/components/LightboxOverlay.d.ts.map +1 -1
- package/dist/components/LightboxOverlay.js +141 -18
- package/dist/components/LightboxOverlay.js.map +1 -1
- 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 +2 -1
- package/dist/context/MCPUIConfigContext.cjs.map +1 -1
- package/dist/context/MCPUIConfigContext.d.ts +90 -12
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
- package/dist/context/MCPUIConfigContext.js +2 -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/context/MCPUIStringsContext.cjs +6 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +22 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +6 -1
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- 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 +8 -0
- 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/dist/utils/pan-zoom.cjs +312 -0
- package/dist/utils/pan-zoom.cjs.map +1 -0
- package/dist/utils/pan-zoom.d.ts +235 -0
- package/dist/utils/pan-zoom.d.ts.map +1 -0
- package/dist/utils/pan-zoom.js +312 -0
- package/dist/utils/pan-zoom.js.map +1 -0
- package/package.json +26 -3
- package/src/components/ChartJSRenderer.tsx +396 -1
- package/src/components/ExpandableWrapper.tsx +26 -0
- package/src/components/LightboxOverlay.tsx +280 -4
- package/src/components/UIResourceRenderer.tsx +24 -3
- package/src/context/MCPUIConfigContext.tsx +92 -12
- package/src/context/MCPUIMathContext.tsx +32 -0
- package/src/context/MCPUIStringsContext.tsx +30 -0
- package/src/hooks/index.ts +13 -0
- package/src/index.ts +20 -0
- package/src/plugins/katex.ts +25 -0
- package/src/utils/iframe-coep.ts +8 -0
- package/src/utils/math-markdown.ts +222 -0
- package/src/utils/pan-zoom.ts +779 -0
|
@@ -274,6 +274,30 @@ export interface MCPUIStrings {
|
|
|
274
274
|
/** `aria-label` of the lightbox "next image" button. */
|
|
275
275
|
lightboxNext?: string
|
|
276
276
|
|
|
277
|
+
// ── Zoom controls (lightbox, chart) ────────────────────────
|
|
278
|
+
/**
|
|
279
|
+
* `aria-label` / `title` of the "zoom in" button.
|
|
280
|
+
* @since 6.22.0
|
|
281
|
+
*/
|
|
282
|
+
zoomIn?: string
|
|
283
|
+
/**
|
|
284
|
+
* `aria-label` / `title` of the "zoom out" button.
|
|
285
|
+
* @since 6.22.0
|
|
286
|
+
*/
|
|
287
|
+
zoomOut?: string
|
|
288
|
+
/**
|
|
289
|
+
* `aria-label` / `title` of the "reset zoom" button — the one the chart
|
|
290
|
+
* toolbar only shows while the chart is actually zoomed.
|
|
291
|
+
* @since 6.22.0
|
|
292
|
+
*/
|
|
293
|
+
zoomReset?: string
|
|
294
|
+
/**
|
|
295
|
+
* Zoom-level readout next to the zoom buttons.
|
|
296
|
+
* Template — `{percent}` = the scale as a whole percentage (`100` at rest).
|
|
297
|
+
* @since 6.22.0
|
|
298
|
+
*/
|
|
299
|
+
zoomLevel?: string
|
|
300
|
+
|
|
277
301
|
// ── ModalRenderer ──────────────────────────────────────────
|
|
278
302
|
/** `aria-label` of the modal close button. */
|
|
279
303
|
modalClose?: string
|
|
@@ -1095,6 +1119,12 @@ export const DEFAULT_MCPUI_STRINGS: Required<MCPUIStrings> = {
|
|
|
1095
1119
|
|
|
1096
1120
|
// ── 6.21.0 — iframe COEP fallback ──────────────────────────
|
|
1097
1121
|
iframeOpenInNewTab: 'Open in a new tab',
|
|
1122
|
+
|
|
1123
|
+
// ── 6.22.0 — zoom / pan controls ───────────────────────────
|
|
1124
|
+
zoomIn: 'Zoom in',
|
|
1125
|
+
zoomOut: 'Zoom out',
|
|
1126
|
+
zoomReset: 'Reset zoom',
|
|
1127
|
+
zoomLevel: 'Zoom {percent}%',
|
|
1098
1128
|
}
|
|
1099
1129
|
|
|
1100
1130
|
/**
|
package/src/hooks/index.ts
CHANGED
|
@@ -59,6 +59,19 @@ export type {
|
|
|
59
59
|
UseAutocompleteReturn,
|
|
60
60
|
} from './useAutocomplete'
|
|
61
61
|
|
|
62
|
+
// Pan/zoom primitive (v6.22.0). It lives under `src/utils` because it is pure
|
|
63
|
+
// arithmetic plus two signals, but it is reactive and registers an onCleanup,
|
|
64
|
+
// so it is used exactly like a hook — hence this re-export on the `./hooks`
|
|
65
|
+
// subpath as well as on the root barrel.
|
|
66
|
+
export { createPanZoom } from '../utils/pan-zoom'
|
|
67
|
+
export type {
|
|
68
|
+
PanZoomOptions,
|
|
69
|
+
PanZoomController,
|
|
70
|
+
PanZoomState,
|
|
71
|
+
PanZoomPoint,
|
|
72
|
+
PanZoomCursor,
|
|
73
|
+
} from '../utils/pan-zoom'
|
|
74
|
+
|
|
62
75
|
// Data Validator hooks (v3.1.0 — anti-hallucination)
|
|
63
76
|
export { useDataValidator } from './useDataValidator'
|
|
64
77
|
export type {
|
package/src/index.ts
CHANGED
|
@@ -128,6 +128,22 @@ export type {
|
|
|
128
128
|
DuplicateMountReporter,
|
|
129
129
|
} from './utils/duplicate-mount-registry'
|
|
130
130
|
|
|
131
|
+
// Cursor-anchored zoom + drag-pan primitive (v6.22.0 — lightbox, expanded views)
|
|
132
|
+
export {
|
|
133
|
+
createPanZoom,
|
|
134
|
+
clampScale,
|
|
135
|
+
contentPointAt,
|
|
136
|
+
viewportPointOf,
|
|
137
|
+
zoomAtAnchor,
|
|
138
|
+
} from './utils/pan-zoom'
|
|
139
|
+
export type {
|
|
140
|
+
PanZoomOptions,
|
|
141
|
+
PanZoomController,
|
|
142
|
+
PanZoomState,
|
|
143
|
+
PanZoomPoint,
|
|
144
|
+
PanZoomCursor,
|
|
145
|
+
} from './utils/pan-zoom'
|
|
146
|
+
|
|
131
147
|
// Chrome i18n — library's own strings, EN defaults (v6.6.0 — D2/R4)
|
|
132
148
|
export {
|
|
133
149
|
MCPUIStringsProvider,
|
|
@@ -151,6 +167,7 @@ export {
|
|
|
151
167
|
} from './context/MCPUIConfigContext'
|
|
152
168
|
export type {
|
|
153
169
|
MCPUIConfig,
|
|
170
|
+
MCPUIConfigInput,
|
|
154
171
|
MCPUIConfigProviderProps,
|
|
155
172
|
} from './context/MCPUIConfigContext'
|
|
156
173
|
|
|
@@ -373,6 +390,9 @@ export {
|
|
|
373
390
|
DEGRADED_PROJECTION_LABELS,
|
|
374
391
|
} from './utils/degraded-projections'
|
|
375
392
|
export type { DegradedProjectionLabels } from './utils/degraded-projections'
|
|
393
|
+
export { MCPUIMathProvider } from './context/MCPUIMathContext'
|
|
394
|
+
export type { MathRenderer, MathRenderOptions, MCPUIMathProviderProps } from './context/MCPUIMathContext'
|
|
395
|
+
|
|
376
396
|
export { chartToDataTable, CHART_DATA_TABLE_LABELS } from './components/chart-data-table'
|
|
377
397
|
export type { ChartDataTableLabels } from './components/chart-data-table'
|
|
378
398
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import katex from 'katex'
|
|
2
|
+
import type { MathRenderer } from '../context/MCPUIMathContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional KaTeX adapter. It emits MathML only: no HTML layout tree, inline
|
|
6
|
+
* styles, fonts, stylesheet, shared macro state, or trusted TeX commands.
|
|
7
|
+
*/
|
|
8
|
+
export const renderKatexMath: MathRenderer = (tex, { displayMode }) => {
|
|
9
|
+
try {
|
|
10
|
+
return katex.renderToString(tex, {
|
|
11
|
+
displayMode,
|
|
12
|
+
output: 'mathml',
|
|
13
|
+
// Invalid TeX must return null so the core can preserve the complete
|
|
14
|
+
// source token, including its delimiters, instead of showing KaTeX's
|
|
15
|
+
// red error MathML and silently consuming the dollars.
|
|
16
|
+
throwOnError: true,
|
|
17
|
+
trust: false,
|
|
18
|
+
maxExpand: 1000,
|
|
19
|
+
maxSize: 20,
|
|
20
|
+
macros: {},
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/utils/iframe-coep.ts
CHANGED
|
@@ -26,6 +26,12 @@ import type { MCPUIConfig } from '../context/MCPUIConfigContext'
|
|
|
26
26
|
* cookies and a COEP host must opt in with `'always'`.
|
|
27
27
|
*
|
|
28
28
|
* An unparsable URL is not trusted, so it gets the attribute.
|
|
29
|
+
*
|
|
30
|
+
* Takes a RESOLVED config — `MCPUIConfig`, which is what `useMCPUIConfig()`
|
|
31
|
+
* and `DEFAULT_MCPUI_CONFIG` hand out, every key present. The authored view is
|
|
32
|
+
* `MCPUIConfigInput`, whose fields are optional so that a new key stays
|
|
33
|
+
* backward compatible; accepting one of those here would mean re-implementing
|
|
34
|
+
* the defaults next to their source, and silently drifting from them.
|
|
29
35
|
*/
|
|
30
36
|
export function shouldSetCredentialless(
|
|
31
37
|
url: string,
|
|
@@ -62,6 +68,8 @@ export function createCrossOriginIsolated(): Accessor<boolean> {
|
|
|
62
68
|
* `'auto'` shows it only on a cross-origin-isolated page — exactly where COEP
|
|
63
69
|
* can block the frame, and where the blocking is silent (a refused
|
|
64
70
|
* cross-origin document fires no `error` event the parent can observe).
|
|
71
|
+
*
|
|
72
|
+
* Takes a resolved config, for the reason given on `shouldSetCredentialless`.
|
|
65
73
|
*/
|
|
66
74
|
export function shouldShowIframeFallbackLink(
|
|
67
75
|
config: MCPUIConfig,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Config } from 'dompurify'
|
|
2
|
+
import { Marked, Renderer, type MarkedExtension, type Tokens } from 'marked'
|
|
3
|
+
import type { MathRenderer } from '../context/MCPUIMathContext'
|
|
4
|
+
import { escapeHtml } from './escape-html'
|
|
5
|
+
import { canSanitizeHtml, sanitizeHtml } from './sanitize-html'
|
|
6
|
+
|
|
7
|
+
export type MathMarkdownProfile = 'prose' | 'cellMarkdown'
|
|
8
|
+
export type MarkdownTextTransform = (escapedText: string) => string
|
|
9
|
+
|
|
10
|
+
export const MATH_MARKDOWN_LIMITS = {
|
|
11
|
+
sourceLength: 100_000,
|
|
12
|
+
expressionLength: 4_096,
|
|
13
|
+
expressionCount: 128,
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
const MATH_TAGS = [
|
|
17
|
+
'math', 'semantics', 'mrow', 'mi', 'mo', 'mn', 'mtext', 'mspace', 'ms',
|
|
18
|
+
'msup', 'msub', 'msubsup', 'mfrac', 'msqrt', 'mroot', 'mtable', 'mtr',
|
|
19
|
+
'mtd', 'mover', 'munder', 'munderover', 'mpadded', 'mphantom', 'mstyle', 'menclose',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
const MATH_ATTRS = [
|
|
23
|
+
'xmlns', 'display', 'class', 'mathvariant', 'stretchy', 'fence', 'separator',
|
|
24
|
+
'lspace', 'rspace', 'accent', 'accentunder', 'columnalign', 'rowalign',
|
|
25
|
+
'columnspacing', 'rowspacing', 'columnlines', 'rowlines', 'frame', 'notation',
|
|
26
|
+
'width', 'height', 'depth', 'voffset', 'scriptlevel', 'displaystyle',
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
const STRICT_MATHML_CONFIG = {
|
|
30
|
+
ALLOWED_TAGS: [...MATH_TAGS, 'span'],
|
|
31
|
+
ALLOWED_ATTR: [...MATH_ATTRS],
|
|
32
|
+
FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'a'],
|
|
33
|
+
// DOMPurify normally keeps the text of forbidden tags. TeX annotations can
|
|
34
|
+
// be large and duplicate the accessible MathML, so remove their subtree.
|
|
35
|
+
ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],
|
|
36
|
+
FORBID_ATTR: ['style'],
|
|
37
|
+
} as Config
|
|
38
|
+
|
|
39
|
+
const CELL_MARKDOWN_TAGS = [
|
|
40
|
+
'a', 'strong', 'em', 'b', 'i', 'code', 'span', 'br', 'button', 'svg', 'path',
|
|
41
|
+
'p', 'ul', 'ol', 'li', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
42
|
+
...MATH_TAGS,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const FINAL_PROFILES: Record<MathMarkdownProfile, Config> = {
|
|
46
|
+
prose: {
|
|
47
|
+
ADD_TAGS: [...MATH_TAGS],
|
|
48
|
+
ADD_ATTR: [...MATH_ATTRS, 'target', 'rel'],
|
|
49
|
+
FORBID_TAGS: ['annotation', 'annotation-xml', 'style', 'form', 'textarea', 'select', 'button', 'iframe', 'object', 'embed'],
|
|
50
|
+
ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],
|
|
51
|
+
FORBID_ATTR: ['style'],
|
|
52
|
+
} as Config,
|
|
53
|
+
cellMarkdown: {
|
|
54
|
+
ALLOWED_TAGS: CELL_MARKDOWN_TAGS,
|
|
55
|
+
ALLOWED_ATTR: [
|
|
56
|
+
'href', 'target', 'rel', 'class', 'data-citation-page', 'data-citation-source',
|
|
57
|
+
'data-citation-doc', 'data-citation-verified', 'title', 'fill', 'stroke',
|
|
58
|
+
'viewBox', 'stroke-linecap', 'stroke-linejoin', 'stroke-width', 'd',
|
|
59
|
+
...MATH_ATTRS,
|
|
60
|
+
],
|
|
61
|
+
FORBID_TAGS: ['annotation', 'annotation-xml', 'style'],
|
|
62
|
+
ADD_FORBID_CONTENTS: ['annotation', 'annotation-xml'],
|
|
63
|
+
FORBID_ATTR: ['style'],
|
|
64
|
+
} as Config,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface MathToken extends Tokens.Generic {
|
|
68
|
+
type: 'mcpDisplayMath' | 'mcpInlineMath'
|
|
69
|
+
raw: string
|
|
70
|
+
tex: string
|
|
71
|
+
displayMode: boolean
|
|
72
|
+
render: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isCurrencyLike(raw: string, tex: string, following: string): boolean {
|
|
76
|
+
if (/^\$\s|\s\$$/.test(raw)) return true
|
|
77
|
+
if (/^\d+(?:[.,]\d+)?-\s*$/.test(tex)) return true
|
|
78
|
+
if (/^\d/.test(tex) && /^\d/.test(following)) return true
|
|
79
|
+
|
|
80
|
+
// A currency opener can otherwise pair with the opening delimiter of a
|
|
81
|
+
// later expression ("$5 then ($x$)"). Reinterpret that first candidate
|
|
82
|
+
// only when it starts with a bounded numeric amount and its apparent closer
|
|
83
|
+
// is immediately followed by a complete later inline body. This deliberately
|
|
84
|
+
// leaves inherently ambiguous standalone forms ($5$, $2 + 3$, $2\pi$) as
|
|
85
|
+
// math and does not depend on the prose between the two delimiters.
|
|
86
|
+
const amount = /^\d+(?:[.,]\d{1,2})?(?=$|[\s,;:!?()[\]+*/-])/.exec(tex)?.[0]
|
|
87
|
+
if (!amount) return false
|
|
88
|
+
|
|
89
|
+
// A standalone numeric token is valid math even when followed by prose or
|
|
90
|
+
// another expression ("$5$, then $x$"). Reassignment requires content
|
|
91
|
+
// beyond the amount itself.
|
|
92
|
+
const trailing = tex.slice(amount.length)
|
|
93
|
+
if (!trailing) return false
|
|
94
|
+
|
|
95
|
+
// During streaming, the later formula may not have closed yet. An unmatched
|
|
96
|
+
// opening parenthesis before its dollar is sufficient structural evidence
|
|
97
|
+
// that the numeric dollar was currency; otherwise a permissive renderer can
|
|
98
|
+
// briefly flash "5 then (" as math.
|
|
99
|
+
const hasUnmatchedOpeningParen = (trailing.match(/\(/g)?.length ?? 0) > (trailing.match(/\)/g)?.length ?? 0)
|
|
100
|
+
const containsTexCommand = /\\[A-Za-z]+/.test(trailing)
|
|
101
|
+
if (hasUnmatchedOpeningParen && !containsTexCommand) return true
|
|
102
|
+
|
|
103
|
+
const closerLooksLikeLaterOpener = /^(?![\s,.;:!?)}\]])(?:\\[^\n]|[^\\\n$])+\$(?!\$)/.test(following)
|
|
104
|
+
return closerLooksLikeLaterOpener
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"'
|
|
108
|
+
|
|
109
|
+
function cellLinkExtension(): MarkedExtension {
|
|
110
|
+
return {
|
|
111
|
+
renderer: {
|
|
112
|
+
link(this: Renderer, token: Tokens.Link): string {
|
|
113
|
+
// Marked owns escaping of the destination, title and nested label
|
|
114
|
+
// tokens. Only inject fixed attributes into its already-safe anchor.
|
|
115
|
+
const anchor = Renderer.prototype.link.call(this, token) as string
|
|
116
|
+
return anchor.replace('<a ', `<a ${CELL_LINK_ATTRIBUTES} `)
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mathExtension(renderer: MathRenderer): MarkedExtension {
|
|
123
|
+
let count = 0
|
|
124
|
+
const token = (raw: string, tex: string, displayMode: boolean): MathToken => ({
|
|
125
|
+
type: displayMode ? 'mcpDisplayMath' : 'mcpInlineMath',
|
|
126
|
+
raw,
|
|
127
|
+
tex,
|
|
128
|
+
displayMode,
|
|
129
|
+
render: ++count <= MATH_MARKDOWN_LIMITS.expressionCount && tex.length <= MATH_MARKDOWN_LIMITS.expressionLength,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
extensions: [
|
|
134
|
+
{
|
|
135
|
+
name: 'mcpDisplayMath',
|
|
136
|
+
level: 'block',
|
|
137
|
+
start(src) {
|
|
138
|
+
const index = src.indexOf('$$')
|
|
139
|
+
return index < 0 ? undefined : index
|
|
140
|
+
},
|
|
141
|
+
tokenizer(src) {
|
|
142
|
+
const match = /^\$\$\s*\n?([\s\S]+?)\n?\s*\$\$(?:\n|$)/.exec(src)
|
|
143
|
+
if (!match) return undefined
|
|
144
|
+
return token(match[0], match[1], true)
|
|
145
|
+
},
|
|
146
|
+
renderer(tokenValue) {
|
|
147
|
+
return renderToken(tokenValue as MathToken, renderer)
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'mcpInlineMath',
|
|
152
|
+
level: 'inline',
|
|
153
|
+
start(src) {
|
|
154
|
+
const index = src.indexOf('$')
|
|
155
|
+
return index < 0 ? undefined : index
|
|
156
|
+
},
|
|
157
|
+
tokenizer(src) {
|
|
158
|
+
const match = /^\$(?!\$)((?:\\[^\n]|[^\\\n$])+?)\$(?!\$)/.exec(src)
|
|
159
|
+
if (!match || isCurrencyLike(match[0], match[1], src.slice(match[0].length))) return undefined
|
|
160
|
+
return token(match[0], match[1], false)
|
|
161
|
+
},
|
|
162
|
+
renderer(tokenValue) {
|
|
163
|
+
return renderToken(tokenValue as MathToken, renderer)
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderToken(token: MathToken, renderer: MathRenderer): string {
|
|
171
|
+
if (!token.render) return escapeHtml(token.raw)
|
|
172
|
+
try {
|
|
173
|
+
const rendered = renderer(token.tex, { displayMode: token.displayMode })
|
|
174
|
+
if (!rendered) return escapeHtml(token.raw)
|
|
175
|
+
return sanitizeHtml(rendered, STRICT_MATHML_CONFIG)
|
|
176
|
+
} catch {
|
|
177
|
+
return escapeHtml(token.raw)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse Markdown with isolated, bounded math support and sanitize the complete
|
|
183
|
+
* result for its final sink. The optional transform sees escaped ordinary text
|
|
184
|
+
* tokens only. Math, code, raw HTML and link destinations never pass through
|
|
185
|
+
* it, so citation syntax cannot corrupt TeX optional arguments or attributes.
|
|
186
|
+
*/
|
|
187
|
+
export function parseMathMarkdown(
|
|
188
|
+
source: string,
|
|
189
|
+
renderer: MathRenderer,
|
|
190
|
+
profile: MathMarkdownProfile,
|
|
191
|
+
transformText?: MarkdownTextTransform,
|
|
192
|
+
): string {
|
|
193
|
+
const input = source === null || source === undefined ? '' : String(source)
|
|
194
|
+
if (!canSanitizeHtml()) return escapeHtml(input)
|
|
195
|
+
if (input.length > MATH_MARKDOWN_LIMITS.sourceLength) return escapeHtml(input)
|
|
196
|
+
|
|
197
|
+
const textRenderer = transformText
|
|
198
|
+
? {
|
|
199
|
+
renderer: {
|
|
200
|
+
text(this: Renderer, token: Tokens.Text | Tokens.Escape): string {
|
|
201
|
+
// Block text tokens (notably list-item text) can own already
|
|
202
|
+
// tokenized inline children. Recurse first so math and code keep
|
|
203
|
+
// their own renderers; leaf text callbacks run as parsing descends.
|
|
204
|
+
if ('tokens' in token && token.tokens) {
|
|
205
|
+
return this.parser.parseInline(token.tokens)
|
|
206
|
+
}
|
|
207
|
+
// Delegate entity handling and nested token rendering to Marked's
|
|
208
|
+
// own renderer. `escapeHtml` is deliberately not equivalent: it
|
|
209
|
+
// turns source `&` into visible `&` by double-escaping it.
|
|
210
|
+
const safeText = Renderer.prototype.text.call(this, token)
|
|
211
|
+
return transformText(safeText as string)
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
: undefined
|
|
216
|
+
const extensions = [mathExtension(renderer)]
|
|
217
|
+
if (profile === 'cellMarkdown') extensions.push(cellLinkExtension())
|
|
218
|
+
if (textRenderer) extensions.push(textRenderer)
|
|
219
|
+
const parser = new Marked({ gfm: true }, ...extensions)
|
|
220
|
+
const parsed = parser.parse(input, { async: false }) as string
|
|
221
|
+
return sanitizeHtml(parsed, FINAL_PROFILES[profile])
|
|
222
|
+
}
|