[Bug]: WordParser splices a text box into the paragraph, and reads it twice when it has a VML fallback
Prerequisites
- I have searched the existing issues and discussions, and this is not a duplicate.
- This is a bug, not a usage question.
Background / Description
WordParser's _extract_text_from_paragraph starts by concatenating every
w:t under the paragraph (rag/_parser/_word.py):
text = ""
for t_elem in para._element.findall(".//" + qn("w:t")):
if t_elem.text:
text += t_elem.text.// is a descendant search, so it also reaches the w:t elements inside a
text box, which live under w:txbxContent in the drawing inside a run. Two
things follow, both silent.
1. The text box is spliced into the sentence with no separator. A paragraph
reading "Paragraph text" that anchors a callout reading "Callout" comes back as
Paragraph textCallout. textCallout is a word that appears in no document and
is what gets embedded.
2. The text box is read twice. Word writes it twice inside
mc:AlternateContent: a wps:txbx under mc:Choice, and the same text as a
VML text box under mc:Fallback. Both hold a w:txbxContent, so the sweep
above sees the callout twice. The function's later v:textbox pass would have
been a third read, but it is unreachable — it only runs if not text, which is
false for any paragraph that has text.
Error Messages
No exception is raised; the text is corrupted in place.Steps to Reproduce
- Code (the second template is the shape Word actually writes):
import asyncio
import io
import docx
from lxml import etree
from agentscope.rag._parser._word import WordParser
NS = " ".join(
f'xmlns:{p}="{u}"'
for p, u in (
("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"),
("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006"),
("wps", "http://schemas.microsoft.com/office/word/2010/wordprocessingShape"),
("a", "http://schemas.openxmlformats.org/drawingml/2006/main"),
("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"),
("v", "urn:schemas-microsoft-com:vml"),
)
)
ALTERNATE = f"""
<w:p {NS}>
<w:r><w:t>PARAGRAPH TEXT</w:t></w:r>
<w:r><mc:AlternateContent>
<mc:Choice Requires="wps"><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="2743200" cy="914400"/><wp:docPr id="1" name="Text Box 1"/>
<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<wps:wsp><wps:txbx><w:txbxContent>
<w:p><w:r><w:t>CALLOUT</w:t></w:r></w:p>
</w:txbxContent></wps:txbx></wps:wsp>
</a:graphicData></a:graphic>
</wp:inline></w:drawing></mc:Choice>
<mc:Fallback><w:pict><v:shape id="_x0000_s1026" type="#_x0000_t202">
<v:textbox><w:txbxContent>
<w:p><w:r><w:t>CALLOUT</w:t></w:r></w:p>
</w:txbxContent></v:textbox>
</v:shape></w:pict></mc:Fallback>
</mc:AlternateContent></w:r>
</w:p>
"""
def build(xml: str) -> bytes:
document = docx.Document()
body = document.element.body
body.insert(len(body) - 1, etree.fromstring(xml.strip()))
document.add_paragraph("AFTER")
buffer = io.BytesIO()
document.save(buffer)
return buffer.getvalue()
async def main() -> None:
sections = await WordParser().parse(build(ALTERNATE), "doc.docx")
print(repr("\n".join(s.content.text for s in sections)))
asyncio.run(main())Run:
python repro.pyObserved:
'PARAGRAPH TEXTCALLOUTCALLOUT\nAFTER'With a plain wps:txbx (no mc:AlternateContent) the same script prints
'PARAGRAPH TEXTCALLOUT\nAFTER' — one copy, still glued to the sentence.
Expected: 'PARAGRAPH TEXT\nCALLOUT\nAFTER' in both cases.
Environment
- AgentScope Version: 2.0.8 (
main@0b157a31) - Python Version: 3.11
- OS: macOS 15 (python-docx is platform independent, so this is not OS specific)
Source: agentscope-ai/agentscope