evOo-local/packages/chat-ui/src/markdown.test.ts

72 lines
3.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
citationDomainLabel,
closeUnterminatedFence,
isExternalMarkdownUrl,
parseCitationBadgeLabel,
parseHexColorToken,
sanitizeMarkdownUrl,
} from "./markdown";
describe("sanitizeMarkdownUrl", () => {
it("allows normal external links and optionally allows local links", () => {
expect(sanitizeMarkdownUrl("https://example.com/docs")).toBe("https://example.com/docs");
expect(sanitizeMarkdownUrl("mailto:hello@example.com")).toBe("mailto:hello@example.com");
expect(sanitizeMarkdownUrl("/docs", true)).toBe("/docs");
expect(sanitizeMarkdownUrl("#section", true)).toBe("#section");
});
it("rejects executable and embedded-data URLs", () => {
expect(sanitizeMarkdownUrl("javascript:alert(1)", true)).toBeUndefined();
expect(sanitizeMarkdownUrl("data:text/html,<script>alert(1)</script>", true)).toBeUndefined();
expect(sanitizeMarkdownUrl("/docs")).toBeUndefined();
});
});
describe("isExternalMarkdownUrl", () => {
it("identifies outbound links while excluding local paths and fragments", () => {
expect(isExternalMarkdownUrl("https://example.com/docs")).toBe(true);
expect(isExternalMarkdownUrl("http://85.215.232.220:8080/gudrun-review/")).toBe(true);
expect(isExternalMarkdownUrl("mailto:hello@example.com")).toBe(true);
expect(isExternalMarkdownUrl("/docs")).toBe(false);
expect(isExternalMarkdownUrl("#section")).toBe(false);
expect(isExternalMarkdownUrl(undefined)).toBe(false);
});
});
describe("parseHexColorToken", () => {
it("parses valid 3, 6, and 8 digit hex color tokens and rejects other strings", () => {
expect(parseHexColorToken("#6B4226")).toBe("#6B4226");
expect(parseHexColorToken(" #e8722a ")).toBe("#e8722a");
expect(parseHexColorToken("#fff")).toBe("#fff");
expect(parseHexColorToken("#FAF6F0FF")).toBe("#FAF6F0FF");
expect(parseHexColorToken("#12345")).toBeUndefined();
expect(parseHexColorToken("npm run dev")).toBeUndefined();
});
});
describe("closeUnterminatedFence", () => {
it("temporarily closes a partial streaming code fence", () => {
expect(closeUnterminatedFence("Before\n```ts\nconst value = 1;")).toBe(
"Before\n```ts\nconst value = 1;\n```",
);
});
it("leaves complete markdown unchanged", () => {
const markdown = "```ts\nconst value = 1;\n```\n\nDone";
expect(closeUnterminatedFence(markdown)).toBe(markdown);
});
});
describe("parseCitationBadgeLabel and citationDomainLabel", () => {
it("recognizes numeric inline citation links and extracts domain labels", () => {
expect(parseCitationBadgeLabel("1", "https://arxiv.org/abs/2401.00001")).toBe("1");
expect(parseCitationBadgeLabel("[12]", "https://www.example.com/article")).toBe("12");
expect(parseCitationBadgeLabel("Read more", "https://example.com/article")).toBeUndefined();
expect(parseCitationBadgeLabel("1", "/relative/path")).toBeUndefined();
expect(citationDomainLabel("https://www.arxiv.org/abs/2401.00001")).toBe("arxiv.org");
expect(citationDomainLabel("invalid-url")).toBeUndefined();
});
});