From fa1ceb48684d6c311349c28699c5e4df824635fb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 26 Oct 2026 01:48:36 +0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20=C2=AB!=C2=BB=20=D0=B2=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B5=20=D0=BB=D0=B5=D0=BD=D1=82=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE=D0=BF=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=8E=D1=82=20=E2=80=94=20=D0=B1=D0=B5=D0=B7=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20`!=D0=B8=D0=BC=D1=8F!`=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20(#124,=20N-1,?= =?UTF-8?q?=20R-20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/web-bugfixes/design.md | 5 +++++ web/src/modules/jira/issues/wiki/inline.ts | 11 +++++++---- web/src/modules/jira/issues/wiki/plain.test.ts | 5 +++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openspec/changes/web-bugfixes/design.md b/openspec/changes/web-bugfixes/design.md index 292017f..1277593 100644 --- a/openspec/changes/web-bugfixes/design.md +++ b/openspec/changes/web-bugfixes/design.md @@ -211,3 +211,8 @@ vitest, для окна карточки дополнительно e2e. Сер - R-19: N-1 (пере-ревью финальной волны) — в режиме `anyAttachment` ветка `!имя!` принимает любую пару «!», поэтому в ленте «Срочно! Проверьте! Ок» теряет восклицательные знаки — отложено в follow-up (требовать расширение файла в имени); второй волны правок нет. Цена ошибки — искажённые «!» в тексте строки ленты, данные не страдают. +- R-20 (design #6, 26.10.2026, решение владельца по R-19): N-1 исправлена до PR, а не отложена. В режиме `anyAttachment` + `!имя!` без списка вложений считается вложением, только если имя — имя файла с расширением без пробелов по краям + (`FILE_NAME` в `inline.ts`); `[^имя]` — явная ссылка на вложение, проверка к ней не применяется. Обоснование: ветка + сама внесла искажение «!» в строке ленты, владелец не хочет таких ошибок. Цена ошибки — `!слово.слово!` в обычном + тексте ленты («Ура!Сделано.ру!») прочитается как имя вложения без «!»; данные не страдают. diff --git a/web/src/modules/jira/issues/wiki/inline.ts b/web/src/modules/jira/issues/wiki/inline.ts index 3296d7b..25988f4 100644 --- a/web/src/modules/jira/issues/wiki/inline.ts +++ b/web/src/modules/jira/issues/wiki/inline.ts @@ -26,6 +26,8 @@ const URL = /https?:\/\/[^\s\]|<>"]+/y; const URL_TAIL = /[.,;:!?)]+$/; const IMAGE = /!([^!|\n]+)(?:\|[^!\n]*)?!/y; const ATT_LINK = /^(?:([^|]*)\|)?\^(.+)$/; +// имя файла с расширением, без пробелов по краям: только такое `!имя!` без списка вложений — вложение (N-1) +const FILE_NAME = /^\S(?:.*\S)?\.[\p{L}\p{N}]{1,10}$/u; // схема ссылки — только http, https, mailto (спека «Разметка Jira в карточке»); иначе ссылки нет function safeHref(raw: string): string | null { @@ -33,10 +35,11 @@ function safeHref(raw: string): string | null { return /^(https?:\/\/|mailto:)\S/i.test(href) ? href : null; } -// вложение по имени: несколько с одним именем — первое в порядке хаба -function findAttachment(opts: WikiOptions, name: string): string | null { +// вложение по имени: несколько с одним именем — первое в порядке хаба; image — `!имя!`, без списка только имя файла, +// иначе «Срочно! Проверьте!» в ленте теряет «!» (N-1) +function findAttachment(opts: WikiOptions, name: string, image = false): string | null { const found = (opts.attachments ?? []).find((a) => a.filename === name)?.id; - return found ?? (opts.anyAttachment ? "" : null); + return found ?? (opts.anyAttachment && (!image || FILE_NAME.test(name)) ? "" : null); } function keyPattern(projectKey: string | undefined): RegExp | null { @@ -86,7 +89,7 @@ export function parseInline(line: string, opts: WikiOptions = {}, label = false) if (c === "!" && !label && (opts.attachments?.length || opts.anyAttachment)) { IMAGE.lastIndex = i; const m = IMAGE.exec(line); - const id = m && !m[1].includes("://") ? findAttachment(opts, m[1]) : null; + const id = m && !m[1].includes("://") ? findAttachment(opts, m[1], true) : null; if (m && id !== null) { out.push({ t: "attachment", id, children: [{ t: "text", text: m[1] }] }); i += m[0].length; continue; } } diff --git a/web/src/modules/jira/issues/wiki/plain.test.ts b/web/src/modules/jira/issues/wiki/plain.test.ts index 38712b9..fe02ccc 100644 --- a/web/src/modules/jira/issues/wiki/plain.test.ts +++ b/web/src/modules/jira/issues/wiki/plain.test.ts @@ -17,6 +17,11 @@ describe("wikiPlain", () => { it("вложение, которого нет в списке, — именем (I-1)", () => { expect(wikiPlain("!скрин.png|width=50! [текст|^отчёт.pdf] [^а.txt]").trim()).toBe("скрин.png текст а.txt"); }); + it("«!» в обычном тексте без списка вложений остаются (N-1)", () => { + expect(wikiPlain("Срочно! Проверьте! Ок").trim()).toBe("Срочно! Проверьте! Ок"); + expect(wikiPlain("Готово! см. лог.txt!").trim()).toBe("Готово! см. лог.txt!"); + expect(wikiPlain("Ура!Сделано!").trim()).toBe("Ура!Сделано!"); + }); it("незакрытая и неизвестная разметка — как есть, пустой текст — пусто (Review Focus 3)", () => { expect(wikiPlain("{color:red}важно *без конца").trim()).toBe("{color:red}важно *без конца"); expect(wikiPlain("")).toBe("");