@@ -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 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()
|
||||
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))
|
||||
Reference in New Issue
Block a user