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_documentsave_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→0tblBorders 1→0tblLook 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_cellreplace_text(推測所有標記 dirty 的 mutation 皆會觸發,未逐一驗證)

Type

bug

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