b38661f588
Python application / build (push) Has been cancelled
- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
"""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))
|