@seed-ship/mcp-ui-solid 6.24.0 → 6.25.1

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 (124) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +47 -0
  3. package/dist/components/ActionGroupRenderer.cjs +6 -6
  4. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  5. package/dist/components/ActionGroupRenderer.js +6 -6
  6. package/dist/components/ActionGroupRenderer.js.map +1 -1
  7. package/dist/components/AutocompleteDropdown.cjs +6 -6
  8. package/dist/components/AutocompleteDropdown.cjs.map +1 -1
  9. package/dist/components/AutocompleteDropdown.js +6 -6
  10. package/dist/components/AutocompleteDropdown.js.map +1 -1
  11. package/dist/components/AutocompleteFormField.cjs +6 -5
  12. package/dist/components/AutocompleteFormField.cjs.map +1 -1
  13. package/dist/components/AutocompleteFormField.d.ts.map +1 -1
  14. package/dist/components/AutocompleteFormField.js +6 -5
  15. package/dist/components/AutocompleteFormField.js.map +1 -1
  16. package/dist/components/ChartJSRenderer.cjs +216 -6
  17. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  18. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  19. package/dist/components/ChartJSRenderer.js +217 -7
  20. package/dist/components/ChartJSRenderer.js.map +1 -1
  21. package/dist/components/ChatPrompt.cjs +5 -5
  22. package/dist/components/ChatPrompt.cjs.map +1 -1
  23. package/dist/components/ChatPrompt.js +5 -5
  24. package/dist/components/ChatPrompt.js.map +1 -1
  25. package/dist/components/ComponentToolbar.cjs +5 -5
  26. package/dist/components/ComponentToolbar.cjs.map +1 -1
  27. package/dist/components/ComponentToolbar.js +5 -5
  28. package/dist/components/ComponentToolbar.js.map +1 -1
  29. package/dist/components/DegradedFallback.cjs +1 -1
  30. package/dist/components/DegradedFallback.cjs.map +1 -1
  31. package/dist/components/DegradedFallback.js +1 -1
  32. package/dist/components/DegradedFallback.js.map +1 -1
  33. package/dist/components/ExpandableWrapper.cjs +10 -7
  34. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  35. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  36. package/dist/components/ExpandableWrapper.js +10 -7
  37. package/dist/components/ExpandableWrapper.js.map +1 -1
  38. package/dist/components/FormFieldRenderer.cjs +13 -12
  39. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  40. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  41. package/dist/components/FormFieldRenderer.js +13 -12
  42. package/dist/components/FormFieldRenderer.js.map +1 -1
  43. package/dist/components/FormRenderer.cjs +1 -1
  44. package/dist/components/FormRenderer.cjs.map +1 -1
  45. package/dist/components/FormRenderer.js +1 -1
  46. package/dist/components/FormRenderer.js.map +1 -1
  47. package/dist/components/GenerativeUIErrorBoundary.cjs +1 -1
  48. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  49. package/dist/components/GenerativeUIErrorBoundary.js +1 -1
  50. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  51. package/dist/components/GhostText.cjs +2 -2
  52. package/dist/components/GhostText.cjs.map +1 -1
  53. package/dist/components/GhostText.js +2 -2
  54. package/dist/components/GhostText.js.map +1 -1
  55. package/dist/components/GraphRenderer.cjs +100 -29
  56. package/dist/components/GraphRenderer.cjs.map +1 -1
  57. package/dist/components/GraphRenderer.d.ts +1 -1
  58. package/dist/components/GraphRenderer.d.ts.map +1 -1
  59. package/dist/components/GraphRenderer.js +100 -29
  60. package/dist/components/GraphRenderer.js.map +1 -1
  61. package/dist/components/MissingValue.cjs +2 -2
  62. package/dist/components/MissingValue.cjs.map +1 -1
  63. package/dist/components/MissingValue.js +2 -2
  64. package/dist/components/MissingValue.js.map +1 -1
  65. package/dist/components/ModalRenderer.cjs +39 -31
  66. package/dist/components/ModalRenderer.cjs.map +1 -1
  67. package/dist/components/ModalRenderer.d.ts.map +1 -1
  68. package/dist/components/ModalRenderer.js +40 -32
  69. package/dist/components/ModalRenderer.js.map +1 -1
  70. package/dist/components/PortalDropdownMenu.cjs +6 -3
  71. package/dist/components/PortalDropdownMenu.cjs.map +1 -1
  72. package/dist/components/PortalDropdownMenu.d.ts.map +1 -1
  73. package/dist/components/PortalDropdownMenu.js +7 -4
  74. package/dist/components/PortalDropdownMenu.js.map +1 -1
  75. package/dist/components/ScratchpadPanel.cjs +38 -44
  76. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  77. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  78. package/dist/components/ScratchpadPanel.js +38 -44
  79. package/dist/components/ScratchpadPanel.js.map +1 -1
  80. package/dist/components/UIResourceRenderer.cjs +20 -20
  81. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  82. package/dist/components/UIResourceRenderer.js +20 -20
  83. package/dist/components/UIResourceRenderer.js.map +1 -1
  84. package/dist/components/chart-missing-plugin.cjs +4 -4
  85. package/dist/components/chart-missing-plugin.cjs.map +1 -1
  86. package/dist/components/chart-missing-plugin.d.ts +3 -1
  87. package/dist/components/chart-missing-plugin.d.ts.map +1 -1
  88. package/dist/components/chart-missing-plugin.js +4 -4
  89. package/dist/components/chart-missing-plugin.js.map +1 -1
  90. package/dist/utils/math-markdown.cjs +1 -1
  91. package/dist/utils/math-markdown.cjs.map +1 -1
  92. package/dist/utils/math-markdown.js +1 -1
  93. package/dist/utils/math-markdown.js.map +1 -1
  94. package/dist/utils/theme.cjs +91 -0
  95. package/dist/utils/theme.cjs.map +1 -0
  96. package/dist/utils/theme.d.ts +11 -0
  97. package/dist/utils/theme.d.ts.map +1 -0
  98. package/dist/utils/theme.js +91 -0
  99. package/dist/utils/theme.js.map +1 -0
  100. package/docs/SECURITY-DEPENDENCIES.md +81 -0
  101. package/docs/THEMING.md +205 -0
  102. package/package.json +5 -3
  103. package/src/components/ActionGroupRenderer.tsx +6 -6
  104. package/src/components/AutocompleteDropdown.tsx +17 -17
  105. package/src/components/AutocompleteFormField.tsx +10 -9
  106. package/src/components/ChartJSRenderer.tsx +483 -239
  107. package/src/components/ChatPrompt.tsx +16 -16
  108. package/src/components/ComponentToolbar.tsx +5 -5
  109. package/src/components/DegradedFallback.tsx +9 -9
  110. package/src/components/ExpandableWrapper.tsx +15 -11
  111. package/src/components/FormFieldRenderer.tsx +52 -51
  112. package/src/components/FormRenderer.tsx +12 -12
  113. package/src/components/GenerativeUIErrorBoundary.tsx +6 -6
  114. package/src/components/GhostText.tsx +7 -7
  115. package/src/components/GraphRenderer.tsx +83 -67
  116. package/src/components/MissingValue.tsx +2 -2
  117. package/src/components/ModalRenderer.tsx +11 -5
  118. package/src/components/PortalDropdownMenu.tsx +6 -2
  119. package/src/components/ScratchpadPanel.tsx +144 -141
  120. package/src/components/UIResourceRenderer.tsx +101 -101
  121. package/src/components/chart-missing-plugin.ts +5 -3
  122. package/src/styles/autocomplete.css +53 -53
  123. package/src/utils/math-markdown.ts +1 -1
  124. package/src/utils/theme.ts +101 -0
@@ -149,7 +149,7 @@ function drawMissingBarStubs(chart) {
149
149
  ctx.restore();
150
150
  }
151
151
  }
