- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
This commit is contained in:
+203
-100
@@ -3,18 +3,24 @@ from itertools import chain
|
||||
import re
|
||||
|
||||
from docx.document import Document
|
||||
from docx.shared import Length, Cm, Parented, Pt, Mm
|
||||
from docx.shared import Length, Parented, Mm, Cm
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
||||
from docx.enum.section import WD_SECTION
|
||||
|
||||
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 .renderable.paragraph import Paragraph
|
||||
from .rendered_info import RenderedInfo
|
||||
from .sub_renderable import SubRenderable
|
||||
from .util import create_element
|
||||
from .layout_tracker import LayoutTracker
|
||||
from .page_geometry import (
|
||||
apply_centered_page_footer,
|
||||
apply_section_geometry,
|
||||
content_size,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .debugger import Debugger
|
||||
@@ -22,7 +28,12 @@ if TYPE_CHECKING:
|
||||
BOTTOM_MARGIN = Mm(20)
|
||||
|
||||
APPENDIX_RE = re.compile(
|
||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-Z])\b",
|
||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# Lettered appendix title: «Приложение А» or «Приложение А Название»
|
||||
APPENDIX_ITEM_RE = re.compile(
|
||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SPECIAL_CENTER = {
|
||||
@@ -48,13 +59,9 @@ class Renderer:
|
||||
self._appendix_index = 0
|
||||
self._after_toc = False
|
||||
self._body_section_started = False
|
||||
self._landscape_depth = 0
|
||||
|
||||
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)
|
||||
max_height, max_width = content_size(landscape=False)
|
||||
self._layout_tracker = LayoutTracker(max_height, max_width)
|
||||
|
||||
# Front-matter section: no page numbers (титул / задание / содержание)
|
||||
@@ -62,6 +69,8 @@ class Renderer:
|
||||
|
||||
self.previous_rendered = None
|
||||
self._to_new_page: list[Renderable] = []
|
||||
self._after_current: list[Renderable] = []
|
||||
self._need_space_after_object = False
|
||||
|
||||
@staticmethod
|
||||
def _clear_footer(section):
|
||||
@@ -77,27 +86,39 @@ class Renderer:
|
||||
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)
|
||||
apply_section_geometry(new_section, landscape=False)
|
||||
apply_centered_page_footer(new_section)
|
||||
|
||||
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 _enter_landscape(self) -> None:
|
||||
if self._landscape_depth > 0:
|
||||
self._landscape_depth += 1
|
||||
return
|
||||
self._flush_to_new_screen()
|
||||
if self._after_toc:
|
||||
self._ensure_body_section_with_page_numbers()
|
||||
elif not self._body_section_started:
|
||||
# No TOC — still need page numbers on subsequent sections.
|
||||
self._body_section_started = True
|
||||
apply_centered_page_footer(self._document.sections[0])
|
||||
section = self._document.add_section(WD_SECTION.NEW_PAGE)
|
||||
apply_section_geometry(section, landscape=True)
|
||||
apply_centered_page_footer(section)
|
||||
max_height, max_width = content_size(landscape=True)
|
||||
self._layout_tracker.set_page_size(max_height, max_width)
|
||||
self._landscape_depth = 1
|
||||
|
||||
def _exit_landscape(self) -> None:
|
||||
if self._landscape_depth <= 0:
|
||||
return
|
||||
self._landscape_depth -= 1
|
||||
if self._landscape_depth > 0:
|
||||
return
|
||||
section = self._document.add_section(WD_SECTION.NEW_PAGE)
|
||||
apply_section_geometry(section, landscape=False)
|
||||
apply_centered_page_footer(section)
|
||||
max_height, max_width = content_size(landscape=False)
|
||||
self._layout_tracker.set_page_size(max_height, max_width)
|
||||
|
||||
def process(self, renderables: list[Renderable]):
|
||||
for i in range(len(renderables)):
|
||||
@@ -107,46 +128,84 @@ class Renderer:
|
||||
# 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"
|
||||
}))
|
||||
apply_centered_page_footer(section)
|
||||
if self._debugger:
|
||||
self._debugger.after_rendered()
|
||||
|
||||
@staticmethod
|
||||
def _rewrite_heading_text(heading: Heading, text: str) -> None:
|
||||
runs = heading._docx_paragraph.runs
|
||||
if runs:
|
||||
runs[0].text = text
|
||||
for r in runs[1:]:
|
||||
r.text = ""
|
||||
else:
|
||||
heading.add_run(text)
|
||||
|
||||
def _make_appendix_title_paragraph(self, title: str) -> Paragraph:
|
||||
"""Title line under «Приложение А»: Normal, centered, no first-line indent."""
|
||||
p = Paragraph(self._document._body)
|
||||
p.add_run(title)
|
||||
p.style = "Normal"
|
||||
p.first_line_indent = Cm(0)
|
||||
pf = p._docx_paragraph.paragraph_format
|
||||
pf.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
pf.left_indent = Cm(0)
|
||||
pf.keep_with_next = True
|
||||
return p
|
||||
|
||||
def _handle_heading(self, heading: Heading):
|
||||
text = (heading.text or "").strip()
|
||||
upper = text.upper()
|
||||
upper = text.upper().replace("Ё", "Е")
|
||||
|
||||
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:
|
||||
# Letter appendix item: «Приложение А» / «Приложение А Название»
|
||||
# (not the section header «ПРИЛОЖЕНИЯ»)
|
||||
item = APPENDIX_ITEM_RE.match(upper) if upper.startswith("ПРИЛОЖЕНИЕ ") else None
|
||||
if item:
|
||||
letter = item.group(1).upper().replace("Ё", "Е")
|
||||
title_tail = (item.group(2) or "").strip()
|
||||
if letter in APPENDIX_LETTERS:
|
||||
self._numberer.enter_appendix(letter)
|
||||
elif self._appendix_index < len(APPENDIX_LETTERS):
|
||||
letter = APPENDIX_LETTERS[self._appendix_index]
|
||||
self._appendix_index += 1
|
||||
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("ПРИЛОЖЕНИЕ"):
|
||||
# Style as Heading 3, centered, new page (method guide §6)
|
||||
heading.style = "Heading 3"
|
||||
heading._level = 3
|
||||
heading._remove_numbering()
|
||||
heading._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
heading._docx_paragraph.paragraph_format.left_indent = Cm(0)
|
||||
heading._docx_paragraph.paragraph_format.first_line_indent = Cm(0)
|
||||
heading.page_break_before = True
|
||||
heading._docx_paragraph.paragraph_format.keep_with_next = True
|
||||
|
||||
# СОДЕРЖАНИЕ / СПИСОК — center even if somehow numbered
|
||||
if upper in ("СОДЕРЖАНИЕ", "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"):
|
||||
orig = (heading.text or "").strip()
|
||||
m_orig = re.match(r"(?i)^Приложение\s+([А-ЯA-ZЁ])\b", orig)
|
||||
letter_display = m_orig.group(1).upper().replace("Ё", "Е") if m_orig else letter
|
||||
self._rewrite_heading_text(heading, f"Приложение {letter_display}")
|
||||
|
||||
if title_tail:
|
||||
m_tail = re.match(r"(?i)^Приложение\s+[А-ЯA-ZЁ]\s+(.+)$", orig)
|
||||
title_text = m_tail.group(1).strip() if m_tail else title_tail
|
||||
self._after_current.append(self._make_appendix_title_paragraph(title_text))
|
||||
return
|
||||
|
||||
# Section «ПРИЛОЖЕНИЯ» / bare «ПРИЛОЖЕНИЕ» — left like H1, not centered
|
||||
if upper in ("ПРИЛОЖЕНИЯ", "ПРИЛОЖЕНИЕ"):
|
||||
heading._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT
|
||||
return
|
||||
|
||||
# Only СОДЕРЖАНИЕ / СПИСОК … are centered (method guide p. 8)
|
||||
if upper in SPECIAL_CENTER:
|
||||
heading._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
heading._docx_paragraph.paragraph_format.left_indent = Cm(0)
|
||||
heading._docx_paragraph.paragraph_format.first_line_indent = Cm(0)
|
||||
|
||||
# Native Word TOC picks up Heading 1–3; «СОДЕРЖАНИЕ» must not list itself
|
||||
if upper == "СОДЕРЖАНИЕ":
|
||||
@@ -159,69 +218,113 @@ class Renderer:
|
||||
# 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
|
||||
from .renderable.table import Table
|
||||
from .renderable.listing import Listing
|
||||
from .renderable.diagram import DiagramFigure
|
||||
from .renderable.list import List as RList
|
||||
|
||||
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)
|
||||
# Space 6 mm before next body paragraph after table/listing
|
||||
if self._need_space_after_object:
|
||||
self._need_space_after_object = False
|
||||
if isinstance(renderable, (Paragraph, RList)) and not isinstance(renderable, Heading):
|
||||
if isinstance(renderable, RList):
|
||||
# Apply to first list item when rendered — mark on list
|
||||
setattr(renderable, "_space_before_mm6", True)
|
||||
else:
|
||||
renderable._docx_paragraph.paragraph_format.space_before = Mm(6)
|
||||
|
||||
infos = renderable.render(self.previous_rendered, self._layout_tracker.current_state)
|
||||
wants_landscape = bool(getattr(renderable, "landscape", False))
|
||||
deferred_listing = None
|
||||
if wants_landscape and isinstance(renderable, DiagramFigure) and renderable.listing is not None:
|
||||
# Method guide: landscape pages are for figures/tables only — listing after
|
||||
deferred_listing = renderable.listing
|
||||
renderable.listing = None
|
||||
|
||||
if wants_landscape:
|
||||
self._enter_landscape()
|
||||
|
||||
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)
|
||||
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:
|
||||
self.render(info.renderable)
|
||||
else:
|
||||
self._add(info.docx_element, info.height)
|
||||
self.previous_rendered = info
|
||||
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
|
||||
finally:
|
||||
if wants_landscape:
|
||||
self._exit_landscape()
|
||||
|
||||
# Appendix title line queued by _handle_heading
|
||||
while self._after_current:
|
||||
extra = self._after_current.pop(0)
|
||||
self.render(extra)
|
||||
|
||||
if deferred_listing is not None:
|
||||
self.render(deferred_listing)
|
||||
|
||||
if isinstance(renderable, (Table, Listing)):
|
||||
self._need_space_after_object = True
|
||||
elif isinstance(renderable, DiagramFigure) and deferred_listing is None and renderable.listing is None:
|
||||
# Diagram figure alone — no table/listing spacing needed after image caption
|
||||
pass
|
||||
|
||||
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_
|
||||
self.render(renderable)
|
||||
|
||||
def _add(self, element: Parented, height: Length):
|
||||
self._document._body._element.append(
|
||||
element._element
|
||||
)
|
||||
# MUST insert before the body-level w:sectPr. append() puts content after it,
|
||||
# so section breaks from +landscape never wrap the figure (Word shows portrait).
|
||||
body = self._document._body._element
|
||||
el = element._element
|
||||
from docx.oxml.ns import qn
|
||||
sect_pr = None
|
||||
for child in body:
|
||||
if child.tag == qn("w:sectPr"):
|
||||
sect_pr = child
|
||||
break
|
||||
if sect_pr is not None:
|
||||
sect_pr.addprevious(el)
|
||||
else:
|
||||
body.append(el)
|
||||
self._layout_tracker.add_height(height)
|
||||
|
||||
if self._debugger:
|
||||
|
||||
Reference in New Issue
Block a user