@seed-ship/mcp-ui-solid 6.22.0 → 6.24.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +209 -2
  3. package/dist/components/ChartJSRenderer.cjs +192 -66
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  6. package/dist/components/ChartJSRenderer.js +192 -66
  7. package/dist/components/ChartJSRenderer.js.map +1 -1
  8. package/dist/components/DegradedFallback.cjs +34 -2
  9. package/dist/components/DegradedFallback.cjs.map +1 -1
  10. package/dist/components/DegradedFallback.d.ts +7 -0
  11. package/dist/components/DegradedFallback.d.ts.map +1 -1
  12. package/dist/components/DegradedFallback.js +34 -2
  13. package/dist/components/DegradedFallback.js.map +1 -1
  14. package/dist/components/MissingValue.cjs +43 -0
  15. package/dist/components/MissingValue.cjs.map +1 -0
  16. package/dist/components/MissingValue.d.ts +27 -0
  17. package/dist/components/MissingValue.d.ts.map +1 -0
  18. package/dist/components/MissingValue.js +43 -0
  19. package/dist/components/MissingValue.js.map +1 -0
  20. package/dist/components/UIResourceRenderer.cjs +250 -202
  21. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  22. package/dist/components/UIResourceRenderer.d.ts +2 -1
  23. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  24. package/dist/components/UIResourceRenderer.js +250 -202
  25. package/dist/components/UIResourceRenderer.js.map +1 -1
  26. package/dist/components/chart-data-table.cjs +30 -5
  27. package/dist/components/chart-data-table.cjs.map +1 -1
  28. package/dist/components/chart-data-table.d.ts +18 -1
  29. package/dist/components/chart-data-table.d.ts.map +1 -1
  30. package/dist/components/chart-data-table.js +30 -5
  31. package/dist/components/chart-data-table.js.map +1 -1
  32. package/dist/components/chart-missing-plugin.cjs +194 -0
  33. package/dist/components/chart-missing-plugin.cjs.map +1 -0
  34. package/dist/components/chart-missing-plugin.d.ts +127 -0
  35. package/dist/components/chart-missing-plugin.d.ts.map +1 -0
  36. package/dist/components/chart-missing-plugin.js +194 -0
  37. package/dist/components/chart-missing-plugin.js.map +1 -0
  38. package/dist/context/MCPUIConfigContext.cjs.map +1 -1
  39. package/dist/context/MCPUIConfigContext.d.ts +57 -28
  40. package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
  41. package/dist/context/MCPUIConfigContext.js.map +1 -1
  42. package/dist/context/MCPUIMathContext.cjs +19 -0
  43. package/dist/context/MCPUIMathContext.cjs.map +1 -0
  44. package/dist/context/MCPUIMathContext.d.ts +18 -0
  45. package/dist/context/MCPUIMathContext.d.ts.map +1 -0
  46. package/dist/context/MCPUIMathContext.js +19 -0
  47. package/dist/context/MCPUIMathContext.js.map +1 -0
  48. package/dist/context/MCPUIStringsContext.cjs +12 -1
  49. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  50. package/dist/context/MCPUIStringsContext.d.ts +62 -0
  51. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  52. package/dist/context/MCPUIStringsContext.js +12 -1
  53. package/dist/context/MCPUIStringsContext.js.map +1 -1
  54. package/dist/index.cjs +2 -0
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +3 -1
  57. package/dist/index.d.ts +3 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/plugins/katex.cjs +23 -0
  62. package/dist/plugins/katex.cjs.map +1 -0
  63. package/dist/plugins/katex.d.cts +7 -0
  64. package/dist/plugins/katex.d.ts +7 -0
  65. package/dist/plugins/katex.d.ts.map +1 -0
  66. package/dist/plugins/katex.js +23 -0
  67. package/dist/plugins/katex.js.map +1 -0
  68. package/dist/services/component-registry.cjs +9 -2
  69. package/dist/services/component-registry.cjs.map +1 -1
  70. package/dist/services/component-registry.d.ts.map +1 -1
  71. package/dist/services/component-registry.js +9 -2
  72. package/dist/services/component-registry.js.map +1 -1
  73. package/dist/services/validation.cjs +31 -8
  74. package/dist/services/validation.cjs.map +1 -1
  75. package/dist/services/validation.d.ts.map +1 -1
  76. package/dist/services/validation.js +31 -8
  77. package/dist/services/validation.js.map +1 -1
  78. package/dist/types/index.d.ts +14 -2
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/utils/degraded-projections.cjs +19 -3
  81. package/dist/utils/degraded-projections.cjs.map +1 -1
  82. package/dist/utils/degraded-projections.d.ts +22 -2
  83. package/dist/utils/degraded-projections.d.ts.map +1 -1
  84. package/dist/utils/degraded-projections.js +19 -3
  85. package/dist/utils/degraded-projections.js.map +1 -1
  86. package/dist/utils/iframe-coep.cjs.map +1 -1
  87. package/dist/utils/iframe-coep.d.ts +7 -6
  88. package/dist/utils/iframe-coep.d.ts.map +1 -1
  89. package/dist/utils/iframe-coep.js.map +1 -1
  90. package/dist/utils/math-markdown.cjs +240 -0
  91. package/dist/utils/math-markdown.cjs.map +1 -0
  92. package/dist/utils/math-markdown.d.ts +16 -0
  93. package/dist/utils/math-markdown.d.ts.map +1 -0
  94. package/dist/utils/math-markdown.js +240 -0
  95. package/dist/utils/math-markdown.js.map +1 -0
  96. package/dist/utils/missing-value.cjs +60 -0
  97. package/dist/utils/missing-value.cjs.map +1 -0
  98. package/dist/utils/missing-value.d.ts +79 -0
  99. package/dist/utils/missing-value.d.ts.map +1 -0
  100. package/dist/utils/missing-value.js +60 -0
  101. package/dist/utils/missing-value.js.map +1 -0
  102. package/package.json +23 -4
  103. package/src/components/ChartJSRenderer.tsx +159 -6
  104. package/src/components/DegradedFallback.tsx +24 -2
  105. package/src/components/MissingValue.tsx +61 -0
  106. package/src/components/UIResourceRenderer.tsx +64 -10
  107. package/src/components/chart-data-table.ts +50 -6
  108. package/src/components/chart-missing-plugin.ts +334 -0
  109. package/src/context/MCPUIConfigContext.tsx +57 -28
  110. package/src/context/MCPUIMathContext.tsx +32 -0
  111. package/src/context/MCPUIStringsContext.tsx +76 -0
  112. package/src/index.ts +4 -0
  113. package/src/plugins/katex.ts +25 -0
  114. package/src/services/component-registry.ts +10 -2
  115. package/src/services/validation.ts +44 -9
  116. package/src/types/index.ts +13 -1
  117. package/src/utils/degraded-projections.ts +44 -4
  118. package/src/utils/iframe-coep.ts +7 -6
  119. package/src/utils/math-markdown.ts +222 -0
  120. package/src/utils/missing-value.ts +127 -0
