diff --git a/docx/api.py b/docx/api.py index c1ac093b7..21de6116f 100644 --- a/docx/api.py +++ b/docx/api.py @@ -71,6 +71,14 @@ def add_paragraph(self, text='', style=None): """ return self._document_part.add_paragraph(text, style) + def add_list(self, style='ListParagraph', level=0): + """ + Return a helper that implements methods to create a list formed by + paragraphs sharing the same numId, added to the end of the document, + having paragraph style *style* and indentation level *level*. + """ + return self._document_part.add_list(style=style, level=level) + def add_picture(self, image_path_or_stream, width=None, height=None): """ Return a new picture shape added in its own paragraph at the end of @@ -137,6 +145,16 @@ def paragraphs(self): """ return self._document_part.paragraphs + @property + def lists(self): + """ + A list of |ListParagraph| instances corresponding to the paragraphs + grouped in lists, in document order. Note that paragraphs within + revision marks such as ```` or ```` do not appear in this + list. + """ + return self._document_part.lists + def save(self, path_or_stream): """ Save this document to *path_or_stream*, which can be either a path to diff --git a/docx/blkcntnr.py b/docx/blkcntnr.py index b11f3a50d..576745f49 100644 --- a/docx/blkcntnr.py +++ b/docx/blkcntnr.py @@ -8,8 +8,11 @@ from __future__ import absolute_import, print_function +import sys +import random from .shared import Parented from .text import Paragraph +from .list import ListParagraph class BlockItemContainer(Parented): @@ -23,6 +26,15 @@ def __init__(self, element, parent): super(BlockItemContainer, self).__init__(parent) self._element = element + def generate_numId(self): + """ + Generate a unique numId value on this container. + """ + while True: + numId = random.randint(0, 999999) + if not len(self._element.xpath("//w:numId[@w:val='%s']" % numId)): + return numId + def add_paragraph(self, text='', style=None): """ Return a paragraph newly added to the end of the content in this @@ -52,6 +64,19 @@ def add_table(self, rows, cols): table.add_row() return table + def add_list(self, style=None, level=0): + """ + Return a list paragraph newly added to the end of the content in this + container, having a paragraph style *style* and an indentation level + *level*. + """ + return ListParagraph( + self, + numId=self.generate_numId(), + style=style, + level=level, + ) + @property def paragraphs(self): """ @@ -60,6 +85,16 @@ def paragraphs(self): """ return [Paragraph(p, self) for p in self._element.p_lst] + @property + def lists(self): + """ + A list containing the paragraphs grouped in lists in this container, + in document order. Read-only. + """ + nums = [paragraph.numId for paragraph in self.paragraphs + if paragraph.numId is not None] + return [ListParagraph(self, numId) for numId in set(nums)] + @property def tables(self): """ diff --git a/docx/list.py b/docx/list.py new file mode 100644 index 000000000..09c4360d9 --- /dev/null +++ b/docx/list.py @@ -0,0 +1,55 @@ +# encoding: utf-8 + +""" +The |ListParagraph| object and related proxy classes. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import random +from .text import Paragraph + + +class ListParagraph(object): + """ + Proxy object for controlling a set of ```` grouped together in a list. + """ + def __init__(self, parent, numId=0, style=None, level=0): + self._parent = parent + self.numId = numId + self.level = level + self.style = style + + def add_item(self, text=None, style=None): + """ + Add a paragraph item to the current list, having text set to *text* and + a paragraph style *style* + """ + item = self._parent.add_paragraph(text, style=style) + item.level = self.level + item.numId = self.numId + return item + + def add_list(self, style=None): + """ + Add a list indented one level below the current one, having a paragraph + style *style*. Note that the document will only be altered once the + first item has been added to the list. + """ + return ListParagraph( + self._parent, + numId=self._parent.generate_numId(), + style=style if style is not None else self.style, + level=self.level+1, + ) + + @property + def items(self): + """ + Sequence of |Paragraph| instances corresponding to the item elements + in this list paragraph. + """ + return [paragraph for paragraph in self._parent.paragraphs + if paragraph.numId == self.numId] diff --git a/docx/oxml/text.py b/docx/oxml/text.py index 9fdd1d64b..0870fea19 100644 --- a/docx/oxml/text.py +++ b/docx/oxml/text.py @@ -166,6 +166,13 @@ def style(self, style): pStyle.val = style +class CT_NumPr(BaseOxmlElement): + """ + ```` element, containing properties useful for lists. + """ + numId = ZeroOrOne('w:numId') + ilvl = ZeroOrOne('w:ilvl') + class CT_R(BaseOxmlElement): """ ```` element, containing the properties and text for a run. diff --git a/docx/parts/document.py b/docx/parts/document.py index e7ff08e8b..010172799 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -45,6 +45,15 @@ def add_table(self, rows, cols): """ return self.body.add_table(rows, cols) + def add_list(self, style=None, level=0): + """ + Return a helper that provides methods to add paragraphs to the end of + the body content, grouped together as a list. The paragraphs will by + default have their paragraph style set to *style*, and their indentation + level set to *level*. + """ + return self.body.add_list(style=style, level=level) + @lazyproperty def body(self): """ @@ -95,6 +104,16 @@ def paragraphs(self): """ return self.body.paragraphs + @property + def lists(self): + """ + A list of |ListParagraph| instances corresponding to lists formed by + paragraphs sharing the same numId in the document, in document order. + Note that list paragraphs within revision marks such as inserted or + deleted do not appear in this list. + """ + return self.body.lists + @lazyproperty def sections(self): """ diff --git a/docx/text.py b/docx/text.py index 0c551beeb..b3df7b503 100644 --- a/docx/text.py +++ b/docx/text.py @@ -80,6 +80,46 @@ def add_run(self, text=None, style=None): run.style = style return run + @property + def numId(self): + """ + Return the numId of the parent list. + """ + if self._p.pPr is None: + return None + if self._p.pPr.numPr is None: + return None + if self._p.pPr.numPr.numId is None: + return None + return self._p.pPr.numPr.numId.val + + @numId.setter + def numId(self, numId): + """ + Set the numId of the parent list. + """ + self._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = numId + + @property + def level(self): + """ + Return the indentation level of the parent list. + """ + if self._p.pPr is None: + return None + if self._p.pPr.numPr is None: + return None + if self._p.pPr.numPr.ilvl is None: + return None + return self._p.pPr.numPr.ilvl.val + + @level.setter + def level(self, lvl): + """ + Set the indentation level of the parent list. + """ + self._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = lvl + @property def alignment(self): """