- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
This commit is contained in:
@@ -16,7 +16,7 @@ from ..util import create_element
|
||||
# Map category → Word style name
|
||||
CAPTION_STYLES = {
|
||||
"Рисунок": "Caption Figure",
|
||||
"Таблица": "Caption Table",
|
||||
"Таблица": "Название таблицы",
|
||||
"Листинг": "Caption Listing",
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class CaptionInfo:
|
||||
unique_name: str | None
|
||||
text: str | None
|
||||
with_listing: bool = False
|
||||
landscape: bool = False
|
||||
|
||||
|
||||
class Caption(Renderable):
|
||||
@@ -40,7 +41,10 @@ class Caption(Renderable):
|
||||
try:
|
||||
self._docx_paragraph.style = style_name
|
||||
except KeyError:
|
||||
self._docx_paragraph.style = "Caption"
|
||||
try:
|
||||
self._docx_paragraph.style = "Caption Table" if category == "Таблица" else "Caption"
|
||||
except KeyError:
|
||||
self._docx_paragraph.style = "Caption"
|
||||
|
||||
# Format: «Рисунок 1.1 — Название» (or « - » if --emdash-to-hyphen)
|
||||
from ..profiles import dash_separator
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..rendered_info import RenderedInfo
|
||||
|
||||
|
||||
class DiagramFigure(Renderable, RequiresNumbering):
|
||||
"""UML/BPMN/C4 fence → PNG figure (+ optional source listing)."""
|
||||
"""UML / Mermaid / scheme fence → figure (+ optional source listing)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -34,6 +34,7 @@ class DiagramFigure(Renderable, RequiresNumbering):
|
||||
)
|
||||
self._number = None
|
||||
self._listing_number = None
|
||||
self.landscape = bool(caption_info and caption_info.landscape)
|
||||
if caption_info and caption_info.unique_name:
|
||||
self.unique_name = caption_info.unique_name
|
||||
|
||||
@@ -56,8 +57,14 @@ class DiagramFigure(Renderable, RequiresNumbering):
|
||||
|
||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) \
|
||||
-> Generator[RenderedInfo, None, None]:
|
||||
png_path = render_diagram(self._lang, self._source)
|
||||
image = Image(self._parent, png_path, self._caption_info)
|
||||
result = render_diagram(self._lang, self._source)
|
||||
image = Image(
|
||||
self._parent,
|
||||
result.png_path,
|
||||
self._caption_info,
|
||||
svg_path=result.svg_path,
|
||||
pixel_scale=result.pixel_scale,
|
||||
)
|
||||
if self._number is not None:
|
||||
image.set_number(self._number)
|
||||
yield from image.render(previous_rendered, layout_state)
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
|
||||
@@ -35,7 +34,8 @@ class Heading(Paragraph):
|
||||
|
||||
if not numbered:
|
||||
self._remove_numbering()
|
||||
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
# 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()
|
||||
|
||||
+70
-10
@@ -7,20 +7,32 @@ import os.path
|
||||
|
||||
import requests
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
||||
from docx.shared import Parented, Length
|
||||
from docx.shared import Parented, Length, Mm
|
||||
from docx.text.paragraph import Paragraph
|
||||
|
||||
from .caption import Caption, CaptionInfo
|
||||
from .renderable import Renderable
|
||||
from .requires_numbering import RequiresNumbering
|
||||
from ..docx_svg import attach_svg_blip
|
||||
from ..layout_tracker import LayoutState
|
||||
from ..rendered_info import RenderedInfo
|
||||
from ..sub_renderable import SubRenderable
|
||||
from ..util import create_element
|
||||
|
||||
# Leave room under the figure so «Рисунок N — …» stays on the same page.
|
||||
_CAPTION_RESERVE = Mm(12)
|
||||
|
||||
|
||||
class Image(Renderable, RequiresNumbering):
|
||||
def __init__(self, parent: Parented, path: str, caption_info: CaptionInfo | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
parent: Parented,
|
||||
path: str,
|
||||
caption_info: CaptionInfo | None = None,
|
||||
svg_path: str | None = None,
|
||||
*,
|
||||
pixel_scale: float = 1.0,
|
||||
):
|
||||
super().__init__("Рисунок")
|
||||
self._parent = parent
|
||||
self._caption_info = caption_info
|
||||
@@ -31,6 +43,8 @@ class Image(Renderable, RequiresNumbering):
|
||||
self._docx_paragraph.paragraph_format.line_spacing = 1
|
||||
self._docx_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
self._invalid = False
|
||||
self._native_width: Length | None = None
|
||||
self._native_height: Length | None = None
|
||||
|
||||
run = self._docx_paragraph.add_run()
|
||||
|
||||
@@ -47,36 +61,82 @@ class Image(Renderable, RequiresNumbering):
|
||||
except FileNotFoundError:
|
||||
logging.warning(f"Invalid image path: {path}, skipping...")
|
||||
self._invalid = True
|
||||
self._image = None
|
||||
|
||||
# High-res PlantUML/Kroki PNG: keep on-page size as if scale were 1.
|
||||
if not self._invalid and pixel_scale and pixel_scale > 1:
|
||||
self._image.width = Length(int(self._image.width / pixel_scale))
|
||||
self._image.height = Length(int(self._image.height / pixel_scale))
|
||||
|
||||
if not self._invalid:
|
||||
self._native_width = Length(int(self._image.width))
|
||||
self._native_height = Length(int(self._image.height))
|
||||
|
||||
if not self._invalid and svg_path:
|
||||
try:
|
||||
attach_svg_blip(run, self._image, svg_path)
|
||||
except Exception as exc:
|
||||
logging.warning("SVG blip attach failed (%s): %s", svg_path, exc)
|
||||
|
||||
self._number = None
|
||||
self.landscape = bool(caption_info and caption_info.landscape)
|
||||
if caption_info and caption_info.unique_name:
|
||||
self.unique_name = caption_info.unique_name
|
||||
|
||||
# Keep figure + caption together across Word pagination.
|
||||
if not self._invalid and caption_info is not None:
|
||||
self._docx_paragraph.paragraph_format.keep_with_next = True
|
||||
|
||||
def set_number(self, number: str):
|
||||
self._number = number
|
||||
|
||||
def _reset_native_size(self) -> None:
|
||||
if self._image is None or self._native_width is None or self._native_height is None:
|
||||
return
|
||||
self._image.width = Length(int(self._native_width))
|
||||
self._image.height = Length(int(self._native_height))
|
||||
|
||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
|
||||
-> Generator[RenderedInfo | SubRenderable, None, None]:
|
||||
if self._invalid:
|
||||
yield from []
|
||||
return
|
||||
|
||||
# Re-fit from native size each time (Paragraph may have measured us earlier).
|
||||
self._reset_native_size()
|
||||
|
||||
has_caption = self._caption_info is not None
|
||||
caption_reserve = _CAPTION_RESERVE if has_caption else Length(0)
|
||||
max_w = layout_state.max_width
|
||||
max_h = Length(max(0, int(layout_state.max_height) - int(caption_reserve)))
|
||||
|
||||
# limit width
|
||||
if self._image.width > layout_state.max_width:
|
||||
if self._image.width > max_w:
|
||||
height_by_width = self._image.height / self._image.width
|
||||
self._image.width = layout_state.max_width
|
||||
self._image.width = max_w
|
||||
self._image.height = Length(self._image.width * height_by_width)
|
||||
|
||||
# limit height
|
||||
if self._image.height > layout_state.max_height:
|
||||
# limit height (leave room for caption on the same page)
|
||||
if self._image.height > max_h:
|
||||
width_by_height = self._image.width / self._image.height
|
||||
self._image.height = layout_state.max_height
|
||||
self._image.height = max_h
|
||||
self._image.width = Length(self._image.height * width_by_height)
|
||||
|
||||
height = self._image.height
|
||||
|
||||
if layout_state.remaining_page_height < height:
|
||||
height += layout_state.remaining_page_height
|
||||
need = Length(int(height) + int(caption_reserve))
|
||||
remaining = layout_state.remaining_page_height
|
||||
if remaining < need:
|
||||
if self.landscape:
|
||||
# Already on a fresh landscape section — shrink instead of soft page-break
|
||||
# (soft break + section break = empty landscape page before the figure).
|
||||
avail = Length(max(0, int(remaining) - int(caption_reserve)))
|
||||
if avail > 0 and self._image.height > avail:
|
||||
ratio = int(avail) / int(self._image.height)
|
||||
self._image.height = avail
|
||||
self._image.width = Length(int(self._image.width * ratio))
|
||||
height = self._image.height
|
||||
else:
|
||||
height = Length(int(height) + int(remaining))
|
||||
|
||||
yield (rendered_image := RenderedInfo(self._docx_paragraph, Length(height)))
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ class List(Renderable):
|
||||
if self._paragraphs:
|
||||
self._paragraphs[-1]._docx_paragraph.paragraph_format.space_after = self._last_paragraph_space_after
|
||||
|
||||
if getattr(self, "_space_before_mm6", False) and self._paragraphs:
|
||||
from docx.shared import Mm
|
||||
self._paragraphs[0]._docx_paragraph.paragraph_format.space_before = Mm(6)
|
||||
|
||||
for paragraph in self._paragraphs:
|
||||
for x in paragraph.render(previous_rendered, copy(layout_state)):
|
||||
layout_state.add_height(x.height)
|
||||
|
||||
@@ -2,23 +2,25 @@ from copy import copy
|
||||
import os
|
||||
from typing import Generator, Callable
|
||||
|
||||
from docx.oxml import CT_Tbl
|
||||
from docx.shared import Length, Pt, RGBColor, Twips
|
||||
from docx.table import Table
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.formatter import Formatter
|
||||
from pygments.lexers import get_lexer_by_name
|
||||
|
||||
from .caption import Caption, CaptionInfo
|
||||
from .page_break import PageBreak
|
||||
from .paragraph import Paragraph
|
||||
from .renderable import Renderable
|
||||
from .requires_numbering import RequiresNumbering
|
||||
from ..docx_elements import create_table, _twips
|
||||
from ..docx_elements import create_table, create_table_row, create_table_cell, set_table_box_borders, _twips
|
||||
from ..layout_tracker import LayoutState
|
||||
from ..profiles import DEFAULT_LISTING_CONTINUATION, LISTING_CONTINUATION_MODES
|
||||
from ..rendered_info import RenderedInfo
|
||||
from ..sub_renderable import SubRenderable
|
||||
|
||||
_WORD_PAGED_MODES = frozenset({"off", "soft", "word"})
|
||||
|
||||
|
||||
class DocxParagraphPygmentsFormatter(Formatter):
|
||||
def __init__(self, paragraphs: list[Paragraph], creator: Callable[[], Paragraph], **options):
|
||||
@@ -55,13 +57,14 @@ class Listing(Renderable, RequiresNumbering):
|
||||
self._caption_info = caption_info
|
||||
self._language = language
|
||||
self._parent = parent
|
||||
self._continuation_mode = DEFAULT_LISTING_CONTINUATION
|
||||
self.paragraphs: list[Paragraph] = []
|
||||
self._number = None
|
||||
if caption_info and caption_info.unique_name:
|
||||
self.unique_name = caption_info.unique_name
|
||||
|
||||
def _create_table(self, parent, width: Length):
|
||||
# todo: style inheritance
|
||||
# Kept for tests / callers that still expect the helper; render uses multi-row.
|
||||
left_margin = Twips(int(
|
||||
parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:left")[0].attrib[
|
||||
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
|
||||
@@ -94,6 +97,51 @@ class Listing(Renderable, RequiresNumbering):
|
||||
def set_number(self, number: str):
|
||||
self._number = number
|
||||
|
||||
def set_continuation_mode(self, mode: str) -> None:
|
||||
if mode not in LISTING_CONTINUATION_MODES:
|
||||
raise ValueError(
|
||||
f"listing continuation must be one of {LISTING_CONTINUATION_MODES}, got {mode!r}"
|
||||
)
|
||||
self._continuation_mode = mode
|
||||
|
||||
def _should_split_fragment(
|
||||
self, line_height, layout_state: LayoutState, lines_in_fragment: int
|
||||
) -> bool:
|
||||
if self._continuation_mode in _WORD_PAGED_MODES:
|
||||
return False
|
||||
if lines_in_fragment == 0:
|
||||
return False
|
||||
return line_height > layout_state.remaining_page_height
|
||||
|
||||
def _make_continuation_paragraph(self) -> Paragraph:
|
||||
continuation_paragraph = Paragraph(self._parent)
|
||||
continuation_paragraph.add_run(f"Продолжение Листинга {self._number}")
|
||||
continuation_paragraph.style = "Caption Listing"
|
||||
continuation_paragraph.first_line_indent = 0
|
||||
return continuation_paragraph
|
||||
|
||||
def _emit_page_break_and_optional_caption(
|
||||
self, layout_state: LayoutState
|
||||
) -> Generator[RenderedInfo, None, None]:
|
||||
mode = self._continuation_mode
|
||||
if mode == "legacy":
|
||||
continuation_paragraph = self._make_continuation_paragraph()
|
||||
continuation_paragraph.page_break_before = True
|
||||
info = next(continuation_paragraph.render(None, copy(layout_state)))
|
||||
layout_state.add_height(info.height)
|
||||
yield info
|
||||
return
|
||||
|
||||
page_break_info = next(PageBreak(self._parent).render(None, layout_state))
|
||||
layout_state.add_height(page_break_info.height)
|
||||
yield page_break_info
|
||||
|
||||
continuation_paragraph = self._make_continuation_paragraph()
|
||||
continuation_paragraph._docx_paragraph.paragraph_format.keep_with_next = True
|
||||
info = next(continuation_paragraph.render(None, copy(layout_state)))
|
||||
layout_state.add_height(info.height)
|
||||
yield info
|
||||
|
||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
|
||||
-> Generator[RenderedInfo | SubRenderable, None, None]:
|
||||
caption_rendered_infos = list(
|
||||
@@ -103,54 +151,65 @@ class Listing(Renderable, RequiresNumbering):
|
||||
layout_state.add_height(sum([info.height for info in caption_rendered_infos]))
|
||||
yield from caption_rendered_infos
|
||||
|
||||
table = self._create_table(self._parent, layout_state.max_width)
|
||||
# One code line = one table row so Word can Split at real page breaks (mode word).
|
||||
# Outer box only — no grid lines between lines (looks like text in a frame).
|
||||
table = create_table(self._parent, 0, 1, self._listing_width(layout_state))
|
||||
set_table_box_borders(table)
|
||||
previous = None
|
||||
|
||||
table_height = Pt(1) # table borders, 4 eights of point for each border
|
||||
lines_in_fragment = 0
|
||||
col_w = self._listing_width(layout_state)
|
||||
|
||||
# if first line doesn't fit move listing to the next page
|
||||
paragraph_layout_state = copy(layout_state)
|
||||
paragraph_layout_state.max_width -= LISTING_OFFSET
|
||||
paragraph_rendered_info = next(self.paragraphs[0].render(previous, paragraph_layout_state))
|
||||
if paragraph_rendered_info.height + table_height > layout_state.remaining_page_height:
|
||||
table_height += layout_state.remaining_page_height
|
||||
layout_state.add_height(layout_state.remaining_page_height)
|
||||
# legacy/caption: if first line doesn't fit, burn the rest of the page
|
||||
if self.paragraphs and self._continuation_mode not in _WORD_PAGED_MODES:
|
||||
paragraph_layout_state = copy(layout_state)
|
||||
paragraph_layout_state.max_width -= LISTING_OFFSET
|
||||
first = next(self.paragraphs[0].render(previous, paragraph_layout_state))
|
||||
if first.height + table_height > layout_state.remaining_page_height:
|
||||
table_height += layout_state.remaining_page_height
|
||||
layout_state.add_height(layout_state.remaining_page_height)
|
||||
|
||||
for paragraph in self.paragraphs:
|
||||
paragraph_layout_state = copy(layout_state)
|
||||
paragraph_layout_state.max_width -= LISTING_OFFSET
|
||||
paragraph_rendered_info = next(paragraph.render(previous, paragraph_layout_state))
|
||||
|
||||
if paragraph_rendered_info.height > layout_state.remaining_page_height: # todo add before after
|
||||
table_rendered_info = RenderedInfo(table, table_height)
|
||||
yield table_rendered_info
|
||||
|
||||
table_height = Pt(1) # table borders, 4 eights of point for each border
|
||||
|
||||
continuation_paragraph = Paragraph(self._parent)
|
||||
continuation_paragraph.add_run(f"Продолжение Листинга {self._number}")
|
||||
continuation_paragraph.style = "Caption Listing"
|
||||
continuation_paragraph.first_line_indent = 0
|
||||
continuation_paragraph.page_break_before = True
|
||||
|
||||
continuation_rendered_info = next(
|
||||
continuation_paragraph.render(None, copy(layout_state)))
|
||||
|
||||
layout_state.add_height(continuation_rendered_info.height)
|
||||
yield continuation_rendered_info
|
||||
|
||||
table = self._create_table(self._parent, layout_state.max_width)
|
||||
if self._should_split_fragment(
|
||||
paragraph_rendered_info.height, layout_state, lines_in_fragment
|
||||
):
|
||||
yield RenderedInfo(table, table_height)
|
||||
yield from self._emit_page_break_and_optional_caption(layout_state)
|
||||
|
||||
table_height = Pt(1)
|
||||
table = create_table(self._parent, 0, 1, col_w)
|
||||
set_table_box_borders(table)
|
||||
previous = None
|
||||
lines_in_fragment = 0
|
||||
|
||||
paragraph_layout_state = copy(layout_state)
|
||||
paragraph_layout_state.max_width -= LISTING_OFFSET
|
||||
paragraph_rendered_info = next(paragraph.render(previous, paragraph_layout_state))
|
||||
|
||||
table._cells[0]._element.append(paragraph_rendered_info.docx_element._element)
|
||||
row = create_table_row(table, header=False)
|
||||
cell = create_table_cell(row, col_w)
|
||||
cell._element.append(paragraph_rendered_info.docx_element._element)
|
||||
row._element.append(cell._element)
|
||||
table._element.append(row._element)
|
||||
|
||||
layout_state.add_height(paragraph_rendered_info.height)
|
||||
table_height += paragraph_rendered_info.height
|
||||
lines_in_fragment += 1
|
||||
|
||||
previous = paragraph_rendered_info
|
||||
|
||||
yield RenderedInfo(table, table_height)
|
||||
|
||||
def _listing_width(self, layout_state: LayoutState) -> Length:
|
||||
left_margin = Twips(int(
|
||||
self._parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:left")[0].attrib[
|
||||
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
|
||||
right_margin = Twips(int(
|
||||
self._parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:right")[0].attrib[
|
||||
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
|
||||
return Twips(_twips(layout_state.max_width) + _twips(left_margin) + _twips(right_margin))
|
||||
|
||||
@@ -89,14 +89,16 @@ class Paragraph(Renderable):
|
||||
return link
|
||||
|
||||
def add_inline_equation(self, formula: str):
|
||||
# omml = inline_omml(latex_to_omml(formula))
|
||||
# for r in omml.xpath("//m:r", namespaces=omml.nsmap):
|
||||
# r.append(create_element("w:rPr", [
|
||||
# create_element("w:sz", {"w:val": "24"}),
|
||||
# create_element("w:szCs", {"w:val": "24"}),
|
||||
# ]))
|
||||
# self._docx_paragraph._element.append(omml)
|
||||
self.add_run(formula, is_italic=True)
|
||||
try:
|
||||
omml = inline_omml(latex_to_omml(formula))
|
||||
for r in omml.xpath("//m:r", namespaces=omml.nsmap):
|
||||
r.append(create_element("w:rPr", [
|
||||
create_element("w:sz", {"w:val": "28"}),
|
||||
create_element("w:szCs", {"w:val": "28"}),
|
||||
]))
|
||||
self._docx_paragraph._element.append(omml)
|
||||
except Exception:
|
||||
self.add_run(formula, is_italic=True)
|
||||
|
||||
@property
|
||||
def page_break_before(self) -> bool:
|
||||
@@ -170,6 +172,12 @@ class Paragraph(Renderable):
|
||||
images = iter(self._images)
|
||||
|
||||
for image in images:
|
||||
# +landscape: section break handles the new page. Do NOT measure against
|
||||
# portrait remaining (that queues add_to_new_page and leaves an empty page).
|
||||
if getattr(image, "landscape", False):
|
||||
yield SubRenderable(image, False)
|
||||
continue
|
||||
|
||||
rendered_image = list(image.render(previous_rendered, copy(layout_state)))
|
||||
rendered_image_height = sum([x.height for x in rendered_image])
|
||||
if rendered_image:
|
||||
|
||||
@@ -18,8 +18,9 @@ CELL_OFFSET = Pt(9) - Twips(108 * 2)
|
||||
# Slack only for modes that fragment by our height estimate (legacy/caption).
|
||||
ROW_HEIGHT_SLACK = Pt(4)
|
||||
|
||||
# Modes that do NOT cut the table into fragments — Word owns page breaks.
|
||||
_WORD_PAGED_MODES = frozenset({"off", "soft"})
|
||||
# Modes that do NOT cut the table into fragments — Word owns page breaks
|
||||
# (word: post-split via COM after save).
|
||||
_WORD_PAGED_MODES = frozenset({"off", "soft", "word"})
|
||||
|
||||
|
||||
class Table(Renderable, RequiresNumbering):
|
||||
@@ -35,6 +36,7 @@ class Table(Renderable, RequiresNumbering):
|
||||
|
||||
self._cell_margin_lr = left_margin + right_margin
|
||||
self._number = "?"
|
||||
self.landscape = bool(caption_info and caption_info.landscape)
|
||||
if caption_info and caption_info.unique_name:
|
||||
self.unique_name = caption_info.unique_name
|
||||
|
||||
@@ -144,7 +146,7 @@ class Table(Renderable, RequiresNumbering):
|
||||
def _make_continuation_paragraph(self) -> Paragraph:
|
||||
continuation_paragraph = Paragraph(self._parent)
|
||||
continuation_paragraph.add_run(f"Продолжение Таблицы {self._number}")
|
||||
continuation_paragraph.style = "Caption Table"
|
||||
continuation_paragraph.style = "Название таблицы"
|
||||
continuation_paragraph.first_line_indent = 0
|
||||
return continuation_paragraph
|
||||
|
||||
|
||||
@@ -116,7 +116,12 @@ class ToC(Renderable):
|
||||
continue
|
||||
|
||||
display = title.strip()
|
||||
if display.upper() in SPECIAL_TITLES or display.upper().startswith("ПРИЛОЖЕНИЕ"):
|
||||
# Special sections and numbered H1 → uppercase in TOC (method guide)
|
||||
if (
|
||||
display.upper() in SPECIAL_TITLES
|
||||
or display.upper().startswith("ПРИЛОЖЕНИЕ")
|
||||
or (level == 1 and numbered)
|
||||
):
|
||||
display = display.upper()
|
||||
|
||||
p.add_run(" " * (level - 1))
|
||||
|
||||
Reference in New Issue
Block a user