@@ -1,4 +1,5 @@
1
1
  import { formatMCPUIString } from "./format-string.js";
2
+ import { isMissingChartValue } from "./missing-value.js";
2
3
  const DEGRADED_PROJECTION_LABELS = {
3
4
  source: "Source",
4
5
  target: "Target",
@@ -10,7 +11,8 @@ const DEGRADED_PROJECTION_LABELS = {
10
11
  info: "Info",
11
12
  marker: "marker",
12
13
  feature: "feature",
13
- series: "Series {n}"
14
+ series: "Series {n}",
15
+ withUnit: "{label} ({unit})"
14
16
  };
15
17
  const withLabels = (labels) => ({
16
18
  ...DEGRADED_PROJECTION_LABELS,
@@ -84,6 +86,7 @@ function mapToDegradedTable(params, labels) {
84
86
  function chartToDegradedTable(params, labels) {
85
87
  var _a, _b;
86
88
  const l = withLabels(labels);
89
+ const unit = typeof params.unit === "string" && params.unit !== "" ? params.unit : void 0;
87
90
  const datasets = ((_a = params.data) == null ? void 0 : _a.datasets) ?? [];
88
91
  const dataLabels = ((_b = params.data) == null ? void 0 : _b.labels) ?? [];
89
92
  const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => {
@@ -92,16 +95,29 @@ function chartToDegradedTable(params, labels) {
92
95
  }), 0);
93
96
  const columns = [
94
97
  "",
95
- ...datasets.map((d, i) => d.label ?? formatMCPUIString(l.series, { n: i + 1 }))
98
+ ...datasets.map((d, i) => {
99
+ const name = d.label ?? formatMCPUIString(l.series, { n: i + 1 });
100
+ return unit === void 0 ? name : formatMCPUIString(l.withUnit ?? DEGRADED_PROJECTION_LABELS.withUnit, { label: name, unit });
101
+ })
96
102
  ];
97
103
  const rows = [];
104
+ const missing = [];
98
105
  for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {
99
106
  rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => {
100
107
  var _a2;
101
108
  return cell((_a2 = d.data) == null ? void 0 : _a2[r]);
102
109
  })]);
110
+ missing.push([
111
+ false,
112
+ ...datasets.map((d) => {
113
+ var _a2, _b2;
114
+ if (r >= (((_a2 = d.data) == null ? void 0 : _a2.length) ?? 0)) return false;
115
+ const entry = (_b2 = d.data) == null ? void 0 : _b2[r];
116
+ return (entry === null || typeof entry !== "object") && isMissingChartValue(entry);
117
+ })
118
+ ]);
103
119
  }
104
- return { columns, rows };
120
+ return { columns, rows, missing };
105
121
  }
