import { describe, expect, it } from "vitest"; import { type Inline, parseInline } from "./inline.ts"; const P = { projectKey: "AURORA" }; const t = (text: string): Inline => ({ t: "text", text }); const wrap = (kind: "strong" | "em" | "del" | "ins" | "sup" | "sub", ...children: Inline[]): Inline => ({ t: kind, children }); const issue = (key: string, ...children: Inline[]): Inline => ({ t: "issue", key, children }); const link = (href: string, ...children: Inline[]): Inline => ({ t: "link", href, children }); describe("parseInline: выделение", () => { it.each<[string, Inline[]]>([ ["*Итог:* ошибка", [wrap("strong", t("Итог:")), t(" ошибка")]], ["_к_", [wrap("em", t("к"))]], ["-з-", [wrap("del", t("з"))]], ["+п+", [wrap("ins", t("п"))]], ["^в^", [wrap("sup", t("в"))]], ["~н~", [wrap("sub", t("н"))]], ["*_x_*", [wrap("strong", wrap("em", t("x")))]], ["(*x*)", [t("("), wrap("strong", t("x")), t(")")]], ["a *b c* d", [t("a "), wrap("strong", t("b c")), t(" d")]], ["{{data_type}}", [{ t: "code", text: "data_type" }]], ["{{*x*}} и", [{ t: "code", text: "*x*" }, t(" и")]], ])("%s", (src, tree) => expect(parseInline(src, P)).toEqual(tree)); }); describe("parseInline: обычный текст не портится", () => { it.each([ "data_type = null", "2026-10-15", "счёт-фактура", "a*b*c", "18 803 903,65 - 9 351 102,89", "**важно**", "* одинокая", "*незакрытый", "[без конца", "*x*y", "{{без конца", "UTF-8", "SHA-256", "DOC-221280", "XAURORA-1", "AURORA-12x", "", ])("%s", (src) => expect(parseInline(src, P)).toEqual(src ? [t(src)] : [])); }); describe("parseInline: экранирование и перенос", () => { it("\\* — сам символ", () => expect(parseInline("\\*не жирный\\*", P)).toEqual([t("*не жирный*")])); it("\\\\ — перенос", () => expect(parseInline("a\\\\b", P)).toEqual([t("a"), { t: "br" }, t("b")])); }); describe("parseInline: ссылки", () => { it.each<[string, Inline[]]>([ ["[Jira|https://x.ru/a?b=1]", [link("https://x.ru/a?b=1", t("Jira"))]], ["[https://x.ru]", [link("https://x.ru", t("https://x.ru"))]], ["[mail|MAILTO:a@b.ru]", [link("MAILTO:a@b.ru", t("mail"))]], ["[*Жирная* ссылка|http://x.ru]", [link("http://x.ru", wrap("strong", t("Жирная")), t(" ссылка"))]], ["см. https://x.ru/p, далее", [t("см. "), link("https://x.ru/p", t("https://x.ru/p")), t(", далее")]], ["(https://x.ru/p).", [t("("), link("https://x.ru/p", t("https://x.ru/p")), t(").")]], ["[~ivanov]", [{ t: "mention", login: "ivanov" }]], ])("%s", (src, tree) => expect(parseInline(src, P)).toEqual(tree)); it("в подписи ссылки ключи и адреса — текст (I-2: без вложенных ссылок)", () => { expect(parseInline("[AURORA-1445|https://j.x/browse/AURORA-1445]", P)).toEqual([link("https://j.x/browse/AURORA-1445", t("AURORA-1445"))]); expect(parseInline("[см. https://a.ru|https://b.ru]", P)).toEqual([link("https://b.ru", t("см. https://a.ru"))]); expect(parseInline("[*AURORA-1*|AURORA-2]", P)).toEqual([issue("AURORA-2", wrap("strong", t("AURORA-1")))]); }); it.each(["[нажми|javascript:alert(1)]", "[x|ftp://h]", "[javascript:alert(1)]", "[просто текст]"])( "опасная или неизвестная цель — текст как есть: %s", (src) => expect(parseInline(src, P)).toEqual([t(src)]), ); }); describe("parseInline: ключи задач", () => { it("ключи проекта в тексте", () => expect(parseInline("ведётся в AURORA-1445; AURORA-1645 снята", P)).toEqual([ t("ведётся в "), issue("AURORA-1445"), t("; "), issue("AURORA-1645"), t(" снята"), ])); it("[ключ] и [текст|ключ]", () => { expect(parseInline("[AURORA-1]", P)).toEqual([issue("AURORA-1")]); expect(parseInline("[см.|AURORA-2]", P)).toEqual([issue("AURORA-2", t("см."))]); }); it("не внутри {{…}} и адреса", () => { expect(parseInline("{{AURORA-1}}", P)).toEqual([{ t: "code", text: "AURORA-1" }]); expect(parseInline("https://j/browse/AURORA-1", P)).toEqual([link("https://j/browse/AURORA-1", t("https://j/browse/AURORA-1"))]); }); it("без projectKey — текст", () => expect(parseInline("AURORA-1445")).toEqual([t("AURORA-1445")])); }); describe("parseInline: ссылки на вложения", () => { const atts = [ { id: "31337", filename: "скрин 1.png", created: "2026-10-15T12:00:00.000+0300" }, { id: "31338", filename: "a.pdf", created: null }, { id: "10", filename: "дубль.png", created: "2026-10-14T12:00:00.000+0300" }, { id: "20", filename: "дубль.png", created: "2026-10-20T12:00:00.000+0300" }, ]; const A = { attachments: atts }; const att = (id: string, ...children: Inline[]): Inline => ({ t: "attachment", id, children }); it("!имя|параметры! и !имя!", () => { expect(parseInline("см. !скрин 1.png|thumbnail!", A)).toEqual([t("см. "), att("31337", t("скрин 1.png"))]); expect(parseInline("!скрин 1.png!", A)).toEqual([att("31337", t("скрин 1.png"))]); }); it("[^имя] и [текст|^имя]", () => { expect(parseInline("[^a.pdf]", A)).toEqual([att("31338", t("a.pdf"))]); expect(parseInline("[отчёт|^a.pdf]", A)).toEqual([att("31338", t("отчёт"))]); }); it("имя точно (с регистром); несколько с одним именем — самое новое", () => { expect(parseInline("!Скрин 1.png!", A)).toEqual([t("!Скрин 1.png!")]); expect(parseInline("!дубль.png!", A)).toEqual([att("20", t("дубль.png"))]); }); it("имени нет среди вложений — исходник как есть", () => { expect(parseInline("!нет.png!", A)).toEqual([t("!нет.png!")]); expect(parseInline("[^нет.pdf] и [x|^нет.pdf]", A)).toEqual([t("[^нет.pdf] и [x|^нет.pdf]")]); }); it("одиночный «!» перед ссылкой не мешает ей (I-1)", () => { expect(parseInline("Внимание! см. !a.pdf!", A)).toContainEqual(att("31338", t("a.pdf"))); const r = parseInline("Важно! [^a.pdf] и !a.pdf!", A); expect(r.filter((n) => n.t === "attachment")).toEqual([att("31338", t("a.pdf")), att("31338", t("a.pdf"))]); }); it("внешняя картинка не вложение", () => { expect(JSON.stringify(parseInline("!http://x/скрин 1.png!", { attachments: [{ id: "1", filename: "http://x/скрин 1.png", created: null }] }))).not.toContain("attachment"); }); it("без опции attachments — как раньше", () => { expect(parseInline("!скрин 1.png!")).toEqual([t("!скрин 1.png!")]); expect(parseInline("[^a.pdf]")).toEqual([t("[^a.pdf]")]); }); it("внутри подписи ссылки вложения нет (кнопка в ссылке недопустима)", () => { expect(parseInline("[!a.pdf!|https://x.y]", A)).toEqual([link("https://x.y", t("!a.pdf!"))]); }); });