- fix Не появляется пустая странициа перед названием таблицы - fix Не появляется пустая страница перед альбомной ориентацией - fix При отказе от изменений создаётся новый файл - fix Замена "..." на «...» больше не затрагивает uml и mermaid блоки
This commit is contained in:
@@ -156,6 +156,86 @@ def test_landscape_places_figure_inside_landscape_section(tmp_path, monkeypatch)
|
||||
assert image_section == 1
|
||||
|
||||
|
||||
W_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
||||
|
||||
|
||||
def _is_break_paragraph(el) -> bool:
|
||||
"""Empty paragraph that is a page break and/or hosts a section break."""
|
||||
if el.tag != f"{W_NS}p":
|
||||
return False
|
||||
texts = [t.text or "" for t in el.findall(f".//{W_NS}t")]
|
||||
if any(t.strip() for t in texts):
|
||||
return False
|
||||
pPr = el.find(f"{W_NS}pPr")
|
||||
has_sect = pPr is not None and pPr.find(f"{W_NS}sectPr") is not None
|
||||
has_br = any(br.get(f"{W_NS}type") == "page" for br in el.findall(f".//{W_NS}br"))
|
||||
return has_sect or has_br
|
||||
|
||||
|
||||
def _breaks_between_last_text_and_table(doc) -> int:
|
||||
"""Empty page/section-break paras between the last body sentence and the table."""
|
||||
children = list(doc.element.body)
|
||||
table_i = next(i for i, el in enumerate(children) if el.tag == f"{W_NS}tbl")
|
||||
j = table_i - 1
|
||||
breaks = 0
|
||||
while j >= 0:
|
||||
el = children[j]
|
||||
if _is_break_paragraph(el):
|
||||
breaks += 1
|
||||
j -= 1
|
||||
continue
|
||||
if el.tag == f"{W_NS}p":
|
||||
text = "".join(t.text or "" for t in el.findall(f".//{W_NS}t")).strip()
|
||||
if text.startswith("Таблица") or text.startswith("Продолжение"):
|
||||
j -= 1
|
||||
continue
|
||||
break
|
||||
return breaks
|
||||
|
||||
|
||||
def test_portrait_to_landscape_is_single_section_break():
|
||||
"""Text then +landscape: one next-page section, no extra blank portrait page."""
|
||||
doc = Document(os.path.join(package_dir(), "Template.docx"))
|
||||
body = doc._body._element
|
||||
for child in list(body):
|
||||
if child.tag.endswith("}sectPr"):
|
||||
continue
|
||||
body.remove(child)
|
||||
|
||||
from md2gost.renderable.heading import Heading
|
||||
from md2gost.renderable.paragraph import Paragraph
|
||||
from md2gost.renderable.toc import ToC
|
||||
|
||||
h = Heading(doc._body, 1, False)
|
||||
h.add_run("СОДЕРЖАНИЕ")
|
||||
toc = ToC(doc._body, toc_mode="native")
|
||||
para = Paragraph(doc._body)
|
||||
para.add_run("Текст перед широкой таблицей.")
|
||||
table = Table(doc._body, 2, 3, CaptionInfo("wide", "Карта", landscape=True))
|
||||
for r in range(2):
|
||||
for c in range(3):
|
||||
table.add_paragraph_to_cell(r, c).add_run(f"{r}{c}")
|
||||
|
||||
Renderer(doc, skip_numbering=True).process([h, toc, para, table])
|
||||
assert _breaks_between_last_text_and_table(doc) == 1
|
||||
|
||||
# Same without TOC — still a single break, not page-break + section-break.
|
||||
doc2 = Document(os.path.join(package_dir(), "Template.docx"))
|
||||
body2 = doc2._body._element
|
||||
for child in list(body2):
|
||||
if child.tag.endswith("}sectPr"):
|
||||
continue
|
||||
body2.remove(child)
|
||||
para2 = Paragraph(doc2._body)
|
||||
para2.add_run("Текст перед широкой таблицей.")
|
||||
table2 = Table(doc2._body, 2, 3, CaptionInfo("wide2", "Карта", landscape=True))
|
||||
for r in range(2):
|
||||
for c in range(3):
|
||||
table2.add_paragraph_to_cell(r, c).add_run(f"{r}{c}")
|
||||
Renderer(doc2, skip_numbering=True).process([para2, table2])
|
||||
assert _breaks_between_last_text_and_table(doc2) == 1
|
||||
|
||||
|
||||
def test_landscape_listing_deferred_after_section(tmp_path, monkeypatch):
|
||||
"""+landscape +listing → listing must not sit in the landscape section."""
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -190,6 +190,27 @@ def test_preprocess_quotes():
|
||||
assert "«тест»" in preprocess_markdown('Он сказал "тест" вслух.')
|
||||
|
||||
|
||||
def test_preprocess_skips_uml_and_mermaid_fences():
|
||||
md = (
|
||||
'В тексте "кавычки".\n\n'
|
||||
"```uml\n"
|
||||
'rectangle "Акционеры" as own\n'
|
||||
"```\n\n"
|
||||
"```mermaid\n"
|
||||
'title "Выручка"\n'
|
||||
"```\n\n"
|
||||
"```uml-c4context\n"
|
||||
'Person(user, "Заявитель")\n'
|
||||
"```\n"
|
||||
)
|
||||
out = preprocess_markdown(md)
|
||||
assert "«кавычки»" in out
|
||||
assert 'rectangle "Акционеры" as own' in out
|
||||
assert 'title "Выручка"' in out
|
||||
assert 'Person(user, "Заявитель")' in out
|
||||
assert "«Акционеры»" not in out
|
||||
|
||||
|
||||
def test_formula_refs():
|
||||
assert find_formula_refs("см. @Формула:eq1 и далее") == {"eq1"}
|
||||
|
||||
@@ -466,6 +487,28 @@ def test_appendix_index_auto_inserted():
|
||||
assert out2 == [sec, manual, a, b]
|
||||
|
||||
|
||||
def test_table_caption_does_not_set_page_break_before():
|
||||
import os
|
||||
import docx
|
||||
from docx.shared import Mm
|
||||
from md2gost import package_dir
|
||||
from md2gost.styles import apply_mirea_styles
|
||||
from md2gost.layout_tracker import LayoutState
|
||||
from md2gost.renderable.caption import Caption, CaptionInfo
|
||||
|
||||
doc = docx.Document(os.path.join(package_dir(), "Template.docx"))
|
||||
apply_mirea_styles(doc)
|
||||
cap = Caption(doc._body, "Таблица", CaptionInfo("t1", "Реквизиты"), "1", True)
|
||||
state = LayoutState(Mm(250), Mm(160))
|
||||
state.add_height(Mm(248))
|
||||
leftover = state.remaining_page_height
|
||||
infos = list(cap.render(None, state))
|
||||
assert len(infos) == 1
|
||||
assert cap._docx_paragraph.style.name == "Название таблицы"
|
||||
assert cap._docx_paragraph.paragraph_format.page_break_before in (None, False)
|
||||
assert infos[0].height > leftover
|
||||
|
||||
|
||||
def test_caption_table_keep_with_next_and_toc_styles():
|
||||
import docx
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||
|
||||
+23
-1
@@ -4,7 +4,13 @@ import os
|
||||
import sys
|
||||
|
||||
from md2gost.dnd import first_markdown, normalize_drop_paths, parse_tkdnd_files
|
||||
from md2gost.pipeline import ConvertRequest, convert, default_output_path, should_launch_gui
|
||||
from md2gost.pipeline import (
|
||||
ConvertRequest,
|
||||
convert,
|
||||
default_output_path,
|
||||
should_launch_gui,
|
||||
timestamped_output_path,
|
||||
)
|
||||
from md2gost.__main__ import build_parser, request_from_args
|
||||
|
||||
|
||||
@@ -20,6 +26,22 @@ def test_default_output_path():
|
||||
assert path.endswith("report.docx")
|
||||
|
||||
|
||||
def test_timestamped_output_path(tmp_path):
|
||||
import re
|
||||
src = tmp_path / "ПР1.docx"
|
||||
src.write_bytes(b"x")
|
||||
out = timestamped_output_path(str(src))
|
||||
name = os.path.basename(out)
|
||||
assert re.fullmatch(r"ПР1_\d{4}-\d{2}-\d{2}-\d{2}-\d{2}\.docx", name)
|
||||
assert not os.path.exists(out)
|
||||
# collision → seconds (or seconds_2)
|
||||
open(out, "wb").close()
|
||||
out2 = timestamped_output_path(str(src))
|
||||
assert out2 != out
|
||||
assert os.path.basename(out2).startswith("ПР1_")
|
||||
assert out2.endswith(".docx")
|
||||
|
||||
|
||||
def test_parse_tkdnd_files():
|
||||
raw = r"{C:\My Files\a.md} C:\tmp\b.md"
|
||||
assert parse_tkdnd_files(raw) == [r"C:\My Files\a.md", r"C:\tmp\b.md"]
|
||||
|
||||
Reference in New Issue
Block a user