@seed-ship/mcp-ui-solid 5.6.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -287,7 +287,98 @@ export function highlightQuery(html: string, query: string): string {
287
287
  })
288
288
  }
289
289
 
290
- export function renderCellValue(value: any): string {
290
+ /**
291
+ * Citation context — opt-in input to `renderCellValue` (v5.7.0).
292
+ *
293
+ * When passed, `[N]`, `Citation [N]`, `[CITATION N]` and `[📄 CITATION N]`
294
+ * markers in the cell text are normalized then replaced with chip HTML.
295
+ * Chips carry `data-citation-page`, `data-citation-doc`, and
296
+ * `data-citation-verified` attributes (already in the DOMPurify whitelist
297
+ * since v5.6.0) so a host's `target.closest('[data-citation-page]')`
298
+ * delegated click handler routes the click to the source-doc panel.
299
+ *
300
+ * See `mcp-ui-solid/docs/briefs/BRIEF-citations-in-table-cells.md`.
301
+ */
302
+ export interface CitationCtx {
303
+ /**
304
+ * `Record<id, mapping>` keyed by the citation marker number (string-keyed
305
+ * because JSON serialization always produces strings; the runtime call
306
+ * sites accept either number or string ids and normalize internally).
307
+ */
308
+ map: Record<string | number, { page: number | string; file?: string; file_id?: number | string }>
309
+ /**
310
+ * Optional override returning sanitized chip HTML for one marker. Wins
311
+ * over the default `defaultCitationChip` shape. Function inputs are
312
+ * intentionally `any`-loose so consumers can swap shapes (e.g. web
313
+ * citations vs doc citations) without subtyping the entry shape here.
314
+ */
315
+ render?: (
316
+ id: number,
317
+ mapping: { page: number | string; file?: string; file_id?: number | string } | undefined
318
+ ) => string
319
+ }
320
+
321
+ /**
322
+ * Default chip HTML emitted by `transformCellCitations` when no
323
+ * `citationRender` override is supplied. Neutral Tailwind classes — hosts
324
+ * can override visual styling via the `.citation-ref` CSS class without
325
+ * passing a render override.
326
+ */
327
+ function defaultCitationChip(
328
+ pageNum: number | string,
329
+ fileName: string,
330
+ verified = true
331
+ ): string {
332
+ const safeDocName = encodeURIComponent(fileName || '')
333
+ const label = fileName ? `${fileName} - ${pageNum}` : `${pageNum}`
334
+ if (!verified) {
335
+ return `<span class="citation-ref inline-flex items-center gap-0.5 align-middle opacity-60"><span class="text-gray-500 line-through">[${label}]</span></span>`
336
+ }
337
+ return [
338
+ '<span class="citation-ref inline-flex items-center gap-0.5 align-middle">',
339
+ `<span class="text-gray-500">[${label}]</span>`,
340
+ '<button class="inline-flex items-center ml-0.5 px-1 py-0.5 text-xs bg-gray-800 hover:bg-gray-700 border border-gray-600 hover:border-teal-500 rounded cursor-pointer transition-colors align-middle"',
341
+ ` data-citation-page="${pageNum}"`,
342
+ ` data-citation-doc="${safeDocName}"`,
343
+ ' data-citation-verified="true"',
344
+ ` title="View source - ${label}">`,
345
+ '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
346
+ '</button>',
347
+ '</span>',
348
+ ].join('')
349
+ }
350
+
351
+ /**
352
+ * Normalize bare `[N]`, `Citation [N]`, `[CITATION N]` markers to canonical
353
+ * `[📄 CITATION N]` then replace each canonical marker with chip HTML.
354
+ *
355
+ * Negative lookbehind `(?<![p.])` skips `[p.5]` (page form). Negative
356
+ * lookahead `(?!\()` skips `[text](url)` markdown links.
357
+ */
358
+ function transformCellCitations(text: string, ctx: CitationCtx): string {
359
+ // 1. normalize bare [N] / Citation [N] / [CITATION N] → [📄 CITATION N]
360
+ let out = text.replace(/(?<![p.])\[(\d{1,2})\](?!\()/g, '[📄 CITATION $1]')
361
+ out = out.replace(/\bCitations?\s*\[(\d+)\]/gi, '[📄 CITATION $1]')
362
+ out = out.replace(/\[CITATION\s+(\d+)\]/gi, '[📄 CITATION $1]')
363
+
364
+ // 2. replace each canonical marker with chip HTML
365
+ return out.replace(
366
+ /[【[]\s*📄\s*CITATION\s*(\d+)\s*[】\]]/gi,
367
+ (_m, idStr) => {
368
+ const id = parseInt(idStr, 10)
369
+ const mapping = ctx.map[id] ?? ctx.map[String(id)]
370
+ if (ctx.render) return ctx.render(id, mapping)
371
+ if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? '', true)
372
+ // Unresolved id: when the map is non-empty (consumer claims to know
373
+ // the citations), drop silently — it's likely an LLM hallucination.
374
+ // When the map is empty (consumer didn't supply one), preserve a
375
+ // human-visible placeholder so the marker isn't lost.
376
+ return Object.keys(ctx.map).length > 0 ? '' : `[réf. ${id}]`
377
+ }
378
+ )
379
+ }
380
+
381
+ export function renderCellValue(value: any, citationCtx?: CitationCtx): string {
291
382
  // Handle null/undefined
292
383
  if (value === null || value === undefined) {
293
384
  return '-'
@@ -331,7 +422,9 @@ export function renderCellValue(value: any): string {
331
422
  return '-'
332
423
  }
333
424
 
334
- // Detect and convert markdown links: [text](url)
425
+ // Detect and convert markdown links: [text](url) — runs FIRST because
426
+ // the citation transform's negative lookahead `(?!\()` would also skip
427
+ // these, but handling them here keeps the existing return path.
335
428
  const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
336
429
  if (markdownLinkRegex.test(strValue)) {
337
430
  // Replace all markdown links with HTML links
@@ -342,8 +435,36 @@ export function renderCellValue(value: any): string {
342
435
  return DOMPurify.sanitize(htmlValue, { ADD_ATTR: ['target', 'rel'] })
343
436
  }
344
437
 
438
+ // v5.7.0 — citation transform (opt-in). Replaces `[N]` style markers
439
+ // with chip HTML carrying `data-citation-*` attrs. Runs BEFORE the
440
+ // hasHtml / hasMarkdown branches so the resulting string flows through
441
+ // them naturally (chips are inline HTML; surviving markdown like
442
+ // **bold** is preserved by marked.parse since marked passes inline HTML
443
+ // through unchanged).
444
+ if (citationCtx) {
445
+ strValue = transformCellCitations(strValue, citationCtx)
446
+ }
447
+
448
+ // Markdown markers WITHOUT square brackets — `[` and `]` were excluded
449
+ // because chip labels (`[Doc - 5]`) and unresolved-marker fallbacks
450
+ // (`[réf. 12]`) would otherwise force a marked.parse for cells that
451
+ // have no actual markdown. The hasMarkdown check ALSO runs before
452
+ // hasHtml so that mixed cells (`**bold** [1]` → `**bold** <chip>`)
453
+ // get marked first; marked preserves the inline chip HTML, then
454
+ // DOMPurify keeps the citation attrs via the extended whitelist.
455
+ const hasMarkdown = /[*_`#]/.test(strValue)
456
+ if (hasMarkdown) {
457
+ const parsed = marked.parse(strValue, { async: false }) as string
458
+ return DOMPurify.sanitize(parsed, {
459
+ ALLOWED_TAGS: ['a', 'strong', 'em', 'b', 'i', 'code', 'span', 'br', 'button', 'svg', 'path', 'p', 'ul', 'ol', 'li', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
460
+ ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'data-citation-page', 'data-citation-source', 'data-citation-doc', 'data-citation-verified', 'title', 'fill', 'stroke', 'viewBox', 'stroke-linecap', 'stroke-linejoin', 'stroke-width', 'd'],
461
+ ADD_ATTR: ['target', 'rel'],
462
+ })
463
+ }
464
+
345
465
  // Detect raw HTML in cell values (e.g. <a href="..." data-citation-page="5">text</a>)
346
466
  // This handles cases where cell data comes from innerHTML extraction
467
+ // OR where the citation transform above injected chip HTML.
347
468
  const hasHtml = /<[a-z][\s\S]*>/i.test(strValue)
348
469
  if (hasHtml) {
349
470
  return DOMPurify.sanitize(strValue, {
@@ -353,14 +474,6 @@ export function renderCellValue(value: any): string {
353
474
  })
354
475
  }
355
476
 
356
- // Check if value contains markdown formatting (bold, italic, code, etc.)
357
- const hasMarkdown = /[*_`[\]#]/.test(strValue)
358
- if (hasMarkdown) {
359
- // Parse with marked and sanitize
360
- const parsed = marked.parse(strValue, { async: false }) as string
361
- return DOMPurify.sanitize(parsed, { ADD_ATTR: ['target', 'rel'] })
362
- }
363
-
364
477
  // Plain text — sanitize to prevent XSS via innerHTML
365
478
  return DOMPurify.sanitize(strValue)
366
479
  }
@@ -376,6 +489,14 @@ function TableRenderer(props: {
376
489
  const tableParams = props.component.params as any
377
490
  let scrollContainerRef: HTMLDivElement | undefined
378
491
 
492
+ // v5.7.0 — opt-in citation chip rendering inside cells. When `citationMap`
493
+ // is present in params, build a CitationCtx once and thread it through
494
+ // every `renderCellValue` call below. Absent → undefined → cells render
495
+ // as before (regression-safe).
496
+ const citationCtx: CitationCtx | undefined = tableParams.citationMap
497
+ ? { map: tableParams.citationMap, render: tableParams.citationRender }
498
+ : undefined
499
+
379
500
  // ─── Client-side sorting (v4.0.5) ────────────────────────
380
501
  const allRows = () => tableParams.rows || []
381
502
  const columns = () => tableParams.columns || []
@@ -638,7 +759,7 @@ function TableRenderer(props: {
638
759
  <For each={tableParams.columns}>
639
760
  {(column: any) => (
640
761
  <td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-200 whitespace-normal break-words leading-relaxed first:pl-6 last:pr-6">
641
- <div innerHTML={highlightQuery(renderCellValue(row[column.key]), debouncedQuery())} />
762
+ <div innerHTML={highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())} />
642
763
  </td>
643
764
  )}
644
765
  </For>
@@ -677,7 +798,7 @@ function TableRenderer(props: {
677
798
  <For each={tableParams.columns}>
678
799
  {(column: any) => (
679
800
  <td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-200 whitespace-normal break-words leading-relaxed first:pl-6 last:pr-6">
680
- <div innerHTML={highlightQuery(renderCellValue(row[column.key]), debouncedQuery())} />
801
+ <div innerHTML={highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())} />
681
802
  </td>
682
803
  )}
683
804
  </For>
package/src/index.ts CHANGED
@@ -92,6 +92,11 @@ export type {
92
92
  // Validation error mode (v5.4.0)
93
93
  export type { ValidationErrorMode } from './components/UIResourceRenderer'
94
94
 
95
+ // Citation chips in table cells (v5.7.0 — brief: BRIEF-citations-in-table-cells.md)
96
+ export { renderCellValue } from './components/UIResourceRenderer'
97
+ export type { CitationCtx } from './components/UIResourceRenderer'
98
+ export type { CitationEntry } from './types'
99
+
95
100
  // Runtime debug mode + perf marks (v5.4.0)
96
101
  export { setDebugMode, isDebugEnabled } from './utils/logger'
97
102
  export { markRenderStart, markRenderEnd, PERF_PREFIX } from './utils/perf'
@@ -173,9 +173,21 @@ export interface TableVirtualizeOptions {
173
173
  threshold?: number
174
174
  }
175
175
 
176
+ /**
177
+ * Citation map entry — source of a `[N]` citation marker rendered inline
178
+ * inside table cells (v5.7.0). See
179
+ * `mcp-ui-solid/docs/briefs/BRIEF-citations-in-table-cells.md`.
180
+ */
181
+ export interface CitationEntry {
182
+ page: number | string
183
+ file?: string
184
+ file_id?: number | string
185
+ }
186
+
176
187
  /**
177
188
  * Table component parameters
178
189
  * Updated Sprint Ultimate U.3: Added virtualization support
190
+ * Updated v5.7.0: Optional citationMap + citationRender for chip rendering
179
191
  */
180
192
  export interface TableComponentParams {
181
193
  title?: string
@@ -211,6 +223,24 @@ export interface TableComponentParams {
211
223
  * Custom CSS class (Sprint 7)
212
224
  */
213
225
  className?: string
226
+ /**
227
+ * Opt-in citation chip rendering (v5.7.0). Maps marker id (e.g. `1` from
228
+ * `[1]` or `[📄 CITATION 1]` in cell text) to its source. When set,
229
+ * `<TableRenderer>` replaces markers in cell strings with clickable
230
+ * chips carrying `data-citation-page` / `data-citation-doc` /
231
+ * `data-citation-verified` attributes that a host's delegated click
232
+ * handler can route. JSON-serializable — safe to send from MCP servers.
233
+ */
234
+ citationMap?: Record<string | number, CitationEntry>
235
+ /**
236
+ * Optional override for the chip HTML (v5.7.0). When supplied, wins over
237
+ * the default chip shape. NOT JSON-serializable — must be wired by the
238
+ * consumer at render time, not from a server payload.
239
+ */
240
+ citationRender?: (
241
+ id: number,
242
+ mapping: CitationEntry | undefined
243
+ ) => string
214
244
  }
215
245
 
216
246
  /**