fix: raise citation markers and stop adjacent ones merging (#22)
This commit was merged in pull request #22.
This commit is contained in:
+43
-7
@@ -104,13 +104,46 @@ export interface CitationResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* A citation marker the model wrote, in the two shapes it reaches for.
|
||||
* One citation marker, in the two shapes a model reaches for, plus any markers
|
||||
* written straight after it.
|
||||
*
|
||||
* Adjacent markers are matched as a single run, because a raised digit carries no
|
||||
* separator of its own: `[1][2]` drawn as `¹²` reads as twelve, or as twenty one, and
|
||||
* either way as one number rather than as two sources.
|
||||
*
|
||||
* The trailing guards are what keep this off markdown the model wrote on purpose:
|
||||
* `](` starts a link, and `]:` is a reference definition. The digit list allows
|
||||
* `[1, 3]`, which is how a sentence resting on two sources is usually written.
|
||||
* `[1, 3]`, which is how a sentence resting on two sources is often written.
|
||||
*/
|
||||
const CITATION_MARKER = /\[\^?(\d+(?:\s*,\s*\d+)*)\](?![(:])/g;
|
||||
const CITATION_RUN = /\[\^?\d+(?:\s*,\s*\d+)*\](?![(:])(?:[ \t]*\[\^?\d+(?:\s*,\s*\d+)*\](?![(:]))*/g;
|
||||
|
||||
/** The numbers inside one run, in the order they were written. */
|
||||
const RUN_NUMBERS = /\d+/g;
|
||||
|
||||
/** The digits a superscript marker is built from, in order. */
|
||||
const DIGITS = "0123456789";
|
||||
const SUPERSCRIPT = "⁰¹²³⁴⁵⁶⁷⁸⁹";
|
||||
|
||||
/**
|
||||
* A number as a superscript, which is what a citation mark is.
|
||||
*
|
||||
* Unicode rather than a `<sup>` tag on purpose. This is the label of a markdown
|
||||
* link, and a plain label cannot be broken by any part of the pipeline, while
|
||||
* inline HTML in a label asks two renderers to agree about something neither this
|
||||
* extension nor its tests can see. The glyphs are raised by design, so the mark
|
||||
* still reads as a superscript even when a font lacks the rarer ones and Qt falls
|
||||
* back for them.
|
||||
*/
|
||||
function superscript(index: number): string {
|
||||
let out = "";
|
||||
|
||||
for (const digit of String(index)) {
|
||||
const at = DIGITS.indexOf(digit);
|
||||
out += at >= 0 ? SUPERSCRIPT[at] : digit;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A footnote definition line, which is `[^1]: title, url` on a line of its own.
|
||||
@@ -165,7 +198,8 @@ function stripDefinitions(text: string, known: Set<number>): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* `[1]`, `[^1]` and `[1, 3]` as links, plus the numbers that became links.
|
||||
* `[1]`, `[^1]`, `[1, 3]` and `[1][2]` as superscript links, 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
|
||||
@@ -179,8 +213,8 @@ export function linkCitations(text: string, sources: Source[]): CitationResult {
|
||||
const prose = stripDefinitions(text, new Set(byIndex.keys()));
|
||||
|
||||
const linked = mapOutsideCodeSpans(prose, (chunk) =>
|
||||
chunk.replace(CITATION_MARKER, (whole: string, digits: string) => {
|
||||
const wanted = digits.split(",").map((part) => Number(part.trim()));
|
||||
chunk.replace(CITATION_RUN, (whole: string) => {
|
||||
const wanted = [...whole.matchAll(RUN_NUMBERS)].map((match) => Number(match[0]));
|
||||
const links: string[] = [];
|
||||
|
||||
for (const index of wanted) {
|
||||
@@ -190,10 +224,12 @@ export function linkCitations(text: string, sources: Source[]): CitationResult {
|
||||
// marker that is partly a link reads as a citation that works.
|
||||
if (!href) return whole;
|
||||
|
||||
links.push(markdownLink(`[${index}]`, href));
|
||||
links.push(markdownLink(superscript(index), href));
|
||||
}
|
||||
|
||||
for (const index of wanted) cited.add(index);
|
||||
// Separated even when the model wrote them touching: `¹, ²` is two citations,
|
||||
// where `¹²` is a number, and the reader has no way to tell it from a real one.
|
||||
return links.join(", ");
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -96,7 +96,7 @@ Fence every command and every snippet, and always name the language: bash for sh
|
||||
|
||||
A web address is a link, never code. A markdown link, written as [label](https://example.com), is clickable in this window, while a URL inside backticks or a fenced block is dead text the user cannot open. So never fence a URL, never put one in inline code, and never answer "here are the links" with a snippet. Label a link with what it is ("OpenClickly on GitHub"), and when you give several, put each on its own bullet with its own link.
|
||||
|
||||
When a tool result gives you a source, cite it: put its number in square brackets right after the sentence it supports, like a claim resting on a search result [1]. Cite the page that carries the point rather than the search that found it, cite one number at a time when a sentence rests on two of them (write [1][2], not [1,2]), and never cite a number you were not shown. The window turns that number into a link and lists the sources under your answer, so write no footnote definitions and no sources or references list of your own.
|
||||
When a tool result gives you a source, cite it: put its number in square brackets right after the sentence it supports, like a claim resting on a search result [1]. Cite the page that carries the point rather than the search that found it, put each number in its own brackets when a sentence rests on two sources (write [1][2]), and never cite a number you were not shown. The window turns that number into a link and lists the sources under your answer, so write no footnote definitions and no sources or references list of your own.
|
||||
|
||||
A fenced block is never a whole answer on its own. Say in one plain sentence what the code does or when to reach for it, so the reply reads like something a person wrote rather than a bare snippet.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user