From 5c3fd4cc4dac0461a9873e2db457fb8ed46d9b73 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 25 Oct 2026 22:56:29 +0400 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D0=BA=D0=B8-=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BA=D0=B0:=20\#,=20=D0=BC=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D1=8B=20=D1=81=20=D0=BF=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=D0=B0=D0=BC=D0=B8,=20[~=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BD|=D1=82=D0=B5=D0=BA=D1=81=D1=82],=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B1=D0=B5=D0=BB=D1=8B=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20{code},=20=D0=BD=D0=B5=D0=BF=D0=B0=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20[=20=D0=B2=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D0=B5=20(#124,=20#56=20M-5,=20M-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../modules/jira/issues/wiki/inline.test.ts | 6 ++++++ web/src/modules/jira/issues/wiki/inline.ts | 4 ++-- .../modules/jira/issues/wiki/parse.test.ts | 19 +++++++++++++++++++ web/src/modules/jira/issues/wiki/parse.ts | 8 ++++---- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/web/src/modules/jira/issues/wiki/inline.test.ts b/web/src/modules/jira/issues/wiki/inline.test.ts index 1073755..618a930 100644 --- a/web/src/modules/jira/issues/wiki/inline.test.ts +++ b/web/src/modules/jira/issues/wiki/inline.test.ts @@ -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" }]); + }); }); diff --git a/web/src/modules/jira/issues/wiki/inline.ts b/web/src/modules/jira/issues/wiki/inline.ts index abfb5a8..b34ebb0 100644 --- a/web/src/modules/jira/issues/wiki/inline.ts +++ b/web/src/modules/jira/issues/wiki/inline.ts @@ -18,7 +18,7 @@ export type Inline = | { t: "br" }; const MARKS: Record = { "*": "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); diff --git a/web/src/modules/jira/issues/wiki/parse.test.ts b/web/src/modules/jira/issues/wiki/parse.test.ts index f6e57bf..22e799b 100644 --- a/web/src/modules/jira/issues/wiki/parse.test.ts +++ b/web/src/modules/jira/issues/wiki/parse.test.ts @@ -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); + }); }); diff --git a/web/src/modules/jira/issues/wiki/parse.ts b/web/src/modules/jira/issues/wiki/parse.ts index f3d8c6b..635b1bd 100644 --- a/web/src/modules/jira/issues/wiki/parse.ts +++ b/web/src/modules/jira/issues/wiki/parse.ts @@ -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++; }