Update 0.4.0
Python application / build (push) Has been cancelled

- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
This commit is contained in:
Igor20264
2026-09-04 22:28:39 +03:00
parent 516abe7b83
commit b38661f588
70 changed files with 69532 additions and 413 deletions
+91 -32
View File
@@ -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))