Files
multica/packages/views/editor/utils/preprocess-links.test.ts

187 lines
7.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { preprocessLinks } from "@multica/ui/markdown/linkify";
// The bug: linkify-it does not treat CJK full-width punctuation as a URL
// boundary, so the href can swallow trailing punctuation and the Chinese
// characters that follow it (up to the next space). The fix truncates the
// detected URL at the first CJK full-width punctuation character.
describe("preprocessLinks — CJK punctuation boundary", () => {
it("stops URL at ideographic full stop 。", () => {
const out = preprocessLinks("见 https://example.com/path。然后继续");
expect(out).toBe("见 [https://example.com/path](https://example.com/path)。然后继续");
});
it("stops URL at fullwidth comma ", () => {
const out = preprocessLinks("打开 https://example.com/a以及其他");
expect(out).toBe("打开 [https://example.com/a](https://example.com/a),以及其他");
});
it("stops URL at ideographic comma 、", () => {
const out = preprocessLinks("两个地址 https://a.com/x、https://b.com/y");
expect(out).toBe(
"两个地址 [https://a.com/x](https://a.com/x)、[https://b.com/y](https://b.com/y)",
);
});
it("stops URL at fullwidth right paren ", () => {
const out = preprocessLinks("(见 https://example.com/x后文");
expect(out).toBe("(见 [https://example.com/x](https://example.com/x))后文");
});
it("stops URL at corner bracket 」", () => {
const out = preprocessLinks("「https://example.com/a」后文");
expect(out).toBe("「[https://example.com/a](https://example.com/a)」后文");
});
it("stops URL at fullwidth exclamation ", () => {
const out = preprocessLinks("太好了 https://example.com/x继续");
expect(out).toBe("太好了 [https://example.com/x](https://example.com/x)!继续");
});
it("handles the original bug report (PR link then 。 then more text)", () => {
const out = preprocessLinks(
"已合并 PR #1623https://github.com/multica-ai/multica/pull/1623。merge commit",
);
expect(out).toBe(
"已合并 PR #1623[https://github.com/multica-ai/multica/pull/1623](https://github.com/multica-ai/multica/pull/1623)。merge commit",
);
});
it("does not swallow the entire remainder when there is no trailing space", () => {
const out = preprocessLinks("https://github.com/x/y/issues/1619。我接下来把这个");
expect(out).toBe(
"[https://github.com/x/y/issues/1619](https://github.com/x/y/issues/1619)。我接下来把这个",
);
});
it("preserves ASCII trailing period handling (no regression)", () => {
const out = preprocessLinks("visit https://example.com/path. next.");
expect(out).toBe("visit [https://example.com/path](https://example.com/path). next.");
});
it("preserves plain URL with no trailing punctuation (no regression)", () => {
const out = preprocessLinks("go https://example.com/path");
expect(out).toBe("go [https://example.com/path](https://example.com/path)");
});
it("preserves CJK letters inside URL path (only trims on punctuation)", () => {
const out = preprocessLinks("https://zh.wikipedia.org/wiki/中国 参考");
expect(out).toBe(
"[https://zh.wikipedia.org/wiki/中国](https://zh.wikipedia.org/wiki/中国) 参考",
);
});
it("does not re-link an already-linked URL that contains 。", () => {
// If a user or upstream already wrote [text](url。), we leave it alone.
const input = "见 [link](https://example.com/x。)后文";
expect(preprocessLinks(input)).toBe(input);
});
it("does not linkify fuzzy domains inside existing markdown link labels", () => {
const input =
"数据来源:[NBA.com Schedule](https://www.nba.com/schedule)、[NBC Insider](https://www.nbc.com/nbc-insider/every-nba-playoff-game-this-week-on-nbc-peacock-april-25-28)";
expect(preprocessLinks(input)).toBe(input);
});
it("leaves bare domains outside existing markdown links as plain text", () => {
const input = "数据来源:[NBA.com Schedule](https://www.nba.com/schedule),官网 NBA.com";
expect(preprocessLinks(input)).toBe(input);
});
});
// The bug (#4222): an agent mentions a project file like `plan.md` in a comment.
// linkify-it fuzzy-matches it as the domain `plan.md` (md is Moldova's ccTLD) and
// turns it into a clickable https://plan.md link that goes nowhere. Bare filename
// tokens must stay plain text — only an explicit scheme makes them a link.
describe("preprocessLinks — bare filenames are not auto-linked as URLs", () => {
it("leaves a bare .md filename in CJK prose as plain text", () => {
const out = preprocessLinks("决策已锁定plan.md 已更新");
expect(out).toBe("决策已锁定plan.md 已更新");
});
it("leaves README.md as plain text", () => {
expect(preprocessLinks("see README.md for details")).toBe("see README.md for details");
});
it("leaves other extensions that collide with TLDs (sh, rs, py) as plain text", () => {
expect(preprocessLinks("run build.sh then main.rs and app.py")).toBe(
"run build.sh then main.rs and app.py",
);
});
it("honors an explicit scheme on a filename-shaped host", () => {
expect(preprocessLinks("open https://plan.md now")).toBe(
"open [https://plan.md](https://plan.md) now",
);
});
it("leaves bare domains as plain text regardless of their TLD", () => {
expect(preprocessLinks("官网 NBA.com")).toBe("官网 NBA.com");
});
it("leaves both a bare filename and a bare domain as plain text", () => {
expect(preprocessLinks("plan.mdexample.com")).toBe("plan.mdexample.com");
});
it("auto-links only explicit web URLs, www URLs, and email addresses", () => {
expect(
preprocessLinks(
"http://4399.com https://4399.com www.4399.com contact@example.com 4399.com ai.md ftp://example.com",
),
).toBe(
"[http://4399.com](http://4399.com) [https://4399.com](https://4399.com) [www.4399.com](https://www.4399.com) [contact@example.com](mailto:contact@example.com) 4399.com ai.md ftp://example.com",
);
});
it("still detects explicit ./ file paths (FILE_PATH_REGEX regression)", () => {
expect(preprocessLinks("see ./src/main.go here")).toBe(
"see [./src/main.go](./src/main.go) here",
);
});
});
// Trailing markdown emphasis / strikethrough delimiters that linkify-it counts
// as URL characters must be dropped from the URL, so the closing `**` of
// `**url**` stays as emphasis instead of being swallowed into the href — that
// swallow was the MUL-4242 render bug. Mirrors GFM's own autolink trailing trim.
describe("preprocessLinks — trailing markdown delimiter is not part of the URL", () => {
it("keeps the closing ** outside a bold-wrapped bare URL", () => {
expect(preprocessLinks("**https://example.com/x**")).toBe(
"**[https://example.com/x](https://example.com/x)**",
);
});
it("handles the original bug shape (bold + fullwidth colon)", () => {
expect(preprocessLinks("**PRhttps://example.com/x**")).toBe(
"**PR[https://example.com/x](https://example.com/x)**",
);
});
it("keeps ** outside when a CJK punctuation immediately follows (variant B)", () => {
expect(preprocessLinks("**https://example.com/x**MUL-4277")).toBe(
"**[https://example.com/x](https://example.com/x)**MUL-4277",
);
});
it("keeps the closing * outside an italic-wrapped bare URL", () => {
expect(preprocessLinks("*https://example.com/x*")).toBe(
"*[https://example.com/x](https://example.com/x)*",
);
});
it("strips a trailing * that really ends a URL — documented tradeoff, matches GFM", () => {
expect(preprocessLinks("see https://example.com/glob/* here")).toBe(
"see [https://example.com/glob/](https://example.com/glob/)* here",
);
});
it("keeps a * in the middle of a URL", () => {
expect(preprocessLinks("https://example.com/a*b/c")).toBe(
"[https://example.com/a*b/c](https://example.com/a*b/c)",
);
});
});