diff --git a/ui/src/markdownNormalization.ts b/ui/src/markdownNormalization.ts index 5267a2624..6102a1f92 100644 --- a/ui/src/markdownNormalization.ts +++ b/ui/src/markdownNormalization.ts @@ -232,17 +232,23 @@ export function normalizeCurrencyDollars( function normalizeProse(text: string, options: NormalizationOptions): string { let normalizedMath = text + .replace(/\uE200cite\uE202([^\uE200\uE201]*)\uE201/g, (_, sources: string) => + sources.split("\uE202") + .filter((source) => /^https?:\/\/[^\s<>]+$/i.test(source)) + .map((source) => `<${source}>`) + .join(" ")) .replace(/\\\[([\s\S]+?)\\\]/g, (_, inner: string) => `$$${inner}$$`) .replace(/\\\(([\s\S]+?)\\\)/g, (_, inner: string) => `$$${inner}$$`); if (options.predictMath) { normalizedMath = normalizedMath + .replace(/\uE200(?:c(?:i(?:t(?:e(?:\uE202[^\uE200\uE201]*)?)?)?)?)?$/, "") .replace(/\\\[([\s\S]*)$/, (_, inner: string) => `$$${inner}`) .replace(/\\\(([\s\S]*)$/, (_, inner: string) => `$$${inner}`); } return normalizeCurrencyDollars(normalizedMath, options); } -/** Normalize math and legacy currency while leaving code and mention tags opaque. */ +/** Normalize citations, math, and legacy currency while leaving code and mention tags opaque. */ export function normalizeMarkdownForRendering( text: string, options: NormalizationOptions = {}, diff --git a/ui/tests/markdownNormalization.test.mjs b/ui/tests/markdownNormalization.test.mjs new file mode 100644 index 000000000..b7d04840d --- /dev/null +++ b/ui/tests/markdownNormalization.test.mjs @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { normalizeMarkdownForRendering } from "../src/markdownNormalization.ts"; + +test("renders citation URLs and removes unresolved internal references", () => { + assert.equal( + normalizeMarkdownForRendering("结果。\uE200cite\uE202turn421719view1\uE201"), + "结果。", + ); + assert.equal( + normalizeMarkdownForRendering("Result. \uE200cite\uE202https://www.alphaxiv.org/pdf/2604.26417\uE201"), + "Result. ", + ); + assert.equal( + normalizeMarkdownForRendering("\uE200cite\uE202turn1view0\uE202https://example.org/a\uE202https://example.org/b\uE201"), + " ", + ); +}); + +test("hides incomplete citation markers while streaming", () => { + const citation = "\uE200cite\uE202https://example.org/paper\uE201"; + for (let end = 1; end < citation.length; end += 1) { + assert.equal(normalizeMarkdownForRendering(`Result. ${citation.slice(0, end)}`, { predictMath: true }), "Result. "); + } + assert.equal(normalizeMarkdownForRendering(`Result. ${citation}`, { predictMath: true }), "Result. "); + assert.equal(normalizeMarkdownForRendering("\uE200cider", { predictMath: true }), "\uE200cider"); +}); + +test("leaves citation syntax in code and mention tags untouched", () => { + const citation = "\uE200cite\uE202turn1view0\uE201"; + const markdown = `\`${citation}\`\n\n\`\`\`\n${citation}\n\`\`\`\n\n`; + assert.equal(normalizeMarkdownForRendering(markdown), markdown); +});