feat(md2doc): XML helpers — borders, shading, multi-level numbering
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Tests for XML helper functions — borders, shading, numbering.
|
||||
|
||||
These prove the EFFECT (XML structure lands in the right place), not the call.
|
||||
Each test unzips the docx and inspects word/document.xml directly.
|
||||
"""
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from docx import Document
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
from md2doc import (
|
||||
add_paragraph_bottom_border,
|
||||
add_paragraph_shading,
|
||||
add_cell_shading,
|
||||
attach_list_numbering,
|
||||
register_multilevel_numbering,
|
||||
)
|
||||
|
||||
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
def get_doc_xml(doc):
|
||||
tmp = Path(__file__).parent / "_tmp_helper_test.docx"
|
||||
doc.save(tmp)
|
||||
try:
|
||||
with zipfile.ZipFile(tmp) as z:
|
||||
xml_bytes = z.read("word/document.xml")
|
||||
return ET.fromstring(xml_bytes)
|
||||
finally:
|
||||
tmp.unlink()
|
||||
|
||||
|
||||
def find_all(root, tag):
|
||||
return root.findall(f".//{{{W_NS}}}{tag}")
|
||||
|
||||
|
||||
def test_add_paragraph_bottom_border_inserts_pBdr():
|
||||
"""H1: paragraph bottom border lands inside w:pPr as w:pBdr/w:bottom."""
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
add_paragraph_bottom_border(p, color="000000", size="12")
|
||||
root = get_doc_xml(doc)
|
||||
pBdr = find_all(root, "pBdr")
|
||||
bottom = find_all(root, "bottom")
|
||||
assert len(pBdr) >= 1, "w:pBdr missing"
|
||||
assert len(bottom) >= 1, "w:bottom missing"
|
||||
b = bottom[0]
|
||||
assert b.get(f"{{{W_NS}}}color") == "000000"
|
||||
assert b.get(f"{{{W_NS}}}sz") == "12"
|
||||
assert b.get(f"{{{W_NS}}}val") == "single"
|
||||
|
||||
|
||||
def test_add_paragraph_shading_inserts_shd():
|
||||
"""H2: paragraph shading w:shd lands inside w:pPr with correct fill."""
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
add_paragraph_shading(p, fill="D9D9D9")
|
||||
root = get_doc_xml(doc)
|
||||
shd = find_all(root, "shd")
|
||||
assert len(shd) >= 1, "w:shd missing"
|
||||
assert shd[0].get(f"{{{W_NS}}}fill") == "D9D9D9"
|
||||
assert shd[0].get(f"{{{W_NS}}}val") == "clear"
|
||||
|
||||
|
||||
def test_add_cell_shading_inserts_shd_in_tcPr():
|
||||
"""H4: cell shading w:shd lands inside w:tcPr."""
|
||||
doc = Document()
|
||||
table = doc.add_table(rows=2, cols=2)
|
||||
cell = table.rows[0].cells[0]
|
||||
add_cell_shading(cell, fill="4472C4")
|
||||
root = get_doc_xml(doc)
|
||||
tcPr_list = find_all(root, "tcPr")
|
||||
shd_in_tc = [tc.find(f"{{{W_NS}}}shd") for tc in tcPr_list
|
||||
if tc.find(f"{{{W_NS}}}shd") is not None]
|
||||
assert len(shd_in_tc) >= 1, "w:shd missing from w:tcPr"
|
||||
assert shd_in_tc[0].get(f"{{{W_NS}}}fill") == "4472C4"
|
||||
|
||||
|
||||
def test_register_multilevel_numbering_creates_abstractNum_with_3_levels():
|
||||
"""H3 Route B: register creates ONE abstractNum with 3 w:lvl elements (ilvl 0/1/2).
|
||||
|
||||
BEHAVIORAL: traces num_id → w:num → w:abstractNumId → w:abstractNum, then
|
||||
asserts THAT abstractNum (not all 9 default abstractNums in the template)
|
||||
has exactly the levels we registered. A `return 1` no-op mutation must FAIL.
|
||||
"""
|
||||
doc = Document()
|
||||
num_id = register_multilevel_numbering(doc, levels=3, kind="bullet")
|
||||
assert isinstance(num_id, int)
|
||||
|
||||
tmp = Path(__file__).parent / "_tmp_num.docx"
|
||||
doc.save(tmp)
|
||||
try:
|
||||
with zipfile.ZipFile(tmp) as z:
|
||||
num_xml = z.read("word/numbering.xml")
|
||||
root = ET.fromstring(num_xml)
|
||||
|
||||
our_num = None
|
||||
for num_el in root.findall(f"{{{W_NS}}}num"):
|
||||
if num_el.get(f"{{{W_NS}}}numId") == str(num_id):
|
||||
our_num = num_el
|
||||
break
|
||||
assert our_num is not None, f"No <w:num> with numId={num_id} (function did nothing)"
|
||||
|
||||
abstract_ref = our_num.find(f"{{{W_NS}}}abstractNumId")
|
||||
assert abstract_ref is not None, "missing w:abstractNumId in our w:num"
|
||||
abstract_id = abstract_ref.get(f"{{{W_NS}}}val")
|
||||
|
||||
our_abstract = None
|
||||
for an in root.findall(f"{{{W_NS}}}abstractNum"):
|
||||
if an.get(f"{{{W_NS}}}abstractNumId") == abstract_id:
|
||||
our_abstract = an
|
||||
break
|
||||
assert our_abstract is not None, f"No <w:abstractNum> with id={abstract_id}"
|
||||
|
||||
lvls = our_abstract.findall(f"{{{W_NS}}}lvl")
|
||||
ilvls = sorted(int(l.get(f"{{{W_NS}}}ilvl")) for l in lvls)
|
||||
assert ilvls == [0, 1, 2], f"expected ilvl=[0,1,2] in OUR abstractNum, got {ilvls}"
|
||||
finally:
|
||||
tmp.unlink()
|
||||
|
||||
|
||||
def test_attach_list_numbering_sets_numPr_with_ilvl():
|
||||
"""H3 Route B: paragraph gets w:numPr with w:ilvl and w:numId."""
|
||||
doc = Document()
|
||||
num_id = register_multilevel_numbering(doc, levels=3, kind="bullet")
|
||||
p = doc.add_paragraph("nested item")
|
||||
attach_list_numbering(p, num_id=num_id, ilvl=1)
|
||||
root = get_doc_xml(doc)
|
||||
numPr = find_all(root, "numPr")
|
||||
ilvl = find_all(root, "ilvl")
|
||||
numId_xml = find_all(root, "numId")
|
||||
assert len(numPr) >= 1, "w:numPr missing"
|
||||
assert len(ilvl) >= 1, "w:ilvl missing"
|
||||
assert ilvl[0].get(f"{{{W_NS}}}val") == "1"
|
||||
assert len(numId_xml) >= 1, "w:numId missing"
|
||||
Reference in New Issue
Block a user