Пакет веб-багов: карточка, лента, вики-разметка, состояние модулей (#124) #125

Merged
aep merged 17 commits from feat/124-web-bugs into main 2026-10-26 00:55:02 +03:00
4 changed files with 31 additions and 6 deletions
Showing only changes of commit 5c3fd4cc4d - Show all commits

View File

@ -116,4 +116,10 @@ describe("parseInline: ссылки на вложения", () => {
it("внутри подписи ссылки вложения нет (кнопка в ссылке недопустима)", () => {
expect(parseInline("[!a.pdf!|https://x.y]", A)).toEqual([link("https://x.y", t("!a.pdf!"))]);
});
it("\\# — символ решётки (M-5)", () => {
expect(parseInline("\\# не пункт")).toEqual([{ t: "text", text: "# не пункт" }]);
});
it("[~логин|текст] — упоминание логина без подписи (M-6)", () => {
expect(parseInline("[~ivanov|Иванов]")).toEqual([{ t: "mention", login: "ivanov" }]);
});
});

View File

@ -18,7 +18,7 @@ export type Inline =
| { t: "br" };
const MARKS: Record<string, Mark> = { "*": "strong", _: "em", "-": "del", "+": "ins", "^": "sup", "~": "sub" };
const ESCAPABLE = new Set([..."*_-+^~{}[]|!\\"]);
const ESCAPABLE = new Set([..."*_-+^~{}[]|!#\\"]);
const WORD = /[\p{L}\p{N}]/u;
const SPACE = /\s/;
const URL = /https?:\/\/[^\s\]|<>"]+/y;
@ -147,7 +147,7 @@ function closing(line: string, i: number): number {
// содержимое [...]: упоминание, ключ задачи, ссылка с проверенной схемой; иначе null — текст как есть
function bracket(body: string, opts: WikiOptions, isKey: (s: string) => boolean): Inline | null {
if (body.startsWith("~")) {
const login = body.slice(1).trim();
const login = body.slice(1).split("|")[0].trim();
return login && !SPACE.test(login) ? { t: "mention", login } : null;
}
const ref = ATT_LINK.exec(body);

View File

@ -77,4 +77,23 @@ describe("parseWiki: блоки-макросы", () => {
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("| внутри [текст|адрес] ячейку по-прежнему не делит", () => {
const [table] = parseWiki("|[док|https://x.ru]|b|");
expect((table as { rows: unknown[][] }).rows[0]).toHaveLength(2);
});
});

View File

@ -14,7 +14,7 @@ export type Block =
| { t: "quote"; children: Block[] }
| { t: "hr" };
const MACRO = /^\s*\{(code(?::[^}]*)?|noformat|quote)\}/;
const MACRO = /^\s*\{(code|noformat|quote)(?::[^}]*)?\}/;
const HEADING = /^\s*h([1-6])\.\s+(.*)$/;
const BQ = /^\s*bq\.\s+(.*)$/;
const HR = /^\s*----\s*$/;
@ -101,14 +101,14 @@ function blocks(src: string[], opts: WikiOptions): Block[] {
// закрывающий тег макроса в той же строке после открывающего или в последующих; null — не закрыт
function closeMacro(lines: string[], start: number, open: RegExpExecArray): { name: string; body: string[]; end: number; after: string } | null {
const name = open[1].split(":")[0];
const name = open[1];
const tag = `{${name}}`;
const rest = lines[start].slice(open[0].length);
const body: string[] = [];
for (let k = start; k < lines.length; k++) {
const text = k === start ? rest : lines[k];
const at = text.indexOf(tag);
if (at < 0) { if (k !== start || text) body.push(text); continue; }
if (at < 0) { if (k !== start || text.trim()) body.push(text); continue; }
if (text.slice(0, at)) body.push(text.slice(0, at));
return { name, body, end: k, after: text.slice(at + tag.length) };
}
@ -126,7 +126,7 @@ function row(line: string, opts: WikiOptions): Cell[] {
let depth = 0;
// `|` внутри [ссылки|адрес] ячейку не режет
while (j < s.length && (s[j] !== "|" || depth > 0)) {
if (s[j] === "[") depth++;
if (s[j] === "[") { if (s.indexOf("]", j + 1) >= 0) depth++; }
else if (s[j] === "]" && depth > 0) depth--;
j++;
}