"""Table of contents: manual (layout-based) or native Word TOC field.""" from copy import copy from typing import Generator from docx.enum.text import WD_TAB_LEADER, WD_TAB_ALIGNMENT, WD_PARAGRAPH_ALIGNMENT from docx.shared import Parented from . import Paragraph from .page_break import PageBreak from .renderable import Renderable from ..layout_tracker import LayoutState from ..rendered_info import RenderedInfo from ..sub_renderable import SubRenderable from ..util import create_element SPECIAL_TITLES = { "СОДЕРЖАНИЕ", "ВВЕДЕНИЕ", "ЗАКЛЮЧЕНИЕ", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ", "СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ", "ПРИЛОЖЕНИЕ", "ПРИЛОЖЕНИЯ", } # Word field: outline levels 1–3, hyperlinks, hide tab/page in web view _NATIVE_TOC_INSTR = r'TOC \o "1-3" \h \z \u' class ToC(Renderable): def __init__(self, parent: Parented, heading_numbering: str = "manual", toc_mode: str = "native"): self._parent = parent self._heading_numbering = heading_numbering # auto | manual self._toc_mode = toc_mode # manual | native self._paragraph = Paragraph(parent) self._paragraph._docx_paragraph.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT self._paragraph.first_line_indent = 0 self._items: list[tuple[int, str, int, bool]] = [] self._native_ready = False def set_heading_numbering(self, mode: str): self._heading_numbering = mode def set_toc_mode(self, mode: str): self._toc_mode = mode @property def toc_mode(self) -> str: return self._toc_mode def add_item(self, level: int, title: str, page: int, numbered: bool): self._items.append((level, title, page, numbered)) def fill(self): if self._toc_mode == "native": self._fill_native() else: self._fill_manual() def _fill_native(self): """Insert a Word TOC field. Pages appear after update in Word.""" if self._native_ready: return p = self._paragraph._docx_paragraph # Clear any leftover runs for child in list(p._p): if child.tag.endswith("}r"): p._p.remove(child) def add_fld_char(fld_char_type: str, dirty: bool = False): attrs = {"w:fldCharType": fld_char_type} if dirty: attrs["w:dirty"] = "true" run = create_element("w:r", [ create_element("w:fldChar", attrs) ]) p._p.append(run) def add_instr(text: str): instr = create_element("w:instrText", {"xml:space": "preserve"}, f" {text} ") run = create_element("w:r", [instr]) p._p.append(run) def add_text(text: str): run = create_element("w:r", [ create_element("w:t", {"xml:space": "preserve"}, text) ]) p._p.append(run) add_fld_char("begin", dirty=True) add_instr(_NATIVE_TOC_INSTR) add_fld_char("separate") add_text( "Обновите содержание в Word: ПКМ по полю → Обновить поле → целиком." ) add_fld_char("end") self._native_ready = True def _fill_manual(self): p = self._paragraph._docx_paragraph usable = (p.part.document.sections[0].page_width - p.part.document.sections[0].left_margin - p.part.document.sections[0].right_margin) p.paragraph_format.tab_stops.add_tab_stop( usable, alignment=WD_TAB_ALIGNMENT.RIGHT, leader=WD_TAB_LEADER.DOTS) p.paragraph_format.tab_stops.add_tab_stop( 0, alignment=WD_TAB_ALIGNMENT.LEFT, leader=WD_TAB_LEADER.SPACES) numbering = [0 for _ in range(10)] for level, title, page, numbered in self._items: numbering[level - 1] += 1 for i in range(level, len(numbering)): numbering[i] = 0 if title.strip().upper() == "СОДЕРЖАНИЕ": continue display = title.strip() # Special sections and numbered H1 → uppercase in TOC (method guide) if ( display.upper() in SPECIAL_TITLES or display.upper().startswith("ПРИЛОЖЕНИЕ") or (level == 1 and numbered) ): display = display.upper() p.add_run(" " * (level - 1)) if numbered and self._heading_numbering == "auto": num = ".".join(str(x) for x in numbering[:level]) p.add_run(f"{num} ") run = p.add_run(display) run.bold = False p.add_run(f"\t{page}") p.add_run("\n") def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\ -> Generator[RenderedInfo | SubRenderable, None, None]: # Native TOC: insert field shell early so it lands in the body before page break. # Final instr is ensured again in fill() after render. if self._toc_mode == "native" and not self._native_ready: self._fill_native() for rendered_info in self._paragraph.render(previous_rendered, copy(layout_state)): yield RenderedInfo(rendered_info.docx_element, 0) yield from PageBreak(self._parent).render(None, copy(layout_state))