"""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 = """ """ 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 = """ """ 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 = """
  1. First
  2. Second
""" 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()