v0.5.3
- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""Backend-agnostic traversal of the top level of a md2gost markdown document."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
#: `(unique_name, text, with_listing)` captured from a `%id Подпись` line.
|
||||
PendingCaption = tuple[str | None, str | None, bool]
|
||||
|
||||
_BIBLIO_RE = re.compile(r"^\[([^\]]+)\]:\s*(.+)$")
|
||||
|
||||
_CONTENTS_TITLE = "СОДЕРЖАНИЕ"
|
||||
|
||||
|
||||
def caption_tuple(element) -> PendingCaption:
|
||||
return (element.unique_name, element.text, getattr(element, "with_listing", False))
|
||||
|
||||
|
||||
def match_bibliography(children) -> tuple[str, str] | None:
|
||||
"""Detect a `[key]: описание` source line, ignoring formatted spans."""
|
||||
probe = ""
|
||||
for child in children or []:
|
||||
if type(child).__name__ in ("RawText", "Literal"):
|
||||
probe += child.children or ""
|
||||
m = _BIBLIO_RE.match(probe.strip())
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1), m.group(2).strip()
|
||||
|
||||
|
||||
def is_contents_heading(text: str) -> bool:
|
||||
upper = text.upper()
|
||||
return upper == _CONTENTS_TITLE or upper.startswith(_CONTENTS_TITLE)
|
||||
|
||||
|
||||
def parse_image_caption(image, default_id: str | None) -> tuple[str | None, str]:
|
||||
"""Split an image title of the form `%id Подпись` into id and caption text."""
|
||||
title = getattr(image, "title", None) or ""
|
||||
if not title.startswith("%"):
|
||||
return default_id, title
|
||||
parts = title[1:].split(None, 1)
|
||||
return parts[0], parts[1] if len(parts) > 1 else ""
|
||||
|
||||
|
||||
class BlockVisitor(ABC):
|
||||
"""
|
||||
Emit hooks for :func:`walk_blocks` and :func:`walk_paragraph`.
|
||||
|
||||
Implementations own `pending_caption` and `biblio` because the concrete
|
||||
emitters need to read them outside the walk as well.
|
||||
"""
|
||||
|
||||
pending_caption: PendingCaption | None
|
||||
biblio: list[tuple[str, str]]
|
||||
|
||||
def take_caption(self) -> PendingCaption:
|
||||
pending = self.pending_caption
|
||||
if pending is None:
|
||||
return (None, None, False)
|
||||
self.pending_caption = None
|
||||
return pending
|
||||
|
||||
@abstractmethod
|
||||
def on_heading(self, element) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_table(self, element, caption_id: str | None, caption_text: str | None) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_code(self, element, lang: str) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_list(self, element) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_equation(self, element) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_quote(self, element) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_other(self, element) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_image(self, image) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_body_text(self, children) -> None:
|
||||
...
|
||||
|
||||
def on_biblio(self, key: str, text: str) -> None:
|
||||
self.biblio.append((key, text))
|
||||
|
||||
def on_paragraph(self, element) -> None:
|
||||
walk_paragraph(element, self)
|
||||
|
||||
|
||||
def walk_paragraph(paragraph, visitor: BlockVisitor) -> None:
|
||||
"""Route one paragraph to the bibliography, image or plain-text hook."""
|
||||
biblio = match_bibliography(paragraph.children)
|
||||
if biblio is not None:
|
||||
visitor.on_biblio(*biblio)
|
||||
return
|
||||
|
||||
children = paragraph.children or []
|
||||
images = [c for c in children if type(c).__name__ == "Image"]
|
||||
if images:
|
||||
for image in images:
|
||||
visitor.on_image(image)
|
||||
visitor.on_body_text([c for c in children if type(c).__name__ != "Image"])
|
||||
return
|
||||
|
||||
visitor.on_body_text(children)
|
||||
|
||||
|
||||
def walk_blocks(children, visitor: BlockVisitor) -> None:
|
||||
"""Dispatch every top-level element of a parsed document to `visitor`."""
|
||||
for element in children:
|
||||
name = type(element).__name__
|
||||
if name in ("BlankLine", "TOC"):
|
||||
continue
|
||||
if name == "Caption":
|
||||
visitor.pending_caption = caption_tuple(element)
|
||||
elif name == "Heading":
|
||||
visitor.on_heading(element)
|
||||
elif name == "Paragraph":
|
||||
visitor.on_paragraph(element)
|
||||
elif name == "Table":
|
||||
caption_id, caption_text, _ = visitor.take_caption()
|
||||
visitor.on_table(element, caption_id, caption_text)
|
||||
elif name in ("FencedCode", "CodeBlock"):
|
||||
visitor.on_code(element, getattr(element, "lang", "") or "")
|
||||
elif name == "List":
|
||||
visitor.on_list(element)
|
||||
elif name == "Equation":
|
||||
visitor.on_equation(element)
|
||||
elif name == "Quote":
|
||||
visitor.on_quote(element)
|
||||
elif hasattr(element, "children"):
|
||||
visitor.on_other(element)
|
||||
Reference in New Issue
Block a user