v0.5.3
- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
+89
-148
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -11,12 +10,28 @@ 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,
|
||||
is_contents_heading,
|
||||
parse_image_caption,
|
||||
reference_full_name,
|
||||
render_inline,
|
||||
walk_blocks,
|
||||
)
|
||||
|
||||
from .escape import escape_text, escape_verbatim_for_listing, latex_label
|
||||
|
||||
_REF_PREFIXES = {
|
||||
"рисунок": "fig",
|
||||
"таблица": "tab",
|
||||
"листинг": "lst",
|
||||
"формула": "eq",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmitState:
|
||||
class EmitState(BlockVisitor):
|
||||
lines: list[str] = field(default_factory=list)
|
||||
pending_caption: tuple[str | None, str | None, bool] | None = None # id, text, +listing
|
||||
biblio: list[tuple[str, str]] = field(default_factory=list)
|
||||
@@ -27,57 +42,75 @@ class EmitState:
|
||||
def w(self, s: str = "") -> None:
|
||||
self.lines.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_longtable(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_text(children).strip()
|
||||
if text:
|
||||
self.w(text)
|
||||
self.w()
|
||||
|
||||
def on_quote(self, element) -> None:
|
||||
self.w(r"\begin{quote}")
|
||||
for child in element.children:
|
||||
if type(child).__name__ == "Paragraph":
|
||||
self.w(_inline_text(child.children))
|
||||
self.w(r"\end{quote}")
|
||||
self.w()
|
||||
|
||||
def on_other(self, element) -> None:
|
||||
self.w(_inline_text(element.children))
|
||||
self.w()
|
||||
|
||||
|
||||
class _LatexInline(InlineRenderer):
|
||||
def escape(self, text: str) -> str:
|
||||
return escape_text(text)
|
||||
|
||||
def code_span(self, node) -> str:
|
||||
return f"\\texttt{{{escape_text(node.children)}}}"
|
||||
|
||||
def emphasis(self, inner: str) -> str:
|
||||
return f"\\textit{{{inner}}}"
|
||||
|
||||
def strong_emphasis(self, inner: str) -> str:
|
||||
return f"\\textbf{{{inner}}}"
|
||||
|
||||
def link(self, node, inner: str) -> str:
|
||||
return f"\\href{{{escape_text(node.dest)}}}{{{inner}}}"
|
||||
|
||||
def reference(self, node) -> str:
|
||||
prefix = _REF_PREFIXES.get(node.type.lower(), "obj")
|
||||
return f"{reference_full_name(node.type)}~\\ref{{{latex_label(node.name, prefix)}}}"
|
||||
|
||||
def inline_equation(self, node) -> str:
|
||||
return f"${node.latex_equation}$"
|
||||
|
||||
def line_break(self) -> str:
|
||||
return " "
|
||||
|
||||
|
||||
_INLINE = _LatexInline()
|
||||
|
||||
|
||||
def _inline_text(children) -> 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"\\texttt{{{escape_text(child.children)}}}")
|
||||
elif t == "Emphasis":
|
||||
parts.append(f"\\textit{{{_inline_text(child.children)}}}")
|
||||
elif t == "StrongEmphasis":
|
||||
parts.append(f"\\textbf{{{_inline_text(child.children)}}}")
|
||||
elif t == "Link":
|
||||
parts.append(
|
||||
f"\\href{{{escape_text(child.dest)}}}{{{_inline_text(child.children)}}}"
|
||||
)
|
||||
elif t == "Reference":
|
||||
# @Рисунок:id → \ref{fig:id} with word
|
||||
cat = {
|
||||
"рисунок": "рис.",
|
||||
"таблица": "табл.",
|
||||
"листинг": "лист.",
|
||||
"формула": "форм.",
|
||||
}.get(child.type.lower(), child.type)
|
||||
# Prefer Russian full names as in ГОСТ
|
||||
full = {
|
||||
"рисунок": "Рисунок",
|
||||
"таблица": "Таблица",
|
||||
"листинг": "Листинг",
|
||||
"формула": "формула",
|
||||
}.get(child.type.lower(), child.type)
|
||||
prefix = {
|
||||
"рисунок": "fig",
|
||||
"таблица": "tab",
|
||||
"листинг": "lst",
|
||||
"формула": "eq",
|
||||
}.get(child.type.lower(), "obj")
|
||||
parts.append(f"{full}~\\ref{{{latex_label(child.name, prefix)}}}")
|
||||
elif t == "InlineEquation":
|
||||
parts.append(f"${child.latex_equation}$")
|
||||
elif t == "Image":
|
||||
# handled at block level usually
|
||||
parts.append("")
|
||||
elif t == "LineBreak":
|
||||
parts.append(" ")
|
||||
elif hasattr(child, "children"):
|
||||
parts.append(_inline_text(child.children))
|
||||
else:
|
||||
parts.append(escape_text(str(child)))
|
||||
return "".join(parts)
|
||||
return render_inline(children, _INLINE)
|
||||
|
||||
|
||||
def _cell_plain(cell) -> str:
|
||||
@@ -140,7 +173,7 @@ def _emit_heading(st: EmitState, heading) -> None:
|
||||
level = heading.level
|
||||
numbered = getattr(heading, "numbered", True)
|
||||
|
||||
if upper == "СОДЕРЖАНИЕ" or upper.startswith("СОДЕРЖАНИЕ"):
|
||||
if is_contents_heading(text):
|
||||
st.skip_toc_heading = True
|
||||
return # TOC already in tocpage.tex
|
||||
|
||||
@@ -158,48 +191,9 @@ def _emit_heading(st: EmitState, heading) -> None:
|
||||
st.w()
|
||||
|
||||
|
||||
def _emit_paragraph(st: EmitState, para) -> None:
|
||||
# detect bibliography lines [n]: ...
|
||||
raw = _inline_text(para.children).strip()
|
||||
# Also scan children for plain biblio from original — marko may wrap
|
||||
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
|
||||
|
||||
# inline images in paragraph
|
||||
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)
|
||||
# remaining text
|
||||
others = [c for c in para.children if type(c).__name__ != "Image"]
|
||||
t = _inline_text(others).strip()
|
||||
if t:
|
||||
st.w(t)
|
||||
st.w()
|
||||
return
|
||||
|
||||
if raw:
|
||||
st.w(raw)
|
||||
st.w()
|
||||
|
||||
|
||||
def _emit_image(st: EmitState, img) -> None:
|
||||
dest = img.dest
|
||||
title = getattr(img, "title", None) or ""
|
||||
# title may be "%id Caption"
|
||||
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))
|
||||
|
||||
# copy image into Images/
|
||||
src = dest
|
||||
@@ -230,10 +224,7 @@ def _emit_image(st: EmitState, img) -> None:
|
||||
|
||||
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:
|
||||
@@ -277,10 +268,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]
|
||||
st.w(r"\begin{equation}")
|
||||
if cap_id:
|
||||
st.w(rf"\label{{{latex_label(cap_id, 'eq')}}}")
|
||||
@@ -294,54 +282,7 @@ def emit_document(md_text: str, *, work_dir: Path, images_dir: Path) -> str:
|
||||
pre = preprocess_markdown(md_text, emdash_to_hyphen=False)
|
||||
doc = markdown.parse(pre)
|
||||
|
||||
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_longtable(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 # tocpage.tex
|
||||
if name == "Quote":
|
||||
st.w(r"\begin{quote}")
|
||||
for ch in el.children:
|
||||
if type(ch).__name__ == "Paragraph":
|
||||
st.w(_inline_text(ch.children))
|
||||
st.w(r"\end{quote}")
|
||||
st.w()
|
||||
continue
|
||||
# fallback
|
||||
if hasattr(el, "children"):
|
||||
st.w(_inline_text(el.children))
|
||||
st.w()
|
||||
walk_blocks(doc.children, st)
|
||||
|
||||
if st.biblio:
|
||||
st.w(r"\begin{thebibliography}{99}")
|
||||
|
||||
Reference in New Issue
Block a user