b38661f588
Python application / build (push) Has been cancelled
- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
from copy import copy
|
||
from typing import Generator
|
||
import re
|
||
|
||
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()
|
||
# Alignment decided later in Renderer: only СОДЕРЖАНИЕ / СПИСОК centered.
|
||
# ВВЕДЕНИЕ / ЗАКЛЮЧЕНИЕ / ПРИЛОЖЕНИЯ stay left like Heading 1 (1.25 cm indent).
|
||
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 1–3 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))
|