Files
md_to_gost/md2gost/renderable/list.py
T
Igor20264 8223e0ac14
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled
v0.6.0 BigUpdate
2026-10-20 11:38:06 +03:00

140 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from copy import copy
from typing import Generator
from docx.shared import Pt, Cm, Twips
from . import Paragraph
from .renderable import Renderable
from ..layout_tracker import LayoutState
from ..rendered_info import RenderedInfo
# ТЗ табл. 2.2: маркер 1,25 см; текст / табуляция 2,25 см
MARKER_POS = Cm(1.25)
TEXT_POS = Cm(2.25)
BULLET = "–" # en dash, единый маркер на весь документ
class List(Renderable):
def __init__(self, parent, ordered: bool):
self._parent = parent
self._ordered = ordered
self._paragraphs: list[Paragraph] = []
self._last_paragraph_space_after = 0
self._numbering = [0 for _ in range(10)]
self._item_count = 0
def add_item(self, level: int) -> Paragraph:
self._numbering[level - 1] += 1
for i in range(level, len(self._numbering)):
self._numbering[i] = 0
self._item_count += 1
paragraph = Paragraph(self._parent)
if self._ordered:
marker = ".".join(str(n) for n in self._numbering[:level]) + "."
else:
marker = BULLET
paragraph.add_run(marker + "\t")
# hanging indent: left = TEXT_POS (+ nesting), first_line = MARKER_POS - TEXT_POS
nest = TEXT_POS * (level - 1)
paragraph._docx_paragraph.paragraph_format.tab_stops.add_tab_stop(TEXT_POS + nest)
paragraph._docx_paragraph.paragraph_format.left_indent = TEXT_POS + nest
paragraph._docx_paragraph.paragraph_format.first_line_indent = MARKER_POS - TEXT_POS
self._last_paragraph_space_after = paragraph._docx_paragraph.paragraph_format.space_after
paragraph._docx_paragraph.paragraph_format.space_before = 0
paragraph._docx_paragraph.paragraph_format.space_after = 0
self._paragraphs.append(paragraph)
return paragraph
@staticmethod
def _is_hyphen_run(run) -> bool:
"""True for a run that carries only w:noBreakHyphen (text is empty)."""
if run.text:
return False
return bool(run._element.xpath("./w:noBreakHyphen"))
@classmethod
def _logical_text(cls, runs) -> str:
parts = []
for run in runs:
if cls._is_hyphen_run(run):
parts.append("-")
else:
parts.append(run.text or "")
return "".join(parts)
def finalize_punctuation(self) -> None:
"""Apply TZ list punctuation: bullet → lowercase + ';' (last '.'); numbered → Capital + '.'.
Case and the final mark are written into existing runs. Runs are not merged:
merging drops w:noBreakHyphen and inline code formatting, so «REST-интерфейс»
became «RESTинтерфейс».
"""
for i, paragraph in enumerate(self._paragraphs):
runs = paragraph._docx_paragraph.runs
if len(runs) < 2:
continue
text_runs = runs[1:]
logical = self._logical_text(text_runs)
full = logical.strip()
if not full:
continue
is_last = i == len(self._paragraphs) - 1
if self._ordered:
fixed = full[0].upper() + full[1:]
if not fixed.endswith("."):
fixed = fixed.rstrip(";,. ") + "."
else:
fixed = full[0].lower() + full[1:]
if is_last:
fixed = fixed.rstrip(";,. ") + "."
else:
fixed = fixed.rstrip(";,. ") + ";"
if logical == fixed:
continue
self._apply_list_punctuation(text_runs, full, fixed)
@classmethod
def _apply_list_punctuation(cls, text_runs, full: str, fixed: str) -> None:
if full[0] != fixed[0]:
for run in text_runs:
if cls._is_hyphen_run(run) or not run.text:
continue
run.text = fixed[0] + run.text[1:]
break
desired_end = fixed[-1]
last = None
for run in text_runs:
if cls._is_hyphen_run(run) or not run.text:
continue
last = run
if last is None:
return
trimmed = last.text.rstrip(";,. ")
if trimmed.endswith(desired_end):
last.text = trimmed
else:
last.text = trimmed + desired_end
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) -> Generator[
RenderedInfo | Renderable, None, None]:
self.finalize_punctuation()
if self._paragraphs:
self._paragraphs[-1]._docx_paragraph.paragraph_format.space_after = self._last_paragraph_space_after
if getattr(self, "_space_before_mm6", False) and self._paragraphs:
from docx.shared import Mm
self._paragraphs[0]._docx_paragraph.paragraph_format.space_before = Mm(6)
for paragraph in self._paragraphs:
for x in paragraph.render(previous_rendered, copy(layout_state)):
layout_state.add_height(x.height)
previous_rendered = x
yield x