BigUpdate
Python application / build (push) Has been cancelled

This commit is contained in:
Igor20264
2026-09-03 10:44:08 +03:00
parent d2da20fdb2
commit 516abe7b83
177 changed files with 40178 additions and 2 deletions
+140
View File
@@ -0,0 +1,140 @@
"""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 13, 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()
if display.upper() in SPECIAL_TITLES or display.upper().startswith("ПРИЛОЖЕНИЕ"):
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))