152
- function drawNoDataText(chart, text, fontFamily) {
152
+ function drawNoDataText(chart, text, fontFamily, color) {
153
153
  const ctx = chart == null ? void 0 : chart.ctx;
154
154
  const area = chart == null ? void 0 : chart.chartArea;
155
155
  if (!ctx || !area || !text) return;
@@ -157,7 +157,7 @@ function drawNoDataText(chart, text, fontFamily) {
157
157
  const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;
158
158
  ctx.save();
159
159
  ctx.font = `12px ${fontFamily || "sans-serif"}`;
160
- ctx.fillStyle = NO_DATA_TEXT_COLOR;
160
+ ctx.fillStyle = color || NO_DATA_TEXT_COLOR;
161
161
  ctx.textAlign = "center";
162
162
  ctx.textBaseline = "middle";
163
163
  ctx.fillText(text, cx, cy);
@@ -167,11 +167,11 @@ function createMissingValuesPlugin(options) {
167
167
  return {
168
168
  id: "mcpuiMissingValues",
169
169
  afterDatasetsDraw(chart) {
170
- var _a;
170
+ var _a, _b;
171
171
  try {
172
172
  drawMissingBarStubs(chart);
173
173
  if (options.allMissing) {
174
- drawNoDataText(chart, options.getNoDataText(), (_a = options.getFontFamily) == null ? void 0 : _a.call(options));
174
+ drawNoDataText(chart, options.getNoDataText(), (_a = options.getFontFamily) == null ? void 0 : _a.call(options), (_b = options.getTextColor) == null ? void 0 : _b.call(options));
175
175
  }
176
176
  } catch {
177
177
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chart-missing-plugin.cjs","sources":["../../src/components/chart-missing-plugin.ts"],"sourcesContent":["/**\n * Missing values in a Chart.js chart (v6.24.0) — everything the renderer adds\n * to its Chart.js config when a payload says a value is MISSING (`null`).\n *\n * The rule: absence is never zero. Chart.js already keeps the slot of a `null`\n * (no label dropped) and draws nothing for it; what it does not do is make the\n * absence READABLE. A null bar is a zero-height bar that is skipped when\n * drawn, a null line point is a hole the line may bridge (`spanGaps`), and the\n * legend, tooltip and axis know nothing of either. This module supplies:\n *\n * - {@link createMissingValuesPlugin} — a PER-CHART plugin (never\n * `Chart.register`ed, so it can leak into no other chart) that outlines a\n * dashed stub where a bar should have been (not on stacked or overlaid\n * bars, whose baseline belongs to another series), and says \"no data\" on a\n * chart with nothing to draw;\n * - {@link createMissingLegendGenerator} — marks a fully-missing series in the\n * legend;\n * - {@link createTooltipLabel} — names a missing value, and appends the unit;\n * - {@link withDatasetsNoGapSpan} — the shallow-cloned data with\n * `spanGaps: false`;\n * - {@link applyUnitAxisTitle} — the unit as the value axis title.\n *\n * Runtime-free on purpose (no `solid-js`, no `chart.js` import): the renderer\n * hands in the Chart.js pieces it loaded and the chrome strings (lazily, as\n * getters, so a late locale change is honoured at draw time). Every function\n * is defensive — a drawing hook that throws would blank the whole chart.\n *\n * Chart.js 4.5.1 facts this relies on (verified in `dist/chart.js`):\n * - a bar whose parsed value is `null` gets `base = head = vScale.getBasePixel()`\n * and no `skip` flag (BarController.updateElements), and `BarController.draw`\n * skips it (`getParsed(i)[vScale.axis] !== null`) — so nothing is painted;\n * - the bar element still carries its true index-axis thickness (`width` for a\n * vertical bar, `height` for a horizontal one).\n */\n\nimport type { MCPUIStrings } from '../context/MCPUIStringsContext';\nimport { formatMCPUIString } from '../utils/format-string';\nimport { isMissingChartValue } from '../utils/missing-value';\n\ntype Strings = Required<MCPUIStrings>;\n\n/** Length, in px along the value axis, of the dashed stub of a missing bar. */\nexport const MISSING_BAR_STUB_LENGTH = 12;\n/** Stroke of the stub when the dataset names no border colour. */\nexport const MISSING_BAR_FALLBACK_COLOR = '#9ca3af';\n/** Colour of the centred \"no data\" text. */\nexport const NO_DATA_TEXT_COLOR = '#6b7280';\nconst STUB_DASH = [4, 3];\n\n/** Chart types whose value axis is `y` and whose points are `{x, y}`. */\nconst POINT_CHART_TYPES = new Set(['scatter', 'bubble']);\n\n/**\n * The category of convention line a chart shows under its canvas:\n * `'gaps'` (a line breaks), `'bars'` (a dashed outline stands in for the\n * bar) or `'omitted'` (nothing is drawn: scatter, bubble, stacked bars).\n */\nexport type MissingNoteKind = 'gaps' | 'bars' | 'omitted';\n\n/**\n * Whether a bar chart's bars are stacked or overlaid (`stacked` on either\n * axis). A missing bar there has no slot of its own at the baseline — the\n * baseline belongs to the series below — so no dashed outline is drawn.\n */\nexport function isStackedChart(options: any): boolean {\n return Boolean(options?.scales?.x?.stacked || options?.scales?.y?.stacked);\n}\n\n/** Which convention note a chart gets: how ITS missing values are drawn. */\nexport function missingNoteKind(chartType: string | undefined, options?: any): MissingNoteKind {\n if (chartType === 'bar') return isStackedChart(options) ? 'omitted' : 'bars';\n if (chartType === 'scatter' || chartType === 'bubble') return 'omitted';\n return 'gaps';\n}\n\n// ─── Data ──────────────────────────────────────────────────────────────────\n\n/**\n * Chart.js data with every dataset shallow-cloned and `spanGaps: false`, so a\n * line breaks at a missing value instead of joining its neighbours. Never\n * mutates its input; `labels` and every other key are carried by reference.\n */\nexport function withDatasetsNoGapSpan<D extends { datasets?: unknown }>(data: D): D {\n const datasets = Array.isArray(data?.datasets) ? data.datasets : null;\n if (!datasets) return data;\n return {\n ...data,\n datasets: datasets.map((dataset: unknown) =>\n dataset !== null && typeof dataset === 'object'\n ? { ...(dataset as object), spanGaps: false }\n : dataset\n ),\n };\n}\n\n// ─── Legend ────────────────────────────────────────────────────────────────\n\ntype LegendGenerator = (chart: any) => any[];\n\n/**\n * Wraps a Chart.js legend label generator so a fully-missing dataset reads\n * `\"{series} (no data)\"` (`strings.chartLegendNoData`). Every other item is\n * passed through untouched.\n */\nexport function createMissingLegendGenerator(\n base: LegendGenerator,\n fullyMissing: readonly boolean[],\n getStrings: () => Strings\n): LegendGenerator {\n return function generateLabels(this: unknown, chart: any) {\n const items = base.call(this, chart);\n if (!Array.isArray(items)) return items;\n return items.map((item) => {\n const index = item?.datasetIndex;\n if (typeof index !== 'number' || !fullyMissing[index]) return item;\n const text = typeof item.text === 'string' ? item.text : String(item.text ?? '');\n return {\n ...item,\n text: formatMCPUIString(getStrings().chartLegendNoData, { series: text }),\n };\n });\n };\n}\n\n// ─── Tooltip ───────────────────────────────────────────────────────────────\n\n/** Whether a Chart.js tooltip item stands for a missing value. */\nexport function isMissingTooltipItem(item: any): boolean {\n if (item === null || item === undefined) return false;\n if (item.raw === null || item.raw === undefined || isMissingChartValue(item.raw)) return true;\n const parsed = item.parsed;\n if (parsed !== null && typeof parsed === 'object') {\n const chartType = item.chart?.config?.type;\n const valueKey =\n chartType === 'radar' ? 'r' : item.chart?.options?.indexAxis === 'y' ? 'x' : 'y';\n if (valueKey in parsed && parsed[valueKey] === null) return true;\n }\n return false;\n}\n\n/**\n * Tooltip `label` callback: names a missing value (`\"{series}: Missing\n * value\"`) and, with a `unit`, appends it to a present one. Otherwise it is\n * Chart.js's default composition (dataset label, `\": \"`, formatted value) —\n * including its `mode: 'dataset'` branch, which prefixes the CATEGORY label\n * instead, since every item of that tooltip belongs to the same dataset.\n * Hence a regular function: Chart.js calls it with the tooltip as `this`.\n *\n * `unit` is left off scatter and bubble tooltips: their formatted value is the\n * pair `(x, y)`, to which one unit does not belong.\n */\nexport function createTooltipLabel(\n getStrings: () => Strings,\n unit: string | undefined,\n chartType: string | undefined\n): (this: any, item: any) => string {\n const applyUnit = unit !== undefined && !POINT_CHART_TYPES.has(chartType ?? '');\n return function label(this: any, item: any) {\n const strings = getStrings();\n const prefixLabel = this?.options?.mode === 'dataset' ? item?.label : item?.dataset?.label;\n const prefix = typeof prefixLabel === 'string' && prefixLabel !== '' ? `${prefixLabel}: ` : '';\n\n if (isMissingTooltipItem(item)) return `${prefix}${strings.missingValue}`;\n\n const formatted = item?.formattedValue;\n if (formatted === null || formatted === undefined) return prefix;\n const value = applyUnit\n ? formatMCPUIString(strings.chartValueWithUnit, { value: formatted, unit: unit as string })\n : String(formatted);\n return `${prefix}${value}`;\n };\n}\n\n// ─── Axis ──────────────────────────────────────────────────────────────────\n\n/**\n * Sets the chart's `unit` as the title of its value axis, in place on the\n * `options` object the renderer just built (never on the payload's).\n *\n * Only bar, line, scatter and bubble have a value axis to title. The value\n * axis is `y`, or `x` when the index axis is `y` (a horizontal bar or line).\n * An existing `scales[axis].title` is the payload's own word and is kept; the\n * rest of the scale config (a `timeAxis` block sets `scales.x`) is kept too.\n */\nexport function applyUnitAxisTitle(\n options: any,\n chartType: string | undefined,\n unit: string | undefined\n): void {\n if (unit === undefined) return;\n if (chartType !== 'bar' && chartType !== 'line' && !POINT_CHART_TYPES.has(chartType ?? '')) {\n return;\n }\n const horizontal = (chartType === 'bar' || chartType === 'line') && options?.indexAxis === 'y';\n const axis = horizontal ? 'x' : 'y';\n if (options.scales?.[axis]?.title !== undefined) return;\n options.scales = {\n ...options.scales,\n [axis]: { ...options.scales?.[axis], title: { display: true, text: unit } },\n };\n}\n\n// ─── Plugin ────────────────────────────────────────────────────────────────\n\nexport interface MissingValuesPluginOptions {\n /** At least one dataset and every one of them has no observed value. */\n allMissing: boolean;\n /** Lazy: read at draw time. */\n getNoDataText: () => string;\n /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */\n getFontFamily?: () => string | undefined;\n}\n\n/** A dataset colour for entry `index`, as a string, or `undefined`. */\nfunction colorAt(color: unknown, index: number): string | undefined {\n if (typeof color === 'string' && color !== '') return color;\n if (Array.isArray(color) && color.length > 0) {\n const picked = color[index % color.length];\n return typeof picked === 'string' && picked !== '' ? picked : undefined;\n }\n return undefined;\n}\n\n/**\n * Draws a dashed outline stub for every missing bar of every visible,\n * non-stacked bar dataset, so a missing value is a visible mark, not a bar\n * that \"is\" zero.\n *\n * The stub sits on the value-axis base, as thick as the bar would have been,\n * and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart\n * area (up from a bottom baseline, down from a top one, right from a left\n * one). Clipped to the chart area.\n */\nexport function drawMissingBarStubs(chart: any): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n const datasets = chart?.data?.datasets;\n if (!ctx || !area || !Array.isArray(datasets)) return;\n\n const left = area.left;\n const top = area.top;\n const right = area.right ?? left + (area.width ?? 0);\n const bottom = area.bottom ?? top + (area.height ?? 0);\n\n let saved = false;\n datasets.forEach((dataset: any, datasetIndex: number) => {\n const meta = chart.getDatasetMeta?.(datasetIndex);\n if (!meta) return;\n const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';\n if (!isBar) return;\n // Stacked or overlaid bars: the baseline under a missing segment is the\n // series below, so an outline there would point at the wrong series.\n if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;\n if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {\n return;\n }\n const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];\n const elements: any[] = Array.isArray(meta.data) ? meta.data : [];\n\n values.forEach((value, index) => {\n if (!isMissingChartValue(value)) return;\n const element = elements[index];\n if (!element) return;\n const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;\n const { x, y, base, width, height } = props;\n if (![x, y, base].every((n) => Number.isFinite(n))) return;\n const horizontal = element.horizontal === true;\n const thickness = horizontal ? height : width;\n if (!Number.isFinite(thickness) || thickness <= 0) return;\n\n let rect: [number, number, number, number];\n if (horizontal) {\n const dir = base < (left + right) / 2 ? 1 : -1;\n rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];\n } else {\n const dir = base > (top + bottom) / 2 ? -1 : 1;\n rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];\n }\n\n if (!saved) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(left, top, right - left, bottom - top);\n ctx.clip();\n saved = true;\n }\n ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;\n ctx.lineWidth = 1;\n ctx.setLineDash(STUB_DASH);\n ctx.strokeRect(...rect);\n });\n });\n if (saved) {\n ctx.setLineDash?.([]);\n ctx.restore();\n }\n}\n\n/** Draws `text` centred in the chart area (a chart with nothing to plot). */\nexport function drawNoDataText(chart: any, text: string, fontFamily?: string): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n if (!ctx || !area || !text) return;\n const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;\n const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;\n ctx.save();\n ctx.font = `12px ${fontFamily || 'sans-serif'}`;\n ctx.fillStyle = NO_DATA_TEXT_COLOR;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, cx, cy);\n ctx.restore();\n}\n\n/**\n * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js\n * scopes it to that one chart. Both hooks swallow their own errors — a\n * decoration must never take the chart down.\n */\nexport function createMissingValuesPlugin(options: MissingValuesPluginOptions) {\n return {\n id: 'mcpuiMissingValues',\n afterDatasetsDraw(chart: any) {\n try {\n drawMissingBarStubs(chart);\n if (options.allMissing) {\n drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.());\n }\n } catch {\n // Cosmetic layer: never break the chart.\n }\n },\n };\n}\n"],"names":["formatMCPUIString","isMissingChartValue","_a","_b"],"mappings":"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"}
1
+ {"version":3,"file":"chart-missing-plugin.cjs","sources":["../../src/components/chart-missing-plugin.ts"],"sourcesContent":["/**\n * Missing values in a Chart.js chart (v6.24.0) — everything the renderer adds\n * to its Chart.js config when a payload says a value is MISSING (`null`).\n *\n * The rule: absence is never zero. Chart.js already keeps the slot of a `null`\n * (no label dropped) and draws nothing for it; what it does not do is make the\n * absence READABLE. A null bar is a zero-height bar that is skipped when\n * drawn, a null line point is a hole the line may bridge (`spanGaps`), and the\n * legend, tooltip and axis know nothing of either. This module supplies:\n *\n * - {@link createMissingValuesPlugin} — a PER-CHART plugin (never\n * `Chart.register`ed, so it can leak into no other chart) that outlines a\n * dashed stub where a bar should have been (not on stacked or overlaid\n * bars, whose baseline belongs to another series), and says \"no data\" on a\n * chart with nothing to draw;\n * - {@link createMissingLegendGenerator} — marks a fully-missing series in the\n * legend;\n * - {@link createTooltipLabel} — names a missing value, and appends the unit;\n * - {@link withDatasetsNoGapSpan} — the shallow-cloned data with\n * `spanGaps: false`;\n * - {@link applyUnitAxisTitle} — the unit as the value axis title.\n *\n * Runtime-free on purpose (no `solid-js`, no `chart.js` import): the renderer\n * hands in the Chart.js pieces it loaded and the chrome strings (lazily, as\n * getters, so a late locale change is honoured at draw time). Every function\n * is defensive — a drawing hook that throws would blank the whole chart.\n *\n * Chart.js 4.5.1 facts this relies on (verified in `dist/chart.js`):\n * - a bar whose parsed value is `null` gets `base = head = vScale.getBasePixel()`\n * and no `skip` flag (BarController.updateElements), and `BarController.draw`\n * skips it (`getParsed(i)[vScale.axis] !== null`) — so nothing is painted;\n * - the bar element still carries its true index-axis thickness (`width` for a\n * vertical bar, `height` for a horizontal one).\n */\n\nimport type { MCPUIStrings } from '../context/MCPUIStringsContext';\nimport { formatMCPUIString } from '../utils/format-string';\nimport { isMissingChartValue } from '../utils/missing-value';\n\ntype Strings = Required<MCPUIStrings>;\n\n/** Length, in px along the value axis, of the dashed stub of a missing bar. */\nexport const MISSING_BAR_STUB_LENGTH = 12;\n/** Stroke of the stub when the dataset names no border colour. */\nexport const MISSING_BAR_FALLBACK_COLOR = '#9ca3af';\n/** Colour of the centred \"no data\" text. */\nexport const NO_DATA_TEXT_COLOR = '#6b7280';\nconst STUB_DASH = [4, 3];\n\n/** Chart types whose value axis is `y` and whose points are `{x, y}`. */\nconst POINT_CHART_TYPES = new Set(['scatter', 'bubble']);\n\n/**\n * The category of convention line a chart shows under its canvas:\n * `'gaps'` (a line breaks), `'bars'` (a dashed outline stands in for the\n * bar) or `'omitted'` (nothing is drawn: scatter, bubble, stacked bars).\n */\nexport type MissingNoteKind = 'gaps' | 'bars' | 'omitted';\n\n/**\n * Whether a bar chart's bars are stacked or overlaid (`stacked` on either\n * axis). A missing bar there has no slot of its own at the baseline — the\n * baseline belongs to the series below — so no dashed outline is drawn.\n */\nexport function isStackedChart(options: any): boolean {\n return Boolean(options?.scales?.x?.stacked || options?.scales?.y?.stacked);\n}\n\n/** Which convention note a chart gets: how ITS missing values are drawn. */\nexport function missingNoteKind(chartType: string | undefined, options?: any): MissingNoteKind {\n if (chartType === 'bar') return isStackedChart(options) ? 'omitted' : 'bars';\n if (chartType === 'scatter' || chartType === 'bubble') return 'omitted';\n return 'gaps';\n}\n\n// ─── Data ──────────────────────────────────────────────────────────────────\n\n/**\n * Chart.js data with every dataset shallow-cloned and `spanGaps: false`, so a\n * line breaks at a missing value instead of joining its neighbours. Never\n * mutates its input; `labels` and every other key are carried by reference.\n */\nexport function withDatasetsNoGapSpan<D extends { datasets?: unknown }>(data: D): D {\n const datasets = Array.isArray(data?.datasets) ? data.datasets : null;\n if (!datasets) return data;\n return {\n ...data,\n datasets: datasets.map((dataset: unknown) =>\n dataset !== null && typeof dataset === 'object'\n ? { ...(dataset as object), spanGaps: false }\n : dataset\n ),\n };\n}\n\n// ─── Legend ────────────────────────────────────────────────────────────────\n\ntype LegendGenerator = (chart: any) => any[];\n\n/**\n * Wraps a Chart.js legend label generator so a fully-missing dataset reads\n * `\"{series} (no data)\"` (`strings.chartLegendNoData`). Every other item is\n * passed through untouched.\n */\nexport function createMissingLegendGenerator(\n base: LegendGenerator,\n fullyMissing: readonly boolean[],\n getStrings: () => Strings\n): LegendGenerator {\n return function generateLabels(this: unknown, chart: any) {\n const items = base.call(this, chart);\n if (!Array.isArray(items)) return items;\n return items.map((item) => {\n const index = item?.datasetIndex;\n if (typeof index !== 'number' || !fullyMissing[index]) return item;\n const text = typeof item.text === 'string' ? item.text : String(item.text ?? '');\n return {\n ...item,\n text: formatMCPUIString(getStrings().chartLegendNoData, { series: text }),\n };\n });\n };\n}\n\n// ─── Tooltip ───────────────────────────────────────────────────────────────\n\n/** Whether a Chart.js tooltip item stands for a missing value. */\nexport function isMissingTooltipItem(item: any): boolean {\n if (item === null || item === undefined) return false;\n if (item.raw === null || item.raw === undefined || isMissingChartValue(item.raw)) return true;\n const parsed = item.parsed;\n if (parsed !== null && typeof parsed === 'object') {\n const chartType = item.chart?.config?.type;\n const valueKey =\n chartType === 'radar' ? 'r' : item.chart?.options?.indexAxis === 'y' ? 'x' : 'y';\n if (valueKey in parsed && parsed[valueKey] === null) return true;\n }\n return false;\n}\n\n/**\n * Tooltip `label` callback: names a missing value (`\"{series}: Missing\n * value\"`) and, with a `unit`, appends it to a present one. Otherwise it is\n * Chart.js's default composition (dataset label, `\": \"`, formatted value) —\n * including its `mode: 'dataset'` branch, which prefixes the CATEGORY label\n * instead, since every item of that tooltip belongs to the same dataset.\n * Hence a regular function: Chart.js calls it with the tooltip as `this`.\n *\n * `unit` is left off scatter and bubble tooltips: their formatted value is the\n * pair `(x, y)`, to which one unit does not belong.\n */\nexport function createTooltipLabel(\n getStrings: () => Strings,\n unit: string | undefined,\n chartType: string | undefined\n): (this: any, item: any) => string {\n const applyUnit = unit !== undefined && !POINT_CHART_TYPES.has(chartType ?? '');\n return function label(this: any, item: any) {\n const strings = getStrings();\n const prefixLabel = this?.options?.mode === 'dataset' ? item?.label : item?.dataset?.label;\n const prefix = typeof prefixLabel === 'string' && prefixLabel !== '' ? `${prefixLabel}: ` : '';\n\n if (isMissingTooltipItem(item)) return `${prefix}${strings.missingValue}`;\n\n const formatted = item?.formattedValue;\n if (formatted === null || formatted === undefined) return prefix;\n const value = applyUnit\n ? formatMCPUIString(strings.chartValueWithUnit, { value: formatted, unit: unit as string })\n : String(formatted);\n return `${prefix}${value}`;\n };\n}\n\n// ─── Axis ──────────────────────────────────────────────────────────────────\n\n/**\n * Sets the chart's `unit` as the title of its value axis, in place on the\n * `options` object the renderer just built (never on the payload's).\n *\n * Only bar, line, scatter and bubble have a value axis to title. The value\n * axis is `y`, or `x` when the index axis is `y` (a horizontal bar or line).\n * An existing `scales[axis].title` is the payload's own word and is kept; the\n * rest of the scale config (a `timeAxis` block sets `scales.x`) is kept too.\n */\nexport function applyUnitAxisTitle(\n options: any,\n chartType: string | undefined,\n unit: string | undefined\n): void {\n if (unit === undefined) return;\n if (chartType !== 'bar' && chartType !== 'line' && !POINT_CHART_TYPES.has(chartType ?? '')) {\n return;\n }\n const horizontal = (chartType === 'bar' || chartType === 'line') && options?.indexAxis === 'y';\n const axis = horizontal ? 'x' : 'y';\n if (options.scales?.[axis]?.title !== undefined) return;\n options.scales = {\n ...options.scales,\n [axis]: { ...options.scales?.[axis], title: { display: true, text: unit } },\n };\n}\n\n// ─── Plugin ────────────────────────────────────────────────────────────────\n\nexport interface MissingValuesPluginOptions {\n /** At least one dataset and every one of them has no observed value. */\n allMissing: boolean;\n /** Lazy: read at draw time. */\n getNoDataText: () => string;\n /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */\n getFontFamily?: () => string | undefined;\n /** Lazy host theme override; omission retains the historical grey. */\n getTextColor?: () => string | undefined;\n}\n\n/** A dataset colour for entry `index`, as a string, or `undefined`. */\nfunction colorAt(color: unknown, index: number): string | undefined {\n if (typeof color === 'string' && color !== '') return color;\n if (Array.isArray(color) && color.length > 0) {\n const picked = color[index % color.length];\n return typeof picked === 'string' && picked !== '' ? picked : undefined;\n }\n return undefined;\n}\n\n/**\n * Draws a dashed outline stub for every missing bar of every visible,\n * non-stacked bar dataset, so a missing value is a visible mark, not a bar\n * that \"is\" zero.\n *\n * The stub sits on the value-axis base, as thick as the bar would have been,\n * and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart\n * area (up from a bottom baseline, down from a top one, right from a left\n * one). Clipped to the chart area.\n */\nexport function drawMissingBarStubs(chart: any): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n const datasets = chart?.data?.datasets;\n if (!ctx || !area || !Array.isArray(datasets)) return;\n\n const left = area.left;\n const top = area.top;\n const right = area.right ?? left + (area.width ?? 0);\n const bottom = area.bottom ?? top + (area.height ?? 0);\n\n let saved = false;\n datasets.forEach((dataset: any, datasetIndex: number) => {\n const meta = chart.getDatasetMeta?.(datasetIndex);\n if (!meta) return;\n const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';\n if (!isBar) return;\n // Stacked or overlaid bars: the baseline under a missing segment is the\n // series below, so an outline there would point at the wrong series.\n if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;\n if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {\n return;\n }\n const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];\n const elements: any[] = Array.isArray(meta.data) ? meta.data : [];\n\n values.forEach((value, index) => {\n if (!isMissingChartValue(value)) return;\n const element = elements[index];\n if (!element) return;\n const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;\n const { x, y, base, width, height } = props;\n if (![x, y, base].every((n) => Number.isFinite(n))) return;\n const horizontal = element.horizontal === true;\n const thickness = horizontal ? height : width;\n if (!Number.isFinite(thickness) || thickness <= 0) return;\n\n let rect: [number, number, number, number];\n if (horizontal) {\n const dir = base < (left + right) / 2 ? 1 : -1;\n rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];\n } else {\n const dir = base > (top + bottom) / 2 ? -1 : 1;\n rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];\n }\n\n if (!saved) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(left, top, right - left, bottom - top);\n ctx.clip();\n saved = true;\n }\n ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;\n ctx.lineWidth = 1;\n ctx.setLineDash(STUB_DASH);\n ctx.strokeRect(...rect);\n });\n });\n if (saved) {\n ctx.setLineDash?.([]);\n ctx.restore();\n }\n}\n\n/** Draws `text` centred in the chart area (a chart with nothing to plot). */\nexport function drawNoDataText(chart: any, text: string, fontFamily?: string, color?: string): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n if (!ctx || !area || !text) return;\n const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;\n const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;\n ctx.save();\n ctx.font = `12px ${fontFamily || 'sans-serif'}`;\n ctx.fillStyle = color || NO_DATA_TEXT_COLOR;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, cx, cy);\n ctx.restore();\n}\n\n/**\n * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js\n * scopes it to that one chart. Both hooks swallow their own errors — a\n * decoration must never take the chart down.\n */\nexport function createMissingValuesPlugin(options: MissingValuesPluginOptions) {\n return {\n id: 'mcpuiMissingValues',\n afterDatasetsDraw(chart: any) {\n try {\n drawMissingBarStubs(chart);\n if (options.allMissing) {\n drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.(), options.getTextColor?.());\n }\n } catch {\n // Cosmetic layer: never break the chart.\n }\n },\n };\n}\n"],"names":["formatMCPUIString","isMissingChartValue","_a","_b"],"mappings":"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"}
@@ -100,6 +100,8 @@ export interface MissingValuesPluginOptions {
100
100
  getNoDataText: () => string;
101
101
  /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */
102
102
  getFontFamily?: () => string | undefined;
103
+ /** Lazy host theme override; omission retains the historical grey. */
104
+ getTextColor?: () => string | undefined;
103
105
  }
104
106
  /**
105
107
  * Draws a dashed outline stub for every missing bar of every visible,
@@ -113,7 +115,7 @@ export interface MissingValuesPluginOptions {
113
115
  */
114
116
  export declare function drawMissingBarStubs(chart: any): void;
115
117
  /** Draws `text` centred in the chart area (a chart with nothing to plot). */
116
- export declare function drawNoDataText(chart: any, text: string, fontFamily?: string): void;
118
+ export declare function drawNoDataText(chart: any, text: string, fontFamily?: string, color?: string): void;
117
119
  /**
118
120
  * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js
119
121
  * scopes it to that one chart. Both hooks swallow their own errors — a
@@ -1 +1 @@
1
- {"version":3,"file":"chart-missing-plugin.d.ts","sourceRoot":"","sources":["../../src/components/chart-missing-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAInE,KAAK,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAEtC,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,kEAAkE;AAClE,eAAO,MAAM,0BAA0B,YAAY,CAAC;AACpD,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAM5C;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAEpD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,eAAe,CAI7F;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAWlF;AAID,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,SAAS,OAAO,EAAE,EAChC,UAAU,EAAE,MAAM,OAAO,GACxB,eAAe,CAcjB;AAID,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAWvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,OAAO,EACzB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,MAAM,CAgBlC;AAID;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,GAAG,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,IAAI,CAYN;AAID,MAAM,WAAW,0BAA0B;IACzC,wEAAwE;IACxE,UAAU,EAAE,OAAO,CAAC;IACpB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAC1C;AAYD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CA+DpD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAalF;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B;;6BAGhD,GAAG;EAW/B"}
1
+ {"version":3,"file":"chart-missing-plugin.d.ts","sourceRoot":"","sources":["../../src/components/chart-missing-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAInE,KAAK,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAEtC,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,kEAAkE;AAClE,eAAO,MAAM,0BAA0B,YAAY,CAAC;AACpD,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAM5C;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAEpD;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,eAAe,CAI7F;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAWlF;AAID,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,SAAS,OAAO,EAAE,EAChC,UAAU,EAAE,MAAM,OAAO,GACxB,eAAe,CAcjB;AAID,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAWvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,OAAO,EACzB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,MAAM,CAgBlC;AAID;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,GAAG,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,IAAI,CAYN;AAID,MAAM,WAAW,0BAA0B;IACzC,wEAAwE;IACxE,UAAU,EAAE,OAAO,CAAC;IACpB,+BAA+B;IAC/B,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACzC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACzC;AAYD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CA+DpD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAalG;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,0BAA0B;;6BAGhD,GAAG;EAW/B"}
@@ -147,7 +147,7 @@ function drawMissingBarStubs(chart) {
147
147
  ctx.restore();
148
148
  }
149
149
  }
150
- function drawNoDataText(chart, text, fontFamily) {
150
+ function drawNoDataText(chart, text, fontFamily, color) {
151
151
  const ctx = chart == null ? void 0 : chart.ctx;
152
152
  const area = chart == null ? void 0 : chart.chartArea;
153
153
  if (!ctx || !area || !text) return;
@@ -155,7 +155,7 @@ function drawNoDataText(chart, text, fontFamily) {
155
155
  const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;
156
156
  ctx.save();
157
157
  ctx.font = `12px ${fontFamily || "sans-serif"}`;
158
- ctx.fillStyle = NO_DATA_TEXT_COLOR;
158
+ ctx.fillStyle = color || NO_DATA_TEXT_COLOR;
159
159
  ctx.textAlign = "center";
160
160
  ctx.textBaseline = "middle";
161
161
  ctx.fillText(text, cx, cy);
@@ -165,11 +165,11 @@ function createMissingValuesPlugin(options) {
165
165
  return {
166
166
  id: "mcpuiMissingValues",
167
167
  afterDatasetsDraw(chart) {
168
- var _a;
168
+ var _a, _b;
169
169
  try {
170
170
  drawMissingBarStubs(chart);
171
171
  if (options.allMissing) {
172
- drawNoDataText(chart, options.getNoDataText(), (_a = options.getFontFamily) == null ? void 0 : _a.call(options));
172
+ drawNoDataText(chart, options.getNoDataText(), (_a = options.getFontFamily) == null ? void 0 : _a.call(options), (_b = options.getTextColor) == null ? void 0 : _b.call(options));
173
173
  }
174
174
  } catch {
175
175
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chart-missing-plugin.js","sources":["../../src/components/chart-missing-plugin.ts"],"sourcesContent":["/**\n * Missing values in a Chart.js chart (v6.24.0) — everything the renderer adds\n * to its Chart.js config when a payload says a value is MISSING (`null`).\n *\n * The rule: absence is never zero. Chart.js already keeps the slot of a `null`\n * (no label dropped) and draws nothing for it; what it does not do is make the\n * absence READABLE. A null bar is a zero-height bar that is skipped when\n * drawn, a null line point is a hole the line may bridge (`spanGaps`), and the\n * legend, tooltip and axis know nothing of either. This module supplies:\n *\n * - {@link createMissingValuesPlugin} — a PER-CHART plugin (never\n * `Chart.register`ed, so it can leak into no other chart) that outlines a\n * dashed stub where a bar should have been (not on stacked or overlaid\n * bars, whose baseline belongs to another series), and says \"no data\" on a\n * chart with nothing to draw;\n * - {@link createMissingLegendGenerator} — marks a fully-missing series in the\n * legend;\n * - {@link createTooltipLabel} — names a missing value, and appends the unit;\n * - {@link withDatasetsNoGapSpan} — the shallow-cloned data with\n * `spanGaps: false`;\n * - {@link applyUnitAxisTitle} — the unit as the value axis title.\n *\n * Runtime-free on purpose (no `solid-js`, no `chart.js` import): the renderer\n * hands in the Chart.js pieces it loaded and the chrome strings (lazily, as\n * getters, so a late locale change is honoured at draw time). Every function\n * is defensive — a drawing hook that throws would blank the whole chart.\n *\n * Chart.js 4.5.1 facts this relies on (verified in `dist/chart.js`):\n * - a bar whose parsed value is `null` gets `base = head = vScale.getBasePixel()`\n * and no `skip` flag (BarController.updateElements), and `BarController.draw`\n * skips it (`getParsed(i)[vScale.axis] !== null`) — so nothing is painted;\n * - the bar element still carries its true index-axis thickness (`width` for a\n * vertical bar, `height` for a horizontal one).\n */\n\nimport type { MCPUIStrings } from '../context/MCPUIStringsContext';\nimport { formatMCPUIString } from '../utils/format-string';\nimport { isMissingChartValue } from '../utils/missing-value';\n\ntype Strings = Required<MCPUIStrings>;\n\n/** Length, in px along the value axis, of the dashed stub of a missing bar. */\nexport const MISSING_BAR_STUB_LENGTH = 12;\n/** Stroke of the stub when the dataset names no border colour. */\nexport const MISSING_BAR_FALLBACK_COLOR = '#9ca3af';\n/** Colour of the centred \"no data\" text. */\nexport const NO_DATA_TEXT_COLOR = '#6b7280';\nconst STUB_DASH = [4, 3];\n\n/** Chart types whose value axis is `y` and whose points are `{x, y}`. */\nconst POINT_CHART_TYPES = new Set(['scatter', 'bubble']);\n\n/**\n * The category of convention line a chart shows under its canvas:\n * `'gaps'` (a line breaks), `'bars'` (a dashed outline stands in for the\n * bar) or `'omitted'` (nothing is drawn: scatter, bubble, stacked bars).\n */\nexport type MissingNoteKind = 'gaps' | 'bars' | 'omitted';\n\n/**\n * Whether a bar chart's bars are stacked or overlaid (`stacked` on either\n * axis). A missing bar there has no slot of its own at the baseline — the\n * baseline belongs to the series below — so no dashed outline is drawn.\n */\nexport function isStackedChart(options: any): boolean {\n return Boolean(options?.scales?.x?.stacked || options?.scales?.y?.stacked);\n}\n\n/** Which convention note a chart gets: how ITS missing values are drawn. */\nexport function missingNoteKind(chartType: string | undefined, options?: any): MissingNoteKind {\n if (chartType === 'bar') return isStackedChart(options) ? 'omitted' : 'bars';\n if (chartType === 'scatter' || chartType === 'bubble') return 'omitted';\n return 'gaps';\n}\n\n// ─── Data ──────────────────────────────────────────────────────────────────\n\n/**\n * Chart.js data with every dataset shallow-cloned and `spanGaps: false`, so a\n * line breaks at a missing value instead of joining its neighbours. Never\n * mutates its input; `labels` and every other key are carried by reference.\n */\nexport function withDatasetsNoGapSpan<D extends { datasets?: unknown }>(data: D): D {\n const datasets = Array.isArray(data?.datasets) ? data.datasets : null;\n if (!datasets) return data;\n return {\n ...data,\n datasets: datasets.map((dataset: unknown) =>\n dataset !== null && typeof dataset === 'object'\n ? { ...(dataset as object), spanGaps: false }\n : dataset\n ),\n };\n}\n\n// ─── Legend ────────────────────────────────────────────────────────────────\n\ntype LegendGenerator = (chart: any) => any[];\n\n/**\n * Wraps a Chart.js legend label generator so a fully-missing dataset reads\n * `\"{series} (no data)\"` (`strings.chartLegendNoData`). Every other item is\n * passed through untouched.\n */\nexport function createMissingLegendGenerator(\n base: LegendGenerator,\n fullyMissing: readonly boolean[],\n getStrings: () => Strings\n): LegendGenerator {\n return function generateLabels(this: unknown, chart: any) {\n const items = base.call(this, chart);\n if (!Array.isArray(items)) return items;\n return items.map((item) => {\n const index = item?.datasetIndex;\n if (typeof index !== 'number' || !fullyMissing[index]) return item;\n const text = typeof item.text === 'string' ? item.text : String(item.text ?? '');\n return {\n ...item,\n text: formatMCPUIString(getStrings().chartLegendNoData, { series: text }),\n };\n });\n };\n}\n\n// ─── Tooltip ───────────────────────────────────────────────────────────────\n\n/** Whether a Chart.js tooltip item stands for a missing value. */\nexport function isMissingTooltipItem(item: any): boolean {\n if (item === null || item === undefined) return false;\n if (item.raw === null || item.raw === undefined || isMissingChartValue(item.raw)) return true;\n const parsed = item.parsed;\n if (parsed !== null && typeof parsed === 'object') {\n const chartType = item.chart?.config?.type;\n const valueKey =\n chartType === 'radar' ? 'r' : item.chart?.options?.indexAxis === 'y' ? 'x' : 'y';\n if (valueKey in parsed && parsed[valueKey] === null) return true;\n }\n return false;\n}\n\n/**\n * Tooltip `label` callback: names a missing value (`\"{series}: Missing\n * value\"`) and, with a `unit`, appends it to a present one. Otherwise it is\n * Chart.js's default composition (dataset label, `\": \"`, formatted value) —\n * including its `mode: 'dataset'` branch, which prefixes the CATEGORY label\n * instead, since every item of that tooltip belongs to the same dataset.\n * Hence a regular function: Chart.js calls it with the tooltip as `this`.\n *\n * `unit` is left off scatter and bubble tooltips: their formatted value is the\n * pair `(x, y)`, to which one unit does not belong.\n */\nexport function createTooltipLabel(\n getStrings: () => Strings,\n unit: string | undefined,\n chartType: string | undefined\n): (this: any, item: any) => string {\n const applyUnit = unit !== undefined && !POINT_CHART_TYPES.has(chartType ?? '');\n return function label(this: any, item: any) {\n const strings = getStrings();\n const prefixLabel = this?.options?.mode === 'dataset' ? item?.label : item?.dataset?.label;\n const prefix = typeof prefixLabel === 'string' && prefixLabel !== '' ? `${prefixLabel}: ` : '';\n\n if (isMissingTooltipItem(item)) return `${prefix}${strings.missingValue}`;\n\n const formatted = item?.formattedValue;\n if (formatted === null || formatted === undefined) return prefix;\n const value = applyUnit\n ? formatMCPUIString(strings.chartValueWithUnit, { value: formatted, unit: unit as string })\n : String(formatted);\n return `${prefix}${value}`;\n };\n}\n\n// ─── Axis ──────────────────────────────────────────────────────────────────\n\n/**\n * Sets the chart's `unit` as the title of its value axis, in place on the\n * `options` object the renderer just built (never on the payload's).\n *\n * Only bar, line, scatter and bubble have a value axis to title. The value\n * axis is `y`, or `x` when the index axis is `y` (a horizontal bar or line).\n * An existing `scales[axis].title` is the payload's own word and is kept; the\n * rest of the scale config (a `timeAxis` block sets `scales.x`) is kept too.\n */\nexport function applyUnitAxisTitle(\n options: any,\n chartType: string | undefined,\n unit: string | undefined\n): void {\n if (unit === undefined) return;\n if (chartType !== 'bar' && chartType !== 'line' && !POINT_CHART_TYPES.has(chartType ?? '')) {\n return;\n }\n const horizontal = (chartType === 'bar' || chartType === 'line') && options?.indexAxis === 'y';\n const axis = horizontal ? 'x' : 'y';\n if (options.scales?.[axis]?.title !== undefined) return;\n options.scales = {\n ...options.scales,\n [axis]: { ...options.scales?.[axis], title: { display: true, text: unit } },\n };\n}\n\n// ─── Plugin ────────────────────────────────────────────────────────────────\n\nexport interface MissingValuesPluginOptions {\n /** At least one dataset and every one of them has no observed value. */\n allMissing: boolean;\n /** Lazy: read at draw time. */\n getNoDataText: () => string;\n /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */\n getFontFamily?: () => string | undefined;\n}\n\n/** A dataset colour for entry `index`, as a string, or `undefined`. */\nfunction colorAt(color: unknown, index: number): string | undefined {\n if (typeof color === 'string' && color !== '') return color;\n if (Array.isArray(color) && color.length > 0) {\n const picked = color[index % color.length];\n return typeof picked === 'string' && picked !== '' ? picked : undefined;\n }\n return undefined;\n}\n\n/**\n * Draws a dashed outline stub for every missing bar of every visible,\n * non-stacked bar dataset, so a missing value is a visible mark, not a bar\n * that \"is\" zero.\n *\n * The stub sits on the value-axis base, as thick as the bar would have been,\n * and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart\n * area (up from a bottom baseline, down from a top one, right from a left\n * one). Clipped to the chart area.\n */\nexport function drawMissingBarStubs(chart: any): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n const datasets = chart?.data?.datasets;\n if (!ctx || !area || !Array.isArray(datasets)) return;\n\n const left = area.left;\n const top = area.top;\n const right = area.right ?? left + (area.width ?? 0);\n const bottom = area.bottom ?? top + (area.height ?? 0);\n\n let saved = false;\n datasets.forEach((dataset: any, datasetIndex: number) => {\n const meta = chart.getDatasetMeta?.(datasetIndex);\n if (!meta) return;\n const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';\n if (!isBar) return;\n // Stacked or overlaid bars: the baseline under a missing segment is the\n // series below, so an outline there would point at the wrong series.\n if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;\n if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {\n return;\n }\n const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];\n const elements: any[] = Array.isArray(meta.data) ? meta.data : [];\n\n values.forEach((value, index) => {\n if (!isMissingChartValue(value)) return;\n const element = elements[index];\n if (!element) return;\n const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;\n const { x, y, base, width, height } = props;\n if (![x, y, base].every((n) => Number.isFinite(n))) return;\n const horizontal = element.horizontal === true;\n const thickness = horizontal ? height : width;\n if (!Number.isFinite(thickness) || thickness <= 0) return;\n\n let rect: [number, number, number, number];\n if (horizontal) {\n const dir = base < (left + right) / 2 ? 1 : -1;\n rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];\n } else {\n const dir = base > (top + bottom) / 2 ? -1 : 1;\n rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];\n }\n\n if (!saved) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(left, top, right - left, bottom - top);\n ctx.clip();\n saved = true;\n }\n ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;\n ctx.lineWidth = 1;\n ctx.setLineDash(STUB_DASH);\n ctx.strokeRect(...rect);\n });\n });\n if (saved) {\n ctx.setLineDash?.([]);\n ctx.restore();\n }\n}\n\n/** Draws `text` centred in the chart area (a chart with nothing to plot). */\nexport function drawNoDataText(chart: any, text: string, fontFamily?: string): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n if (!ctx || !area || !text) return;\n const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;\n const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;\n ctx.save();\n ctx.font = `12px ${fontFamily || 'sans-serif'}`;\n ctx.fillStyle = NO_DATA_TEXT_COLOR;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, cx, cy);\n ctx.restore();\n}\n\n/**\n * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js\n * scopes it to that one chart. Both hooks swallow their own errors — a\n * decoration must never take the chart down.\n */\nexport function createMissingValuesPlugin(options: MissingValuesPluginOptions) {\n return {\n id: 'mcpuiMissingValues',\n afterDatasetsDraw(chart: any) {\n try {\n drawMissingBarStubs(chart);\n if (options.allMissing) {\n drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.());\n }\n } catch {\n // Cosmetic layer: never break the chart.\n }\n },\n };\n}\n"],"names":["_a","_b"],"mappings":"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"}
1
+ {"version":3,"file":"chart-missing-plugin.js","sources":["../../src/components/chart-missing-plugin.ts"],"sourcesContent":["/**\n * Missing values in a Chart.js chart (v6.24.0) — everything the renderer adds\n * to its Chart.js config when a payload says a value is MISSING (`null`).\n *\n * The rule: absence is never zero. Chart.js already keeps the slot of a `null`\n * (no label dropped) and draws nothing for it; what it does not do is make the\n * absence READABLE. A null bar is a zero-height bar that is skipped when\n * drawn, a null line point is a hole the line may bridge (`spanGaps`), and the\n * legend, tooltip and axis know nothing of either. This module supplies:\n *\n * - {@link createMissingValuesPlugin} — a PER-CHART plugin (never\n * `Chart.register`ed, so it can leak into no other chart) that outlines a\n * dashed stub where a bar should have been (not on stacked or overlaid\n * bars, whose baseline belongs to another series), and says \"no data\" on a\n * chart with nothing to draw;\n * - {@link createMissingLegendGenerator} — marks a fully-missing series in the\n * legend;\n * - {@link createTooltipLabel} — names a missing value, and appends the unit;\n * - {@link withDatasetsNoGapSpan} — the shallow-cloned data with\n * `spanGaps: false`;\n * - {@link applyUnitAxisTitle} — the unit as the value axis title.\n *\n * Runtime-free on purpose (no `solid-js`, no `chart.js` import): the renderer\n * hands in the Chart.js pieces it loaded and the chrome strings (lazily, as\n * getters, so a late locale change is honoured at draw time). Every function\n * is defensive — a drawing hook that throws would blank the whole chart.\n *\n * Chart.js 4.5.1 facts this relies on (verified in `dist/chart.js`):\n * - a bar whose parsed value is `null` gets `base = head = vScale.getBasePixel()`\n * and no `skip` flag (BarController.updateElements), and `BarController.draw`\n * skips it (`getParsed(i)[vScale.axis] !== null`) — so nothing is painted;\n * - the bar element still carries its true index-axis thickness (`width` for a\n * vertical bar, `height` for a horizontal one).\n */\n\nimport type { MCPUIStrings } from '../context/MCPUIStringsContext';\nimport { formatMCPUIString } from '../utils/format-string';\nimport { isMissingChartValue } from '../utils/missing-value';\n\ntype Strings = Required<MCPUIStrings>;\n\n/** Length, in px along the value axis, of the dashed stub of a missing bar. */\nexport const MISSING_BAR_STUB_LENGTH = 12;\n/** Stroke of the stub when the dataset names no border colour. */\nexport const MISSING_BAR_FALLBACK_COLOR = '#9ca3af';\n/** Colour of the centred \"no data\" text. */\nexport const NO_DATA_TEXT_COLOR = '#6b7280';\nconst STUB_DASH = [4, 3];\n\n/** Chart types whose value axis is `y` and whose points are `{x, y}`. */\nconst POINT_CHART_TYPES = new Set(['scatter', 'bubble']);\n\n/**\n * The category of convention line a chart shows under its canvas:\n * `'gaps'` (a line breaks), `'bars'` (a dashed outline stands in for the\n * bar) or `'omitted'` (nothing is drawn: scatter, bubble, stacked bars).\n */\nexport type MissingNoteKind = 'gaps' | 'bars' | 'omitted';\n\n/**\n * Whether a bar chart's bars are stacked or overlaid (`stacked` on either\n * axis). A missing bar there has no slot of its own at the baseline — the\n * baseline belongs to the series below — so no dashed outline is drawn.\n */\nexport function isStackedChart(options: any): boolean {\n return Boolean(options?.scales?.x?.stacked || options?.scales?.y?.stacked);\n}\n\n/** Which convention note a chart gets: how ITS missing values are drawn. */\nexport function missingNoteKind(chartType: string | undefined, options?: any): MissingNoteKind {\n if (chartType === 'bar') return isStackedChart(options) ? 'omitted' : 'bars';\n if (chartType === 'scatter' || chartType === 'bubble') return 'omitted';\n return 'gaps';\n}\n\n// ─── Data ──────────────────────────────────────────────────────────────────\n\n/**\n * Chart.js data with every dataset shallow-cloned and `spanGaps: false`, so a\n * line breaks at a missing value instead of joining its neighbours. Never\n * mutates its input; `labels` and every other key are carried by reference.\n */\nexport function withDatasetsNoGapSpan<D extends { datasets?: unknown }>(data: D): D {\n const datasets = Array.isArray(data?.datasets) ? data.datasets : null;\n if (!datasets) return data;\n return {\n ...data,\n datasets: datasets.map((dataset: unknown) =>\n dataset !== null && typeof dataset === 'object'\n ? { ...(dataset as object), spanGaps: false }\n : dataset\n ),\n };\n}\n\n// ─── Legend ────────────────────────────────────────────────────────────────\n\ntype LegendGenerator = (chart: any) => any[];\n\n/**\n * Wraps a Chart.js legend label generator so a fully-missing dataset reads\n * `\"{series} (no data)\"` (`strings.chartLegendNoData`). Every other item is\n * passed through untouched.\n */\nexport function createMissingLegendGenerator(\n base: LegendGenerator,\n fullyMissing: readonly boolean[],\n getStrings: () => Strings\n): LegendGenerator {\n return function generateLabels(this: unknown, chart: any) {\n const items = base.call(this, chart);\n if (!Array.isArray(items)) return items;\n return items.map((item) => {\n const index = item?.datasetIndex;\n if (typeof index !== 'number' || !fullyMissing[index]) return item;\n const text = typeof item.text === 'string' ? item.text : String(item.text ?? '');\n return {\n ...item,\n text: formatMCPUIString(getStrings().chartLegendNoData, { series: text }),\n };\n });\n };\n}\n\n// ─── Tooltip ───────────────────────────────────────────────────────────────\n\n/** Whether a Chart.js tooltip item stands for a missing value. */\nexport function isMissingTooltipItem(item: any): boolean {\n if (item === null || item === undefined) return false;\n if (item.raw === null || item.raw === undefined || isMissingChartValue(item.raw)) return true;\n const parsed = item.parsed;\n if (parsed !== null && typeof parsed === 'object') {\n const chartType = item.chart?.config?.type;\n const valueKey =\n chartType === 'radar' ? 'r' : item.chart?.options?.indexAxis === 'y' ? 'x' : 'y';\n if (valueKey in parsed && parsed[valueKey] === null) return true;\n }\n return false;\n}\n\n/**\n * Tooltip `label` callback: names a missing value (`\"{series}: Missing\n * value\"`) and, with a `unit`, appends it to a present one. Otherwise it is\n * Chart.js's default composition (dataset label, `\": \"`, formatted value) —\n * including its `mode: 'dataset'` branch, which prefixes the CATEGORY label\n * instead, since every item of that tooltip belongs to the same dataset.\n * Hence a regular function: Chart.js calls it with the tooltip as `this`.\n *\n * `unit` is left off scatter and bubble tooltips: their formatted value is the\n * pair `(x, y)`, to which one unit does not belong.\n */\nexport function createTooltipLabel(\n getStrings: () => Strings,\n unit: string | undefined,\n chartType: string | undefined\n): (this: any, item: any) => string {\n const applyUnit = unit !== undefined && !POINT_CHART_TYPES.has(chartType ?? '');\n return function label(this: any, item: any) {\n const strings = getStrings();\n const prefixLabel = this?.options?.mode === 'dataset' ? item?.label : item?.dataset?.label;\n const prefix = typeof prefixLabel === 'string' && prefixLabel !== '' ? `${prefixLabel}: ` : '';\n\n if (isMissingTooltipItem(item)) return `${prefix}${strings.missingValue}`;\n\n const formatted = item?.formattedValue;\n if (formatted === null || formatted === undefined) return prefix;\n const value = applyUnit\n ? formatMCPUIString(strings.chartValueWithUnit, { value: formatted, unit: unit as string })\n : String(formatted);\n return `${prefix}${value}`;\n };\n}\n\n// ─── Axis ──────────────────────────────────────────────────────────────────\n\n/**\n * Sets the chart's `unit` as the title of its value axis, in place on the\n * `options` object the renderer just built (never on the payload's).\n *\n * Only bar, line, scatter and bubble have a value axis to title. The value\n * axis is `y`, or `x` when the index axis is `y` (a horizontal bar or line).\n * An existing `scales[axis].title` is the payload's own word and is kept; the\n * rest of the scale config (a `timeAxis` block sets `scales.x`) is kept too.\n */\nexport function applyUnitAxisTitle(\n options: any,\n chartType: string | undefined,\n unit: string | undefined\n): void {\n if (unit === undefined) return;\n if (chartType !== 'bar' && chartType !== 'line' && !POINT_CHART_TYPES.has(chartType ?? '')) {\n return;\n }\n const horizontal = (chartType === 'bar' || chartType === 'line') && options?.indexAxis === 'y';\n const axis = horizontal ? 'x' : 'y';\n if (options.scales?.[axis]?.title !== undefined) return;\n options.scales = {\n ...options.scales,\n [axis]: { ...options.scales?.[axis], title: { display: true, text: unit } },\n };\n}\n\n// ─── Plugin ────────────────────────────────────────────────────────────────\n\nexport interface MissingValuesPluginOptions {\n /** At least one dataset and every one of them has no observed value. */\n allMissing: boolean;\n /** Lazy: read at draw time. */\n getNoDataText: () => string;\n /** Lazy: `Chart.defaults.font.family`, when the build exposes it. */\n getFontFamily?: () => string | undefined;\n /** Lazy host theme override; omission retains the historical grey. */\n getTextColor?: () => string | undefined;\n}\n\n/** A dataset colour for entry `index`, as a string, or `undefined`. */\nfunction colorAt(color: unknown, index: number): string | undefined {\n if (typeof color === 'string' && color !== '') return color;\n if (Array.isArray(color) && color.length > 0) {\n const picked = color[index % color.length];\n return typeof picked === 'string' && picked !== '' ? picked : undefined;\n }\n return undefined;\n}\n\n/**\n * Draws a dashed outline stub for every missing bar of every visible,\n * non-stacked bar dataset, so a missing value is a visible mark, not a bar\n * that \"is\" zero.\n *\n * The stub sits on the value-axis base, as thick as the bar would have been,\n * and runs {@link MISSING_BAR_STUB_LENGTH}px toward the middle of the chart\n * area (up from a bottom baseline, down from a top one, right from a left\n * one). Clipped to the chart area.\n */\nexport function drawMissingBarStubs(chart: any): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n const datasets = chart?.data?.datasets;\n if (!ctx || !area || !Array.isArray(datasets)) return;\n\n const left = area.left;\n const top = area.top;\n const right = area.right ?? left + (area.width ?? 0);\n const bottom = area.bottom ?? top + (area.height ?? 0);\n\n let saved = false;\n datasets.forEach((dataset: any, datasetIndex: number) => {\n const meta = chart.getDatasetMeta?.(datasetIndex);\n if (!meta) return;\n const isBar = meta.type === 'bar' || meta.controller?.constructor?.id === 'bar';\n if (!isBar) return;\n // Stacked or overlaid bars: the baseline under a missing segment is the\n // series below, so an outline there would point at the wrong series.\n if (meta.vScale?.options?.stacked || meta.iScale?.options?.stacked) return;\n if (typeof chart.isDatasetVisible === 'function' && !chart.isDatasetVisible(datasetIndex)) {\n return;\n }\n const values: unknown[] = Array.isArray(dataset?.data) ? dataset.data : [];\n const elements: any[] = Array.isArray(meta.data) ? meta.data : [];\n\n values.forEach((value, index) => {\n if (!isMissingChartValue(value)) return;\n const element = elements[index];\n if (!element) return;\n const props = element.getProps?.(['x', 'y', 'base', 'width', 'height'], true) ?? element;\n const { x, y, base, width, height } = props;\n if (![x, y, base].every((n) => Number.isFinite(n))) return;\n const horizontal = element.horizontal === true;\n const thickness = horizontal ? height : width;\n if (!Number.isFinite(thickness) || thickness <= 0) return;\n\n let rect: [number, number, number, number];\n if (horizontal) {\n const dir = base < (left + right) / 2 ? 1 : -1;\n rect = [Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), y - thickness / 2, MISSING_BAR_STUB_LENGTH, thickness];\n } else {\n const dir = base > (top + bottom) / 2 ? -1 : 1;\n rect = [x - thickness / 2, Math.min(base, base + dir * MISSING_BAR_STUB_LENGTH), thickness, MISSING_BAR_STUB_LENGTH];\n }\n\n if (!saved) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(left, top, right - left, bottom - top);\n ctx.clip();\n saved = true;\n }\n ctx.strokeStyle = colorAt(dataset?.borderColor, index) ?? MISSING_BAR_FALLBACK_COLOR;\n ctx.lineWidth = 1;\n ctx.setLineDash(STUB_DASH);\n ctx.strokeRect(...rect);\n });\n });\n if (saved) {\n ctx.setLineDash?.([]);\n ctx.restore();\n }\n}\n\n/** Draws `text` centred in the chart area (a chart with nothing to plot). */\nexport function drawNoDataText(chart: any, text: string, fontFamily?: string, color?: string): void {\n const ctx = chart?.ctx;\n const area = chart?.chartArea;\n if (!ctx || !area || !text) return;\n const cx = (area.left + (area.right ?? area.left + (area.width ?? 0))) / 2;\n const cy = (area.top + (area.bottom ?? area.top + (area.height ?? 0))) / 2;\n ctx.save();\n ctx.font = `12px ${fontFamily || 'sans-serif'}`;\n ctx.fillStyle = color || NO_DATA_TEXT_COLOR;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.fillText(text, cx, cy);\n ctx.restore();\n}\n\n/**\n * The per-chart plugin object. Passed as `config.plugins = [plugin]`: Chart.js\n * scopes it to that one chart. Both hooks swallow their own errors — a\n * decoration must never take the chart down.\n */\nexport function createMissingValuesPlugin(options: MissingValuesPluginOptions) {\n return {\n id: 'mcpuiMissingValues',\n afterDatasetsDraw(chart: any) {\n try {\n drawMissingBarStubs(chart);\n if (options.allMissing) {\n drawNoDataText(chart, options.getNoDataText(), options.getFontFamily?.(), options.getTextColor?.());\n }\n } catch {\n // Cosmetic layer: never break the chart.\n }\n },\n };\n}\n"],"names":["_a","_b"],"mappings":"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"}
@@ -146,7 +146,7 @@ function isCurrencyLike(raw, tex, following) {
146
146
  const closerLooksLikeLaterOpener = /^(?![\s,.;:!?)}\]])(?:\\[^\n]|[^\\\n$])+\$(?!\$)/.test(following);
147
147
  return closerLooksLikeLaterOpener;
148
148
  }
149
- const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"';
149
+ const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-blue-400))] hover:underline"';
150
150
  function cellLinkExtension() {
151
151
  return {
152
152
  renderer: {
@@ -1 +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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"}
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-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-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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"}
@@ -144,7 +144,7 @@ function isCurrencyLike(raw, tex, following) {
144
144
  const closerLooksLikeLaterOpener = /^(?![\s,.;:!?)}\]])(?:\\[^\n]|[^\\\n$])+\$(?!\$)/.test(following);
145
145
  return closerLooksLikeLaterOpener;
146
146
  }
147
- const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"';
147
+ const CELL_LINK_ATTRIBUTES = 'target="_blank" rel="noopener noreferrer" class="text-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-blue-400))] hover:underline"';
148
148
  function cellLinkExtension() {
149
149
  return {
150
150
  renderer: {
@@ -1 +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 `&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":[],"mappings":"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"}
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-[color:var(--mcp-ui-accent,var(--color-blue-600))] dark:text-[color:var(--mcp-ui-accent,var(--color-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":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const solidJs = require("solid-js");
4
+ const THEME_PREFIX = "--mcp-ui-";
5
+ function readThemeTokens(element, names) {
6
+ const result = {};
7
+ const view = element == null ? void 0 : element.ownerDocument.defaultView;
8
+ const style = element && view ? view.getComputedStyle(element) : void 0;
9
+ for (const name of names) result[name] = (style == null ? void 0 : style.getPropertyValue(name).trim()) || void 0;
10
+ return result;
11
+ }
12
+ function observeTheme(element, callback) {
13
+ const view = element.ownerDocument.defaultView;
14
+ if (!(view == null ? void 0 : view.MutationObserver)) return () => {
15
+ };
16
+ let stopped = false;
17
+ let pending = false;
18
+ const schedule = () => {
19
+ if (pending || stopped) return;
20
+ pending = true;
21
+ queueMicrotask(() => {
22
+ pending = false;
23
+ if (!stopped) callback();
24
+ });
25
+ };
26
+ const observer = new view.MutationObserver((records) => {
27
+ if (records.some((record) => {
28
+ if (record.type === "attributes") {
29
+ return record.target instanceof view.Element && record.target.contains(element);
30
+ }
31
+ return [...record.addedNodes, ...record.removedNodes].some(
32
+ (node) => node === element || node.contains(element)
33
+ );
34
+ })) schedule();
35
+ });
36
+ observer.observe(element.ownerDocument.documentElement, {
37
+ attributes: true,
38
+ childList: true,
39
+ subtree: true
40
+ });
41
+ return () => {
42
+ stopped = true;
43
+ observer.disconnect();
44
+ };
45
+ }
46
+ function bindPortalTheme(portal, source) {
47
+ solidJs.createEffect(() => {
48
+ const anchor = source();
49
+ const view = anchor == null ? void 0 : anchor.ownerDocument.defaultView;
50
+ if (!anchor || !view) return;
51
+ const copied = /* @__PURE__ */ new Set();
52
+ const sync = () => {
53
+ const style = view.getComputedStyle(anchor);
54
+ const inherited = portal.parentElement ? view.getComputedStyle(portal.parentElement) : void 0;
55
+ const names = new Set(copied);
56
+ for (const computed of [style, inherited]) {
57
+ if (!computed) continue;
58
+ for (let index = 0; index < computed.length; index++) {
59
+ const name = computed.item(index);
60
+ if (name.startsWith(THEME_PREFIX)) names.add(name);
61
+ }
62
+ }
63
+ const values = /* @__PURE__ */ new Map();
64
+ for (const name of names) {
65
+ const value = style.getPropertyValue(name).trim();
66
+ if (value) values.set(name, value);
67
+ else if (inherited == null ? void 0 : inherited.getPropertyValue(name).trim()) values.set(name, "initial");
68
+ }
69
+ for (const name of copied) {
70
+ if (!values.has(name)) portal.style.removeProperty(name);
71
+ }
72
+ copied.clear();
73
+ for (const [name, value] of values) {
74
+ if (portal.style.getPropertyValue(name) !== value) portal.style.setProperty(name, value);
75
+ copied.add(name);
76
+ }
77
+ portal.classList.toggle("dark", anchor.closest(".dark") !== null);
78
+ };
79
+ sync();
80
+ const stop = observeTheme(anchor, sync);
81
+ solidJs.onCleanup(() => {
82
+ stop();
83
+ for (const name of copied) portal.style.removeProperty(name);
84
+ portal.classList.remove("dark");
85
+ });
86
+ });
87
+ }
88
+ exports.bindPortalTheme = bindPortalTheme;
89
+ exports.observeTheme = observeTheme;
90
+ exports.readThemeTokens = readThemeTokens;
91
+ //# sourceMappingURL=theme.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.cjs","sources":["../../src/utils/theme.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\n\nconst THEME_PREFIX = '--mcp-ui-'\n\n/** Read only explicitly supplied host tokens; blank values retain engine defaults. */\nexport function readThemeTokens(\n element: Element | undefined,\n names: readonly string[],\n): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = {}\n const view = element?.ownerDocument.defaultView\n const style = element && view ? view.getComputedStyle(element) : undefined\n for (const name of names) result[name] = style?.getPropertyValue(name).trim() || undefined\n return result\n}\n\n/** Observe inherited theme changes, including moving a mounted renderer into a portal. */\nexport function observeTheme(element: HTMLElement, callback: () => void): () => void {\n const view = element.ownerDocument.defaultView\n if (!view?.MutationObserver) return () => {}\n let stopped = false\n let pending = false\n const schedule = () => {\n if (pending || stopped) return\n pending = true\n queueMicrotask(() => {\n pending = false\n if (!stopped) callback()\n })\n }\n const observer = new view.MutationObserver((records) => {\n if (records.some((record) => {\n if (record.type === 'attributes') {\n return record.target instanceof view.Element && record.target.contains(element)\n }\n return [...record.addedNodes, ...record.removedNodes].some(\n (node) => node === element || node.contains(element),\n )\n })) schedule()\n })\n // Attribute names are deliberately not filtered: hosts may use any data-* selector.\n observer.observe(element.ownerDocument.documentElement, {\n attributes: true,\n childList: true,\n subtree: true,\n })\n return () => {\n stopped = true\n observer.disconnect()\n }\n}\n\n/**\n * A body portal cannot inherit a local host theme. Copy only our effective\n * custom properties and dark state from its stationary anchor, and keep them\n * synchronized for the lifetime of the Solid owner. Geometry stays unchanged.\n */\nexport function bindPortalTheme(portal: HTMLElement, source: () => HTMLElement | undefined): void {\n createEffect(() => {\n const anchor = source()\n const view = anchor?.ownerDocument.defaultView\n if (!anchor || !view) return\n const copied = new Set<string>()\n const sync = () => {\n const style = view.getComputedStyle(anchor)\n const inherited = portal.parentElement ? view.getComputedStyle(portal.parentElement) : undefined\n const names = new Set(copied)\n for (const computed of [style, inherited]) {\n if (!computed) continue\n for (let index = 0; index < computed.length; index++) {\n const name = computed.item(index)\n if (name.startsWith(THEME_PREFIX)) names.add(name)\n }\n }\n const values = new Map<string, string>()\n for (const name of names) {\n const value = style.getPropertyValue(name).trim()\n if (value) values.set(name, value)\n // Empty setProperty removes a declaration. Explicitly block a body\n // token when the local scope reset it to the guaranteed-invalid value.\n else if (inherited?.getPropertyValue(name).trim()) values.set(name, 'initial')\n }\n for (const name of copied) {\n if (!values.has(name)) portal.style.removeProperty(name)\n }\n copied.clear()\n for (const [name, value] of values) {\n if (portal.style.getPropertyValue(name) !== value) portal.style.setProperty(name, value)\n copied.add(name)\n }\n portal.classList.toggle('dark', anchor.closest('.dark') !== null)\n }\n sync()\n const stop = observeTheme(anchor, sync)\n onCleanup(() => {\n stop()\n for (const name of copied) portal.style.removeProperty(name)\n portal.classList.remove('dark')\n })\n })\n}\n"],"names":["createEffect","onCleanup"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** Read only explicitly supplied host tokens; blank values retain engine defaults. */
2
+ export declare function readThemeTokens(element: Element | undefined, names: readonly string[]): Record<string, string | undefined>;
3
+ /** Observe inherited theme changes, including moving a mounted renderer into a portal. */
4
+ export declare function observeTheme(element: HTMLElement, callback: () => void): () => void;
5
+ /**
6
+ * A body portal cannot inherit a local host theme. Copy only our effective
7
+ * custom properties and dark state from its stationary anchor, and keep them
8
+ * synchronized for the lifetime of the Solid owner. Geometry stays unchanged.
9
+ */
10
+ export declare function bindPortalTheme(portal: HTMLElement, source: () => HTMLElement | undefined): void;
11
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/utils/theme.ts"],"names":[],"mappings":"AAIA,sFAAsF;AACtF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAMpC;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAiCnF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,WAAW,GAAG,SAAS,GAAG,IAAI,CA2ChG"}