Exercise ST_NumberFormat values (decimal, upperRoman, ideographDigital, aiueo, ...) on a single-level numbered list via <w:lvl>/<w:numFmt>.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 30 |
docxjs | 0 | 0 | 30 |
numfmt (30 values): decimal, upper-roman, lower-roman, upper-letter, lower-letter, ordinal, cardinal-text, ordinal-text, hex, chicago, ideograph-digital, japanese-counting, aiueo, iroha, decimal-full-width, decimal-half-width, japanese-legal, japanese-digital-ten-thousand, decimal-enclosed-circle, decimal-full-width2, aiueo-full-width, iroha-full-width, decimal-zero, bullet, ganada, chosung, decimal-enclosed-fullstop, decimal-enclosed-paren, decimal-enclosed-circle-chinese, ideograph-enclosed-circlescripts/gen_list_numbering_format.py — runs with --arg_template --numfmt {numfmt.val} --out fixtures/docx/list-numbering-format--{numfmt.id}.docx
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/list-numbering-format--<id>.docx`` fixture.
Parameterised family exercising ST_NumberFormat values (ECMA-376 Part 1
clause 17.9.17). Each generated fixture contains a single numbered list
item (``numId=1``, ``ilvl=0``) whose abstract-num level declares the
specified ``w:numFmt/@w:val``.
python-docx's high-level numbering API does not expose per-level
``w:numFmt`` control, so the numbering part is built from scratch via
``OxmlElement`` (bypassing the :class:`WD_NUMBER_FORMAT` enum, which
only covers the most common subset).
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _write(numfmt_val: str, out: Path) -> None:
doc = Document()
# Ensure numbering part exists, then wipe any existing abstractNum / num
# so the fixture starts from a clean slate.
numbering = doc.part.numbering_part.element
for child in list(numbering):
if child.tag in (qn("w:num"), qn("w:abstractNum")):
numbering.remove(child)
abs_num = OxmlElement("w:abstractNum")
abs_num.set(qn("w:abstractNumId"), "0")
lvl = OxmlElement("w:lvl")
lvl.set(qn("w:ilvl"), "0")
start = OxmlElement("w:start")
start.set(qn("w:val"), "1")
lvl.append(start)
fmt = OxmlElement("w:numFmt")
fmt.set(qn("w:val"), numfmt_val)
lvl.append(fmt)
lvl_text = OxmlElement("w:lvlText")
lvl_text.set(qn("w:val"), "%1.")
lvl.append(lvl_text)
abs_num.append(lvl)
numbering.append(abs_num)
num = OxmlElement("w:num")
num.set(qn("w:numId"), "1")
abs_ref = OxmlElement("w:abstractNumId")
abs_ref.set(qn("w:val"), "0")
num.append(abs_ref)
numbering.append(num)
p = doc.add_paragraph("Numbered item one")
pPr = p._p.get_or_add_pPr()
numPr = OxmlElement("w:numPr")
ilvl = OxmlElement("w:ilvl")
ilvl.set(qn("w:val"), "0")
numPr.append(ilvl)
numId = OxmlElement("w:numId")
numId.set(qn("w:val"), "1")
numPr.append(numId)
pPr.append(numPr)
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--numfmt", required=True, help="ST_NumberFormat value, e.g. 'decimal'")
parser.add_argument("--out", required=True)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.numfmt, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/list-numbering-format",
"kind": "parameterised",
"title": "List numbering format",
"format": "docx",
"category": "lists",
"summary": "Exercise ST_NumberFormat values (decimal, upperRoman, ideographDigital, aiueo, ...) on a single-level numbered list via <w:lvl>/<w:numFmt>.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.9.17",
"element": "w:numFmt",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "ST_NumberFormat is the simple type backing <w:numFmt w:val='...'/>. It enumerates ~60 numbering-style tokens (decimal, upperRoman, ordinal, cardinalText, chicago, ideographDigital, japaneseCounting, aiueo, iroha, decimalFullWidth, ganada, chosung, decimalEnclosedCircle, bullet, ...). Each parameter case in this family emits a fresh numbering part containing one <w:abstractNum> with a single <w:lvl ilvl='0'> whose <w:numFmt/@w:val> is the parameter token, plus one <w:num numId='1'> that references it. A paragraph in word/document.xml carries <w:numPr><w:numId val='1'/></w:numPr> to pull the item into that list. python-docx's WD_NUMBER_FORMAT enum only covers the common subset; the generator builds the numFmt via OxmlElement to bypass that restriction."
},
"fixtures": {
"machine": "docx/list-numbering-format"
},
"generator": {
"python": "scripts/gen_list_numbering_format.py",
"arg_template": "--numfmt {numfmt.val} --out fixtures/docx/list-numbering-format--{numfmt.id}.docx"
},
"parameters": {
"numfmt": [
{
"id": "decimal",
"val": "decimal"
},
{
"id": "upper-roman",
"val": "upperRoman"
},
{
"id": "lower-roman",
"val": "lowerRoman"
},
{
"id": "upper-letter",
"val": "upperLetter"
},
{
"id": "lower-letter",
"val": "lowerLetter"
},
{
"id": "ordinal",
"val": "ordinal"
},
{
"id": "cardinal-text",
"val": "cardinalText"
},
{
"id": "ordinal-text",
"val": "ordinalText"
},
{
"id": "hex",
"val": "hex"
},
{
"id": "chicago",
"val": "chicago"
},
{
"id": "ideograph-digital",
"val": "ideographDigital"
},
{
"id": "japanese-counting",
"val": "japaneseCounting"
},
{
"id": "aiueo",
"val": "aiueo"
},
{
"id": "iroha",
"val": "iroha"
},
{
"id": "decimal-full-width",
"val": "decimalFullWidth"
},
{
"id": "decimal-half-width",
"val": "decimalHalfWidth"
},
{
"id": "japanese-legal",
"val": "japaneseLegal"
},
{
"id": "japanese-digital-ten-thousand",
"val": "japaneseDigitalTenThousand"
},
{
"id": "decimal-enclosed-circle",
"val": "decimalEnclosedCircle"
},
{
"id": "decimal-full-width2",
"val": "decimalFullWidth2"
},
{
"id": "aiueo-full-width",
"val": "aiueoFullWidth"
},
{
"id": "iroha-full-width",
"val": "irohaFullWidth"
},
{
"id": "decimal-zero",
"val": "decimalZero"
},
{
"id": "bullet",
"val": "bullet"
},
{
"id": "ganada",
"val": "ganada"
},
{
"id": "chosung",
"val": "chosung"
},
{
"id": "decimal-enclosed-fullstop",
"val": "decimalEnclosedFullstop"
},
{
"id": "decimal-enclosed-paren",
"val": "decimalEnclosedParen"
},
{
"id": "decimal-enclosed-circle-chinese",
"val": "decimalEnclosedCircleChinese"
},
{
"id": "ideograph-enclosed-circle",
"val": "ideographEnclosedCircle"
}
]
},
"assertions_template": [
{
"id": "list-numbering-format-{numfmt.id}-numfmt-present",
"part": "word/numbering.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:lvl/w:numFmt[@w:val='{numfmt.val}']",
"must": "exist",
"description": "The numbering part must contain a <w:lvl>/<w:numFmt> whose @w:val is '{numfmt.val}'."
}
]
}