201 lines
7.8 KiB
Python
201 lines
7.8 KiB
Python
"""Обработка таблиц по ГОСТ.
|
|
|
|
Подпись оформляется абзацем НАД таблицей (в отличие от рисунка, у которого
|
|
подпись снизу):
|
|
|
|
Таблица 1 —— Сравнение скорости
|
|
|
|
| № запуска | Python (сек) | C++ (сек) |
|
|
| :--- | :---: | :---: |
|
|
| 1 | 0.027852 | 0.000285 |
|
|
|
|
Правила:
|
|
- подпись распознаётся по шаблону «Таблица N <тире> Название»; номер
|
|
отбрасывается — LaTeX нумерует таблицы сам (последовательность
|
|
гарантируется);
|
|
- выравнивание колонок берётся из строки-разделителя GFM: ``:---`` — по
|
|
левому краю, ``:---:`` — по центру, ``---:`` — по правому; по умолчанию
|
|
по левому краю;
|
|
- таблица без подписи рендерится голым ``tabular``, как и раньше.
|
|
|
|
Паттерн чистый: match_table() смотрит на срез токенов, captioned_table()
|
|
строит LaTeX из того же среза; renderer.pos двигает рендерер.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from markdown_it.token import Token
|
|
|
|
from md2gost.handlers.inline import escape_latex, render_inline
|
|
from md2gost.registry import block, pattern
|
|
|
|
_CAPTION_RE = re.compile(r"^\s*Таблица\s+\d+\s*[—–-]+\s*(.+?)\s*$")
|
|
|
|
# Длина слова, начиная с которой ему нужны точки разрыва. Идентификаторы вроде
|
|
# HackRFTestingServer:идентификатор не содержат пробелов и дефисов, поэтому в
|
|
# узкой колонке LaTeX не может их перенести: строка уезжает за границу колонки
|
|
# и печатается поверх соседней.
|
|
_LONG_WORD = 18
|
|
|
|
# Колонка, где самое длинное значение короче этого, в растягивании не нуждается:
|
|
# отдав ей равную долю ширины, мы отнимаем место у колонок с текстом, и те
|
|
# начинают переноситься по одному слову.
|
|
_NARROW_CELL = 16
|
|
_BREAK_AFTER = ":/_.-"
|
|
|
|
_ALIGN_BY_STYLE = {
|
|
"text-align:left": "l",
|
|
"text-align:center": "c",
|
|
"text-align:right": "r",
|
|
}
|
|
|
|
|
|
def _breakable(cell: str) -> str:
|
|
"""Расставить точки переноса внутри длинных слов ячейки.
|
|
|
|
Ячейка уже отрендерена в LaTeX, поэтому слова с обратной косой чертой или
|
|
фигурными скобками пропускаются: вставка внутрь имени команды сломала бы её.
|
|
"""
|
|
words = []
|
|
for word in cell.split(" "):
|
|
if len(word) < _LONG_WORD or any(ch in word for ch in "\\{}"):
|
|
words.append(word)
|
|
continue
|
|
out = []
|
|
for ch in word:
|
|
out.append(ch)
|
|
if ch in _BREAK_AFTER:
|
|
out.append("\\allowbreak ")
|
|
words.append("".join(out))
|
|
return " ".join(words)
|
|
|
|
|
|
def _cell_align(tok: Token) -> str:
|
|
style = str(tok.attrGet("style") or "").replace(" ", "")
|
|
return _ALIGN_BY_STYLE.get(style, "l")
|
|
|
|
|
|
def _collect_rows(
|
|
tokens: list[Token], pos: int
|
|
) -> tuple[int, list[list[str]], list[str]]:
|
|
"""Разобрать table_open..table_close -> (позиция за таблицей, строки, выравнивания)."""
|
|
rows: list[list[str]] = []
|
|
aligns: list[str] = []
|
|
pos += 1
|
|
while pos < len(tokens):
|
|
tok = tokens[pos]
|
|
if tok.type == "table_close":
|
|
pos += 1
|
|
break
|
|
if tok.type != "tr_open":
|
|
pos += 1
|
|
continue
|
|
row: list[str] = []
|
|
row_aligns: list[str] = []
|
|
pos += 1
|
|
while pos < len(tokens):
|
|
ctok = tokens[pos]
|
|
if ctok.type == "tr_close":
|
|
pos += 1
|
|
break
|
|
if ctok.type in ("th_open", "td_open"):
|
|
row.append(_breakable(render_inline(tokens[pos + 1])))
|
|
row_aligns.append(_cell_align(ctok))
|
|
pos += 3
|
|
else:
|
|
pos += 1
|
|
if not aligns:
|
|
aligns = row_aligns
|
|
rows.append(row)
|
|
return pos, rows, aligns
|
|
|
|
|
|
_STRETCH = ">{\\raggedright\\arraybackslash}X"
|
|
|
|
|
|
def _tabular(rows: list[list[str]], aligns: list[str]) -> str:
|
|
"""Табличное окружение для строк с заданным выравниванием колонок.
|
|
|
|
Колонка, выровненная по левому краю, считается текстовой: её содержимое
|
|
должно переноситься по словам, иначе широкая таблица уезжает за правое
|
|
поле страницы. Такие колонки верстаются растягиваемым типом ``X``, а вся
|
|
таблица — окружением ``tabularx`` шириной в текстовое поле. Колонки по
|
|
центру и по правому краю (обычно числовые) остаются узкими.
|
|
|
|
Если текстовых колонок нет, растягивать нечего и используется обычный
|
|
``tabular``: ``tabularx`` без единой ``X``-колонки не собирается.
|
|
"""
|
|
cols = max(len(row) for row in rows)
|
|
for row in rows:
|
|
row.extend([""] * (cols - len(row)))
|
|
|
|
spec = list(aligns[:cols]) + ["l"] * max(0, cols - len(aligns))
|
|
widths = [max(len(row[i]) for row in rows) for i in range(cols)]
|
|
stretch = [
|
|
align == "l" and width >= _NARROW_CELL for align, width in zip(spec, widths)
|
|
]
|
|
stretchable = any(stretch)
|
|
col_spec = "|".join(
|
|
_STRETCH if is_wide else col for col, is_wide in zip(spec, stretch)
|
|
)
|
|
|
|
env = "tabularx" if stretchable else "tabular"
|
|
width = "{\\textwidth}" if stretchable else ""
|
|
lines = [f"\\begin{{{env}}}{width}{{|{col_spec}|}}", "\\hline"]
|
|
for row in rows:
|
|
lines.append(" & ".join(row) + " \\\\")
|
|
lines.append("\\hline")
|
|
lines.append(f"\\end{{{env}}}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
@block("table_open")
|
|
def table(r, tok: Token) -> str:
|
|
"""Таблица без подписи — голый tabular."""
|
|
r.pos, rows, aligns = _collect_rows(r.tokens, r.pos)
|
|
if not rows:
|
|
return ""
|
|
return _tabular(rows, aligns)
|
|
|
|
|
|
def match_table(tokens: list[Token], pos: int) -> int | None:
|
|
"""Абзац «Таблица N —— Название» + непосредственно следующая таблица."""
|
|
if pos + 3 >= len(tokens):
|
|
return None
|
|
if (
|
|
tokens[pos].type != "paragraph_open"
|
|
or tokens[pos + 1].type != "inline"
|
|
or tokens[pos + 2].type != "paragraph_close"
|
|
or tokens[pos + 3].type != "table_open"
|
|
):
|
|
return None
|
|
if not _CAPTION_RE.match(tokens[pos + 1].content):
|
|
return None
|
|
for i in range(pos + 4, len(tokens)):
|
|
if tokens[i].type == "table_close":
|
|
return i - pos + 1
|
|
return None
|
|
|
|
|
|
@pattern(match_table)
|
|
def captioned_table(tokens: list[Token]) -> str:
|
|
m = _CAPTION_RE.match(tokens[1].content)
|
|
assert m is not None
|
|
caption = escape_latex(m.group(1))
|
|
|
|
_, rows, aligns = _collect_rows(tokens, 3)
|
|
if not rows:
|
|
return ""
|
|
|
|
return "\n".join(
|
|
[
|
|
"\\begin{table}[ht]",
|
|
f" \\caption{{{caption}}}",
|
|
" \\centering",
|
|
_tabular(rows, aligns),
|
|
"\\end{table}",
|
|
]
|
|
)
|