lihongyuan99 2ad2132336
fix(citations): pair inline code within a paragraph, not across blank lines (#5876)
* fix(citations): pair inline code within a paragraph, not across blank lines

maskInlineCode paired backticks over the whole document, so a stray backtick in
one paragraph and another in a later one formed a span CommonMark never renders.
The citation between them was masked out of the Sources list, and the stolen
opener simultaneously un-masked a real code span further down.

Scope the pairing to blank-line-delimited paragraphs, keeping the captured
separator so string length and occurrence indices still align with the original
markdown.

* fix(citations): end the paragraph on a CRLF blank line too

core/messages/utils.ts recognises a blank line as \r?\n[ \t]*\r?\n; the citations
scanner now uses the same shape, so CRLF markdown gets the same pairing limit
instead of falling back to whole-document pairing.

* fix(citations): mask fences in containers and end inline spans at block bounds

Follow-up to the paragraph-scoped inline pairing review on #5876.

Indented fences inside a list item, and blockquote-prefixed fences, were only
blanked by accident before: the whole-document backtick pairing happened to
close them. Scoping the pairing to a paragraph removed that safety net and let
sample citations surface as sources, so the fence matcher now accepts container
prefixes and is marker-aware.

Inline spans also end at the other block boundaries CommonMark uses - ATX
headings, thematic breaks / setext underlines, interrupting list items and a
blockquote's empty continuation line - not only at blank lines.

* fix(citations): anchor fence detection and end fences with their container

An unanchored container-prefix match turns any line that merely contains a
three-backtick run into a fence opener, which masks everything after it and
drops live citations. Anchor the match to the line start.

A fence also has to end with the container it was opened in: a blockquote loses
its fence at the first line without a `>` marker, and an indented fence at the
first non-blank line that is less indented than its opener. Without that, the
citation following a closed blockquote is masked away, which is the same failure
direction as the original bug.

* fix(citations): track the container a fence was opened in

Separate a fence marker's own indentation from the indentation its container
gives it, require whitespace after a list marker before treating it as a
container, count blockquote depth instead of testing it as a boolean, and check
a closer's indentation against the container before ending the block.

Cover the four shapes from the review on #5876 with tests, and check the scanner
against remark-parse over twenty fence shapes.

* fix(citations): cap an opener's indentation at three columns past its container

A marker four columns past the container's content column is an indented code
block, so it must not open a fence; otherwise the citation below it is blanked
to end of input instead of surfacing as a source.

* fix(citations): match fence openers and closers to what their tails allow

A closing fence is bare, and a backtick fence's info string carries no
backtick. So ` ```text ` inside a ``` block is literal content rather
than a closer, and ` ```md `x` ` is a paragraph line rather than an
opener. Missing either check sends the scanner the wrong way: the first
lets a citation after the fake closer surface as a phantom source, the
second blanks a citation the reader can actually click.

* fix(citations): measure fence columns in columns, and only close on a closer

A tab after a list marker advances to the next four-column stop, so sizing a
list item by the character length of its marker put the content column two
columns too far left and kept a dedented citation masked inside the fence.

A closing fence now comes from its own pattern instead of the opener's: the
opener also accepts list markers, which are plain content once a block is open,
so `- ``` ` inside a fence closed it early and surfaced the citation under it.

The optional space after a block quote marker is padding, not indentation, so
measuring a quoted marker from the raw prefix cost the fence one column of its
three-column budget and rejected a valid closer.

* test(citations): pin the list-nested closer shape as its own case

* fix(citations): keep a blockquote fence escape inside an indented code run

Losing the `>` closes a quoted fence, but the escaping line is not free text:
four columns of indentation at the top level starts an indented code block, so
its citations are still code. Without that, the paragraph-scoped inline pairing
this scanner replaced leaves them visible as phantom sources.

* fix(citations): read blank lines and surviving containers around quote escapes

* fix(citations): count a quote that opens behind a list marker

BLOCKQUOTE_PREFIX_RE matches from the start of a line, so a fence opened as
"- > ```md" recorded quoteDepth 0 even though it sits inside a quote. The
escape test then never fired and the fence masked to end of input, blanking
citations that remark-parse renders as live links: 35 of the 924 swept
container combinations were worse than main, all in that family.

Read the opener's own container prefix instead, and park the list stack the
quote hides in so a dedented line is measured against the item that survives
it.

* fix(citations): keep literal quote lines and multi-marker fences contained

A fence that was not opened in a block quote has no quote container a later
line can leave, so a `>` inside it is literal code: stripping it made an
indented content line read as column zero and escape the fence. A line can also
open several list items at once, and the fence belongs to the deepest of them,
so recording only the first marker put `- - - ```md` four columns past its
container and rejected a valid opener.

* fix(citations): scan the container prefix in one pass

Reading the containers a fence marker may sit behind was a single regex whose
alternation could take a space either as a step of its own or as the padding of
the next marker, so it described the same prefix exponentially many ways. A line
carrying no fence therefore made the engine enumerate every decomposition: the
deep-quote shapes tests/e2e/user-message-plain-text.spec.ts renders cost a second
at 26 markers, roughly double for each marker added, and never return for the
three thousand markers those messages carry, which is why the e2e job timed out
waiting for a message that had already rendered on main.

The scan now walks each position once. Which shapes open a fence is unchanged:
the 32-shape and 35-shape differentials against remark-parse, the 924-combination
sweep and this file's 50 existing cases all give the same answers as before.

* fix(citations): keep the list a finished quote lived in, and let paragraph text block a non-1 ordered fence

maskFencedCodeBlocks cleared the item stack on every blockquote-depth change, so a
fence opened after a quote ended measured against the document root and hid a
citation the reader had already left the item for. It also pushed any ordered
marker as a container, so a `2. ~~~md` line that CommonMark keeps inside the
preceding paragraph opened a fence and masked a rendered citation.

* fix(citations): test the paragraph boundary on the CR-normalized line

A CRLF document keeps its "\r" on every line after the split on "\n", and
THEMATIC_BREAK_RE is anchored at the end of the line, so a setext underline
"---\r" was not recognised as a leaf block. The paragraph it had already ended
stayed live, the following "2. ```md" was read as paragraph text, its fence
never opened, and the real closer was then re-read as a top-level opener that
masked the rendered citation to EOF.

Use the same CR-normalized line the position classification already uses, and
pin the LF/CRLF pair plus the cross-depth quote shape against the parser tree.

* fix(citations): keep list items and quote markers the reader keeps

Three container rules in the fence scanner disagreed with remark-parse:

- A dedented line of paragraph text is a lazy continuation, so it does not
  leave the list item. Popping the item there recorded the next fence at
  column zero and hid a rendered citation to end of input.
- Padding after a list marker tops out at four columns: the fifth belongs to
  an indented code block inside an item whose content starts one column past
  the marker, not to a column-six container.
- A `>` deeper than the quote markers a quoted fence already consumed is
  literal code. Stripping it made the content look dedented past the fence's
  column and ended the fence one line early, surfacing a citation the reader
  shows as code.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-22 16:05:03 +08:00

1046 lines
36 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import {
extractCitationSources,
formatCitationMarkdownReference,
} from "@/core/citations/sources";
describe("extractCitationSources", () => {
it("extracts citation markdown links in first-seen order", () => {
const markdown = [
"Deep research needs evidence [citation:Paper A](https://example.com/a).",
"A second claim cites [citation:Report B](https://news.example.org/report?x=1).",
].join("\n");
const firstIndex = markdown.indexOf("[citation:Paper A]");
const secondIndex = markdown.indexOf("[citation:Report B]");
expect(extractCitationSources(markdown)).toEqual([
{
id: "https://example.com/a",
title: "Paper A",
url: "https://example.com/a",
domain: "example.com",
count: 1,
occurrences: [{ index: firstIndex, title: "Paper A" }],
},
{
id: "https://news.example.org/report?x=1",
title: "Report B",
url: "https://news.example.org/report?x=1",
domain: "news.example.org",
count: 1,
occurrences: [{ index: secondIndex, title: "Report B" }],
},
]);
});
it("deduplicates repeated citation URLs and preserves occurrence titles", () => {
const markdown = [
"First [citation:Original Title](https://example.com/research).",
"Later [citation:Updated Title](https://example.com/research).",
].join("\n");
const firstIndex = markdown.indexOf("[citation:Original Title]");
const secondIndex = markdown.indexOf("[citation:Updated Title]");
expect(extractCitationSources(markdown)).toEqual([
{
id: "https://example.com/research",
title: "Original Title",
url: "https://example.com/research",
domain: "example.com",
count: 2,
occurrences: [
{ index: firstIndex, title: "Original Title" },
{ index: secondIndex, title: "Updated Title" },
],
},
]);
});
it("ignores normal links, image links, and citations inside fenced code", () => {
const markdown = [
"[Normal](https://example.com/normal)",
"![citation:Image](https://example.com/image.png)",
"```md",
"[citation:Example](https://example.com/example)",
"```",
"Real source [citation:Real](https://example.com/real).",
].join("\n");
const realIndex = markdown.indexOf("[citation:Real]");
expect(extractCitationSources(markdown)).toEqual([
{
id: "https://example.com/real",
title: "Real",
url: "https://example.com/real",
domain: "example.com",
count: 1,
occurrences: [{ index: realIndex, title: "Real" }],
},
]);
});
it("keeps every source when citations are directly adjacent", () => {
const markdown =
"[citation:A](https://example.com/a)[citation:B](https://example.com/b)[citation:C](https://example.com/c)";
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/a",
"https://example.com/b",
"https://example.com/c",
]);
});
it("keeps URLs that contain multiple balanced parenthetical groups", () => {
const markdown = "[citation:W](https://en.wikipedia.org/wiki/Foo_(a)_(b))";
expect(extractCitationSources(markdown)[0]).toMatchObject({
url: "https://en.wikipedia.org/wiki/Foo_(a)_(b)",
domain: "en.wikipedia.org",
});
});
it("ignores citations inside inline code spans", () => {
const markdown =
"Example: `[citation:X](https://x.com/inline)` then real [citation:Real](https://example.com/real).";
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a citation whose paragraph sits between two unclosed backtick runs", () => {
// An inline span cannot cross a blank line, so neither stray backtick opens
// a span here and the citation is a rendered link, not sample code.
const markdown = [
"Install it with `npm i",
"",
"The upstream guide is [citation:Docs](https://example.com/docs).",
"",
"Then run `npm start` to serve it.",
].join("\n");
const sources = extractCitationSources(markdown);
expect(sources.map((source) => source.url)).toEqual([
"https://example.com/docs",
]);
expect(sources[0]?.occurrences[0]?.index).toBe(
markdown.indexOf("[citation:Docs]"),
);
});
it("still masks a code span that closes inside its own paragraph", () => {
const markdown = [
"Install it with `npm i",
"",
"Example: `[citation:Fake](https://example.com/fake)` then real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("ends a paragraph on a CRLF blank line, as the inline scanner does", () => {
const markdown =
"Install it with `npm i\r\n\r\nThe upstream guide is [citation:Docs](https://example.com/docs).\r\n\r\nThen run `npm start` to serve it.";
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/docs",
]);
});
it("ignores citations inside an unclosed fenced code block", () => {
const markdown = [
"Streaming output:",
"```md",
"[citation:Streaming](https://example.com/streaming)",
].join("\n");
expect(extractCitationSources(markdown)).toEqual([]);
});
it("ends an inline span at a heading, thematic break or list marker", () => {
// CommonMark ends the paragraph at these block boundaries too, so "Use
// `npm" cannot steal the opener of `npm start` two lines later and the
// citation between them is a rendered link, not sample code.
for (const boundary of ["## Title", "---", "- item"]) {
const markdown = [
"Use `npm",
boundary,
"See [citation:X](https://example.com/x) and `npm start`.",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/x",
]);
}
});
it("ends an inline span at a blank line inside a blockquote", () => {
const markdown = [
"> Install it with `npm i",
">",
"> The guide is [citation:Docs](https://example.com/docs) and `npm start`.",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/docs",
]);
});
it("masks citations inside a fenced block nested in a list item", () => {
// The fence is indented past the list marker, so recognising it needs the
// container prefix; the blank line inside keeps it open across paragraphs.
const markdown = [
"- run:",
" ```md",
" [citation:Fake1](https://example.com/fake1)",
"",
" [citation:Fake2](https://example.com/fake2)",
" ```",
"",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("masks a blockquoted fence, and stops at the line that ends the quote", () => {
// Fake1 is inside the fence. The blank line has no `>` marker, so it closes
// the blockquote — and the fence with it — which makes Fake2 a rendered link
// again. Verified against remark-parse rather than by eye.
const markdown = [
"> ```md",
"> [citation:Fake1](https://example.com/fake1)",
"",
"> [citation:Fake2](https://example.com/fake2)",
"> ```",
"",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake2",
"https://example.com/real",
]);
});
it("masks a dedented citation that lands in an indented code block", () => {
// Losing the `>` closes the block quote and the fence inside it, but the
// escaping line is not free: four columns of indentation at the top level
// is an indented code block, so Fake still renders as code. Verified
// against remark-parse, whose tree is quote>code, code, quote>code, paragraph.
const markdown = [
"> ```md",
" [citation:Fake](https://example.com/fake)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("counts a tab-indented escaping line as the same four-column run", () => {
// A tab reaches the next tab stop, so `\t` is four columns and `\t\t` eight;
// both are indented code, and the marker padding on the opener is irrelevant.
const markdown = [
">\t```md",
"\t[citation:Fake](https://example.com/fake)",
">\t```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps an indented run across a blank line inside it", () => {
// The CRLF variant pins the blankness classification: after a split on
// "\n" the blank line still carries its "\r", and reading it as content
// cleared the run and let Fake2 through.
for (const eol of ["\n", "\r\n"]) {
const markdown = [
"> ```md",
" [citation:Fake1](https://example.com/fake1)",
"",
" [citation:Fake2](https://example.com/fake2)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join(eol);
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
}
});
it("masks the indented code block that follows the blank line ending a quoted fence", () => {
// The blank line ends the blockquote, and the four-space line after it is a
// top-level indented code block, so Fake stays code instead of surfacing as
// a phantom source. Verified against remark-parse.
const markdown = [
"> ```md",
"",
" [citation:Fake](https://example.com/fake)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("renders the paragraph that follows the blank line ending a quoted fence", () => {
// Two columns is paragraph territory, so the pending quote exit must not
// mask the line that classifies it: this pins the boundary against the
// four-column case above.
const markdown = [
"> ```md",
"",
" [citation:Para](https://example.com/para)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/para",
"https://example.com/real",
]);
});
it("measures a quote escape against the list item the quote sat in", () => {
// The quote is a child of the list item and the item outlives it, so four
// columns of indentation are only two past the item's content column: the
// citation is paragraph content, not an indented code block. Verified
// against remark-parse.
const markdown = [
"- item",
" > ```md",
" [citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("still masks a quote escape indented past the surviving list item", () => {
// Six columns are four past the item's content column, so the escaped line
// does start an indented code block — and the unindented line after it ends
// the item and the run, rendering again.
const markdown = [
"- item",
" > ```md",
" [citation:Fake](https://example.com/fake)",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("ends the indented run at a dedent so that citation renders again", () => {
// Two columns is inside the item-less paragraph the run gives way to, so the
// second citation is a rendered link while the four-column one above stays
// code.
const markdown = [
"> ```md",
" [citation:Code](https://example.com/code)",
" [citation:Para](https://example.com/para)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/para",
"https://example.com/real",
]);
});
it("resumes normal scanning once an indented run meets a quoted line", () => {
// The run ends at `> [citation:Quoted]`, which is quote content again, and
// the `> ``` ` below it opens the fence that the final line escapes.
const markdown = [
"> ```md",
" [citation:Fake](https://example.com/fake)",
"> [citation:Quoted](https://example.com/quoted)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/quoted",
"https://example.com/real",
]);
});
it("does not mask an escaping citation three columns in", () => {
// Three columns is not enough to start an indented code block, so the line
// is a top-level paragraph and its citation is a real rendered link. This
// pins the threshold against the four-column case above.
const markdown = [
"> ```md",
" [citation:Fake](https://example.com/fake)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("does not open a fence from a backtick run in the middle of a line", () => {
const markdown = [
"Run ```md please",
"[citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a fence open across a shorter backtick run inside it", () => {
const markdown = [
"````md",
"```",
"[citation:Inside](https://example.com/inside)",
"```",
"````",
"",
"Outside [citation:Outside](https://example.com/outside).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/outside",
]);
});
it("keeps an indented top-level fence open for unindented content", () => {
// The opener's own indentation is not container indentation: content lines
// may sit further left than the marker, and the closer only has to come back
// within three columns of the opener.
const markdown = [
"Intro text.",
" ```md",
"[citation:Fake](https://example.com/fake)",
" ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("treats a four-space indented marker inside a column-zero fence as content", () => {
const markdown = [
"```md",
" ```",
"[citation:Fake](https://example.com/fake)",
"```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not open a fence from a marker indented four columns at the top level", () => {
// Four columns past the container's content column is an indented code
// block, not a fence opener, so the citation below it renders as a link.
const markdown = [
" ```md",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("still opens a fence from a marker indented three columns at the top level", () => {
// The limit is three columns, so the next case up pins the boundary rather
// than overshooting it.
const markdown = [
" ```md",
"[citation:Fake](https://example.com/fake)",
" ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not open a fence from a list marker with no space after it", () => {
// `-` has to be followed by indentation to start a list item, so `-```md`
// is ordinary paragraph text and nothing behind it is code.
const markdown = [
"-```md",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("counts a tab after a list marker as columns when it sizes the item", () => {
// `-<TAB>` advances to the next four-column stop, so this item's content
// starts at column four and the citation two spaces in has left the fence
// rather than sitting inside it.
const markdown = [
"-\t```md",
" [citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("ends a fence when a line drops out of the inner blockquote", () => {
const markdown = [
">> ```md",
">> [citation:Fake](https://example.com/fake)",
"> Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("lets a citation after a list-nested quote escape its fence", () => {
// `- > ```md` opens the quote behind a list marker, so the line has no
// leading `>` for a line-start pattern to find even though the fence really
// is quoted. The dedented citation below sits two columns past the item's
// content column, which is a paragraph rather than an indented code block.
const markdown = [
"- > ```md",
" [citation:Fake](https://example.com/fake)",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("ends a list-nested quote fence at its first unquoted line", () => {
const markdown = [
"- > ```md",
" [citation:One](https://example.com/one)",
" [citation:Two](https://example.com/two)",
"\t[citation:Three](https://example.com/three)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/one",
"https://example.com/two",
"https://example.com/three",
]);
});
it("escapes a quote fence nested behind a star marker", () => {
const markdown = [
"* > ```md",
" [citation:Fake](https://example.com/fake)",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("escapes a quote fence nested behind an ordered marker", () => {
// `1. ` gives the item a content column of three, so four columns of
// indentation is one column of content rather than an indented code block.
const markdown = [
"1. > ```md",
" [citation:Fake](https://example.com/fake)",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("keeps a list-nested quote open for a line that carries its marker", () => {
// The mirror of the cases above: `>` on the second line means the quote did
// not end, so nothing about the escape path applies here.
const markdown = [
"- >> ```md",
">> [citation:Fake](https://example.com/fake)",
"> [citation:Mid](https://example.com/mid)",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/mid",
"https://example.com/real",
]);
});
it("ends a list-nested fence at a closer that is itself inside the quote", () => {
const markdown = [
"- > ```md",
"> [citation:Fake](https://example.com/fake)",
"> ```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("gives a quoted closer its full three-column budget", () => {
// The one optional space after `>` belongs to the block quote marker, not to
// the fence, so `>` plus four spaces indents the closer by three columns and
// it closes the block.
const markdown = [
"> ```md",
"> [citation:Fake](https://example.com/fake)",
"> ```",
"> [citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not let a marker with an info string close the fence", () => {
// A closing fence is bare, so ` ```text ` here is literal content and the
// citation after it stays inside the block instead of becoming a source.
const markdown = [
"```md",
"```text",
"[citation:Fake](https://example.com/fake)",
"```",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not close a fence from list-looking content inside it", () => {
// Inside a fenced block `- ``` ` is literal content: a closing fence may
// carry indentation and a marker, nothing else. Reading it as a closer leaks
// Fake, and the real closer then masks away Real.
const markdown = [
"```md",
"- ```",
"[citation:Fake](https://example.com/fake)",
"```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("closes a list-nested fence at the item's content column", () => {
// The same rule one container deeper: `- ```md` opens the fence at the list
// item's content column, ` - ``` ` stays literal content, and the bare
// ` ``` ` at that column closes it. Verified against remark-parse, not by
// eye.
const markdown = [
"- ```md",
" - ```",
" [citation:Fake](https://example.com/fake)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not open a backtick fence whose info string holds a backtick", () => {
// ` ```md `x` ` is paragraph text, so the citation below it is a link the
// reader can reach and must not be masked away.
const markdown = [
"```md `x`",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("opens a tilde fence whatever its info string says", () => {
// Tilde fences take backticks in the info string, so the boundary above is
// about the marker character rather than a blanket rule.
const markdown = [
"~~~md `x`",
"[citation:Fake](https://example.com/fake)",
"~~~",
"Real [citation:Real](https://example.com/real).",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a literal quote marker inside an open list fence", () => {
// Once a fence is open its lines are literal, so the `>` here is code rather
// than a new block quote. Reading it as a container made the line look
// dedented to column zero, which escaped the column-two list fence and
// surfaced the sample's citation while reinterpreting the real closer as a
// top-level opener.
const markdown = [
"- ```md",
" > example",
" [citation:Fake](https://example.com/fake)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a citation on a literal quote line inside a list fence", () => {
// Same shape with the citation on the quoted line itself: it stays code.
const markdown = [
"- ```md",
" > [citation:Fake](https://example.com/fake)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a citation inside a fence opened behind several list markers", () => {
// `- - - ```md` is a fence in the third list item, whose content column is
// six. Recording only the first marker put the fence at column two, which
// rejected the valid opener as four columns past its container.
const markdown = [
"- - - ```md",
" [citation:Fake1](https://example.com/fake1)",
"",
" [citation:Fake2](https://example.com/fake2)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps a literal quote marker inside a quoted list fence", () => {
// The fence already consumed the quote it was opened in, so the second `>`
// here is literal code. `linePosition()` stripped both markers, which made
// the sample line look dedented past the fence's column and ended the fence
// one line early, surfacing the citation the renderer shows as code.
const markdown = [
"> - ```md",
"> > example",
"> [citation:Fake](https://example.com/fake)",
"> ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("keeps the list item open across a lazy paragraph continuation", () => {
// An unindented line of paragraph text continues the item's paragraph rather
// than leaving it, so the item's content column still measures the fence two
// columns in. Popping the item there recorded the fence at column zero, which
// swallowed the unindented citation to end of input.
const markdown = [
"- note",
"lazy continuation",
"",
" ~~~md",
" sample",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("reads five columns after a list marker as indented code, not padding", () => {
// Only up to four columns of whitespace pad a list marker; the fifth belongs
// to an indented code block inside an item whose content column is two.
// Consuming the whole run recorded column six, so the two-space fence looked
// like a dedent that left the item and the citation after it hid to EOF.
const markdown = [
"- code",
"",
" ~~~md",
" sample",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("restores the enclosing list when an ordinary quote ends", () => {
// The blank line ends `> note`, but the list item outlives the quote, so the
// two-space fence still belongs to an item whose content column is two. The
// citation at column zero leaves that item and ends the unclosed fence with
// it, which makes it a rendered link.
const markdown = [
"- item",
" > note",
"",
" ~~~md",
" [citation:Fake](https://example.com/fake)",
" sample",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("restores the enclosing list when a quote ends at the fence line", () => {
// The same exit without a blank line: a fence cannot be lazily continued by
// a block quote, so `~~~md` already left the quote and the item's column is
// what the following lines measure against.
const markdown = [
"- item",
" > note",
" ~~~md",
" [citation:Fake](https://example.com/fake)",
" sample",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("restores the enclosing list after a two-deep quote ends", () => {
const markdown = [
"- item",
" >> note",
"",
" ~~~md",
" [citation:Fake](https://example.com/fake)",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not open a fence from an ordered marker that cannot interrupt a paragraph", () => {
// An ordered list that does not start at 1 cannot interrupt a paragraph, so
// all three lines are one paragraph and its citation renders.
const markdown = [
"Intro",
"2. ~~~md",
" [citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("does not open a fence from a non-interrupting marker inside an item paragraph", () => {
const markdown = [
"- item",
" 2. ~~~md",
" [citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("still opens a fence from a `1.` marker that does interrupt the paragraph", () => {
const markdown = [
"Intro",
"1. ~~~md",
" [citation:Fake](https://example.com/fake)",
].join("\n");
expect(extractCitationSources(markdown)).toEqual([]);
});
it("still opens a fence from a `2.` marker after a block boundary", () => {
// The blank line closes the paragraph, so the ordered item starts a real
// container and its fence hides the citation again.
const markdown = [
"Intro",
"",
"2. ~~~md",
" [citation:Fake](https://example.com/fake)",
].join("\n");
expect(extractCitationSources(markdown)).toEqual([]);
});
it("keeps the citation a newly entered quote renders as a paragraph", () => {
// Pinned against the parser rather than against a rule: remark's tree for
// these lines is blockquote > [paragraph "2. ```md", paragraph with the
// Fake link, code], so `2. ```md` never becomes a list item inside the
// fresh quote and `> ``` ` is the opener of an unclosed fence. Both
// citations render, and Sources has to surface both.
const markdown = [
"Intro",
"> 2. ```md",
">",
"> [citation:Fake](https://example.com/fake)",
"> ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/fake",
"https://example.com/real",
]);
});
it("ends the paragraph for a setext underline over LF", () => {
const markdown = [
"Intro",
"---",
"2. ```md",
" [citation:Fake1](https://example.com/fake1)",
"",
" [citation:Fake2](https://example.com/fake2)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("ends the paragraph for a setext underline over CRLF", () => {
// The trailing "\r" belongs to the line ending, so `---` is still a setext
// underline and the paragraph is over: `2.` opens a real list, both of its
// citations are fenced code, and the ` ``` ` line closes that fence rather
// than starting a new one that would hide the rendered citation to EOF.
const markdown = [
"Intro",
"---",
"2. ```md",
" [citation:Fake1](https://example.com/fake1)",
"",
" [citation:Fake2](https://example.com/fake2)",
" ```",
"[citation:Real](https://example.com/real)",
].join("\r\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/real",
]);
});
it("uses the source domain when the citation label is generic", () => {
const markdown = "See [citation:Source](https://www.example.com/path).";
expect(extractCitationSources(markdown)[0]).toMatchObject({
title: "example.com",
domain: "example.com",
url: "https://www.example.com/path",
});
});
});
describe("scans a deeply repeated blockquote chain in one pass", () => {
// tests/e2e/user-message-plain-text.spec.ts renders an AI message whose whole
// body is one line of thousands of `>` markers, and the citation pass runs on
// that text before the renderer does. Reading the containers a fence marker
// may sit behind has to reach each position once: an alternation that can take
// a space either as a step of its own or as the padding of the next marker
// describes the same prefix many ways, so a line carrying no fence leaves the
// engine to enumerate them all. Measured against the pattern this file guards,
// one check costs about a second at 26 markers and roughly doubles for each
// marker added, so a three-thousand-marker line never returns. The budget sits
// two orders of magnitude above what a single pass takes, which pins the growth
// rather than the machine.
const shapes: Array<[string, string]> = [
["> ".repeat(26) + "deep", "26-level quote"],
["- " + "> ".repeat(26) + "deep-list", "26-level quote behind a list"],
["> ".repeat(3000) + "deep", "3000-level quote"],
["- " + "> ".repeat(3000) + "deep-list", "3000-level quote behind a list"],
];
for (const [markdown, label] of shapes) {
it(`masks nothing in the ${label} shape within one pass of the line`, () => {
const started = performance.now();
const sources = extractCitationSources(markdown);
expect(sources).toEqual([]);
expect(performance.now() - started).toBeLessThan(500);
});
}
it("still reaches a citation that follows a deep quote chain", () => {
// Skipping the scan would keep every case above green, so the same shape is
// checked with real work to find after it.
const markdown = [
"> ".repeat(26) + "deep",
"[citation:Deep](https://example.com/deep)",
].join("\n");
expect(extractCitationSources(markdown).map((s) => s.url)).toEqual([
"https://example.com/deep",
]);
});
});
describe("formatCitationMarkdownReference", () => {
it("formats a source as a reusable markdown reference", () => {
const [source] = extractCitationSources(
"Evidence [citation:Paper A](https://example.com/a).",
);
expect(formatCitationMarkdownReference(source!)).toBe(
"[Paper A](https://example.com/a)",
);
});
});