Files
hermes-skills/skills/legal/contract-editor/references/strip-numpr-before-manual-numbering.md
T

3.8 KiB

Strip numPr Before Inserting Manual Numbering (2026-07-01)

Problem

When adding manual numbering (e.g. "第一条 ") as w:ins at the beginning of a paragraph that already has <w:numPr> (automatic numbering like "%1." decimal format), OnlyOffice renders BOTH:

  • The automatic number: "1."
  • The manual INS text: "第一条"

Result: "1. 第一条 乙方应严格按照..."

Root Cause

<w:numPr> in pPr tells the rendering engine to prepend an auto-generated number. The w:ins text is just another run in the paragraph — it doesn't suppress the auto-numbering.

Additionally, <w:pPrChange> records the pre-revision pPr state. If pPrChange still contains <w:numPr>, some renderers will show the old numbering in markup view.

Affected Scenarios

  1. 反委托代发工资协议 (2026-07-01): Original paragraphs P6-P12, P19 had numId=1 or numId=3 (decimal "%1." format). After adding "第一条" through "第十一条" as INS, OnlyOffice showed "1. 第一条", "2. 第二条", etc.

  2. Any contract where the original used auto-numbering: Check numbering.xml for active numId references with numFmt=decimal or numFmt=chineseCounting.

Fix Pattern

import zipfile
from lxml import etree

WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'

with zipfile.ZipFile(docx_path, 'r') as z:
    all_files = {name: z.read(name) for name in z.namelist()}

doc = etree.fromstring(all_files['word/document.xml'])
body = doc.find(f'{WNS}body')
paras = body.findall(f'{WNS}p')

# Identify paragraphs where we added manual numbering INS
# These are paragraphs that have both:
# 1. A w:ins with author=WB containing "第X条" text
# 2. A pPr with numPr

for i, p in enumerate(paras):
    ppr = p.find(f'{WNS}pPr')
    if ppr is None:
        continue
    
    # Check if this paragraph has our manual numbering INS
    has_manual_numbering = False
    for child in p:
        tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag
        if tag == 'ins' and child.get(f'{WNS}author') == 'WB':
            text = ''.join(t.text for t in child.iter(f'{WNS}t') if t.text)
            if '第' in text and '条' in text:
                has_manual_numbering = True
                break
    
    if not has_manual_numbering:
        continue
    
    # Strip numPr from pPr
    num_pr = ppr.find(f'{WNS}numPr')
    if num_pr is not None:
        ppr.remove(num_pr)
        print(f"P{i}: Stripped numPr from pPr")
    
    # Strip numPr from pPrChange
    ppc = ppr.find(f'{WNS}pPrChange')
    if ppc is not None:
        inner_ppr = ppc.find(f'{WNS}pPr')
        if inner_ppr is not None:
            inner_num = inner_ppr.find(f'{WNS}numPr')
            if inner_num is not None:
                inner_ppr.remove(inner_num)
                print(f"P{i}: Stripped numPr from pPrChange")

# Save back
all_files['word/document.xml'] = etree.tostring(doc, xml_declaration=True, encoding='UTF-8', standalone=True)
# Write to temp file then replace (never write to same zip you're reading)

Verification

After stripping, render with OnlyOffice and confirm:

  1. Markup view shows only the manual numbering (no "1." prefix)
  2. Accepted-revisions view shows clean "第一条" through "第十一条"
  3. python-docx can still open the file without errors

Edge Cases

  • DEL-only empty paragraphs (e.g. P8, P9 where all content is w:del): These may still have numPr. If they render a visible "3." or "4." in the gap, strip those too.
  • Cross-paragraph clauses (P6+P7 = one clause): P7 may have its own independent numPr even though it's a continuation paragraph. Strip it.
  • numId=0 (disabled numbering): numId=0 in OOXML means "numbering OFF" — it doesn't render anything. Only strip numPr where numId > 0 and the corresponding abstractNum has a visible numFmt (decimal, chineseCounting, etc.).