Files
Igor20264 febf62c257
Python application / build (push) Has been cancelled
hot-fix 0.4.3
- fix Не появляется пустая странициа перед названием таблицы
- fix Не появляется пустая страница перед альбомной ориентацией
- fix При отказе от изменений создаётся новый файл
- fix Замена "..." на «...» больше не затрагивает uml и mermaid блоки
2026-09-05 10:24:13 +03:00

208 lines
9.2 KiB
Python

from copy import copy
import os
from typing import Generator, Callable
from docx.shared import Length, Pt, RGBColor, Twips
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, 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):
Formatter.__init__(self, style="sas", **options)
self._creator = creator
self._paragraphs = paragraphs
self._styles = {}
for token, style in self.style:
self._styles[token] = style
def _add_run_to_last_paragraph(self, text, style):
self._paragraphs[-1].add_run(text, style["bold"] or None, style["italic"] in style or None,
RGBColor.from_string(style['color']) if style['color'] else None)
def format(self, tokensource, outfile):
self._paragraphs.append(self._creator())
for ttype, value in tokensource:
style = self._styles[ttype]
lines = iter(value.split("\n"))
self._add_run_to_last_paragraph(next(lines), style)
for line in lines:
self._paragraphs.append(self._creator())
self._add_run_to_last_paragraph(line, style)
self._paragraphs.pop(-1) # remove last empty line
LISTING_OFFSET = Pt(31) - Twips(108 * 2) # todo: fix
class Listing(Renderable, RequiresNumbering):
def __init__(self, parent, language: str, caption_info: CaptionInfo):
super().__init__("Листинг")
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):
# 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"]))
right_margin = Twips(int(
parent.part.styles["Normal Table"]._element.xpath("w:tblPr/w:tblCellMar/w:right")[0].attrib[
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}w"]))
return create_table(
parent, 1, 1,
Twips(_twips(width) + _twips(left_margin) + _twips(right_margin)),
)
def set_text(self, text: str):
def create_paragraph() -> Paragraph:
paragraph = Paragraph(self._parent)
paragraph.style = "Code"
return paragraph
text = text.removesuffix("\n")
if self._language and "SYNTAX_HIGHLIGHTING" in os.environ and os.environ["SYNTAX_HIGHLIGHTING"] == "1":
formatter = DocxParagraphPygmentsFormatter(self.paragraphs, lambda: create_paragraph())
highlight(text, get_lexer_by_name(self._language), formatter)
else:
for line in text.removesuffix("\n").split("\n"):
paragraph = create_paragraph()
paragraph.add_run(line)
self.paragraphs.append(paragraph)
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]:
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
continuation_paragraph._docx_paragraph.paragraph_format.page_break_before = False
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(
Caption(self._parent, "Листинг", self._caption_info, self._number, True)
.render(previous_rendered, copy(layout_state))
)
layout_state.add_height(sum([info.height for info in caption_rendered_infos]))
yield from caption_rendered_infos
# 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)
# 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 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))
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))