479 lines
17 KiB
TypeScript
479 lines
17 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
fenceAsciiArt,
|
||
inferLanguage,
|
||
labelCodeFences,
|
||
mapOutsideCodeSpans,
|
||
normalizeFenceLabels,
|
||
promoteStandaloneCodeSpans,
|
||
repairCodeBlocks,
|
||
restoreUrlLinks,
|
||
separateTables,
|
||
} from "../lib/code";
|
||
|
||
describe("inferLanguage", () => {
|
||
it("returns nothing for empty or whitespace-only code", () => {
|
||
expect(inferLanguage("")).toBeUndefined();
|
||
expect(inferLanguage(" \n ")).toBeUndefined();
|
||
});
|
||
|
||
describe("shell", () => {
|
||
it("recognises a command as the first word", () => {
|
||
expect(inferLanguage("pwd")).toBe("bash");
|
||
expect(inferLanguage("git status")).toBe("bash");
|
||
expect(inferLanguage("brew install ripgrep")).toBe("bash");
|
||
});
|
||
|
||
it("recognises an explicit prompt marker", () => {
|
||
expect(inferLanguage("$ git status")).toBe("bash");
|
||
expect(inferLanguage("> ls -la")).toBe("bash");
|
||
});
|
||
|
||
it("recognises a home-relative path", () => {
|
||
expect(inferLanguage("~/Documents/Code")).toBe("bash");
|
||
});
|
||
|
||
it("recognises a pipeline between known commands", () => {
|
||
expect(inferLanguage("git log --oneline | head -20")).toBe("bash");
|
||
});
|
||
|
||
it("ignores comments when looking at the first word", () => {
|
||
expect(inferLanguage("# check the branch\ngit branch --show-current")).toBe("bash");
|
||
});
|
||
});
|
||
|
||
describe("json", () => {
|
||
it("recognises a parseable object", () => {
|
||
expect(inferLanguage('{\n "model": "gpt-oss"\n}')).toBe("json");
|
||
});
|
||
|
||
it("recognises a parseable array", () => {
|
||
expect(inferLanguage('["a", "b"]')).toBe("json");
|
||
});
|
||
|
||
it("does not claim malformed braces", () => {
|
||
expect(inferLanguage("{ not actually json")).not.toBe("json");
|
||
});
|
||
});
|
||
|
||
describe("typescript", () => {
|
||
it("recognises type annotations", () => {
|
||
expect(inferLanguage("function add(a: number, b: number) {\n return a + b;\n}")).toBe("ts");
|
||
});
|
||
|
||
it("recognises interfaces and type aliases", () => {
|
||
expect(inferLanguage("interface User {\n id: string;\n}")).toBe("ts");
|
||
expect(inferLanguage("type Id = string | number;")).toBe("ts");
|
||
});
|
||
|
||
it("does not label plain JavaScript, which is ambiguous", () => {
|
||
// Guessing wrong costs the highlighting, so this stays unlabelled.
|
||
expect(inferLanguage("console.log('hi')")).toBeUndefined();
|
||
expect(inferLanguage("const x = [1, 2, 3].map((n) => n * 2);")).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe("python", () => {
|
||
it("recognises a function definition", () => {
|
||
expect(inferLanguage("def greet(name):\n return f'hi {name}'")).toBe("python");
|
||
});
|
||
|
||
it("recognises imports and self", () => {
|
||
expect(inferLanguage("from pathlib import Path")).toBe("python");
|
||
expect(inferLanguage("class A:\n def go(self):\n pass")).toBe("python");
|
||
});
|
||
});
|
||
|
||
it("leaves genuinely ambiguous content alone", () => {
|
||
expect(inferLanguage("some plain text\nacross two lines")).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe("promoteStandaloneCodeSpans", () => {
|
||
it("promotes a paragraph that is one inline span", () => {
|
||
// The reported case: asking for a command and getting a lone chip back.
|
||
expect(promoteStandaloneCodeSpans("`pwd`")).toBe("```bash\npwd\n```");
|
||
});
|
||
|
||
it("promotes the same span on its own line within a larger answer", () => {
|
||
const promoted = promoteStandaloneCodeSpans("Here you go:\n\n`pwd`\n\nThat prints the path.");
|
||
|
||
expect(promoted).toContain("```bash\npwd\n```");
|
||
expect(promoted).toContain("Here you go:");
|
||
expect(promoted).toContain("That prints the path.");
|
||
});
|
||
|
||
it("tolerates a sentence-ending period", () => {
|
||
expect(promoteStandaloneCodeSpans("`pwd`.")).toBe("```bash\npwd\n```");
|
||
});
|
||
|
||
it("leaves a span embedded in a sentence alone", () => {
|
||
const input = "Run `pwd` to see where you are.";
|
||
expect(promoteStandaloneCodeSpans(input)).toBe(input);
|
||
});
|
||
|
||
it("leaves a bulleted span alone", () => {
|
||
const input = "- `pwd -P` resolves symlinks";
|
||
expect(promoteStandaloneCodeSpans(input)).toBe(input);
|
||
});
|
||
|
||
it("never rewrites a span-like line inside an existing fence", () => {
|
||
const input = "```bash\n`not-a-span`\n```";
|
||
expect(promoteStandaloneCodeSpans(input)).toBe(input);
|
||
});
|
||
|
||
it("fences a span whose language cannot be inferred, without guessing one", () => {
|
||
expect(promoteStandaloneCodeSpans("`Get-Location`")).toBe("```\nGet-Location\n```");
|
||
});
|
||
|
||
it("is idempotent", () => {
|
||
const once = promoteStandaloneCodeSpans("`pwd`");
|
||
expect(promoteStandaloneCodeSpans(once)).toBe(once);
|
||
});
|
||
|
||
it("is a no-op on ordinary prose", () => {
|
||
const input = "There is no code here at all.";
|
||
expect(promoteStandaloneCodeSpans(input)).toBe(input);
|
||
});
|
||
});
|
||
|
||
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```");
|
||
});
|
||
|
||
it("handles a promoted block and a plain fence in the same answer", () => {
|
||
const repaired = repairCodeBlocks("First:\n\n`pwd`\n\nThen:\n\n```\nls -la\n```");
|
||
|
||
expect(repaired).toContain("```bash\npwd\n```");
|
||
expect(repaired).toContain("```bash\nls -la\n```");
|
||
});
|
||
});
|
||
|
||
describe("labelCodeFences", () => {
|
||
it("labels a bare fence", () => {
|
||
const labelled = labelCodeFences("```\ngit status\n```");
|
||
expect(labelled).toBe("```bash\ngit status\n```");
|
||
});
|
||
|
||
it("leaves an already labelled fence exactly as the model wrote it", () => {
|
||
expect(labelCodeFences("```rust\nfn main() {}\n```")).toBe("```rust\nfn main() {}\n```");
|
||
});
|
||
|
||
it("labels each block independently", () => {
|
||
const labelled = labelCodeFences(["```", "pwd", "```", "", "```", '{"a": 1}', "```"].join("\n"));
|
||
|
||
expect(labelled).toContain("```bash");
|
||
expect(labelled).toContain("```json");
|
||
});
|
||
|
||
it("leaves an unlabellable block bare rather than guessing", () => {
|
||
const input = "```\njust some words\n```";
|
||
expect(labelCodeFences(input)).toBe(input);
|
||
});
|
||
|
||
it("preserves surrounding prose", () => {
|
||
const labelled = labelCodeFences("Before:\n\n```\nls\n```\n\nAfter.");
|
||
|
||
expect(labelled).toContain("Before:");
|
||
expect(labelled).toContain("After.");
|
||
expect(labelled).toContain("```bash");
|
||
});
|
||
|
||
it("handles a fence that is never closed", () => {
|
||
expect(labelCodeFences("```\ngit log")).toContain("```bash");
|
||
});
|
||
|
||
it("is a no-op on markdown with no fences", () => {
|
||
const input = "Just a paragraph with `inline code` in it.";
|
||
expect(labelCodeFences(input)).toBe(input);
|
||
});
|
||
|
||
it("is idempotent", () => {
|
||
const once = labelCodeFences("```\nls\n```");
|
||
expect(labelCodeFences(once)).toBe(once);
|
||
});
|
||
|
||
it("does not mistake a closing fence for a new opening one", () => {
|
||
// A closing ``` and an opening bare ``` are identical lines. Treating the
|
||
// closing one as an opening fence swallowed everything after it, so the
|
||
// second block never got labelled.
|
||
const input = "```bash\npwd\n```\n\n```\nls -la\n```";
|
||
expect(labelCodeFences(input)).toContain("```bash\nls -la\n```");
|
||
});
|
||
|
||
it("labels a later block even when an earlier one is already labelled", () => {
|
||
const input = "```rust\nfn main() {}\n```\n\n```\npwd\n```";
|
||
const labelled = labelCodeFences(input);
|
||
|
||
expect(labelled).toContain("```rust");
|
||
expect(labelled).toContain("```bash\npwd\n```");
|
||
});
|
||
|
||
it("preserves a language the model chose, including multi-word freeform tags", () => {
|
||
expect(labelCodeFences("```c++\nint main() {}\n```")).toContain("```c++");
|
||
expect(labelCodeFences("```objective-c\nint x;\n```")).toContain("```objective-c");
|
||
});
|
||
|
||
it("preserves indentation of a nested fence", () => {
|
||
expect(labelCodeFences(" ```\n ls\n ```")).toContain(" ```bash");
|
||
});
|
||
});
|
||
|
||
describe("mapOutsideCodeSpans", () => {
|
||
it("applies the transform everywhere a span is not", () => {
|
||
expect(mapOutsideCodeSpans("a `b` c", (text) => text.toUpperCase())).toBe("A `b` C");
|
||
});
|
||
|
||
it("matches a delimiter of the same length only", () => {
|
||
// A single-backtick span inside a double-backtick span is content, not a
|
||
// closer, so the inner one is left exactly as written.
|
||
expect(mapOutsideCodeSpans("`` `a` ``", (text) => text.toUpperCase())).toBe("`` `a` ``");
|
||
});
|
||
|
||
it("treats an unclosed run as text and carries on after it", () => {
|
||
expect(mapOutsideCodeSpans("a ` b", (text) => text.toUpperCase())).toBe("A ` B");
|
||
});
|
||
});
|
||
|
||
describe("normalizeFenceLabels", () => {
|
||
it("names a language that resolves in place of one that does not", () => {
|
||
expect(normalizeFenceLabels("```console\nls\n```")).toBe("```bash\nls\n```");
|
||
});
|
||
|
||
it("drops a label that carries no information", () => {
|
||
expect(normalizeFenceLabels("```text\nplain\n```")).toBe("```\nplain\n```");
|
||
});
|
||
|
||
it("leaves a language the model chose alone", () => {
|
||
const input = "```rust\nfn main() {}\n```";
|
||
expect(normalizeFenceLabels(input)).toBe(input);
|
||
});
|
||
});
|
||
|
||
describe("fenceAsciiArt", () => {
|
||
it("fences a tree so its columns stay monospaced", () => {
|
||
expect(fenceAsciiArt("src/\n├── a.ts\n└── b.ts")).toBe("```\nsrc/\n├── a.ts\n└── b.ts\n```");
|
||
});
|
||
|
||
it("fences an ASCII table drawn with borders", () => {
|
||
const art = "+---+---+\n| a | b |\n+---+---+";
|
||
expect(fenceAsciiArt(art)).toBe("```\n" + art + "\n```");
|
||
});
|
||
|
||
it("leaves a real GFM table to the table renderer", () => {
|
||
const table = "| a | b |\n| --- | --- |\n| 1 | 2 |";
|
||
expect(fenceAsciiArt(table)).toBe(table);
|
||
});
|
||
|
||
it("leaves prose that contains one pipe alone", () => {
|
||
const prose = "Here is what it does:\n| a | b |";
|
||
expect(fenceAsciiArt(prose)).toBe(prose);
|
||
});
|
||
|
||
it("leaves a diagram that is already fenced alone", () => {
|
||
const fenced = "```\n├── a.ts\n└── b.ts\n```";
|
||
expect(fenceAsciiArt(fenced)).toBe(fenced);
|
||
});
|
||
});
|