107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""Обработка рисунков по ГОСТ.
|
|
|
|
Синтаксис в Markdown — вики-ссылка отдельным абзацем, подпись — абзац
|
|
под ней (см. parser._preprocess_wiki_images):
|
|
|
|
![[images/antenna.png]]
|
|
|
|
Рисунок 1 —— Антенна
|
|
|
|
Правила:
|
|
- подпись распознаётся по шаблону «Рисунок N <тире> Название»; номер
|
|
отбрасывается — LaTeX нумерует рисунки сам (последовательность
|
|
гарантируется);
|
|
- абзац с картинкой и посторонним текстом не является рисунком: инлайн-
|
|
картинка внутри текста пропускается с предупреждением;
|
|
- ширина задаётся суффиксом ``|N`` или ``|N%`` (доля от textwidth),
|
|
по умолчанию 80 %;
|
|
- размещение ``[ht]``, а не ``[H]``: жёсткая привязка к месту оставляла до
|
|
половины страницы пустой, когда рисунок не помещался в её остаток. ГОСТ
|
|
допускает перенос иллюстрации на следующую страницу.
|
|
|
|
Паттерн чистый: match() смотрит на срез токенов, render_figure() строит
|
|
LaTeX из того же среза; renderer.pos двигает рендерер.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from urllib.parse import unquote
|
|
|
|
from markdown_it.token import Token
|
|
|
|
from md2gost.handlers.inline import escape_latex
|
|
from md2gost.registry import pattern
|
|
|
|
DEFAULT_WIDTH = 0.8
|
|
_CAPTION_RE = re.compile(r"^\s*Рисунок\s+\d+\s*[—–-]+\s*(.+?)\s*$")
|
|
_WIDTH_RE = re.compile(r"^\d{1,3}%?$")
|
|
|
|
|
|
def _parse_image_src(raw: str) -> tuple[str, str]:
|
|
"""Разобрать 'путь|ширина' -> (путь, LaTeX-опция ширины)."""
|
|
path, sep, width = raw.rpartition("|")
|
|
if sep and _WIDTH_RE.match(width.strip()):
|
|
fraction = int(width.strip().rstrip("%")) / 100
|
|
return path, f"{fraction:g}\\textwidth"
|
|
return raw, f"{DEFAULT_WIDTH:g}\\textwidth"
|
|
|
|
|
|
def _single_image_paragraph(tokens: list[Token], pos: int) -> Token | None:
|
|
"""Inline-токен, если абзац на pos состоит ровно из одной картинки."""
|
|
if pos + 2 >= len(tokens):
|
|
return None
|
|
if tokens[pos].type != "paragraph_open" or tokens[pos + 1].type != "inline":
|
|
return None
|
|
children = tokens[pos + 1].children or []
|
|
images = [t for t in children if t.type == "image"]
|
|
if len(images) != 1:
|
|
return None
|
|
if any(
|
|
t.type not in ("image", "text", "softbreak") or t.content.strip()
|
|
for t in children
|
|
):
|
|
return None
|
|
return images[0]
|
|
|
|
|
|
def match_figure(tokens: list[Token], pos: int) -> int | None:
|
|
"""Абзац-картинка (+2 за подпись, если следующий абзац её содержит)."""
|
|
img_tok = _single_image_paragraph(tokens, pos)
|
|
if img_tok is None:
|
|
return None
|
|
|
|
caption_pos = pos + 3
|
|
if (
|
|
caption_pos + 2 < len(tokens)
|
|
and tokens[caption_pos].type == "paragraph_open"
|
|
and tokens[caption_pos + 1].type == "inline"
|
|
and _CAPTION_RE.match(tokens[caption_pos + 1].content)
|
|
):
|
|
return 5
|
|
return 3
|
|
|
|
|
|
@pattern(match_figure)
|
|
def figure(tokens: list[Token]) -> str:
|
|
img_tok = _single_image_paragraph(tokens, 0)
|
|
assert img_tok is not None
|
|
raw_src = unquote(str(img_tok.attrGet("src") or ""))
|
|
path, width = _parse_image_src(raw_src)
|
|
|
|
caption = ""
|
|
if len(tokens) >= 5:
|
|
m = _CAPTION_RE.match(tokens[4].content)
|
|
if m:
|
|
caption = escape_latex(m.group(1))
|
|
|
|
lines = [
|
|
"\\begin{figure}[htb]",
|
|
" \\centering",
|
|
rf" \includegraphics[width={width}]{{\detokenize{{{path}}}}}",
|
|
]
|
|
if caption:
|
|
lines.append(f" \\caption{{{caption}}}")
|
|
lines.append("\\end{figure}")
|
|
return "\n".join(lines)
|