- add Local Render Mermaid - add page-starе для указания смещения страниц - add Гиперссылки в документе на списки литературы
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user