106
122
  export {
107
123
  DEGRADED_PROJECTION_LABELS,
@@ -1 +1 @@
1
- {"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nimport { formatMCPUIString } from './format-string';\n\n/**\n * Column headers and series names of the degraded tables.\n *\n * The projections are pure helpers with no access to `MCPUIStrings`, so the\n * wording is injected: each function takes an optional trailing `labels`\n * partial, and the renderers feed it from `useMCPUIStrings()`. Left out, the\n * English defaults below apply — the pre-v6.20.0 behaviour.\n *\n * @since v6.20.0\n */\nexport interface DegradedProjectionLabels {\n /** Graph edge table — source column. */\n source: string;\n /** Graph edge table — target column. */\n target: string;\n /** Graph edge / node table — label column. */\n label: string;\n /** Graph node table — node id column. */\n node: string;\n /** Map table — feature type column. */\n type: string;\n /** Map table — latitude column. */\n lat: string;\n /** Map table — longitude column. */\n lng: string;\n /** Map table — property summary column. */\n info: string;\n /** Map table — `Type` cell of a marker row (v6.20.0, 6th pass). */\n marker: string;\n /** Map table — `Type` cell of a GeoJSON feature without a geometry type (v6.20.0). */\n feature: string;\n /** Chart table — fallback dataset name. Template — `{n}`. */\n series: string;\n}\n\n/** English defaults for {@link DegradedProjectionLabels}. */\nexport const DEGRADED_PROJECTION_LABELS: DegradedProjectionLabels = {\n source: 'Source',\n target: 'Target',\n label: 'Label',\n node: 'Node',\n type: 'Type',\n lat: 'Lat',\n lng: 'Lng',\n info: 'Info',\n marker: 'marker',\n feature: 'feature',\n series: 'Series {n}',\n};\n\nconst withLabels = (labels?: Partial<DegradedProjectionLabels>): DegradedProjectionLabels => ({\n ...DEGRADED_PROJECTION_LABELS,\n ...labels,\n});\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: [l.source, l.target, l.label],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: [l.node, l.label],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push([l.marker, cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? l.feature),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: [l.type, l.lat, l.lng, l.info],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n */\nexport function chartToDegradedTable(params: {\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const datasets = params.data?.datasets ?? [];\n const dataLabels = params.data?.labels ?? [];\n const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = [\n '',\n ...datasets.map((d, i) => d.label ?? formatMCPUIString(l.series, { n: i + 1 })),\n ];\n const rows: Array<Array<string | number>> = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n }\n return { columns, rows };\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"degraded-projections.js","sources":["../../src/utils/degraded-projections.ts"],"sourcesContent":["/**\n * Degraded-fallback projections (audit 2026-05-30, P2.5).\n *\n * Pure functions that turn a heavy renderer's params into a flat\n * `{ columns, rows }` table for `<DegradedFallback>` — the middle rung of\n * the fallback ladder shown when the native render (G6 / Leaflet / Chart.js)\n * is unavailable or throws. No peer deps, no side effects, fully testable.\n *\n * These are best-effort views: they surface the underlying data so the user\n * isn't left with a blank space, not faithful reproductions of the chart/\n * map/graph.\n */\n\nimport { formatMCPUIString } from './format-string';\nimport { isMissingChartValue } from './missing-value';\n\n/**\n * Column headers and series names of the degraded tables.\n *\n * The projections are pure helpers with no access to `MCPUIStrings`, so the\n * wording is injected: each function takes an optional trailing `labels`\n * partial, and the renderers feed it from `useMCPUIStrings()`. Left out, the\n * English defaults below apply — the pre-v6.20.0 behaviour.\n *\n * @since v6.20.0\n */\nexport interface DegradedProjectionLabels {\n /** Graph edge table — source column. */\n source: string;\n /** Graph edge table — target column. */\n target: string;\n /** Graph edge / node table — label column. */\n label: string;\n /** Graph node table — node id column. */\n node: string;\n /** Map table — feature type column. */\n type: string;\n /** Map table — latitude column. */\n lat: string;\n /** Map table — longitude column. */\n lng: string;\n /** Map table — property summary column. */\n info: string;\n /** Map table — `Type` cell of a marker row (v6.20.0, 6th pass). */\n marker: string;\n /** Map table — `Type` cell of a GeoJSON feature without a geometry type (v6.20.0). */\n feature: string;\n /** Chart table — fallback dataset name. Template — `{n}`. */\n series: string;\n /**\n * Chart table — a dataset column header followed by the chart's `unit`.\n * Template — `{label}`, `{unit}`. Only applied when the chart has a unit.\n * Optional so that a complete labels table written for 6.20.0–6.23.0 still\n * type-checks. @since v6.24.0\n */\n withUnit?: string;\n}\n\n/** English defaults for {@link DegradedProjectionLabels}. */\nexport const DEGRADED_PROJECTION_LABELS: Required<DegradedProjectionLabels> = {\n source: 'Source',\n target: 'Target',\n label: 'Label',\n node: 'Node',\n type: 'Type',\n lat: 'Lat',\n lng: 'Lng',\n info: 'Info',\n marker: 'marker',\n feature: 'feature',\n series: 'Series {n}',\n withUnit: '{label} ({unit})',\n};\n\nconst withLabels = (labels?: Partial<DegradedProjectionLabels>): DegradedProjectionLabels => ({\n ...DEGRADED_PROJECTION_LABELS,\n ...labels,\n});\n\nexport interface DegradedTable {\n columns: string[];\n rows: Array<Array<string | number>>;\n}\n\n/** A degraded chart table: its cells, plus which of them are missing values. */\nexport interface DegradedChartTable extends DegradedTable {\n /** Same shape as `rows`; `true` for a dataset entry that is missing. @since v6.24.0 */\n missing: boolean[][];\n}\n\nconst MAX_PROJECTED_ROWS = 200;\n\n/** Compact a value to a single table cell string. */\nfunction cell(value: unknown): string {\n if (value == null) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\n// ─── Graph ───────────────────────────────────────────────────────────────\n\n/**\n * Graph → edge table when edges exist (Source / Target / Label), else a\n * node list (Node / Label). The graph renderer can therefore degrade to a\n * readable relationship listing instead of a blank canvas.\n */\nexport function graphToDegradedTable(params: {\n nodes?: Array<{ id: string; label?: string }>;\n edges?: Array<{ source: string; target: string; label?: string; weight?: number }>;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const edges = params.edges ?? [];\n if (edges.length > 0) {\n return {\n columns: [l.source, l.target, l.label],\n rows: edges.slice(0, MAX_PROJECTED_ROWS).map((e) => {\n const label = [e.weight != null ? String(e.weight) : '', e.label ?? '']\n .filter(Boolean)\n .join(' · ');\n return [cell(e.source), cell(e.target), label];\n }),\n };\n }\n const nodes = params.nodes ?? [];\n return {\n columns: [l.node, l.label],\n rows: nodes.slice(0, MAX_PROJECTED_ROWS).map((n) => [cell(n.id), cell(n.label ?? n.id)]),\n };\n}\n\n// ─── Map ───────────────────────────────────────────────────────────────────\n\ninterface GeoJSONLikeFeature {\n geometry?: { type?: string; coordinates?: unknown } | null;\n properties?: Record<string, unknown> | null;\n}\n\n/** Pull a representative `[lng, lat]` pair out of a geometry's coordinates. */\nfunction firstLngLat(coords: unknown): [number, number] | null {\n let c: unknown = coords;\n // Descend nested arrays until we reach a [number, number, ...] position.\n while (Array.isArray(c) && Array.isArray(c[0])) c = c[0];\n if (Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number') {\n return [c[0], c[1]];\n }\n return null;\n}\n\n/**\n * Map → a coordinate table. Markers become Lat/Lng/Label rows; GeoJSON\n * features become Type / Lat / Lng (+ a compact properties summary). So a\n * map that can't paint still lists where its points are.\n */\nexport function mapToDegradedTable(params: {\n markers?: Array<{\n position: [number, number] | { lat: number; lng: number };\n tooltip?: string;\n popup?: string;\n }>;\n geojson?: unknown;\n}, labels?: Partial<DegradedProjectionLabels>): DegradedTable {\n const l = withLabels(labels);\n const rows: Array<Array<string | number>> = [];\n\n for (const m of params.markers ?? []) {\n const lat = Array.isArray(m.position) ? m.position[0] : m.position?.lat;\n const lng = Array.isArray(m.position) ? m.position[1] : m.position?.lng;\n rows.push([l.marker, cell(lat), cell(lng), cell(m.tooltip ?? m.popup ?? '')]);\n }\n\n const fc = params.geojson as { features?: GeoJSONLikeFeature[] } | undefined;\n const features = Array.isArray(fc?.features) ? fc!.features : [];\n for (const f of features) {\n const ll = firstLngLat(f.geometry?.coordinates);\n const props = f.properties ?? {};\n const propSummary = Object.keys(props)\n .slice(0, 3)\n .map((k) => `${k}=${cell(props[k])}`)\n .join(', ');\n rows.push([\n cell(f.geometry?.type ?? l.feature),\n ll ? cell(ll[1]) : '',\n ll ? cell(ll[0]) : '',\n propSummary,\n ]);\n }\n\n return {\n columns: [l.type, l.lat, l.lng, l.info],\n rows: rows.slice(0, MAX_PROJECTED_ROWS),\n };\n}\n\n// ─── Chart ───────────────────────────────────────────────────────────────\n\n/**\n * Chart → a series table: one row per label, one column per dataset. So a\n * chart that can't draw still shows its numbers. Point/object data (scatter,\n * bubble, time series) is stringified per cell.\n *\n * `missing` flags the cells whose dataset entry is a missing value (`null`\n * or non-finite) so the table can mark them instead of showing an empty\n * cell. A slot past the end of a shorter or empty dataset has no entry and is\n * not flagged; a point object keeps its JSON text (where a `\"y\":null` reads\n * as such) and is not flagged either. With a `unit`, dataset column headers\n * become `labels.withUnit`.\n */\nexport function chartToDegradedTable(params: {\n unit?: string;\n data?: {\n labels?: Array<string | number>;\n datasets?: Array<{ label?: string; data?: unknown[] }>;\n };\n}, labels?: Partial<DegradedProjectionLabels>): DegradedChartTable {\n const l = withLabels(labels);\n const unit = typeof params.unit === 'string' && params.unit !== '' ? params.unit : undefined;\n const datasets = params.data?.datasets ?? [];\n const dataLabels = params.data?.labels ?? [];\n const rowCount = Math.max(dataLabels.length, ...datasets.map((d) => d.data?.length ?? 0), 0);\n\n const columns = [\n '',\n ...datasets.map((d, i) => {\n const name = d.label ?? formatMCPUIString(l.series, { n: i + 1 });\n return unit === undefined\n ? name\n : formatMCPUIString(l.withUnit ?? DEGRADED_PROJECTION_LABELS.withUnit, { label: name, unit });\n }),\n ];\n const rows: Array<Array<string | number>> = [];\n const missing: boolean[][] = [];\n for (let r = 0; r < Math.min(rowCount, MAX_PROJECTED_ROWS); r++) {\n rows.push([cell(dataLabels[r] ?? r + 1), ...datasets.map((d) => cell(d.data?.[r]))]);\n missing.push([\n false,\n ...datasets.map((d) => {\n // Past the end of a shorter or empty dataset there is no entry, so\n // nothing to flag (the cell stays empty, as before 6.24.0).\n if (r >= (d.data?.length ?? 0)) return false;\n const entry = d.data?.[r];\n return (entry === null || typeof entry !== 'object') && isMissingChartValue(entry);\n }),\n ]);\n }\n return { columns, rows, missing };\n}\n"],"names":["_a","_b"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"iframe-coep.cjs","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 (`Required<MCPUIConfig>` — what `useMCPUIConfig()`\n * and `DEFAULT_MCPUI_CONFIG` hand out). `MCPUIConfig`'s own fields are\n * optional so new keys stay backward compatible, and reading an unresolved\n * one here would silently re-implement the defaults next to their source.\n */\nexport function shouldSetCredentialless(\n url: string,\n config: Required<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: Required<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":["isTrustedIframeDomain","createSignal","onMount"],"mappings":";;;;AAkCO,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,CAACA,WAAAA,sBAAsB,KAAK;AAAA,IACjC,sBAAsB,OAAO;AAAA,EAAA,CAC9B;AACH;AASO,SAAS,4BAA+C;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAIC,QAAAA,aAAa,KAAK;AAClDC,UAAAA,QAAQ,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;;;;"}
1
+ {"version":3,"file":"iframe-coep.cjs","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":["isTrustedIframeDomain","createSignal","onMount"],"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,CAACA,WAAAA,sBAAsB,KAAK;AAAA,IACjC,sBAAsB,OAAO;AAAA,EAAA,CAC9B;AACH;AASO,SAAS,4BAA+C;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAIC,QAAAA,aAAa,KAAK;AAClDC,UAAAA,QAAQ,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;;;;"}
@@ -24,12 +24,13 @@ import type { MCPUIConfig } from '../context/MCPUIConfigContext';
24
24
  *
25
25
  * An unparsable URL is not trusted, so it gets the attribute.
26
26
  *
27
- * Takes a RESOLVED config (`Required<MCPUIConfig>` — what `useMCPUIConfig()`
28
- * and `DEFAULT_MCPUI_CONFIG` hand out). `MCPUIConfig`'s own fields are
29
- * optional so new keys stay backward compatible, and reading an unresolved
30
- * one here would silently re-implement the defaults next to their source.
27
+ * Takes a RESOLVED config — `MCPUIConfig`, which is what `useMCPUIConfig()`
28
+ * and `DEFAULT_MCPUI_CONFIG` hand out, every key present. The authored view is
29
+ * `MCPUIConfigInput`, whose fields are optional so that a new key stays
30
+ * backward compatible; accepting one of those here would mean re-implementing
31
+ * the defaults next to their source, and silently drifting from them.
31
32
  */
32
- export declare function shouldSetCredentialless(url: string, config: Required<MCPUIConfig>, options: {
33
+ export declare function shouldSetCredentialless(url: string, config: MCPUIConfig, options: {
33
34
  sandboxedWithoutSameOrigin: boolean;
34
35
  }): boolean;
35
36
  /**
@@ -49,5 +50,5 @@ export declare function createCrossOriginIsolated(): Accessor<boolean>;
49
50
  *
50
51
  * Takes a resolved config, for the reason given on `shouldSetCredentialless`.
51
52
  */
52
- export declare function shouldShowIframeFallbackLink(config: Required<MCPUIConfig>, crossOriginIsolated: boolean): boolean;
53
+ export declare function shouldShowIframeFallbackLink(config: MCPUIConfig, crossOriginIsolated: boolean): boolean;
53
54
  //# sourceMappingURL=iframe-coep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"iframe-coep.d.ts","sourceRoot":"","sources":["../../src/utils/iframe-coep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC7B,OAAO,EAAE;IAAE,0BAA0B,EAAE,OAAO,CAAA;CAAE,GAC/C,OAAO,CAQT;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI,QAAQ,CAAC,OAAO,CAAC,CAM7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC7B,mBAAmB,EAAE,OAAO,GAC3B,OAAO,CAKT"}
1
+ {"version":3,"file":"iframe-coep.d.ts","sourceRoot":"","sources":["../../src/utils/iframe-coep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAEhE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,0BAA0B,EAAE,OAAO,CAAA;CAAE,GAC/C,OAAO,CAQT;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,IAAI,QAAQ,CAAC,OAAO,CAAC,CAM7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,WAAW,EACnB,mBAAmB,EAAE,OAAO,GAC3B,OAAO,CAKT"}
@@ -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 (`Required<MCPUIConfig>` — what `useMCPUIConfig()`\n * and `DEFAULT_MCPUI_CONFIG` hand out). `MCPUIConfig`'s own fields are\n * optional so new keys stay backward compatible, and reading an unresolved\n * one here would silently re-implement the defaults next to their source.\n */\nexport function shouldSetCredentialless(\n url: string,\n config: Required<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: Required<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":";;AAkCO,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;"}
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 `&amp;` into visible `&amp;` 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"}