Files
md_to_gost/md2gost/renderer.py
T
Igor20264 516abe7b83
Python application / build (push) Has been cancelled
BigUpdate
2026-09-03 10:44:08 +03:00

233 lines
9.3 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 typing import TYPE_CHECKING
from itertools import chain
import re
from docx.document import Document
from docx.shared import Length, Cm, Parented, Pt, Mm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from .numberer import Numberer, APPENDIX_LETTERS
from .renderable import Renderable
from .renderable.requires_numbering import RequiresNumbering
from .renderable.heading import Heading
from .renderable.equation import Equation
from .rendered_info import RenderedInfo
from .sub_renderable import SubRenderable
from .util import create_element
from .layout_tracker import LayoutTracker
if TYPE_CHECKING:
from .debugger import Debugger
BOTTOM_MARGIN = Mm(20)
APPENDIX_RE = re.compile(
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-Z])\b",
re.IGNORECASE,
)
SPECIAL_CENTER = {
"СОДЕРЖАНИЕ",
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
"СПИСОК ИСПОЛЬЗУЕМЫХ ИСТОЧНИКОВ",
}
class Renderer:
"""Renders Renderable elements to docx file"""
def __init__(self, document: Document, debugger: "Debugger | None" = None,
numbered_equations: set[str] | None = None,
skip_numbering: bool = False,
numbering_scope: str = "section"):
self._document: Document = document
self._numberer = Numberer(mode=numbering_scope)
self._debugger = debugger
self._numbered_equations = numbered_equations or set()
self._skip_numbering = skip_numbering
self._section_count = 0
self._appendix_index = 0
self._after_toc = False
self._body_section_started = False
max_height = (document.sections[0].page_height
- document.sections[0].top_margin
- BOTTOM_MARGIN)
max_width = (self._document.sections[0].page_width
- self._document.sections[0].left_margin
- self._document.sections[0].right_margin)
self._layout_tracker = LayoutTracker(max_height, max_width)
# Front-matter section: no page numbers (титул / задание / содержание)
self._clear_footer(self._document.sections[0])
self.previous_rendered = None
self._to_new_page: list[Renderable] = []
@staticmethod
def _clear_footer(section):
footer = section.footer
footer.is_linked_to_previous = False
for p in footer.paragraphs:
p.clear()
if not footer.paragraphs:
footer.add_paragraph()
def _ensure_body_section_with_page_numbers(self):
"""After TOC: start a new section with centered PAGE (TNR 12)."""
if self._body_section_started:
return
self._body_section_started = True
# Continuous/new page section break is inserted via last paragraph sectPr;
# python-docx: add_section creates new section.
from docx.enum.section import WD_ORIENT, WD_SECTION
new_section = self._document.add_section(WD_SECTION.NEW_PAGE)
new_section.page_width = self._document.sections[0].page_width
new_section.page_height = self._document.sections[0].page_height
new_section.left_margin = Mm(30)
new_section.right_margin = Mm(10)
new_section.top_margin = Mm(20)
new_section.bottom_margin = Mm(20)
paragraph = new_section.footer.paragraphs[0]
paragraph.paragraph_format.first_line_indent = 0
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
run = paragraph.add_run()
run.font.name = "Times New Roman"
run.font.size = Pt(12)
# PAGE field
paragraph._p.append(create_element("w:fldSimple", {
"w:instr": "PAGE \\* MERGEFORMAT"
}))
def process(self, renderables: list[Renderable]):
for i in range(len(renderables)):
self.render(renderables[i])
self._flush_to_new_screen()
# If document had no TOC, still add page numbers to the only section
if not self._body_section_started:
section = self._document.sections[0]
paragraph = section.footer.paragraphs[0]
paragraph.paragraph_format.first_line_indent = 0
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
paragraph._p.append(create_element("w:fldSimple", {
"w:instr": "PAGE \\* MERGEFORMAT"
}))
if self._debugger:
self._debugger.after_rendered()
def _handle_heading(self, heading: Heading):
text = (heading.text or "").strip()
upper = text.upper()
if heading.level == 1 and heading.is_numbered:
self._section_count += 1
self._numberer.enter_section(self._section_count)
# Appendix: «Приложение А» / «ПРИЛОЖЕНИЕ А»
m = APPENDIX_RE.match(upper.replace("Ё", "Е"))
if m or (heading.level <= 3 and upper.startswith("ПРИЛОЖЕНИЕ")):
letter = None
if m:
letter = m.group(1).upper()
else:
parts = upper.split()
if len(parts) >= 2 and parts[1] in APPENDIX_LETTERS:
letter = parts[1]
elif self._appendix_index < len(APPENDIX_LETTERS):
letter = APPENDIX_LETTERS[self._appendix_index]
self._appendix_index += 1
if letter:
self._numberer.enter_appendix(letter)
# Center special unnumbered headings that must be centered (already centered if unnumbered)
if upper in SPECIAL_CENTER or upper.startswith("ПРИЛОЖЕНИЕ"):
heading._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
# СОДЕРЖАНИЕ / СПИСОК — center even if somehow numbered
if upper in ("СОДЕРЖАНИЕ", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"):
heading._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
# Native Word TOC picks up Heading 13; «СОДЕРЖАНИЕ» must not list itself
if upper == "СОДЕРЖАНИЕ":
heading.exclude_from_toc()
def render(self, renderable: Renderable):
if isinstance(renderable, Heading):
self._handle_heading(renderable)
# After ToC page-break renderable we open body section — detected via ToC's PageBreak
from .renderable.toc import ToC
from .renderable.page_break import PageBreak
if isinstance(renderable, PageBreak) and self._after_toc:
self._ensure_body_section_with_page_numbers()
if isinstance(renderable, ToC):
self._after_toc = True
if not self._skip_numbering and isinstance(renderable, RequiresNumbering):
if isinstance(renderable, Equation):
label = renderable.unique_name
if renderable.needs_numbering or (label and label in self._numbered_equations):
renderable.enable_numbering()
number = self._numberer.next_number(
renderable.numbering_category, label)
renderable.set_number(number)
else:
number = self._numberer.next_number(
renderable.numbering_category,
getattr(renderable, "unique_name", None),
)
renderable.set_number(number)
infos = renderable.render(self.previous_rendered, self._layout_tracker.current_state)
try:
first = next(infos)
if isinstance(first, RenderedInfo) and first.height \
>= self._layout_tracker.current_state.remaining_page_height:
self._flush_to_new_screen()
infos = renderable.render(self.previous_rendered, self._layout_tracker.current_state)
else:
infos = chain([first], infos)
except StopIteration:
pass
for info in infos:
if isinstance(info, SubRenderable):
if info.add_to_new_page:
self._to_new_page.append(info.renderable)
else:
self.render(info.renderable)
else:
self._add(info.docx_element, info.height)
self.previous_rendered = info
def _flush_to_new_screen(self):
while self._to_new_page:
renderable = self._to_new_page.pop(0)
if not self._skip_numbering and isinstance(renderable, RequiresNumbering):
number = self._numberer.next_number(
renderable.numbering_category,
getattr(renderable, "unique_name", None),
)
renderable.set_number(number)
for info_ in renderable.render(self.previous_rendered, self._layout_tracker.current_state):
if isinstance(info_, SubRenderable):
continue
self._add(info_.docx_element, info_.height)
self.previous_rendered = info_
def _add(self, element: Parented, height: Length):
self._document._body._element.append(
element._element
)
self._layout_tracker.add_height(height)
if self._debugger:
self._debugger.add(element, height)
@property
def numberer(self) -> Numberer:
return self._numberer