BigUpdate 0.5.0
Python application / build (push) Has been cancelled

- add Local Render Mermaid
- add page-starе для указания смещения страниц
- add Гиперссылки в документе на списки литературы
This commit is contained in:
Igor20264
2026-09-07 09:54:33 +03:00
parent ac0aa33399
commit 1a5b35eb54
29 changed files with 4118 additions and 69 deletions
+46
View File
@@ -0,0 +1,46 @@
"""Bibliography citation hyperlinks."""
from docx import Document
from docx.oxml.ns import qn
from md2gost.bibliography import biblio_bookmark_name
from md2gost.bibliography_renderable import Bibliography
from md2gost.bibliography import BiblioEntry
from md2gost.renderable.paragraph import Paragraph
def test_biblio_bookmark_name():
assert biblio_bookmark_name("1") == "biblio_1"
assert biblio_bookmark_name("1.5") == "biblio_1_5"
def test_entry_has_bookmark():
doc = Document()
e = BiblioEntry(key="1", text="Иванов И. И. Книга.")
p = Bibliography._make_entry(doc._body, e)
xml = p._docx_paragraph._p.xml
assert 'w:name="biblio_1"' in xml
assert "bookmarkStart" in xml
assert "bookmarkEnd" in xml
def test_cite_becomes_internal_hyperlink():
doc = Document()
p = Paragraph(doc._body)
p.add_run_with_citations("See [1] and [2, 3].")
xml = p._docx_paragraph._p.xml
assert 'w:anchor="biblio_1"' in xml
assert 'w:anchor="biblio_2"' in xml
assert 'w:anchor="biblio_3"' in xml
assert ">1</w:t>" in xml
assert ">2</w:t>" in xml
assert ">3</w:t>" in xml
def test_plain_text_without_cites_unchanged():
doc = Document()
p = Paragraph(doc._body)
p.add_run_with_citations("Просто текст без ссылок.")
xml = p._docx_paragraph._p.xml
assert "w:hyperlink" not in xml
assert "Просто текст без ссылок." in p._docx_paragraph.text
+4
View File
@@ -356,6 +356,9 @@ def test_render_mermaid_skips_jar(tmp_path):
calls["jar"] += 1
return False
def fake_local(*args, **kwargs):
return False
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
calls["kroki"].append((diagram_type, fmt, base_url))
if fmt == "png":
@@ -366,6 +369,7 @@ def test_render_mermaid_skips_jar(tmp_path):
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_local), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path))
assert calls["jar"] == 0
+153
View File
@@ -0,0 +1,153 @@
"""Tests for local Mermaid rendering chain."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from md2gost.diagram_renderer import configure_diagrams, render_diagram
from md2gost.mermaid_renderer import (
CACHE_KEY_PREFIX,
render_mermaid_local,
write_mermaid_outputs,
)
def test_cache_key_prefix_stable():
assert CACHE_KEY_PREFIX.startswith("mmd-local")
def test_prepare_mermaid_source_injects_init():
from md2gost.mermaid_renderer import prepare_mermaid_source
out = prepare_mermaid_source("flowchart LR\nA-->B", html_labels=True)
assert out.startswith("%%{init")
assert "htmlLabels': true" in out
assert "flowchart LR" in out
assert prepare_mermaid_source(out) == out
qj = prepare_mermaid_source("flowchart LR\nA-->B", html_labels=False)
assert "htmlLabels': false" in qj
def test_quickjs_cyrillic_no_foreign_object():
"""QuickJS uses htmlLabels:false → SVG text; resvg can paint Cyrillic."""
from md2gost.mermaid_renderer import _render_quickjs
got = _render_quickjs('flowchart LR\nA["Акционеры"] --> B["Тест"]', scale=1)
assert got is not None
svg, png = got
assert "Акционеры" in svg
assert "foreignObject" not in svg
assert "<text" in svg
assert png.startswith(b"\x89PNG")
assert len(png) > 2000
def test_write_mermaid_outputs_quickjs(tmp_path, monkeypatch):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 40
svg = '<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>'
def fake_local(source, *, scale=1.0):
assert "flowchart" in source
return svg, png, "quickjs"
monkeypatch.setattr(
"md2gost.mermaid_renderer.render_mermaid_local", fake_local,
)
out_png = tmp_path / "a.png"
out_svg = tmp_path / "a.svg"
assert write_mermaid_outputs("flowchart LR\nA-->B", out_png, out_svg, scale=2)
assert out_png.read_bytes().startswith(b"\x89PNG")
assert "<svg" in out_svg.read_text(encoding="utf-8")
def test_render_mermaid_local_order_browser_then_quickjs(monkeypatch):
calls = []
def fake_browser(source, *, scale=1.0):
calls.append("browser")
return None
def fake_qj(source, *, scale=1.0):
calls.append("quickjs")
return (
'<svg xmlns="http://www.w3.org/2000/svg"/>',
b"\x89PNG\r\n\x1a\n" + b"\x00" * 20,
)
monkeypatch.setattr("md2gost.mermaid_renderer._render_browser", fake_browser)
monkeypatch.setattr("md2gost.mermaid_renderer._render_quickjs", fake_qj)
got = render_mermaid_local("flowchart LR\nA-->B")
assert got is not None
assert got[2] == "quickjs"
assert calls == ["browser", "quickjs"]
def test_render_diagram_uses_local_before_kroki(tmp_path):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 20
svg = b'<svg xmlns="http://www.w3.org/2000/svg"/>'
kroki_calls = []
def fake_write(source, out_png, out_svg, *, scale=1.0):
Path(out_png).write_bytes(png)
if out_svg is not None:
Path(out_svg).write_bytes(svg)
return True
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
kroki_calls.append((diagram_type, fmt, base_url))
return False
configure_diagrams(fallback="remote", cache_dir=str(tmp_path), diagram_scale=2)
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_write), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram(
"mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path),
)
assert kroki_calls == []
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
assert result.pixel_scale == 2.0
def test_render_diagram_falls_to_kroki_if_local_fails(tmp_path):
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 20
kroki_calls = []
def fake_write(*args, **kwargs):
return False
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
kroki_calls.append((diagram_type, fmt, base_url))
if fmt == "png":
out_path.write_bytes(png)
else:
out_path.write_bytes(b"<svg xmlns='http://www.w3.org/2000/svg'/>")
return True
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
patch("md2gost.mermaid_renderer.write_mermaid_outputs", side_effect=fake_write), \
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
result = render_diagram("mermaid", "flowchart LR\nA-->B", cache_dir=str(tmp_path))
assert any(t == "mermaid" and f == "png" for t, f, _ in kroki_calls)
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
assert result.pixel_scale == 1.0
@pytest.mark.skipif(
__import__("importlib").util.find_spec("mermaidx") is None,
reason="mermaidx not installed",
)
def test_quickjs_real_render():
from md2gost.mermaid_renderer import _render_quickjs
got = _render_quickjs("flowchart LR\nA-->B", scale=1)
assert got is not None
svg, png = got
assert "<svg" in svg.lower()
assert png.startswith(b"\x89PNG")
+35
View File
@@ -0,0 +1,35 @@
"""Page number start / offset helpers."""
from docx import Document
from docx.oxml.ns import qn
from md2gost.page_geometry import apply_page_number_start, set_section_page_start
def test_set_section_page_start():
doc = Document()
section = doc.sections[0]
set_section_page_start(section, 3)
pg = section._sectPr.find(qn("w:pgNumType"))
assert pg is not None
assert pg.get(qn("w:start")) == "3"
def test_apply_page_number_start_skips_front():
doc = Document()
doc.add_section() # front
doc.add_section() # body
apply_page_number_start(doc, 2, front_sections=1)
front = doc.sections[0]._sectPr.find(qn("w:pgNumType"))
body = doc.sections[1]._sectPr.find(qn("w:pgNumType"))
assert front is None or front.get(qn("w:start")) is None
assert body is not None
assert body.get(qn("w:start")) == "2"
def test_apply_page_number_start_none_strips():
doc = Document()
set_section_page_start(doc.sections[0], 5)
apply_page_number_start(doc, None)
pg = doc.sections[0]._sectPr.find(qn("w:pgNumType"))
assert pg is None or pg.get(qn("w:start")) is None