Files
md_to_gost/md2gost/renderable/heading.py
T
Igor20264 b38661f588
Python application / build (push) Has been cancelled
Update 0.4.0
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00

163 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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))