diff --git a/haystack/components/converters/pptx.py b/haystack/components/converters/pptx.py index f4d9fed086..ec8961b592 100644 --- a/haystack/components/converters/pptx.py +++ b/haystack/components/converters/pptx.py @@ -14,7 +14,8 @@ with LazyImport("Run 'pip install python-pptx'") as pptx_import: from pptx import Presentation - from pptx.text.text import _Paragraph + from pptx.oxml.text import CT_RegularTextRun, CT_TextLineBreak + from pptx.text.text import _Paragraph, _Run logger = logging.getLogger(__name__) @@ -100,17 +101,26 @@ def _process_paragraph(self, paragraph: "_Paragraph") -> str: """ if self.link_format == "none": return paragraph.text + # paragraph.runs skips soft line breaks (). Walk every content child so a + # Shift+Enter break survives, matching paragraph.text (a vertical tab). parts = [] - for run in paragraph.runs: - if run.hyperlink and run.hyperlink.address: - if self.link_format == "markdown": - parts.append(f"[{run.text}]({run.hyperlink.address})") - else: - parts.append(f"{run.text} ({run.hyperlink.address})") + for child in paragraph._element.content_children: + if isinstance(child, CT_TextLineBreak): + parts.append(child.text) + elif isinstance(child, CT_RegularTextRun): + parts.append(self._format_run(_Run(child, paragraph))) else: - parts.append(run.text) + parts.append(child.text) return "".join(parts) + def _format_run(self, run: "_Run") -> str: + """Format one run, keeping a hyperlink when link_format asks for it.""" + if run.hyperlink and run.hyperlink.address: + if self.link_format == "markdown": + return f"[{run.text}]({run.hyperlink.address})" + return f"{run.text} ({run.hyperlink.address})" + return run.text + @component.output_types(documents=list[Document]) def run( self, sources: list[str | Path | ByteStream], meta: dict[str, Any] | list[dict[str, Any]] | None = None diff --git a/releasenotes/notes/pptx-soft-line-break-4e8b1c9a2d7f6053.yaml b/releasenotes/notes/pptx-soft-line-break-4e8b1c9a2d7f6053.yaml new file mode 100644 index 0000000000..9586876a45 --- /dev/null +++ b/releasenotes/notes/pptx-soft-line-break-4e8b1c9a2d7f6053.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + ``PPTXToDocument`` keeps soft line breaks (Shift+Enter) when ``link_format`` is + ``markdown`` or ``plain``. Those breaks were already kept for ``link_format="none"``. diff --git a/test/components/converters/test_pptx_to_document.py b/test/components/converters/test_pptx_to_document.py index 647a2a3571..2eeb816de8 100644 --- a/test/components/converters/test_pptx_to_document.py +++ b/test/components/converters/test_pptx_to_document.py @@ -123,3 +123,21 @@ def test_no_link_extraction(self, test_files_path): assert "https://example.com" not in content assert "Example" in content + + def test_soft_line_break_survives_every_link_format(self): + from io import BytesIO + + from pptx import Presentation + from pptx.util import Inches + + presentation = Presentation() + slide = presentation.slides.add_slide(presentation.slide_layouts[6]) + frame = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(7), Inches(2)).text_frame + frame.text = "Account: Alice\vBalance: 100" + buffer = BytesIO() + presentation.save(buffer) + source = ByteStream(data=buffer.getvalue()) + + for link_format in ("none", "markdown", "plain"): + documents = PPTXToDocument(link_format=link_format).run(sources=[source])["documents"] + assert documents[0].content == "Account: Alice\vBalance: 100"