The old renderer rewrote the text: it found "[n]" with regexes, renumbered them, and swapped the result back in — which broke inside `arr[2][1]`, inside HTML attributes, and whenever two turns disagreed about what "[3]" meant. It also had a fallback markdown renderer of its own for when the rewrite produced something markdown-it would not parse. Now "[n]" is an inline rule registered on the same markdown-it instance that renders everything else. The parser decides what is prose and what is code, a link, or a URL, so the rule never sees "[1]" inside a code span, and it steps aside for "[1](url)". Math is two more rules on the same parser instead of a regex pre-pass, so "$" inside a URL is no longer math. Identity vs display: the stored "[n]" and each card's id are the source's identity (sourceNumber) and are never rewritten. The number a reader sees is the order of first appearance, computed at render time from the token stream (orderSourcesByCitation), so "one, then seven" cannot happen and a saved chat re-opens pointing at the same cards it was saved with. Stored messages and sources are untouched; export and the modal resolve by identity. Translated HTML gets the same links through a TreeWalker over text nodes (linkCitationsInHtml) rather than a regex over markup. Deleted: renderCitationLinks, normalizeAdjacentCitationClusters, the fallback renderer (fallbackMarkdown/renderMixedList/renderFallbackTable), renderLatexText, CITATION_SCAN. Tests that asserted rewritten text now assert token output; harnesses that render for real are given a parser. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
43 lines
2.5 KiB
JavaScript
43 lines
2.5 KiB
JavaScript
import { escapeAttr, escapeHtml } from './citations.js';
|
|
|
|
export function renderSourcesList(sources) {
|
|
if (!sources || sources.length === 0) return '<p class="assistant-muted">No citations returned.</p>';
|
|
return sources.map(function (s, idx) {
|
|
// `number` is what the reader sees — by first citation when the list has
|
|
// been ordered. The card's id is the source's identity, which is what a
|
|
// chip's href names, so a click lands on the right card whatever the order.
|
|
var n = s.number || idx + 1;
|
|
var identity = s.sourceNumber != null ? s.sourceNumber : n;
|
|
var page = s.page || s.page_number || s.pageNumber;
|
|
var meta = [];
|
|
if (page) meta.push('page ' + page);
|
|
if (s.visual_caption_source) meta.push('caption: ' + s.visual_caption_source);
|
|
if (s.visual_kind || s.source_priority) meta.push(s.visual_kind || s.source_priority);
|
|
if (s.category) meta.push(s.category);
|
|
if (s.doc_type || s.type) meta.push(s.doc_type || s.type);
|
|
if (s.score != null) meta.push('score ' + Number(s.score).toFixed(3));
|
|
// Retrieved, but the answer did not lean on it. Worth showing — the panel
|
|
// is also a view of what the search found — but worth distinguishing from
|
|
// the numbers the answer actually used.
|
|
if (s.uncited) meta.unshift('not cited');
|
|
return '<div class="assistant-source" id="assistant-source-' + escapeAttr(identity) + '">' +
|
|
'<strong>[' + escapeHtml(n) + '] ' + escapeHtml(s.title || s.resource || 'Untitled source') + '</strong>' +
|
|
renderSourceBadges(s) +
|
|
'<div class="assistant-source-meta">' + escapeHtml(meta.join(' · ') || 'indexed source') + '</div>' +
|
|
(s.excerpt ? '<div class="assistant-source-excerpt"><p style="white-space: pre-wrap">' + escapeHtml(s.excerpt) + '</p></div>' : '') +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function renderSourceBadges(source) {
|
|
var badges = [];
|
|
if (source.source_priority) badges.push(source.source_priority);
|
|
if (source.visual_kind && badges.indexOf(source.visual_kind) === -1) badges.push(source.visual_kind);
|
|
if (source.visual_caption_source && badges.indexOf(source.visual_caption_source) === -1) badges.push(source.visual_caption_source);
|
|
if (source.category) badges.push(source.category);
|
|
if (source.source_boost && Number(source.source_boost) !== 1) badges.push(Number(source.source_boost).toFixed(2) + 'x');
|
|
if (!badges.length) return '';
|
|
return '<div class="assistant-source-badges">' + badges.slice(0, 4).map(function (badge) {
|
|
return '<span>' + escapeHtml(badge) + '</span>';
|
|
}).join('') + '</div>';
|
|
}
|