Skip to content

bug: 任何 mutation 都會清空 <w:tblPr> — table style/borders/look 靜默遺失(純 save round-trip 無損) #142

Description

@kiki830621

Problem

任何一個 mutation 操作(update_cell / replace_text …)都會讓 <w:tbl> 的 <w:tblPr> 被清空到只剩 <w:tblW>(與 <w:jc>,若原本有)。<w:tblStyle>、<w:tblBorders>、<w:tblLook> 全部靜默消失 —— 工具回報成功、檔案照常開得起來,只是表格框線與樣式不見了。

關鍵區分:純 save round-trip 無損。 只 open_document → save_document(零編輯)產出的 word/document.xml 與原檔 byte-identical。一旦文件被標記 dirty、改走「從解析後的模型重新序列化」,tblPr 就被截斷。這指向 parse 階段沒有把 tblPr 的子元素收進模型,而非 writer 的問題。

Reproduction

從零建立最小 fixture(python-docx),不依賴任何既有檔案:

from docx import Document
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

d = Document()
t = d.add_table(rows=2, cols=2)
t.style = 'Table Grid'                       # -> <w:tblStyle w:val="TableGrid"/>
t.cell(0,0).text = 'A'; t.cell(0,1).text = 'B'
t.cell(1,0).text = 'C'

tblPr = t._tbl.tblPr
b = OxmlElement('w:tblBorders')              # explicit borders, as Word writes them
for edge, sz in (('top','12'),('left','12'),('bottom','12'),('right','12'),
                 ('insideH','6'),('insideV','6')):
    e = OxmlElement(f'w:{edge}')
    e.set(qn('w:val'),'single'); e.set(qn('w:sz'),sz)
    e.set(qn('w:space'),'0'); e.set(qn('w:color'),'auto')
    b.append(e)
tblPr.append(b)
d.save('minimal_bordered.docx')

然後:

open_document(path='minimal_bordered.docx', doc_id='x', track_changes=false)
update_cell(doc_id='x', table_index=0, row=1, col=1, text='D')
save_document(doc_id='x', path='minimal_bordered_after.docx')

Expected

<w:tblPr> 內容不變 —— 一次 cell 文字更新不應觸碰表格層級屬性。

Actual

BEFORE(逐字取自 fixture 的 word/document.xml):

<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:type="auto" w:w="0"/><w:tblLook w:firstColumn="1" w:firstRow="1" w:lastColumn="0" w:lastRow="0" w:noHBand="0" w:noVBand="1" w:val="04A0"/><w:tblBorders><w:top w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="12" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="6" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="6" w:space="0" w:color="auto"/></w:tblBorders><w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr>

AFTER(同一份,只跑過一次 update_cell):

<w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>

tblStyle 1→0、tblBorders 1→0、tblLook 2→0。

Evidence matrix

同一份真實 Word 表單(2 欄 8 列表格,tblStyle + tblBorders + tblLook 齊全)跑四種變體,word/document.xml 字元數與元素計數:

變體 document.xml (chars) tblStyle tblBorders tblLook
原始 49,865 1 1 1
open → save,零編輯 49,865 1 1 1
只跑一次 update_cell 43,297 0 0 0
只跑一次 replace_text 42,367 0 0 0
兩者都跑 38,816 0 0 0

tcPr / trPr / tcW / vAlign / gridCol 計數在所有變體都不變 —— 只有 table 層級屬性遺失,cell/row 層級完好。這進一步把嫌疑收斂到 tblPr 的 parse/model 覆蓋範圍。

Round-trip 同時也會把 <w:szCs> 大量補寫進每個帶 <w:sz> 的 run(33 → 176)。那是 normalization、不是遺失,但顯示 rPr 也在被重寫,值得一併確認覆蓋範圍。

Impact

Suggested direction

  1. 修:把 tblPr 的完整子元素集合納入 model(或對未建模的子元素採 pass-through 保留原 XML node)。tblPr 的合法子元素還包含 tblpPr / tblOverlap / bidiVisual / tblStyleRowBandSize / tblStyleColBandSize / tblCellMar / tblCaption / tblDescription / shd 等,建議一次檢查完整清單而非只補這三個。
  2. 測:加一條 regression test —— 帶 tblStyle + tblBorders + tblLook 的 fixture,跑任一 mutation 後 assert tblPr 子元素集合不變。理想上對 tcPr / pPr / rPr / sectPr 做同樣的「mutation 不得改變未觸碰的屬性」不變式測試。
  3. 防:pass-through 保留未建模節點,比逐一補齊 model 更能防住同類問題(tblPr 只是第一個被發現的,其他容器很可能有相同缺口)。

Workaround

在修好之前,對含框線表格的既有 .docx 填表,改走 OPC zip 手術(直接改 word/document.xml 後原樣重打包,其餘 part 逐一 byte-for-byte 複製)。實測:14 個 zip part 中 13 個 byte-identical,只有 document.xml 變動,tblPr 完整保留。

Environment

  • plugin che-word-mcp 3.22.0(macdoc marketplace;mcp/che-word-mcp submodule 位於 v3.20.0-26-gb59d5f2)
  • macOS 26(Darwin 27.0.0)
  • fixture 由 python-docx 1.2.0 產生
  • 觸發工具:update_cell、replace_text(推測所有標記 dirty 的 mutation 皆會觸發,未逐一驗證)

Type

bug

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Priority 1 — 本週處理bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions