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 = f"{self._numbering[level - 1]}." 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 def finalize_punctuation(self) -> None: """Apply TZ list punctuation: bullet → lowercase + ';' (last '.'); numbered → Capital + '.'.""" for i, paragraph in enumerate(self._paragraphs): runs = paragraph._docx_paragraph.runs if len(runs) < 2: continue # Text after marker run text_runs = runs[1:] full = "".join(r.text or "" for r in text_runs).strip() if not full: continue is_last = i == len(self._paragraphs) - 1 if self._ordered: fixed = full[0].upper() + full[1:] if full else full if not fixed.endswith("."): fixed = fixed.rstrip(";,. ") + "." else: fixed = full[0].lower() + full[1:] if full else full if is_last: fixed = fixed.rstrip(";,. ") + "." else: fixed = fixed.rstrip(";,. ") + ";" # Put all text into first content run, clear others text_runs[0].text = fixed for r in text_runs[1:]: r.text = "" 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