| 1 | """In-memory flattening of positional ``<tspan>`` elements. |
| 2 | |
| 3 | DrawingML's text-run model has no way to express "jump to a new x/y inside |
| 4 | the same paragraph". Every ``<tspan>`` carrying ``x``, ``y`` or non-zero |
| 5 | ``dy`` is therefore a layout instruction this converter cannot honour |
| 6 | inline — without flattening, a 4-line dy-stacked block collapses onto a |
| 7 | single baseline and an x-anchored tspan jumps to the wrong column. |
| 8 | |
| 9 | The on-disk ``finalize_svg`` pipeline solves this by promoting each |
| 10 | positional tspan to an independent ``<text>`` element. This module |
| 11 | performs the same transformation in memory so ``svg_to_pptx`` can consume |
| 12 | ``svg_output/`` directly without that disk step. |
| 13 | |
| 14 | Public API: |
| 15 | flatten_positional_tspans(tree) -> bool |
| 16 | Walk the SVG element tree, replace every positional ``<tspan>`` |
| 17 | with an independent ``<text>``, and return whether anything |
| 18 | changed. |
| 19 | |
| 20 | Heavy lifting is delegated to ``svg_finalize.flatten_tspan`` so the two |
| 21 | pipelines stay behaviourally aligned. |
| 22 | """ |
| 23 | |
| 24 | from __future__ import annotations |
| 25 | |
| 26 | import sys |
| 27 | from pathlib import Path |
| 28 | from xml.etree import ElementTree as ET |
| 29 | |
| 30 | |
| 31 | def _flatten_module(): |
| 32 | """Load the shared on-disk flattener after exposing the scripts root.""" |
| 33 | scripts_dir = Path(__file__).resolve().parent.parent |
| 34 | if str(scripts_dir) not in sys.path: |
| 35 | sys.path.insert(0, str(scripts_dir)) |
| 36 | from svg_finalize import flatten_tspan # type: ignore |
| 37 | return flatten_tspan |
| 38 | |
| 39 | |
| 40 | def flatten_positional_tspans( |
| 41 | tree: ET.ElementTree, |
| 42 | merge_paragraphs: bool = False, |
| 43 | preserve_line_breaks: bool = False, |
| 44 | ) -> bool: |
| 45 | """Flatten positional ``<tspan>`` elements into independent ``<text>``. |
| 46 | |
| 47 | Delegates to ``svg_finalize.flatten_tspan.flatten_text_with_tspans`` so |
| 48 | the in-memory transform exactly matches the on-disk one. When |
| 49 | ``merge_paragraphs`` is True, mergeable paragraph blocks are preserved |
| 50 | as a single <text>. ``preserve_line_breaks`` marks visual rows for hard |
| 51 | DrawingML line breaks instead of reflow. |
| 52 | |
| 53 | Returns True if any tspan was rewritten. |
| 54 | """ |
| 55 | return _flatten_module().flatten_text_with_tspans( |
| 56 | tree, |
| 57 | merge_paragraphs=merge_paragraphs, |
| 58 | preserve_line_breaks=preserve_line_breaks, |
| 59 | ) |
| 60 | |
| 61 | |
| 62 | def nested_positional_tspan_errors(root: ET.Element) -> list[str]: |
| 63 | """Return shared diagnostics for unsupported nested baseline jumps.""" |
| 64 | return _flatten_module().nested_positional_tspan_errors(root) |
| 65 |