b38661f588
Python application / build (push) Has been cancelled
- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
193 lines
7.9 KiB
Python
193 lines
7.9 KiB
Python
from copy import copy
|
|
from typing import Generator, Any
|
|
|
|
from docx.shared import Length, Parented, RGBColor
|
|
from docx.text.paragraph import Paragraph as DocxParagraph
|
|
from docx.text.paragraph import Run as DocxRun
|
|
from docx.enum.text import WD_LINE_SPACING
|
|
from docx.opc.constants import RELATIONSHIP_TYPE
|
|
|
|
from . import Renderable
|
|
from .caption import CaptionInfo
|
|
from .image import Image
|
|
from .paragraph_sizer import ParagraphSizer
|
|
from ..layout_tracker import LayoutState
|
|
from ..sub_renderable import SubRenderable
|
|
from ..util import create_element
|
|
from ..rendered_info import RenderedInfo
|
|
from ..latex_math import latex_to_omml, inline_omml
|
|
|
|
|
|
class Link:
|
|
def __init__(self, url, docx_paragraph: DocxParagraph):
|
|
self._docx_paragraph = docx_paragraph
|
|
r_id = docx_paragraph.part.relate_to(url, RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
|
|
|
|
self._hyperlink = create_element("w:hyperlink", {
|
|
"r:id": r_id
|
|
})
|
|
|
|
def add_run(self, text: str, is_bold: bool = None, is_italic: bool = None, color: RGBColor = None,
|
|
strike_through: bool = None):
|
|
|
|
parts = text.split("-")
|
|
for i, part in enumerate(parts):
|
|
docx_run = DocxRun(create_element("w:r"), self._docx_paragraph)
|
|
self._hyperlink.append(docx_run._element)
|
|
docx_run.text = text
|
|
docx_run.style = "Hyperlink"
|
|
docx_run.bold = is_bold
|
|
docx_run.italic = is_italic
|
|
docx_run.font.color.rgb = color
|
|
docx_run.font.strike = strike_through
|
|
if i != len(parts) - 1:
|
|
self._hyperlink.append(create_element("w:r", [create_element("w:noBreakHyphen")]))
|
|
|
|
@property
|
|
def element(self):
|
|
return self._hyperlink
|
|
|
|
|
|
class Paragraph(Renderable):
|
|
def __init__(self, parent: Parented):
|
|
self._parent = parent
|
|
self._docx_paragraph = DocxParagraph(create_element("w:p"), parent)
|
|
self._docx_paragraph.style = "Normal"
|
|
self._images: list[Image] = []
|
|
self._pending_refs: list[tuple[object, str, str]] = [] # (run, type, name)
|
|
|
|
def add_run(self, text: str, is_bold: bool = None, is_italic: bool = None, color: RGBColor = None,
|
|
strike_through: bool = None):
|
|
# replace all hyphens with non-breaking hyphens
|
|
parts = text.split("-")
|
|
for i, part in enumerate(parts):
|
|
docx_run = self._docx_paragraph.add_run(part)
|
|
docx_run.bold = is_bold
|
|
docx_run.italic = is_italic
|
|
docx_run.font.color.rgb = color
|
|
docx_run.font.strike = strike_through
|
|
if i != len(parts)-1:
|
|
self._docx_paragraph.add_run()._element.\
|
|
append(create_element("w:noBreakHyphen"))
|
|
|
|
def add_reference(self, type_: str, name: str):
|
|
"""Placeholder run resolved later via resolve_pending_refs()."""
|
|
run = self._docx_paragraph.add_run(f"{type_}?")
|
|
self._pending_refs.append((run, type_, name))
|
|
|
|
def resolve_pending_refs(self, resolve_fn):
|
|
for run, type_, name in self._pending_refs:
|
|
run.text = resolve_fn(type_, name)
|
|
self._pending_refs.clear()
|
|
|
|
def add_image(self, path: str, caption_info: CaptionInfo):
|
|
self._images.append(Image(self._parent, path, caption_info))
|
|
|
|
def add_link(self, url: str):
|
|
link = Link(url, self._docx_paragraph)
|
|
self._docx_paragraph._p.append(link.element)
|
|
return link
|
|
|
|
def add_inline_equation(self, formula: str):
|
|
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:
|
|
return self._docx_paragraph.paragraph_format.page_break_before
|
|
|
|
@page_break_before.setter
|
|
def page_break_before(self, value: bool):
|
|
self._docx_paragraph.paragraph_format.page_break_before = value
|
|
|
|
@property
|
|
def style(self):
|
|
return self._docx_paragraph.style
|
|
|
|
@style.setter
|
|
def style(self, value: str):
|
|
self._docx_paragraph.style = value
|
|
|
|
@property
|
|
def first_line_indent(self):
|
|
return self._docx_paragraph.paragraph_format.first_line_indent
|
|
|
|
@first_line_indent.setter
|
|
def first_line_indent(self, value: Length):
|
|
self._docx_paragraph.paragraph_format.first_line_indent = value
|
|
|
|
def render(self, previous_rendered: RenderedInfo, layout_state: LayoutState)\
|
|
-> Generator[RenderedInfo | SubRenderable, None, None]:
|
|
remaining_space = layout_state.remaining_page_height
|
|
|
|
if self.page_break_before:
|
|
layout_state.add_height(layout_state.remaining_page_height)
|
|
if self._docx_paragraph.text or not self._images:
|
|
height_data = ParagraphSizer(
|
|
self._docx_paragraph,
|
|
previous_rendered.docx_element
|
|
if previous_rendered and isinstance(previous_rendered.docx_element, DocxParagraph) else None,
|
|
layout_state.max_width).calculate_height()
|
|
|
|
if layout_state.current_page_height == 0 and layout_state.page > 1:
|
|
height_data.before = 0
|
|
|
|
fitting_lines = 0
|
|
for lines in range(1, height_data.lines+1):
|
|
if height_data.before + ((lines - 1) * height_data.line_spacing + 1) * height_data.line_height \
|
|
> layout_state.remaining_page_height:
|
|
break
|
|
fitting_lines += 1
|
|
|
|
if fitting_lines == height_data.lines:
|
|
# the whole paragraph fits page
|
|
height = min(height_data.full, layout_state.remaining_page_height)
|
|
elif fitting_lines <= 1 or (height_data.lines-fitting_lines == 1 and height_data.lines == 3):
|
|
# if only no or only one line fits the page, paragraph goes to the next page
|
|
height = layout_state.remaining_page_height + height_data.full
|
|
elif height_data.lines-fitting_lines == 1:
|
|
# if all lines except last fit the page, the last two lines go to the new page
|
|
height = layout_state.remaining_page_height + \
|
|
height_data.before + height_data.line_height * height_data.line_spacing * 2 \
|
|
+ height_data.after
|
|
else:
|
|
height = layout_state.remaining_page_height + \
|
|
height_data.before + height_data.line_height * height_data.line_spacing * \
|
|
(height_data.lines-fitting_lines) + height_data.after
|
|
|
|
if self.page_break_before:
|
|
height += remaining_space
|
|
|
|
yield (previous_rendered := RenderedInfo(self._docx_paragraph, Length(height)))
|
|
layout_state.add_height(height)
|
|
|
|
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:
|
|
previous_rendered = rendered_image[-1]
|
|
if rendered_image_height <= layout_state.remaining_page_height:
|
|
yield SubRenderable(image, False)
|
|
layout_state.add_height(rendered_image_height)
|
|
else:
|
|
yield SubRenderable(image, True)
|
|
break
|
|
|
|
yield from images
|