From 90b03ed7012879a07e779a405a00c0d3ca378bf0 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 27 Oct 2026 19:00:49 +0400 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D0=BA=D0=B8:=20=D0=B2=D0=BD=D0=B5?= =?UTF-8?q?=D1=88=D0=BD=D1=8F=D1=8F=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D1=82=D0=B5=D1=80=D1=8F=D0=B5=D1=82=20=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D1=8F=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B5=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20(#135,=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20I-1,?= =?UTF-8?q?=20M-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/wiki-package-d/design.md | 12 ++++++++++++ src/sdlc_hub/modules/wiki/convert.py | 7 ++++++- tests/test_wiki_convert.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/openspec/changes/wiki-package-d/design.md b/openspec/changes/wiki-package-d/design.md index 172308b..eb97726 100644 --- a/openspec/changes/wiki-package-d/design.md +++ b/openspec/changes/wiki-package-d/design.md @@ -124,3 +124,15 @@ T8.2: `request_fetch` отвечает `ready` по полю `state`, а `GET различая «новый/идущий». Цена ошибки — ответ PATCH называет уже идущий пакет. - 2026-10-27 — T6.1: картинка-другое-вложение внутри ссылки на вложение выводится своей ссылкой перед ссылкой внешнего вложения. Цена ошибки — порядок двух ссылок в строке. +- 2026-10-27 — R-1: в шапку плана добавлена строка «Ветка» — хук require-plan ищет имя ветки в плане. Цена ошибки — + нет (на поведение не влияет). +- 2026-10-27 — R-2: в MCP `sync` с ожиданием ошибка команды тоже выводится одной строкой (`_line`) — это тот же текст + Eva, что «ошибка синхронизации» решения 8. Цена ошибки — нет. +- 2026-10-27 — R-3: хранилище картинок живёт внутри `useWikiDownload` (третий параметр `attachments`, геттер `image`); + `components` в `PageView` пересоздаётся при смене карты, как и при смене `status`. Решение 10 допускало оба + прочтения; выбрано прежнее поведение `status`: состояние `busy`/`error` живёт в хуке и перемонтированием не + сбрасывается (тест). Цена ошибки — перемонтирование ссылок и картинок страницы при смене состояния другого вложения + (возможное мигание, потеря фокуса; финальное ревью M-3, deferred). +- 2026-10-27 — R-4: картинка выводится над кнопкой-ссылкой сохранения — по сценарию «Скачать картинку». Цена ошибки — нет. +- 2026-10-27 — R-5: у внешней ссылки края текста сохраняются, адрес вместо пустого текста — только при вынесенном + вложении (финальное ревью I-1, M-1). Цена ошибки — нет: прежнее поведение для ссылок без вложений. diff --git a/src/sdlc_hub/modules/wiki/convert.py b/src/sdlc_hub/modules/wiki/convert.py index 4abf258..8bf57ed 100644 --- a/src/sdlc_hub/modules/wiki/convert.py +++ b/src/sdlc_hub/modules/wiki/convert.py @@ -119,7 +119,12 @@ class _Converter(MarkdownConverter): if _unsafe(href): return text # исполняемая или внутренняя ссылка автора: остаётся только текст el["href"] = _href(href) - return marks + sep + super().convert_a(el, _tail(rest.strip()) or _label(href), parent_tags) + # края текста возвращаются как были: markdownify сам выносит их за ссылку, иначе слова склеиваются (ревью I-1); + # адрес вместо пустого текста — только если из ссылки вынесено вложение (M-1) + core = rest.strip() + lead, trail = rest[:len(rest) - len(rest.lstrip())], rest[len(rest.rstrip()):] + label = lead + _tail(core) + trail if core else (_label(href) if marks else rest) + return marks + sep + super().convert_a(el, label, parent_tags) def _strip_private(soup) -> None: diff --git a/tests/test_wiki_convert.py b/tests/test_wiki_convert.py index a1ab96a..cbc05de 100644 --- a/tests/test_wiki_convert.py +++ b/tests/test_wiki_convert.py @@ -335,3 +335,15 @@ def test_trailing_backslash_in_page_link_text(): def test_trailing_backslash_in_external_link_text(): md, _ = convert.to_markdown('

путь C:\\

', [], []) assert md == "[путь C:\\\\](http://ex.com/)\n" + + +def test_external_link_keeps_edge_spaces(): + """Финальное ревью I-1: пробелы на краях текста внешней ссылки выносятся наружу, слова не склеиваются.""" + md, _ = convert.to_markdown('

слово док дальше

', [], []) + assert md == "слово [док](http://ex.com/) дальше\n" + + +def test_empty_external_link_dropped(): + """Финальное ревью M-1: пустая внешняя ссылка без вложения внутри не выводится адресом.""" + md, _ = convert.to_markdown('

abc

', [], []) + assert md == "abc\n"