commit 4f65f8381c849f2c96a80c4b270199948d0f4956
parent ca110d06051901cc1eab9488daa942e0ba9af149
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 7 Oct 2026 23:33:28 -0400
citations: a cite label may hold an editorial [insertion]
CITE_LINK_RE rejected any `]` in a label, so `[“told [the mayor] so”](cite:id)`
was invisible to extractCiteRefs and to everything built on it: numbering,
the page's citation map (the label rendered as plain text), the per-section
reference lists, validation of an undefined id and the MCP's report reader.
markdown-to-jsx already links such a label. The label now takes one level of
balanced brackets; a lone `]` is still not a link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
6 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/common/components/citations/citations.test.ts b/common/components/citations/citations.test.ts
@@ -194,6 +194,9 @@ test("cited markdown: cite links become inline cites, other links stay links, co
assert.match(html, /\[5\]/);
assert.match(html, /<a [^>]*href="https:\/\/example.org\/z" target="_blank" rel="noopener noreferrer"[^>]*>elsewhere<\/a>/);
assert.match(html, /<code[^>]*>\[x\]\(cite:c01\)<\/code>/);
+ // a label holding an editorial [insertion] is still a cite, with its number
+ const bracketed = render(h(CitedMarkdown, { citations: byId, children: "She said [“told [the mayor] so”](cite:c01)." }));
+ assert.match(bracketed, /data-inline-cite="c01"[^]*?>“told \[the mayor\] so”<\/a>[^]*?\[1\]/);
// a link to a place on the same page stays in the tab
const jump = render(h(CitedMarkdown, { citations: {}, children: "[the claims](#claims)" }));
assert.match(jump, /<a [^>]*href="#claims"[^>]*>the claims<\/a>/);
diff --git a/common/lib/citations/citations.test.ts b/common/lib/citations/citations.test.ts
@@ -245,6 +245,16 @@ test("extractCiteRefs: every cite link in order, with labels and offsets; other
assert.deepEqual(extractCiteRefs("[empty](cite:)").map((r) => r.id), [""]);
});
+test("extractCiteRefs: a label may hold an editorial [insertion], never a lone ]", () => {
+ const md = "She said [“the plan is not what [the mayor] signed”](cite:p01), then [x ] y](cite:p02) and [a [b] [c] d](cite:p03).";
+ const refs = extractCiteRefs(md);
+ assert.deepEqual(refs.map((r) => [r.id, r.label]), [
+ ["p01", "“the plan is not what [the mayor] signed”"],
+ ["p03", "a [b] [c] d"],
+ ]);
+ assert.equal(md.slice(refs[0].offset, refs[0].offset + 2), "[“");
+});
+
test("extractCiteRefs: a cite link in code is text about the syntax", () => {
const md = [
"Write `[label](cite:id)` to cite, like [this](cite:c01).",
diff --git a/common/lib/citations/inline.ts b/common/lib/citations/inline.ts
@@ -28,9 +28,11 @@ export type CiteRef = {
offset: number;
};
-// `[label](cite:id)`. The label may not contain `]`; the id runs to the first
-// `)` or space (an empty id is still a ref, so validation can name it).
-const CITE_LINK_RE = /\[([^\]]*)\]\(\s*cite:([^)\s]*)\s*\)/g;
+// `[label](cite:id)`. The label may hold one level of balanced brackets — an
+// editorial insertion in a quote, `[“told [the mayor] so”](cite:id)`, which
+// markdown-to-jsx links too — but not a lone `]`; the id runs to the first `)`
+// or space (an empty id is still a ref, so validation can name it).
+const CITE_LINK_RE = /\[((?:[^[\]]|\[[^[\]]*\])*)\]\(\s*cite:([^)\s]*)\s*\)/g;
// Fenced blocks: a line opening with ``` or ~~~ (up to three spaces in) to
// the line closing it with at least as many of the same, or the end.
diff --git a/common/lib/report/report.test.ts b/common/lib/report/report.test.ts
@@ -94,7 +94,7 @@ test("every reference must resolve: listed citations, source sentences, cite lin
k1.citations = ["c01", "c99", "c01"];
k1.sourceQuote = { citation: "c01" }; // not a source citation
k1.findings = "See [gone](cite:c98) and [blank](cite:).";
- f.summary = "Also [missing](cite:zz).";
+ f.summary = "Also [missing](cite:zz) and [“told [the mayor] so”](cite:zy).";
const ps = validateReport(f);
assert.deepEqual(paths(ps).sort(), [
"sections[0].claims[0].citations[1]",
@@ -104,8 +104,10 @@ test("every reference must resolve: listed citations, source sentences, cite lin
"sections[0].claims[0].sourceQuote.citation",
"subject.source",
"summary",
+ "summary",
]);
assert.ok(ps.some((p) => /\[gone\]\(cite:c98\) names no citation/.test(p.message)));
+ assert.ok(ps.some((p) => /cite:zy\) names no citation/.test(p.message)));
assert.ok(ps.some((p) => /lists "c01" twice/.test(p.message)));
assert.ok(ps.some((p) => /names a video citation/.test(p.message)));
});
diff --git a/common/lib/report/views.test.ts b/common/lib/report/views.test.ts
@@ -78,7 +78,7 @@ const report: Report = {
{
id: "two",
title: "Two",
- body: "A body citing [a post](cite:p1).",
+ body: "A body citing [“a [quoted] post”](cite:p1).",
claims: [{ id: "c2", text: "Claim two.", verdict: "PARTLY", citations: ["a1"] }],
},
],
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **A quote with an editorial insertion is still a citation.** An inline citation whose quoted words hold brackets, `[“told [the mayor] so”](cite:id)`, is numbered, previews and links like any other, is listed in the reference list and its section's, and validation names its id when the report lacks it; it used to render as plain text. A label with a lone `]` is still not a link. The MCP's report reader counts it too. Needs `reports prepare` and a rebuild and deploy of each site with reports.
- **A report can carry a video.** `report.json` `video` (`{ "src": "video.mp4", "poster": "poster.jpg", "caption": "…" }`, files in the report's directory: an mp4, a png/jpg/webp poster, a one-line caption) plays at the head of the report's page, under its header. Composing the site refuses a report whose video or poster is missing, or whose video is over the 24 MiB publish limit. Needs a rebuild and deploy of the site.
- **Search reads every English track of a video, and the transcript switches tracks.** Where a video has another English caption track whose words differ from its transcript — the uploaded captions beside the original audio's, a regional or auto-translated track — a query matches it too: a hit only that track holds says so ("in uploaded captions") and opens the transcript on that track at that moment, and a word both say is found once, in the transcript. The transcript reader shows a small "Track:" switcher beside the mode buttons on such a video; the transcript stays the default, and the choice rides on the share link (`vt`). Downloads and Copy MD take the track on show. Needs an index build and a rebuild and deploy of each site.
- **A citation of a Wayback Machine copy links its original and the copy.** A cited record downloaded from a Wayback capture shows "Original (may be gone)", the original at the cited second where its platform takes one, and "Wayback Machine copy, <capture date>", the capture page, which plays. Its moment link is the capture: a capture URL never takes a time param.