import { describe, expect, it } from "vitest"; import type { Inline } from "./inline.ts"; import { type Block, type ListBlock, type ListItem, parseWiki } from "./parse.ts"; const P = { projectKey: "AURORA" }; const t = (text: string): Inline => ({ t: "text", text }); const br: Inline = { t: "br" }; const p = (...children: Inline[]): Block => ({ t: "p", children }); const item = (text: string, ...lists: ListBlock[]): ListItem => ({ children: text ? [t(text)] : [], lists }); const ul = (...items: ListItem[]): ListBlock => ({ t: "list", ordered: false, items }); const ol = (...items: ListItem[]): ListBlock => ({ t: "list", ordered: true, items }); const h = (level: 1 | 2 | 3 | 4 | 5 | 6, text: string): Block => ({ t: "heading", level, children: [t(text)] }); const STRUCTURE = "h3. Контекст\nШаги:\n# Загрузить ОСВ\n# Открыть документ\n\n*Итог:* ошибка в {{data_type}}"; describe("parseWiki: абзацы и заголовки", () => { it("сценарий «Описание со структурой»", () => expect(parseWiki(STRUCTURE, P)).toEqual([ h(3, "Контекст"), p(t("Шаги:")), ol(item("Загрузить ОСВ"), item("Открыть документ")), p({ t: "strong", children: [t("Итог:")] }, t(" ошибка в "), { t: "code", text: "data_type" }), ])); it("CRLF даёт то же дерево", () => { const tree = parseWiki(STRUCTURE.replace(/\n/g, "\r\n"), P); expect(tree).toEqual(parseWiki(STRUCTURE, P)); expect(JSON.stringify(tree)).not.toContain("\\r"); }); it("абзац из двух строк — через br", () => expect(parseWiki("a\nb")).toEqual([p(t("a"), br, t("b"))])); it("пустые строки разделяют абзацы, пустых абзацев нет", () => expect(parseWiki("a\n\n\n\nb")).toEqual([p(t("a")), p(t("b"))])); it.each(["", " \n ", "\r\n"])("пустой текст %j → []", (src) => expect(parseWiki(src)).toEqual([])); it.each([1, 2, 3, 4, 5, 6] as const)("h%i.", (n) => expect(parseWiki(`h${n}. Заголовок`)).toEqual([h(n, "Заголовок")])); it.each(["h7. x", "h3.без пробела"])("%s — абзац", (src) => expect(parseWiki(src)).toEqual([p(t(src))])); it("----", () => expect(parseWiki("a\n----\nb")).toEqual([p(t("a")), { t: "hr" }, p(t("b"))])); }); describe("parseWiki: списки", () => { it("сценарий «Списки с отступом и пустыми строками» (R-4)", () => expect(parseWiki("Шаги воспроизведения:\n * Загрузить ОСВ.\n\n * Дождаться обработки.")).toEqual([ p(t("Шаги воспроизведения:")), ul(item("Загрузить ОСВ."), item("Дождаться обработки.")), ])); it("вложенность и смешанные маркеры", () => expect(parseWiki("* a\n** b\n*# c\n* d")).toEqual([ul(item("a", ul(item("b")), ol(item("c"))), item("d"))])); it("- пункты", () => expect(parseWiki("- x\n- y")).toEqual([ul(item("x"), item("y"))])); it.each(["-- x", "*- x", "-# x"])("%s — не список", (src) => expect(parseWiki(src)).toEqual([p(t(src))])); it("## Заголовок — второй уровень нумерованного", () => expect(parseWiki("## Заголовок")).toEqual([ol(item("", ol(item("Заголовок"))))])); it("строка без маркера после списка — абзац", () => expect(parseWiki("* a\nтекст")).toEqual([ul(item("a")), p(t("текст"))])); it("смена вида на первом уровне — новый список", () => expect(parseWiki("* a\n# b")).toEqual([ul(item("a")), ol(item("b"))])); it("*жирный* в начале строки — не список", () => expect(parseWiki("*Уточнение.* текст")).toEqual([p({ t: "strong", children: [t("Уточнение.")] }, t(" текст"))])); }); describe("parseWiki: таблицы", () => { it("заголовки, ячейки, | внутри ссылки", () => expect(parseWiki("||Поле||Значение||\n|data_type|null|\n|ссылка|[a|https://x.ru]|", P)).toEqual([{ t: "table", rows: [ [{ header: true, children: [t("Поле")] }, { header: true, children: [t("Значение")] }], [{ header: false, children: [t("data_type")] }, { header: false, children: [t("null")] }], [{ header: false, children: [t("ссылка")] }, { header: false, children: [{ t: "link", href: "https://x.ru", children: [t("a")] }] }], ], }])); it("смешанная строка ||h|c|", () => expect(parseWiki("||h|c|")).toEqual([{ t: "table", rows: [[{ header: true, children: [t("h")] }, { header: false, children: [t("c")] }]] }])); }); describe("parseWiki: блоки-макросы", () => { it("{code:sql}", () => expect(parseWiki("{code:sql}\nselect *\nfrom t\n{code}")).toEqual([{ t: "pre", text: "select *\nfrom t" }])); it("{noformat} в одной строке", () => expect(parseWiki("{noformat}*x*{noformat}")).toEqual([{ t: "pre", text: "*x*" }])); it("незакрытый {code} — текст", () => expect(parseWiki("{code}\nx")).toEqual([p(t("{code}"), br, t("x"))])); it("{quote} и хвост после закрывающего тега", () => expect(parseWiki("{quote}\nh3. Внутри\nтекст\n{quote}хвост")).toEqual([ { t: "quote", children: [h(3, "Внутри"), p(t("текст"))] }, p(t("хвост")), ])); it("bq.", () => expect(parseWiki("bq. цитата")).toEqual([{ t: "quote", children: [p(t("цитата"))] }])); it("{quote} не в начале строки — текст (R-8)", () => expect(parseWiki("см. {quote}x{quote}")).toEqual([p(t("см. {quote}x{quote}"))])); it.each(["{color:red}важно{color}", "!схема.png!", "{panel}x{panel}"])("неизвестное %s — текст как есть", (src) => expect(parseWiki(src)).toEqual([p(t(src))])); it("\\# в начале строки — абзац, не список", () => { expect(parseWiki("\\# не пункт")).toEqual([{ t: "p", children: [{ t: "text", text: "# не пункт" }] }]); }); it("quote и noformat с параметрами (M-5)", () => { expect(parseWiki("{quote:title=Итог}\nсогласовано\n{quote}")).toEqual([{ t: "quote", children: [{ t: "p", children: [{ t: "text", text: "согласовано" }] }] }]); expect(parseWiki("{noformat:borderStyle=solid}\n*x*\n{noformat}")).toEqual([{ t: "pre", text: "*x*" }]); }); it("пробелы после открывающего {code} не дают пустой строки (M-6)", () => { expect(parseWiki("{code} \nx = 1\n{code}")).toEqual([{ t: "pre", text: "x = 1" }]); }); it("непарная [ в ячейке не съедает разделители (M-6)", () => { const [table] = parseWiki("|[draft|итог|готово|"); expect(table).toMatchObject({ t: "table" }); expect((table as { rows: { children: { text: string }[] }[][] }).rows[0].map((c) => c.children[0].text)).toEqual(["[draft", "итог", "готово"]); }); it("непарная [ не берёт чужую ] из ссылки в следующей ячейке (T1.1)", () => { const [table] = parseWiki("|[draft|итог|[док|https://x.ru]|"); const cells = (table as { rows: { children: Inline[] }[][] }).rows[0]; expect(cells).toHaveLength(3); expect(cells[0].children[0]).toEqual({ t: "text", text: "[draft" }); expect(cells[1].children[0]).toEqual({ t: "text", text: "итог" }); expect(cells[2].children[0]).toMatchObject({ t: "link", href: "https://x.ru" }); }); it("{code} {code} в одной строке — пустой блок, не pre из пробелов (T1.2)", () => { expect(parseWiki("{code} {code}")).toEqual([{ t: "pre", text: "" }]); }); it("| внутри [текст|адрес] ячейку по-прежнему не делит", () => { const [table] = parseWiki("|[док|https://x.ru]|b|"); expect((table as { rows: unknown[][] }).rows[0]).toHaveLength(2); }); });