fix: order a citation run ascending, and name each page once (#24)

This commit was merged in pull request #24.
This commit is contained in:
2026-10-07 08:29:33 +00:00
parent ade6145425
commit 34c4383428
4 changed files with 37 additions and 8 deletions
+14
View File
@@ -170,6 +170,20 @@ describe("linkCitations", () => {
);
});
it("orders a run ascending, because a model lists by rank and a reader reads by number", () => {
// A real answer sent [2][1] for a sentence resting on both, which drew as
// "two, one" and left the reader to work out that it meant the first two sources.
expect(linkCitations("Both [3][1] agree.", sources).text).toBe(
"Both [¹](https://first.example) [³](https://third.example) agree.",
);
});
it("cites one page once, however often a run names it", () => {
expect(linkCitations("Both [3][1][3] agree.", sources).text).toBe(
"Both [¹](https://first.example) [³](https://third.example) agree.",
);
});
it("leaves a marker alone when the destination is not one this window should open", () => {
const unsafe = [{ index: 1, title: "Nasty", url: "javascript:alert(1)" }];
const result = linkCitations("See [1].", unsafe);
+13 -3
View File
@@ -198,8 +198,8 @@ function stripDefinitions(text: string, known: Set<number>): string {
}
/**
* `[1]`, `[^1]`, `[1, 3]` and `[1][2]` as superscript links, plus the numbers that
* became links.
* `[1]`, `[^1]`, `[1, 3]` and `[1][2]` as superscript links, ordered ascending and
* each page once, plus the numbers that became links.
*
* One run of prose only: fences are the caller's business, because the caller
* already walks the document in segments for the maths pass and there is no
@@ -214,7 +214,17 @@ export function linkCitations(text: string, sources: Source[]): CitationResult {
const linked = mapOutsideCodeSpans(prose, (chunk) =>
chunk.replace(CITATION_RUN, (whole: string) => {
const wanted = [...whole.matchAll(RUN_NUMBERS)].map((match) => Number(match[0]));
/**
* Ascending, and each page once.
*
* A run is a set of sources, and a model lists them in the order it thought of
* them rather than by number: a real answer sent `[2][1]` for a sentence resting
* on both, which drew as "two, one", and `[8][4]` for another. The numbers
* themselves are never renumbered, so `[3]` is still the third source the turn
* found and the reader can find it in the list.
*/
const named = [...whole.matchAll(RUN_NUMBERS)].map((match) => Number(match[0]));
const wanted = [...new Set(named)].sort((left, right) => left - right);
const links: string[] = [];
for (const index of wanted) {