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
+162
View File
@@ -0,0 +1,162 @@
from copy import copy
from typing import Generator
import re
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.text.paragraph import Paragraph as DocxParagraph
from docx.shared import Parented, Length
from .paragraph_sizer import ParagraphSizer
from ..layout_tracker import LayoutState
from .paragraph import Paragraph
from ..rendered_info import RenderedInfo
from ..sub_renderable import SubRenderable
from ..util import create_element
# Leading section numbers written in markdown: "1 ", "1.1 ", "1.2.3. "
_LEADING_NUMBER_RE = re.compile(r"^\d+(?:\.\d+)*\.?\s+")
class Heading(Paragraph):
def __init__(self, parent: Parented, level: int, numbered: bool,
numbering_mode: str = "manual"):
super().__init__(parent)
self._numbered = numbered
self._numbering_mode = numbering_mode # auto | manual
self._parent = parent
self._level = level
if not 1 <= level <= 9:
raise ValueError("Heading level must be in range from 1 to 9")
self.style = f"Heading {level}"
if not numbered:
self._remove_numbering()
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
elif numbering_mode == "manual":
# Digits already in markdown text — kill Word list numbering to avoid "1 1 …"
self._remove_numbering()
self._rendered_page = 0
self._title_stripped = False
@property
def is_numbered(self) -> bool:
return self._numbered
@property
def numbering_mode(self) -> str:
return self._numbering_mode
@numbering_mode.setter
def numbering_mode(self, value: str):
self._numbering_mode = value
if self._numbered and value == "manual":
self._remove_numbering()
@property
def rendered_page(self) -> int:
return self._rendered_page
@property
def level(self) -> int:
return self._level
@property
def text(self) -> str:
return self._docx_paragraph.text
def apply_numbering_mode(self, mode: str):
"""Apply auto/manual after runs are filled."""
self._numbering_mode = mode
if not self._numbered:
return
if mode == "manual":
self._remove_numbering()
elif mode == "auto":
self._strip_leading_number_from_runs()
def _strip_leading_number_from_runs(self):
"""Remove '1 ', '1.1 ' etc. from text so Word list provides the number."""
if self._title_stripped or not self._numbered:
return
full = self._docx_paragraph.text or ""
m = _LEADING_NUMBER_RE.match(full)
if not m:
return
# Rewrite runs: put remaining text into first run, clear the rest
remaining = full[m.end():]
runs = self._docx_paragraph.runs
if not runs:
return
runs[0].text = remaining
for r in runs[1:]:
r.text = ""
self._title_stripped = True
def _remove_numbering(self):
pPr = self._docx_paragraph._p.get_or_add_pPr()
# Remove existing numPr if any, then set numId=0
for child in list(pPr):
if child.tag.endswith("}numPr"):
pPr.remove(child)
pPr.append(
create_element("w:numPr", [
create_element("w:ilvl", {
"w:val": "0"
}),
create_element("w:numId", {
"w:val": "0"
})
])
)
def exclude_from_toc(self):
"""Keep Heading N look, but outline level = Body Text so Word TOC skips it."""
pPr = self._docx_paragraph._p.get_or_add_pPr()
for child in list(pPr):
if child.tag.endswith("}outlineLvl"):
pPr.remove(child)
# 9 = body text (not outline levels 13 used by TOC \o "1-3")
pPr.append(create_element("w:outlineLvl", {"w:val": "9"}))
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
-> Generator[RenderedInfo | SubRenderable, None, None]:
remaining_height = layout_state.remaining_page_height
if self._level == 1 and layout_state.page != 1 and\
not (isinstance(previous_rendered.docx_element, DocxParagraph)
and previous_rendered.docx_element.text == "\n"):
self.page_break_before = True
height_data = ParagraphSizer(
self._docx_paragraph,
previous_rendered.docx_element
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
layout_state.max_width).calculate_height()
if layout_state.current_page_height == 0 and layout_state.page != 1:
height_data.before = 0
# if a heading + 3 lines don't fit to the page, they go to the next page
if ((height_data.lines + 3 - 1) * height_data.line_spacing + 1) * height_data.line_height\
> layout_state.remaining_page_height:
self._docx_paragraph.paragraph_format.space_before = 0 # libreoffice fix
height = height_data.full - height_data.before
# force this behaviour as there could be a table or an image instead of text
self.page_break_before = True
else:
height = height_data.full
if self.page_break_before:
height += remaining_height
layout_state.add_height(height)
self._rendered_page = layout_state.page
yield RenderedInfo(self._docx_paragraph, Length(height))