v0.5.3
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled

- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
Igor20264
2026-09-10 10:50:15 +03:00
parent 510f7e7adf
commit e6b58efeb3
95 changed files with 7165 additions and 8771 deletions
+102 -151
View File
@@ -11,13 +11,23 @@ from pathlib import Path
from md2gost.extended_markdown import markdown
from md2gost.diagram_renderer import DIAGRAM_LANGS
from md2gost.profiles import preprocess_markdown
from mdexport import (
BlockVisitor,
InlineRenderer,
caption_tuple,
is_contents_heading,
parse_image_caption,
reference_full_name,
render_inline,
walk_blocks,
)
from .escape import escape_text, xml_id
from .styles import FODT_NS, automatic_styles_xml, styles_xml
@dataclass
class EmitState:
class EmitState(BlockVisitor):
body: list[str] = field(default_factory=list)
pending_caption: tuple[str | None, str | None, bool] | None = None
biblio: list[tuple[str, str]] = field(default_factory=list)
@@ -31,55 +41,75 @@ class EmitState:
def w(self, s: str = "") -> None:
self.body.append(s)
def on_heading(self, element) -> None:
_emit_heading(self, element)
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
_emit_table(self, element, caption_id, caption_text)
def on_code(self, element, lang: str) -> None:
_emit_code(self, element, lang)
def on_list(self, element) -> None:
_emit_list(self, element)
def on_equation(self, element) -> None:
_emit_equation(self, element)
def on_image(self, image) -> None:
_emit_image(self, image)
def on_body_text(self, children) -> None:
text = _inline(children, self).strip()
if text:
self.w(_p("Standard", text))
def on_quote(self, element) -> None:
for child in element.children:
if type(child).__name__ == "Paragraph":
self.w(_p("Standard", _inline(child.children, self)))
def on_other(self, element) -> None:
self.on_body_text(element.children)
class _FodtInline(InlineRenderer):
def __init__(self, st: EmitState | None):
self.st = st
def escape(self, text: str) -> str:
return escape_text(text)
def code_span(self, node) -> str:
return f'<text:span text:style-name="Tcode">{escape_text(node.children)}</text:span>'
def emphasis(self, inner: str) -> str:
return f'<text:span text:style-name="Titalic">{inner}</text:span>'
def strong_emphasis(self, inner: str) -> str:
return f'<text:span text:style-name="Tbold">{inner}</text:span>'
def link(self, node, inner: str) -> str:
href = escape_text(node.dest)
return f'<text:a xlink:type="simple" xlink:href="{href}">{inner}</text:a>'
def reference(self, node) -> str:
key = f"{node.type.lower()}:{node.name}"
num = (self.st.labels.get(key) if self.st else None) or "?"
return escape_text(f"{reference_full_name(node.type)} {num}")
def inline_equation(self, node) -> str:
return (
f'<text:span text:style-name="Tcode">'
f"{escape_text(node.latex_equation)}</text:span>"
)
def line_break(self) -> str:
return "<text:line-break/>"
def _inline(children, st: EmitState | None = None) -> str:
parts: list[str] = []
for child in children or []:
t = type(child).__name__
if t in ("RawText", "Literal"):
parts.append(escape_text(getattr(child, "children", "") or ""))
elif t == "CodeSpan":
parts.append(
f'<text:span text:style-name="Tcode">{escape_text(child.children)}</text:span>'
)
elif t == "Emphasis":
parts.append(
f'<text:span text:style-name="Titalic">{_inline(child.children, st)}</text:span>'
)
elif t == "StrongEmphasis":
parts.append(
f'<text:span text:style-name="Tbold">{_inline(child.children, st)}</text:span>'
)
elif t == "Link":
href = escape_text(child.dest)
parts.append(
f'<text:a xlink:type="simple" xlink:href="{href}">'
f"{_inline(child.children, st)}</text:a>"
)
elif t == "Reference":
full = {
"рисунок": "Рисунок",
"таблица": "Таблица",
"листинг": "Листинг",
"формула": "формула",
}.get(child.type.lower(), child.type)
key = f"{child.type.lower()}:{child.name}"
num = (st.labels.get(key) if st else None) or "?"
parts.append(escape_text(f"{full} {num}"))
elif t == "InlineEquation":
parts.append(
f'<text:span text:style-name="Tcode">'
f"{escape_text(child.latex_equation)}</text:span>"
)
elif t == "Image":
parts.append("")
elif t == "LineBreak":
parts.append("<text:line-break/>")
elif hasattr(child, "children"):
parts.append(_inline(child.children, st))
else:
parts.append(escape_text(str(child)))
return "".join(parts)
return render_inline(children, _FodtInline(st))
def _plain_inline(children) -> str:
@@ -109,10 +139,9 @@ def _h(level: int, content: str) -> str:
def _emit_heading(st: EmitState, heading) -> None:
text = _inline(heading.children, st).strip()
plain = _plain_inline(heading.children).strip()
upper = plain.upper()
level = min(int(heading.level), 3)
if upper == "СОДЕРЖАНИЕ" or upper.startswith("СОДЕРЖАНИЕ"):
if is_contents_heading(plain):
st.w(_p("Centered", escape_text("СОДЕРЖАНИЕ")))
st.w(
'<text:table-of-content text:style-name="Standard" text:protected="false" '
@@ -132,32 +161,6 @@ def _emit_heading(st: EmitState, heading) -> None:
st.w(_h(level, text))
def _emit_paragraph(st: EmitState, para) -> None:
text_probe = ""
for ch in para.children or []:
if type(ch).__name__ in ("RawText", "Literal"):
text_probe += ch.children or ""
m = re.match(r"^\[([^\]]+)\]:\s*(.+)$", text_probe.strip())
if m:
st.biblio.append((m.group(1), m.group(2).strip()))
return
has_image = any(type(c).__name__ == "Image" for c in (para.children or []))
if has_image:
for c in para.children:
if type(c).__name__ == "Image":
_emit_image(st, c)
others = [c for c in para.children if type(c).__name__ != "Image"]
t = _inline(others, st).strip()
if t:
st.w(_p("Standard", t))
return
raw = _inline(para.children, st).strip()
if raw:
st.w(_p("Standard", raw))
def _mime_for(path: Path) -> str:
ext = path.suffix.lower()
return {
@@ -173,13 +176,7 @@ def _mime_for(path: Path) -> str:
def _emit_image(st: EmitState, img) -> None:
dest = img.dest
title = getattr(img, "title", None) or ""
caption_id = getattr(img, "unique_name", None)
caption_text = title
if title.startswith("%"):
parts = title[1:].split(None, 1)
caption_id = parts[0]
caption_text = parts[1] if len(parts) > 1 else ""
caption_id, caption_text = parse_image_caption(img, getattr(img, "unique_name", None))
if st.pending_caption and not caption_text:
caption_id = caption_id or st.pending_caption[0]
@@ -345,10 +342,7 @@ def _emit_table_row(
def _emit_code(st: EmitState, block, lang: str) -> None:
source = block.children[0].children if block.children else ""
cap_id, cap_text, _with_listing = (None, None, False)
if st.pending_caption:
cap_id, cap_text, _with_listing = st.pending_caption
st.pending_caption = None
cap_id, cap_text, _with_listing = st.take_caption()
lang_l = (lang or "").strip().lower()
if lang_l in DIAGRAM_LANGS:
@@ -402,10 +396,7 @@ def _emit_list(st: EmitState, lst) -> None:
def _emit_equation(st: EmitState, eq) -> None:
cap_id = None
if st.pending_caption:
cap_id = st.pending_caption[0]
st.pending_caption = None
cap_id = st.take_caption()[0]
latex = eq.latex_equation.strip()
bookmark = ""
if cap_id:
@@ -413,20 +404,16 @@ def _emit_equation(st: EmitState, eq) -> None:
st.w(_p("Centered", bookmark + f'<text:span text:style-name="Tcode">{escape_text(latex)}</text:span>'))
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState:
st = EmitState(work_dir=work_dir)
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen)
# Two-pass: first collect captions/labels roughly by walking once for numbers,
# then emit. Simple approach: single pass (refs before definition show "?").
# Better: first pass assign numbers, second emit.
doc = markdown.parse(pre)
# Pass 1 — assign numbers for captions attached to tables/images/listings
def _assign_numbers(st: EmitState, children) -> None:
"""
Number captioned objects up front so `@Тип:id` references resolve even when
they appear before the object they point at.
"""
pending = None
for el in doc.children:
for el in children:
name = type(el).__name__
if name == "Caption":
pending = (el.unique_name, el.text, getattr(el, "with_listing", False))
pending = caption_tuple(el)
continue
if name == "Table" and pending:
st.tab_n += 1
@@ -441,66 +428,30 @@ def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -
for c in el.children or []:
if type(c).__name__ == "Image":
st.fig_n += 1
cid = pending[0]
title = getattr(c, "title", "") or ""
if title.startswith("%"):
cid = title[1:].split(None, 1)[0]
cid = parse_image_caption(c, pending[0])[0]
st.labels[f"рисунок:{cid}"] = str(st.fig_n)
pending = None
elif name == "Equation" and pending:
pending = None
def emit_body(md_text: str, *, work_dir: Path, emdash_to_hyphen: bool = False) -> EmitState:
st = EmitState(work_dir=work_dir)
pre = preprocess_markdown(md_text, emdash_to_hyphen=emdash_to_hyphen)
# Two-pass: first collect captions/labels roughly by walking once for numbers,
# then emit. Simple approach: single pass (refs before definition show "?").
# Better: first pass assign numbers, second emit.
doc = markdown.parse(pre)
_assign_numbers(st, doc.children)
# reset counters for emit
st.fig_n = 0
st.tab_n = 0
st.lst_n = 0
st.pending_caption = None
for el in doc.children:
name = type(el).__name__
if name == "BlankLine":
continue
if name == "Caption":
st.pending_caption = (
el.unique_name,
el.text,
getattr(el, "with_listing", False),
)
continue
if name == "Heading":
_emit_heading(st, el)
continue
if name == "Paragraph":
_emit_paragraph(st, el)
continue
if name == "Table":
cid = ct = None
if st.pending_caption:
cid, ct, _ = st.pending_caption
st.pending_caption = None
_emit_table(st, el, cid, ct)
continue
if name in ("FencedCode", "CodeBlock"):
lang = getattr(el, "lang", "") or ""
_emit_code(st, el, lang)
continue
if name == "List":
_emit_list(st, el)
continue
if name == "Equation":
_emit_equation(st, el)
continue
if name == "TOC":
continue
if name == "Quote":
for ch in el.children:
if type(ch).__name__ == "Paragraph":
st.w(_p("Standard", _inline(ch.children, st)))
continue
if hasattr(el, "children"):
t = _inline(el.children, st).strip()
if t:
st.w(_p("Standard", t))
walk_blocks(doc.children, st)
if st.biblio:
st.w(_h(1, escape_text("СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ")))