feat: snip a region into chat, and make links clickable (#14)

This commit was merged in pull request #14.
This commit is contained in:
2026-10-05 08:38:50 +00:00
parent f5d1ced4d6
commit 576c5946f7
44 changed files with 3890 additions and 120 deletions
+198
View File
@@ -4,6 +4,8 @@ import {
labelCodeFences,
promoteStandaloneCodeSpans,
repairCodeBlocks,
restoreUrlLinks,
separateTables,
} from "../lib/code";
describe("inferLanguage", () => {
@@ -132,11 +134,207 @@ describe("promoteStandaloneCodeSpans", () => {
});
});
describe("restoreUrlLinks", () => {
it("unwraps a fence that is nothing but URLs", () => {
// The reported case: "here are the links" arriving as a code block, so the
// one thing a URL is for, being clicked, does not work.
const input = [
"```",
"https://github.com/jasonkneen/openclicky",
"https://github.com/unn-Known1/clickyX",
"```",
].join("\n");
expect(restoreUrlLinks(input)).toBe(
[
"[https://github.com/jasonkneen/openclicky](https://github.com/jasonkneen/openclicky)",
"[https://github.com/unn-Known1/clickyX](https://github.com/unn-Known1/clickyX)",
].join("\n"),
);
});
it("unwraps a URL fence even when the model labelled it", () => {
expect(restoreUrlLinks("```bash\nhttps://example.com\n```")).toBe(
"[https://example.com](https://example.com)",
);
});
it("links a bare URL standing alone on a line", () => {
expect(restoreUrlLinks("https://example.com")).toBe("[https://example.com](https://example.com)");
});
it("links a lone inline span holding a URL", () => {
expect(restoreUrlLinks("`https://example.com`")).toBe(
"[https://example.com](https://example.com)",
);
});
it("keeps a bulleted list of links a bulleted list", () => {
const input = "- https://a.example\n- https://b.example";
expect(restoreUrlLinks(input)).toBe(
"- [https://a.example](https://a.example)\n- [https://b.example](https://b.example)",
);
});
it("keeps an ordered list marker", () => {
expect(restoreUrlLinks("1. https://example.com")).toBe(
"1. [https://example.com](https://example.com)",
);
});
it("drops sentence punctuation that is not part of the address", () => {
expect(restoreUrlLinks("See this:\n\nhttps://example.com.")).toContain(
"[https://example.com](https://example.com)",
);
});
it("brackets a destination that contains parentheses", () => {
const url = "https://en.wikipedia.org/wiki/Foo_(bar)";
expect(restoreUrlLinks(url)).toBe(`[${url}](<${url}>)`);
});
it("escapes brackets in the label, which an IPv6 literal contains", () => {
// Unescaped, the `]` in the label ends the link text early and the rest of
// the address is left as prose outside the link.
expect(restoreUrlLinks("http://[::1]:8080/")).toBe(
"[http://\\[::1\\]:8080/](http://[::1]:8080/)",
);
});
it("never touches a fence holding real code", () => {
const input = "```bash\ncurl https://example.com | jq .\n```";
expect(restoreUrlLinks(input)).toBe(input);
});
it("never touches a fence when one line is not a URL", () => {
const input = "```\nhttps://example.com\nnot a url\n```";
expect(restoreUrlLinks(input)).toBe(input);
});
it("leaves a URL embedded in a sentence alone", () => {
const input = "The docs at https://example.com cover it.";
expect(restoreUrlLinks(input)).toBe(input);
});
it("leaves a markdown link the model already wrote alone", () => {
const input = "[OpenClickly](https://github.com/jasonkneen/openclicky)";
expect(restoreUrlLinks(input)).toBe(input);
});
it("preserves a fence that is never closed and is real code", () => {
expect(restoreUrlLinks("```\ngit log")).toBe("```\ngit log");
});
it("is idempotent", () => {
const once = restoreUrlLinks("```\nhttps://example.com\n```");
expect(restoreUrlLinks(once)).toBe(once);
});
it("is a no-op on ordinary prose", () => {
const input = "There are no addresses in this sentence.";
expect(restoreUrlLinks(input)).toBe(input);
});
});
describe("separateTables", () => {
/** The table from the answer that prompted this, verbatim. */
const table = [
"| snip | source | aspect | dead space before | box after |",
"| --- | --- | --- | --- | --- |",
"| `…153223` | 626×184 | 3.40 | **~82px** | 400×118 |",
].join("\n");
it("gives a table the blank line it needs to be a table", () => {
// GFM tables cannot interrupt a paragraph, so glued to the line above they
// render as a paragraph of pipes and dashes.
const repaired = separateTables(`Here are the numbers:\n${table}`);
expect(repaired).toBe(`Here are the numbers:\n\n${table}`);
});
it("leaves a table alone when it already has one", () => {
const already = `Here are the numbers:\n\n${table}`;
expect(separateTables(already)).toBe(already);
});
it("leaves a table alone when it opens the answer", () => {
expect(separateTables(table)).toBe(table);
});
it("leaves a table alone when it follows a fence", () => {
const input = `\`\`\`bash\npwd\n\`\`\`\n${table}`;
expect(separateTables(input)).toBe(input);
});
it("does not touch a table inside a fence", () => {
// A table shown as example code has to stay exactly as written, blank line
// or not.
const fenced = `\`\`\`markdown\n${table}\n\`\`\``;
expect(separateTables(fenced)).toBe(fenced);
});
it("leaves prose containing a pipe alone", () => {
const input = "Use the pipe character:\nls | grep foo";
expect(separateTables(input)).toBe(input);
});
it("does not insert a blank line before a data row", () => {
// Only the header starts a table. A blank line inside one would split it.
const input = `text\n${table}\n| a | b | c | d | e |`;
expect(separateTables(input)).toBe(`text\n\n${table}\n| a | b | c | d | e |`);
});
it("recognises a one-dash delimiter, which GFM allows", () => {
const minimal = "| a | b |\n| - | - |\n| 1 | 2 |";
expect(separateTables(`text\n${minimal}`)).toBe(`text\n\n${minimal}`);
});
it("recognises alignment colons", () => {
const aligned = "| a | b |\n|:---|:---:|\n| 1 | 2 |";
expect(separateTables(`text\n${aligned}`)).toBe(`text\n\n${aligned}`);
});
it("is idempotent", () => {
const once = separateTables(`text\n${table}`);
expect(separateTables(once)).toBe(once);
});
});
describe("repairCodeBlocks", () => {
it("promotes a lone command and labels it in one pass", () => {
expect(repairCodeBlocks("`pwd`")).toBe("```bash\npwd\n```");
});
it("leaves a table exactly as the model wrote it", () => {
// The whole repair pass runs over every answer, and a table has to survive
// it untouched: pipes are not fences, not links, and not lone spans.
const table = [
"| snip | source | aspect | box after |",
"| --- | --- | --- | --- |",
"| `…153223` | 626×184 | 3.40 | 400×118 |",
"| [docs](https://example.com) | `pwd` | https://example.com | 400×60 |",
].join("\n");
expect(repairCodeBlocks(table)).toBe(table);
});
it("does not fence a lone URL", () => {
// Restoring links runs before promotion, so a recovered URL never becomes a
// fence again. Fencing it here was the bug the user saw.
const repaired = repairCodeBlocks("`https://example.com`");
expect(repaired).not.toContain("```");
expect(repaired).toBe("[https://example.com](https://example.com)");
});
it("unwraps a fenced list of links and leaves no fence behind", () => {
const repaired = repairCodeBlocks("```\nhttps://a.example\nhttps://b.example\n```");
expect(repaired).not.toContain("```");
expect(repaired).toContain("[https://a.example](https://a.example)");
});
it("still labels a bare multi-line fence", () => {
expect(repairCodeBlocks("```\ngit status\n```")).toBe("```bash\ngit status\n```");
});