@@ -293,7 +293,9 @@ class Paragraph(Renderable):
|
||||
yield SubRenderable(image, False)
|
||||
layout_state.add_height(rendered_image_height)
|
||||
else:
|
||||
# Defer this image and any remaining siblings to the next page
|
||||
# (as SubRenderable — bare Image objects break the renderer).
|
||||
yield SubRenderable(image, True)
|
||||
break
|
||||
|
||||
yield from images
|
||||
for rest in images:
|
||||
yield SubRenderable(rest, True)
|
||||
return
|
||||
|
||||
+93
-11
@@ -113,6 +113,10 @@ class Table(Renderable, RequiresNumbering):
|
||||
cell_height = 0
|
||||
if not (apply_merge and merge_v == "continue"):
|
||||
for paragraph in row[col]:
|
||||
# Method guide §3: do not leave caption + column headers alone
|
||||
# at page bottom — glue header row to the following data row.
|
||||
if row_idx == 0:
|
||||
paragraph._docx_paragraph.paragraph_format.keep_with_next = True
|
||||
cell_layout_state = LayoutState(
|
||||
layout_state.max_height, layout_state.max_width
|
||||
)
|
||||
@@ -133,6 +137,62 @@ class Table(Renderable, RequiresNumbering):
|
||||
)
|
||||
return docx_row, row_height
|
||||
|
||||
def _peek_row_height(self, row_idx: int, layout_state: LayoutState) -> Length:
|
||||
"""Estimate row height without attaching cell paragraphs to a table."""
|
||||
from .paragraph_sizer import ParagraphSizer
|
||||
|
||||
row = self._rows[row_idx]
|
||||
merge_row = self._merge[row_idx]
|
||||
row_height = 0
|
||||
col_w = self._col_width(layout_state)
|
||||
col = 0
|
||||
while col < self._cols:
|
||||
merge_v, merge_h = merge_row[col]
|
||||
if merge_h == "continue":
|
||||
col += 1
|
||||
continue
|
||||
span = self._grid_span(merge_row, col)
|
||||
if span < 1:
|
||||
span = 1
|
||||
cell_height = 0
|
||||
if merge_v != "continue":
|
||||
max_w = col_w * span - CELL_OFFSET
|
||||
for paragraph in row[col]:
|
||||
cell_height += ParagraphSizer(
|
||||
paragraph._docx_paragraph, None, max_w
|
||||
).calculate_height().full
|
||||
row_height = max(cell_height, row_height)
|
||||
col += span
|
||||
row_height += Pt(0.5) + (
|
||||
ROW_HEIGHT_SLACK if self._continuation_mode not in _WORD_PAGED_MODES else Pt(0)
|
||||
)
|
||||
return row_height
|
||||
|
||||
def _start_block_height(
|
||||
self, previous_rendered: RenderedInfo | None, layout_state: LayoutState
|
||||
) -> Length:
|
||||
"""Caption + header + first data row (method: no orphan title/header)."""
|
||||
from docx.text.paragraph import Paragraph as DocxParagraph
|
||||
from .paragraph_sizer import ParagraphSizer
|
||||
|
||||
# Dry-run caption sizing (temporary Caption; not yielded into the body).
|
||||
cap = Caption(
|
||||
self._parent, "Таблица", self._caption_info, self._number, True
|
||||
)
|
||||
prev_p = (
|
||||
previous_rendered.docx_element
|
||||
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph)
|
||||
else None
|
||||
)
|
||||
total = ParagraphSizer(
|
||||
cap._docx_paragraph, prev_p, layout_state.max_width
|
||||
).calculate_height().full
|
||||
if self._rows:
|
||||
total += self._peek_row_height(0, layout_state)
|
||||
if len(self._rows) > 1:
|
||||
total += self._peek_row_height(1, layout_state)
|
||||
return total
|
||||
|
||||
def _should_split_fragment(
|
||||
self, row_height: Length, layout_state: LayoutState, rows_in_fragment: int
|
||||
) -> bool:
|
||||
@@ -169,10 +229,29 @@ class Table(Renderable, RequiresNumbering):
|
||||
|
||||
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState) \
|
||||
-> Generator[RenderedInfo, None, None]:
|
||||
# Method guide: never leave only «Таблица N» or «Таблица N» + column
|
||||
# headers at the bottom of a page. If caption+header+first data row do
|
||||
# not fit, push the whole block to the next page (layout leftover;
|
||||
# Word also gets keep_with_next on caption/header).
|
||||
leftover = Pt(0)
|
||||
start_h = self._start_block_height(previous_rendered, layout_state)
|
||||
if (
|
||||
layout_state.current_page_height > 0
|
||||
and start_h > layout_state.remaining_page_height
|
||||
):
|
||||
leftover = layout_state.remaining_page_height
|
||||
layout_state.add_height(leftover)
|
||||
previous_rendered = None
|
||||
|
||||
caption_rendered_infos = list(
|
||||
Caption(self._parent, "Таблица", self._caption_info, self._number, True)
|
||||
.render(previous_rendered, copy(layout_state))
|
||||
)
|
||||
if leftover and caption_rendered_infos:
|
||||
first = caption_rendered_infos[0]
|
||||
caption_rendered_infos[0] = RenderedInfo(
|
||||
first.docx_element, first.height + leftover
|
||||
)
|
||||
layout_state.add_height(sum(info.height for info in caption_rendered_infos))
|
||||
yield from caption_rendered_infos
|
||||
|
||||
@@ -190,19 +269,22 @@ class Table(Renderable, RequiresNumbering):
|
||||
)
|
||||
|
||||
if self._should_split_fragment(row_height, layout_state, rows_in_fragment):
|
||||
yield RenderedInfo(docx_table, table_height)
|
||||
yield from self._emit_page_break_and_optional_caption(layout_state)
|
||||
# Never cut after the header alone (method: no title+column headers
|
||||
# without a data row). Prefer slight overflow over an orphan fragment.
|
||||
if rows_in_fragment >= 2:
|
||||
yield RenderedInfo(docx_table, table_height)
|
||||
yield from self._emit_page_break_and_optional_caption(layout_state)
|
||||
|
||||
docx_table = create_table(
|
||||
self._parent, 0, self._cols, self._table_width(layout_state)
|
||||
)
|
||||
apply_merge = False
|
||||
rows_in_fragment = 0
|
||||
table_height = Pt(0.5)
|
||||
docx_table = create_table(
|
||||
self._parent, 0, self._cols, self._table_width(layout_state)
|
||||
)
|
||||
apply_merge = False
|
||||
rows_in_fragment = 0
|
||||
table_height = Pt(0.5)
|
||||
|
||||
docx_row, row_height = self._build_docx_row(
|
||||
docx_table, row_idx, apply_merge, layout_state
|
||||
)
|
||||
docx_row, row_height = self._build_docx_row(
|
||||
docx_table, row_idx, apply_merge, layout_state
|
||||
)
|
||||
|
||||
docx_table._element.append(docx_row._element)
|
||||
layout_state.add_height(row_height)
|
||||
|
||||
Reference in New Issue
Block a user