feat(md2doc): recursive list renderer with manual numbering
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""Tests for list rendering — verifies nested levels use correct ilvl."""
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from md2doc import STYLES, _render_list
|
||||
|
||||
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
def get_doc_xml(doc):
|
||||
tmp = Path(__file__).parent / "_tmp_list.docx"
|
||||
doc.save(tmp)
|
||||
try:
|
||||
with zipfile.ZipFile(tmp) as z:
|
||||
return ET.fromstring(z.read("word/document.xml"))
|
||||
finally:
|
||||
tmp.unlink()
|
||||
|
||||
|
||||
def test_flat_bullet_list_produces_3_items_at_ilvl_0():
|
||||
html = """
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item B</li>
|
||||
<li>Item C</li>
|
||||
</ul>
|
||||
"""
|
||||
doc = Document()
|
||||
node = BeautifulSoup(html, "html.parser").find("ul")
|
||||
_render_list(doc, node, style=STYLES["elegant"])
|
||||
root = get_doc_xml(doc)
|
||||
ilvls = [int(el.get(f"{{{W_NS}}}val")) for el in root.iter(f"{{{W_NS}}}ilvl")]
|
||||
assert ilvls == [0, 0, 0], f"expected all ilvl=0, got {ilvls}"
|
||||
|
||||
|
||||
def test_nested_bullet_list_uses_increasing_ilvl_and_resolves_to_multilevel_abstractNum():
|
||||
"""BEHAVIORAL: paragraphs have ilvl=[0,1,2], AND the numId they reference
|
||||
resolves to an abstractNum that actually defines those levels. A no-op
|
||||
register_multilevel_numbering (return 1) would leave paragraphs pointing to
|
||||
default-template numId=1 (single-level), so the test must FAIL.
|
||||
"""
|
||||
html = """
|
||||
<ul>
|
||||
<li>Top
|
||||
<ul>
|
||||
<li>Nested 1
|
||||
<ul>
|
||||
<li>Deep nested</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
doc = Document()
|
||||
node = BeautifulSoup(html, "html.parser").find("ul")
|
||||
_render_list(doc, node, style=STYLES["elegant"])
|
||||
|
||||
tmp = Path(__file__).parent / "_tmp_list_full.docx"
|
||||
doc.save(tmp)
|
||||
try:
|
||||
with zipfile.ZipFile(tmp) as z:
|
||||
doc_xml = z.read("word/document.xml")
|
||||
num_xml = z.read("word/numbering.xml")
|
||||
doc_root = ET.fromstring(doc_xml)
|
||||
num_root = ET.fromstring(num_xml)
|
||||
|
||||
# 1. Paragraphs must have ilvl=[0,1,2]
|
||||
ilvls = [int(el.get(f"{{{W_NS}}}val"))
|
||||
for el in doc_root.iter(f"{{{W_NS}}}ilvl")]
|
||||
assert ilvls == [0, 1, 2], f"expected paragraph ilvl=[0,1,2], got {ilvls}"
|
||||
|
||||
# 2. All paragraphs must reference the SAME numId
|
||||
numIds = [el.get(f"{{{W_NS}}}val")
|
||||
for el in doc_root.iter(f"{{{W_NS}}}numId")]
|
||||
assert len(set(numIds)) == 1, \
|
||||
f"expected all paragraphs to share one numId, got {set(numIds)}"
|
||||
our_num_id = numIds[0]
|
||||
|
||||
# 3. numId resolves to abstractNum that defines ilvl=0,1,2
|
||||
our_num = None
|
||||
for num_el in num_root.findall(f"{{{W_NS}}}num"):
|
||||
if num_el.get(f"{{{W_NS}}}numId") == our_num_id:
|
||||
our_num = num_el
|
||||
break
|
||||
assert our_num is not None, f"our numId={our_num_id} not found in numbering.xml"
|
||||
|
||||
abstract_ref = our_num.find(f"{{{W_NS}}}abstractNumId")
|
||||
assert abstract_ref is not None
|
||||
abstract_id = abstract_ref.get(f"{{{W_NS}}}val")
|
||||
|
||||
our_abstract = None
|
||||
for an in num_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"abstractNum {abstract_id} not found"
|
||||
|
||||
abstract_ilvls = sorted(int(l.get(f"{{{W_NS}}}ilvl"))
|
||||
for l in our_abstract.findall(f"{{{W_NS}}}lvl"))
|
||||
assert abstract_ilvls == [0, 1, 2], \
|
||||
f"our abstractNum must define ilvl=[0,1,2], got {abstract_ilvls}"
|
||||
finally:
|
||||
tmp.unlink()
|
||||
|
||||
|
||||
def test_numbered_list_uses_decimal_format():
|
||||
html = """
|
||||
<ol>
|
||||
<li>First</li>
|
||||
<li>Second</li>
|
||||
</ol>
|
||||
"""
|
||||
doc = Document()
|
||||
node = BeautifulSoup(html, "html.parser").find("ol")
|
||||
_render_list(doc, node, style=STYLES["elegant"])
|
||||
tmp = Path(__file__).parent / "_tmp_num.docx"
|
||||
doc.save(tmp)
|
||||
try:
|
||||
with zipfile.ZipFile(tmp) as z:
|
||||
num_xml = z.read("word/numbering.xml")
|
||||
num_root = ET.fromstring(num_xml)
|
||||
fmts = [el.get(f"{{{W_NS}}}val") for el in num_root.iter(f"{{{W_NS}}}numFmt")]
|
||||
assert "decimal" in fmts
|
||||
finally:
|
||||
tmp.unlink()
|
||||
Reference in New Issue
Block a user