From ccb1606056c8503ed86f0a2c7565b8610d5da685 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 11:01:31 +0400 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D0=BA=D0=B8:=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA=D1=83?= =?UTF-8?q?=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0=D0=BC=D0=B8=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B0=20Eva=20=D0=B8=D0=B7=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BF=D0=B8=D0=B8=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Редактор Eva (TinyMCE) сохраняет в HTML страницы блок table-row-controls (номер строки, «+», точка); markdownify превращал его в абзацы «№», «1», «2»… перед таблицей. Блок удаляется целиком до конвертации; карточки вложений с тем же классом mceNonEditable остаются. Co-Authored-By: Claude Opus 5.5 --- src/jira_hub/modules/wiki/convert.py | 9 +++++ tests/fixtures/wiki/page_table_controls.html | 38 ++++++++++++++++++++ tests/test_wiki_convert.py | 23 ++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 tests/fixtures/wiki/page_table_controls.html diff --git a/src/jira_hub/modules/wiki/convert.py b/src/jira_hub/modules/wiki/convert.py index 8876b5d..b6c24cf 100644 --- a/src/jira_hub/modules/wiki/convert.py +++ b/src/jira_hub/modules/wiki/convert.py @@ -103,9 +103,18 @@ def _strip_private(soup) -> None: el[key] = _PRIVATE.sub("", value) +def _strip_editor_ui(soup) -> None: + """Колонка управления строками таблицы редактора Eva (TinyMCE: номер строки, «+», точка) сохраняется в HTML + страницы блоком `table-row-controls` и давала абзацы «№», «1», «2»… перед таблицей (#95). Блок убирается целиком; + прочие `mceNonEditable` остаются: у карточек вложений тот же класс, в них ссылки на файлы.""" + for el in soup.select(".table-row-controls"): + el.decompose() + + def to_markdown(html: str, attachments: list[dict] | None, links: list[dict] | None) -> tuple[str, list[dict]]: """HTML страницы → (Markdown, вложения страницы [{url, name}]).""" soup = BeautifulSoup(html, "html.parser") + _strip_editor_ui(soup) _strip_private(soup) conv = _Converter(attachments or [], links or [], heading_style="ATX", autolinks=False) md = conv.convert_soup(soup).strip() + "\n" diff --git a/tests/fixtures/wiki/page_table_controls.html b/tests/fixtures/wiki/page_table_controls.html new file mode 100644 index 0000000..d4eb503 --- /dev/null +++ b/tests/fixtures/wiki/page_table_controls.html @@ -0,0 +1,38 @@ +

Замечания к модели

+
+ model.dbml +
+
+
+
+ + + + + + + +

№

Замечание

Решение

1

Первое замечание

Первое решение

2

Второе замечание

Второе решение

2-бис

Дополнение

Принято

+
+
+
+
+
+
№
+
+
+
+
+
1
+
+
+
+
2
+
+
+
+
3
+
+
+
+

Текст после таблицы.

diff --git a/tests/test_wiki_convert.py b/tests/test_wiki_convert.py index 496484b..632d60c 100644 --- a/tests/test_wiki_convert.py +++ b/tests/test_wiki_convert.py @@ -87,6 +87,29 @@ def test_fill_ids(): assert convert.fill_ids("[x](attachment:@1)", [7, 9]) == "[x](attachment:9)" +# Колонка управления строками таблицы редактора Eva (TinyMCE, `table-row-controls`) попадает в HTML страницы и +# раньше давала абзацы «№», «1», «2»… перед таблицей (#95) +CARD_REF = "/files/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A3f2a9c1e-0b7d-4e8a-9c21-5d6e7f8a9b0c/model.dbml?version=1" + + +def test_table_row_controls_removed(): + md, _ = convert.to_markdown(_html("page_table_controls.html"), [], []) + lines = [line.strip() for line in md.splitlines() if line.strip()] + assert not [line for line in lines if re.fullmatch(r"№|\d{1,3}", line)], lines + assert "| № | Замечание | Решение |" in lines + assert "| 2-бис | Дополнение | Принято |" in lines + assert "Текст после таблицы." in lines + + +def test_card_preview_kept_with_controls(): + md, atts = convert.to_markdown( + _html("page_table_controls.html"), + [{"ref": CARD_REF, "url": "https://eva.example.test" + CARD_REF, "name": "model.dbml"}], + [], + ) + assert "[📎 model.dbml](attachment:@0)" in md and len(atts) == 1 + + # --- fix round 1 (review Task 6: I1, I2, I3, M3, M4) --- def test_unsafe_schemes_dropped_from_links():