Skip to content

Commit a27d14c

Browse files
author
Steve Canny
committed
num: add numbering oxml classes
1 parent 4bec90c commit a27d14c

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

docx/oxml/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# custom element class mappings
1313
# ===========================================================================
1414

15-
from docx.oxml.shared import CT_OnOff, CT_String
15+
from docx.oxml.shared import CT_DecimalNumber, CT_OnOff, CT_String
1616

1717
from docx.oxml.shape import (
1818
CT_Blip, CT_BlipFillProperties, CT_GraphicalObject,
@@ -30,7 +30,10 @@
3030
register_custom_element_class('w:body', CT_Body)
3131
register_custom_element_class('w:document', CT_Document)
3232

33-
from docx.oxml.parts.numbering import CT_Numbering
33+
from docx.oxml.parts.numbering import CT_Num, CT_Numbering, CT_NumLvl
34+
register_custom_element_class('w:abstractNumId', CT_DecimalNumber)
35+
register_custom_element_class('w:lvlOverride', CT_NumLvl)
36+
register_custom_element_class('w:num', CT_Num)
3437
register_custom_element_class('w:numbering', CT_Numbering)
3538

3639
from docx.oxml.table import CT_Row, CT_Tbl, CT_TblGrid, CT_TblPr, CT_Tc

docx/oxml/parts/numbering.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,121 @@
44
Custom element classes related to the numbering part
55
"""
66

7-
from docx.oxml.shared import OxmlBaseElement, qn
7+
from docx.oxml.shared import (
8+
CT_DecimalNumber, nsmap, OxmlBaseElement, OxmlElement, qn
9+
)
10+
11+
12+
class CT_Num(OxmlBaseElement):
13+
"""
14+
``<w:num>`` element, which represents a concrete list definition
15+
instance, having a required child <w:abstractNumId> that references an
16+
abstract numbering definition that defines most of the formatting details.
17+
"""
18+
@property
19+
def abstractNumId(self):
20+
return self.find(qn('w:abstractNumId'))
21+
22+
def add_lvlOverride(self, ilvl):
23+
"""
24+
Return a newly added CT_NumLvl (<w:lvlOverride>) element having its
25+
``ilvl`` attribute set to *ilvl*.
26+
"""
27+
lvlOverride = CT_NumLvl.new(ilvl)
28+
self.append(lvlOverride)
29+
return lvlOverride
30+
31+
@classmethod
32+
def new(cls, num_id, abstractNum_id):
33+
"""
34+
Return a new ``<w:num>`` element having numId of *num_id* and having
35+
a ``<w:abstractNumId>`` child with val attribute set to
36+
*abstractNum_id*.
37+
"""
38+
abstractNumId = CT_DecimalNumber.new(
39+
'w:abstractNumId', abstractNum_id
40+
)
41+
num = OxmlElement('w:num', {qn('w:numId'): str(num_id)})
42+
num.append(abstractNumId)
43+
return num
44+
45+
@property
46+
def numId(self):
47+
numId_str = self.get(qn('w:numId'))
48+
return int(numId_str)
49+
50+
51+
class CT_NumLvl(OxmlBaseElement):
52+
"""
53+
``<w:lvlOverride>`` element, which identifies a level in a list
54+
definition to override with settings it contains.
55+
"""
56+
def add_startOverride(self, val):
57+
"""
58+
Return a newly added CT_DecimalNumber element having tagname
59+
``w:startOverride`` and ``val`` attribute set to *val*.
60+
"""
61+
startOverride = CT_DecimalNumber.new('w:startOverride', val)
62+
self.insert(0, startOverride)
63+
return startOverride
64+
65+
@classmethod
66+
def new(cls, ilvl):
67+
"""
68+
Return a new ``<w:lvlOverride>`` element having its ``ilvl``
69+
attribute set to *ilvl*.
70+
"""
71+
return OxmlElement('w:lvlOverride', {qn('w:ilvl'): str(ilvl)})
872

973

1074
class CT_Numbering(OxmlBaseElement):
1175
"""
1276
``<w:numbering>`` element, the root element of a numbering part, i.e.
1377
numbering.xml
1478
"""
79+
def add_num(self, abstractNum_id):
80+
"""
81+
Return a newly added CT_Num (<w:num>) element that references
82+
the abstract numbering definition having id *abstractNum_id*.
83+
"""
84+
next_num_id = self._next_numId
85+
num = CT_Num.new(next_num_id, abstractNum_id)
86+
# insert in proper sequence among children ---------
87+
successor = self.first_child_found_in('w:numIdMacAtCleanup')
88+
if successor is not None:
89+
successor.addprevious(num)
90+
else:
91+
self.append(num)
92+
return num
93+
1594
@property
1695
def num_lst(self):
1796
"""
1897
List of <w:num> child elements.
1998
"""
2099
return self.findall(qn('w:num'))
100+
101+
def num_having_numId(self, numId):
102+
"""
103+
Return the ``<w:num>`` child element having ``numId`` attribute
104+
matching *numId*.
105+
"""
106+
xpath = './w:num[@w:numId="%d"]' % numId
107+
try:
108+
return self.xpath(xpath, namespaces=nsmap)[0]
109+
except IndexError:
110+
raise KeyError('no <w:num> element with numId %d' % numId)
111+
112+
@property
113+
def _next_numId(self):
114+
"""
115+
The first ``numId`` unused by a ``<w:num>`` element, starting at
116+
1 and filling any gaps in numbering between existing ``<w:num>``
117+
elements.
118+
"""
119+
numId_strs = self.xpath('./w:num/@w:numId', namespaces=nsmap)
120+
num_ids = [int(numId_str) for numId_str in numId_strs]
121+
for num in range(1, len(num_ids)+2):
122+
if num not in num_ids:
123+
break
124+
return num

docx/oxml/shared.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ class OxmlBaseElement(etree.ElementBase):
229229
Base class for all custom element classes, to add standardized behavior
230230
to all classes in one place.
231231
"""
232+
def first_child_found_in(self, *tagnames):
233+
"""
234+
Return the first child found with tag in *tagnames*, or None if
235+
not found.
236+
"""
237+
for tagname in tagnames:
238+
child = self.find(qn(tagname))
239+
if child is not None:
240+
return child
241+
return None
242+
232243
@property
233244
def xml(self):
234245
"""
@@ -239,6 +250,29 @@ def xml(self):
239250
return serialize_for_reading(self)
240251

241252

253+
class CT_DecimalNumber(OxmlBaseElement):
254+
"""
255+
Used for ``<w:numId>``, ``<w:ilvl>``, ``<w:abstractNumId>`` and several
256+
others, containing a text representation of a decimal number (e.g. 42) in
257+
its ``val`` attribute.
258+
"""
259+
@classmethod
260+
def new(cls, nsptagname, val):
261+
"""
262+
Return a new ``CT_DecimalNumber`` element having tagname *nsptagname*
263+
and ``val`` attribute set to *val*.
264+
"""
265+
return OxmlElement(nsptagname, attrs={qn('w:val'): str(val)})
266+
267+
@property
268+
def val(self):
269+
"""
270+
Required attribute containing a decimal integer
271+
"""
272+
number_str = self.get(qn('w:val'))
273+
return int(number_str)
274+
275+
242276
class CT_OnOff(OxmlBaseElement):
243277
"""
244278
Used for ``<w:b>``, ``<w:i>`` elements and others, containing a bool-ish

0 commit comments

Comments
 (0)