Files
md_to_gost/md2gost/renderable/paragraph.py
T
Igor20264 b38661f588
Python application / build (push) Has been cancelled
Update 0.4.0
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00

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