hot-fix 0.4.3
Python application / build (push) Has been cancelled

- fix Не появляется пустая странициа перед названием таблицы
- fix Не появляется пустая страница перед альбомной ориентацией
- fix При отказе от изменений создаётся новый файл
- fix Замена "..." на «...» больше не затрагивает uml и mermaid блоки
This commit is contained in:
Igor20264
2026-09-05 10:24:13 +03:00
parent 8508d04bbc
commit febf62c257
18 changed files with 267 additions and 49 deletions
+2 -2
View File
@@ -9,7 +9,6 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
build-fast/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
@@ -181,4 +180,5 @@ cython_debug/
md2gost.schemes.json md2gost.schemes.json
md2gost.include-cache.json md2gost.include-cache.json
include-cache/ include-cache/
.md2gost-cache/ .md2gost-cache/
/build-fast
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,12 +15,12 @@ IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks! tracking down the missing module yourself. Thanks!
missing module named 'org.python' - imported by copy (optional), xml.sax (delayed, conditional) missing module named 'org.python' - imported by copy (optional), xml.sax (delayed, conditional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), backports.tarfile (optional), distutils.archive_util (optional), setuptools._distutils.archive_util (optional) missing module named grp - imported by subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), backports.tarfile (optional), distutils.archive_util (optional), setuptools._distutils.archive_util (optional)
missing module named pwd - imported by posixpath (delayed, conditional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), getpass (delayed), distutils.util (delayed, conditional, optional), netrc (delayed, conditional), backports.tarfile (optional), distutils.archive_util (optional), http.server (delayed, optional), webbrowser (delayed), psutil (optional), setuptools._distutils.util (delayed, conditional, optional), setuptools._distutils.archive_util (optional) missing module named pwd - imported by posixpath (delayed, conditional), subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), getpass (delayed), distutils.util (delayed, conditional, optional), netrc (delayed, conditional), backports.tarfile (optional), distutils.archive_util (optional), http.server (delayed, optional), webbrowser (delayed), psutil (optional), setuptools._distutils.util (delayed, conditional, optional), setuptools._distutils.archive_util (optional)
missing module named urllib.urlopen - imported by urllib (delayed, optional), lxml.html (delayed, optional) missing module named urllib.urlopen - imported by urllib (delayed, optional), lxml.html (delayed, optional)
missing module named urllib.urlencode - imported by urllib (delayed, optional), lxml.html (delayed, optional) missing module named urllib.urlencode - imported by urllib (delayed, optional), lxml.html (delayed, optional)
missing module named pep517 - imported by importlib.metadata (delayed) missing module named pep517 - imported by importlib.metadata (delayed)
missing module named posix - imported by shutil (conditional), importlib._bootstrap_external (conditional), os (conditional, optional) missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level) missing module named resource - imported by posix (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level) missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level) excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
@@ -27176,6 +27176,7 @@ imported by:
&#8226; <a href="#md2gost.renderable.caption">md2gost.renderable.caption</a> &#8226; <a href="#md2gost.renderable.caption">md2gost.renderable.caption</a>
&#8226; <a href="#md2gost.renderable.listing">md2gost.renderable.listing</a> &#8226; <a href="#md2gost.renderable.listing">md2gost.renderable.listing</a>
&#8226; <a href="#md2gost.renderable.table">md2gost.renderable.table</a> &#8226; <a href="#md2gost.renderable.table">md2gost.renderable.table</a>
&#8226; <a href="#md2gost.renderer">md2gost.renderer</a>
&#8226; <a href="#md2gost_exe.py">md2gost_exe.py</a> &#8226; <a href="#md2gost_exe.py">md2gost_exe.py</a>
</div> </div>
@@ -27780,6 +27781,7 @@ imports:
&#8226; <a href="#md2gost.layout_tracker">md2gost.layout_tracker</a> &#8226; <a href="#md2gost.layout_tracker">md2gost.layout_tracker</a>
&#8226; <a href="#md2gost.numberer">md2gost.numberer</a> &#8226; <a href="#md2gost.numberer">md2gost.numberer</a>
&#8226; <a href="#md2gost.page_geometry">md2gost.page_geometry</a> &#8226; <a href="#md2gost.page_geometry">md2gost.page_geometry</a>
&#8226; <a href="#md2gost.profiles">md2gost.profiles</a>
&#8226; <a href="#md2gost.renderable">md2gost.renderable</a> &#8226; <a href="#md2gost.renderable">md2gost.renderable</a>
&#8226; <a href="#md2gost.renderable.diagram">md2gost.renderable.diagram</a> &#8226; <a href="#md2gost.renderable.diagram">md2gost.renderable.diagram</a>
&#8226; <a href="#md2gost.renderable.equation">md2gost.renderable.equation</a> &#8226; <a href="#md2gost.renderable.equation">md2gost.renderable.equation</a>
+15 -4
View File
@@ -21,7 +21,7 @@ from .diagram_schemes import (
) )
from .dnd import enable_file_drop, first_markdown from .dnd import enable_file_drop, first_markdown
from .help_content import SCHEMES_HELP, USAGE_HELP, load_prompt_catalog from .help_content import SCHEMES_HELP, USAGE_HELP, load_prompt_catalog
from .pipeline import ConvertRequest, convert, default_output_path from .pipeline import ConvertRequest, convert, default_output_path, timestamped_output_path
from .profiles import ( from .profiles import (
DEFAULT_HEADING_NUMBERING, DEFAULT_HEADING_NUMBERING,
DEFAULT_TABLE_CONTINUATION, DEFAULT_TABLE_CONTINUATION,
@@ -1200,9 +1200,19 @@ class Md2GostApp:
if not req.filename: if not req.filename:
messagebox.showinfo("md2gost", "Сначала перетащите или выберите markdown-файл.") messagebox.showinfo("md2gost", "Сначала перетащите или выберите markdown-файл.")
return return
if not req.check_only and req.output and os.path.isfile(req.output): if not req.check_only:
if not messagebox.askyesno("md2gost", f"Файл уже есть:\n{req.output}\n\nПерезаписать?"): target = (req.output or "").strip() or (
return default_output_path(req.filename) if req.filename else ""
)
if target and os.path.isfile(target):
if not messagebox.askyesno(
"md2gost", f"Файл уже есть:\n{target}\n\nПерезаписать?"
):
target = timestamped_output_path(target)
self._append_log(f"Не перезаписываем, пишем в {target}")
req.output = target
self.output_var.set(target)
self._auto_output = False
self._busy = True self._busy = True
self.convert_btn.configure(state=tk.DISABLED, bg=NAVY_DIM) self.convert_btn.configure(state=tk.DISABLED, bg=NAVY_DIM)
self.progress.start(12) self.progress.start(12)
@@ -1225,6 +1235,7 @@ class Md2GostApp:
self._append_log("Дебаг сброшен после этой сборки.") self._append_log("Дебаг сброшен после этой сборки.")
if result.ok: if result.ok:
if result.output_path: if result.output_path:
self.output_var.set(result.output_path)
self._append_log("Готово.") self._append_log("Готово.")
elif result.check_report: elif result.check_report:
self._append_log("Проверка завершена.") self._append_log("Проверка завершена.")
+1
View File
@@ -16,6 +16,7 @@ USAGE_HELP = """md2gost — Markdown → DOCX (ТЗ МИРЭА / ГОСТ 7.32)
PlantUML / Kroki — Настройки → Диаграммы. PlantUML / Kroki — Настройки → Диаграммы.
Свои UML-схемы — меню «Шаблоны UML». Свои UML-схемы — меню «Шаблоны UML».
3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»). 3. Нажмите «Конвертировать». Документ сохранится рядом с исходником (или по пути «Выходной DOCX»).
Если файл уже есть и вы откажетесь перезаписывать — сохранится как имя_гггг-мм-дд-ЧЧ-ММ.docx.
Дебаг (меню сверху) — следующая сборка с отладочными данными в документе. Дебаг (меню сверху) — следующая сборка с отладочными данными в документе.
Типы: practice (по умолчанию) / coursework / vkr — ГОСТ МИРЭА; PIS_custom — отчёт по практикам ПИС; APID_coursework — курсовая АПИД. Типы: practice (по умолчанию) / coursework / vkr — ГОСТ МИРЭА; PIS_custom — отчёт по практикам ПИС; APID_coursework — курсовая АПИД.
+27 -1
View File
@@ -8,6 +8,7 @@ import platform
import subprocess import subprocess
import traceback import traceback
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from getpass import getuser from getpass import getuser
from typing import Callable from typing import Callable
@@ -73,6 +74,25 @@ def default_output_path(filename: str) -> str:
return os.path.join(os.path.dirname(os.path.abspath(filename)), base + ".docx") return os.path.join(os.path.dirname(os.path.abspath(filename)), base + ".docx")
def timestamped_output_path(path: str) -> str:
"""report.docx → report_2026-09-05-10-05.docx (local time; seconds if taken)."""
directory, name = os.path.split(os.path.abspath(path))
stem, ext = os.path.splitext(name)
if ext.lower() != ".docx":
ext = ".docx"
stamp = datetime.now().strftime("%Y-%m-%d-%H-%M")
candidate = os.path.join(directory, f"{stem}_{stamp}{ext}")
if not os.path.exists(candidate):
return candidate
stamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
candidate = os.path.join(directory, f"{stem}_{stamp}{ext}")
n = 2
while os.path.exists(candidate):
candidate = os.path.join(directory, f"{stem}_{stamp}_{n}{ext}")
n += 1
return candidate
def default_template_path() -> str: def default_template_path() -> str:
return os.path.join(package_dir(), "Template.docx") return os.path.join(package_dir(), "Template.docx")
@@ -235,7 +255,13 @@ def convert(req: ConvertRequest, log: LogFn | None = None) -> ConvertResult:
document.core_properties.author = getuser() document.core_properties.author = getuser()
document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)" document.core_properties.comments = "Создано при помощи md2gost (ТЗ МИРЭА)"
document.save(output) try:
document.save(output)
except PermissionError:
alt = timestamped_output_path(output)
emit(f"Не удалось записать {output} (файл занят). Сохраняю как {alt}")
document.save(alt)
output = alt
except Exception as exc: except Exception as exc:
emit(traceback.format_exc()) emit(traceback.format_exc())
return ConvertResult( return ConvertResult(
+40 -6
View File
@@ -99,6 +99,38 @@ def get_profile(doc_type: str) -> DocProfile:
# --- text autofixes --- # --- text autofixes ---
# ```uml / ```mermaid / ```uml-c4 / ```bpmn … — кавычки и тире там синтаксис
_FENCE_RE = re.compile(
r"^(?P<fence>`{3,}|~{3,})(?P<info>[^\n]*)\n"
r"(?P<body>[\s\S]*?)"
r"^(?P=fence)[ \t]*(?:\n|$)",
re.M,
)
def _fence_lang(info: str) -> str:
return (info.strip().split() or [""])[0].lower()
def _is_diagram_fence_info(info: str) -> bool:
from .diagram_schemes import is_diagram_lang
return is_diagram_lang(_fence_lang(info))
def _map_outside_diagram_fences(text: str, fn) -> str:
"""Apply fn only outside UML / Mermaid / scheme fences."""
out: list[str] = []
pos = 0
for m in _FENCE_RE.finditer(text):
if not _is_diagram_fence_info(m.group("info")):
continue
out.append(fn(text[pos:m.start()]))
out.append(m.group(0))
pos = m.end()
out.append(fn(text[pos:]))
return "".join(out)
def fix_russian_quotes(text: str) -> str: def fix_russian_quotes(text: str) -> str:
"""Replace "..." with «...» for Russian text segments (heuristic).""" """Replace "..." with «...» for Russian text segments (heuristic)."""
def repl(m): def repl(m):
@@ -142,12 +174,14 @@ def dash_separator() -> str:
def preprocess_markdown(text: str, emdash_to_hyphen: bool | None = None) -> str: def preprocess_markdown(text: str, emdash_to_hyphen: bool | None = None) -> str:
if emdash_to_hyphen is not None: if emdash_to_hyphen is not None:
set_emdash_to_hyphen(emdash_to_hyphen) set_emdash_to_hyphen(emdash_to_hyphen)
text = fix_russian_quotes(text)
if _EMDASH_TO_HYPHEN: def _apply(chunk: str) -> str:
# Keep ASCII hyphens; flatten any em dashes from the source chunk = fix_russian_quotes(chunk)
text = replace_emdash_with_hyphen(text) if _EMDASH_TO_HYPHEN:
else: return replace_emdash_with_hyphen(chunk)
text = fix_dashes(text) return fix_dashes(chunk)
text = _map_outside_diagram_fences(text, _apply)
text = separate_biblio_lines(text) text = separate_biblio_lines(text)
return text return text
+14 -5
View File
@@ -67,14 +67,23 @@ class Caption(Renderable):
layout_state.max_width layout_state.max_width
).calculate_height() ).calculate_height()
if self._before and ((height_data.lines + 2 - 1) * height_data.line_spacing + 1) * height_data.line_height \ # Table/listing captions use keep_with_next. pageBreakBefore + keepWithNext
> layout_state.remaining_page_height: # on a paragraph that Word already wraps to a new page produces a blank
self._docx_paragraph.paragraph_format.page_break_before = True # page that the layout tracker / debug overlay never sees.
leftover = 0
need_new_page = (
self._before
and layout_state.current_page_height > 0
and ((height_data.lines + 2 - 1) * height_data.line_spacing + 1) * height_data.line_height
> layout_state.remaining_page_height
)
if need_new_page:
leftover = layout_state.remaining_page_height
height_data = ParagraphSizer( height_data = ParagraphSizer(
self._docx_paragraph, self._docx_paragraph,
None, None,
layout_state.max_width layout_state.max_width
).calculate_height() ).calculate_height()
yield RenderedInfo(self._docx_paragraph, height_data.full + (layout_state.remaining_page_height self._docx_paragraph.paragraph_format.page_break_before = False
if self._docx_paragraph.paragraph_format.page_break_before else 0)) yield RenderedInfo(self._docx_paragraph, height_data.full + leftover)
+1 -9
View File
@@ -123,21 +123,13 @@ class Listing(Renderable, RequiresNumbering):
def _emit_page_break_and_optional_caption( def _emit_page_break_and_optional_caption(
self, layout_state: LayoutState self, layout_state: LayoutState
) -> Generator[RenderedInfo, None, None]: ) -> Generator[RenderedInfo, None, None]:
mode = self._continuation_mode
if mode == "legacy":
continuation_paragraph = self._make_continuation_paragraph()
continuation_paragraph.page_break_before = True
info = next(continuation_paragraph.render(None, copy(layout_state)))
layout_state.add_height(info.height)
yield info
return
page_break_info = next(PageBreak(self._parent).render(None, layout_state)) page_break_info = next(PageBreak(self._parent).render(None, layout_state))
layout_state.add_height(page_break_info.height) layout_state.add_height(page_break_info.height)
yield page_break_info yield page_break_info
continuation_paragraph = self._make_continuation_paragraph() continuation_paragraph = self._make_continuation_paragraph()
continuation_paragraph._docx_paragraph.paragraph_format.keep_with_next = True continuation_paragraph._docx_paragraph.paragraph_format.keep_with_next = True
continuation_paragraph._docx_paragraph.paragraph_format.page_break_before = False
info = next(continuation_paragraph.render(None, copy(layout_state))) info = next(continuation_paragraph.render(None, copy(layout_state)))
layout_state.add_height(info.height) layout_state.add_height(info.height)
yield info yield info
+3 -11
View File
@@ -154,23 +154,15 @@ class Table(Renderable, RequiresNumbering):
self, layout_state: LayoutState self, layout_state: LayoutState
) -> Generator[RenderedInfo, None, None]: ) -> Generator[RenderedInfo, None, None]:
"""For legacy/caption only: break page and insert «Продолжение Таблицы N».""" """For legacy/caption only: break page and insert «Продолжение Таблицы N»."""
mode = self._continuation_mode
if mode == "legacy":
continuation_paragraph = self._make_continuation_paragraph()
continuation_paragraph.page_break_before = True
info = next(continuation_paragraph.render(None, copy(layout_state)))
layout_state.add_height(info.height)
yield info
return
# caption
page_break_info = next(PageBreak(self._parent).render(None, layout_state)) page_break_info = next(PageBreak(self._parent).render(None, layout_state))
layout_state.add_height(page_break_info.height) layout_state.add_height(page_break_info.height)
yield page_break_info yield page_break_info
# Never page_break_before on «Название таблицы»: keep_with_next +
# pageBreakBefore makes a blank page that debug does not count.
continuation_paragraph = self._make_continuation_paragraph() continuation_paragraph = self._make_continuation_paragraph()
continuation_paragraph._docx_paragraph.paragraph_format.keep_with_next = True continuation_paragraph._docx_paragraph.paragraph_format.keep_with_next = True
continuation_paragraph._docx_paragraph.paragraph_format.page_break_before = False
info = next(continuation_paragraph.render(None, copy(layout_state))) info = next(continuation_paragraph.render(None, copy(layout_state)))
layout_state.add_height(info.height) layout_state.add_height(info.height)
yield info yield info
+2 -2
View File
@@ -7,7 +7,6 @@ from docx.enum.text import WD_TAB_LEADER, WD_TAB_ALIGNMENT, WD_PARAGRAPH_ALIGNME
from docx.shared import Parented from docx.shared import Parented
from . import Paragraph from . import Paragraph
from .page_break import PageBreak
from .renderable import Renderable from .renderable import Renderable
from ..layout_tracker import LayoutState from ..layout_tracker import LayoutState
from ..rendered_info import RenderedInfo from ..rendered_info import RenderedInfo
@@ -142,4 +141,5 @@ class ToC(Renderable):
for rendered_info in self._paragraph.render(previous_rendered, copy(layout_state)): for rendered_info in self._paragraph.render(previous_rendered, copy(layout_state)):
yield RenderedInfo(rendered_info.docx_element, 0) yield RenderedInfo(rendered_info.docx_element, 0)
yield from PageBreak(self._parent).render(None, copy(layout_state)) # Page break after TOC is a section break in Renderer (body + PAGE).
# An extra w:br here + NEW_PAGE section = blank portrait page.
+11 -5
View File
@@ -95,12 +95,11 @@ class Renderer:
self._landscape_depth += 1 self._landscape_depth += 1
return return
self._flush_to_new_screen() self._flush_to_new_screen()
if self._after_toc: if not self._body_section_started:
self._ensure_body_section_with_page_numbers() # Do not add_section(portrait) here. A portrait NEW_PAGE plus the
elif not self._body_section_started: # landscape NEW_PAGE below is two breaks: text → empty portrait → album.
# No TOC — still need page numbers on subsequent sections.
self._body_section_started = True self._body_section_started = True
apply_centered_page_footer(self._document.sections[0]) apply_centered_page_footer(self._document.sections[-1])
section = self._document.add_section(WD_SECTION.NEW_PAGE) section = self._document.add_section(WD_SECTION.NEW_PAGE)
apply_section_geometry(section, landscape=True) apply_section_geometry(section, landscape=True)
apply_centered_page_footer(section) apply_centered_page_footer(section)
@@ -366,6 +365,13 @@ class Renderer:
extra = self._after_current.pop(0) extra = self._after_current.pop(0)
self.render(extra) self.render(extra)
if isinstance(renderable, ToC):
# One NEXT_PAGE section after the TOC field — not a w:br plus a section.
self._ensure_body_section_with_page_numbers()
state = self._layout_tracker.current_state
if state.current_page_height > 0:
self._layout_tracker.new_page()
if deferred_listing is not None: if deferred_listing is not None:
self.render(deferred_listing) self.render(deferred_listing)
+80
View File
@@ -156,6 +156,86 @@ def test_landscape_places_figure_inside_landscape_section(tmp_path, monkeypatch)
assert image_section == 1 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): def test_landscape_listing_deferred_after_section(tmp_path, monkeypatch):
"""+landscape +listing → listing must not sit in the landscape section.""" """+landscape +listing → listing must not sit in the landscape section."""
from unittest.mock import patch from unittest.mock import patch
+43
View File
@@ -190,6 +190,27 @@ def test_preprocess_quotes():
assert "«тест»" in preprocess_markdown('Он сказал "тест" вслух.') 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(): def test_formula_refs():
assert find_formula_refs("см. @Формула:eq1 и далее") == {"eq1"} assert find_formula_refs("см. @Формула:eq1 и далее") == {"eq1"}
@@ -466,6 +487,28 @@ def test_appendix_index_auto_inserted():
assert out2 == [sec, manual, a, b] 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(): def test_caption_table_keep_with_next_and_toc_styles():
import docx import docx
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
+23 -1
View File
@@ -4,7 +4,13 @@ import os
import sys import sys
from md2gost.dnd import first_markdown, normalize_drop_paths, parse_tkdnd_files 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 from md2gost.__main__ import build_parser, request_from_args
@@ -20,6 +26,22 @@ def test_default_output_path():
assert path.endswith("report.docx") 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(): def test_parse_tkdnd_files():
raw = r"{C:\My Files\a.md} C:\tmp\b.md" 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"] assert parse_tkdnd_files(raw) == [r"C:\My Files\a.md", r"C:\tmp\b.md"]