Bullet Lists in python-docx

前端 未结 1 975
眼角桃花
眼角桃花 2021-01-18 10:02

I am trying to get this to work in python-docx:

A bullet list I can get using this:

from docx import Document
doc = Document()
         


        
相关标签:
1条回答
  • 2021-01-18 10:55

    There is a way to do it, but it involves a bit of extra work on your part. There is currently no "native" interface in python-docx for doing this. Each bulleted item must be an individual paragraph. Runs apply only to the text characters.

    The idea is that list bulleting or numbering is controlled by a concrete bullet or number style, which refers to an abstract style. The abstract style determines the styling of the afflicted paragraph, while the concrete numbering determines the number/bullet within the abstract sequence. This means that you can have paragraphs without bullets and numbering interspersed among the bulleted paragraphs. At the same time, you can restart the numbering/bulleting sequence at any point by creating a new concrete style.

    All this information is hashed out (in detail but unsuccessfully) in Issue #25. I don't have the time or resources to lay this to rest right now, but I did write a function that I left in a comment in the discussion thread. This function will look up an abstract style based on the level of indentation and paragraph style you want. It will then create or retrieve a concrete style based on that abstract style and assign it to your paragraph object:

    def list_number(doc, par, prev=None, level=None, num=True):
        """
        Makes a paragraph into a list item with a specific level and
        optional restart.
    
        An attempt will be made to retreive an abstract numbering style that
        corresponds to the style of the paragraph. If that is not possible,
        the default numbering or bullet style will be used based on the
        ``num`` parameter.
    
        Parameters
        ----------
        doc : docx.document.Document
            The document to add the list into.
        par : docx.paragraph.Paragraph
            The paragraph to turn into a list item.
        prev : docx.paragraph.Paragraph or None
            The previous paragraph in the list. If specified, the numbering
            and styles will be taken as a continuation of this paragraph.
            If omitted, a new numbering scheme will be started.
        level : int or None
            The level of the paragraph within the outline. If ``prev`` is
            set, defaults to the same level as in ``prev``. Otherwise,
            defaults to zero.
        num : bool
            If ``prev`` is :py:obj:`None` and the style of the paragraph
            does not correspond to an existing numbering style, this will
            determine wether or not the list will be numbered or bulleted.
            The result is not guaranteed, but is fairly safe for most Word
            templates.
        """
        xpath_options = {
            True: {'single': 'count(w:lvl)=1 and ', 'level': 0},
            False: {'single': '', 'level': level},
        }
    
        def style_xpath(prefer_single=True):
            """
            The style comes from the outer-scope variable ``par.style.name``.
            """
            style = par.style.style_id
            return (
                'w:abstractNum['
                    '{single}w:lvl[@w:ilvl="{level}"]/w:pStyle[@w:val="{style}"]'
                ']/@w:abstractNumId'
            ).format(style=style, **xpath_options[prefer_single])
    
        def type_xpath(prefer_single=True):
            """
            The type is from the outer-scope variable ``num``.
            """
            type = 'decimal' if num else 'bullet'
            return (
                'w:abstractNum['
                    '{single}w:lvl[@w:ilvl="{level}"]/w:numFmt[@w:val="{type}"]'
                ']/@w:abstractNumId'
            ).format(type=type, **xpath_options[prefer_single])
    
        def get_abstract_id():
            """
            Select as follows:
    
                1. Match single-level by style (get min ID)
                2. Match exact style and level (get min ID)
                3. Match single-level decimal/bullet types (get min ID)
                4. Match decimal/bullet in requested level (get min ID)
                3. 0
            """
            for fn in (style_xpath, type_xpath):
                for prefer_single in (True, False):
                    xpath = fn(prefer_single)
                    ids = numbering.xpath(xpath)
                    if ids:
                        return min(int(x) for x in ids)
            return 0
    
        if (prev is None or
                prev._p.pPr is None or
                prev._p.pPr.numPr is None or
                prev._p.pPr.numPr.numId is None):
            if level is None:
                level = 0
            numbering = doc.part.numbering_part.numbering_definitions._numbering
            # Compute the abstract ID first by style, then by num
            anum = get_abstract_id()
            # Set the concrete numbering based on the abstract numbering ID
            num = numbering.add_num(anum)
            # Make sure to override the abstract continuation property
            num.add_lvlOverride(ilvl=level).add_startOverride(1)
            # Extract the newly-allocated concrete numbering ID
            num = num.numId
        else:
            if level is None:
                level = prev._p.pPr.numPr.ilvl.val
            # Get the previous concrete numbering ID
            num = prev._p.pPr.numPr.numId.val
        par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = num
        par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = level
    

    Using the styles in the default built-in document stub, you can do something like this:

    d = docx.Document()
    p0 = d.add_paragraph('Item 1', style='List Bullet')
    list_number(d, p0, level=0, num=False)
    p1 = d.add_paragraph('Item A', style='List Bullet 2')
    list_number(d, p1, p0, level=1)
    p2 = d.add_paragraph('Item 2', style='List Bullet')
    list_number(d, p2, p1, level=0)
    p3 = d.add_paragraph('Item B', style='List Bullet 2')
    list_number(d, p3, p2, level=1)
    

    The style will not only affect the tab stops and other display characteristics of the paragraph, but will also help look up the appropriate abstract numbering scheme. When you implicitly set prev=None in the call for p0, the function creates a new concrete numbering scheme. All the remaining paragraphs will inherit the same scheme because they get a prev parameter. The calls to list_number don't have to be interleaved with the calls to add_paragraph like that, as long as the numbering for the paragraph used as prev is set before the call.

    0 讨论(0)
提交回复
热议问题