Skip to content

Commit

Permalink
Merge pull request #125 from openlawlibrary/tibor/merge-all-footnote
Browse files Browse the repository at this point in the history
merge all footnote
  • Loading branch information
tiberlas authored Mar 18, 2024
2 parents dc45681 + 910f163 commit a5330dc
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 61 deletions.
2 changes: 1 addition & 1 deletion docx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from docx.api import Document # noqa

__version__ = '0.8.10.30'
__version__ = '0.8.10.31'


# register custom Part classes with opc package reader
Expand Down
26 changes: 13 additions & 13 deletions docx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,31 +261,31 @@ def _calculate_next_footnote_reference_id(self, p):
# in |Footnotes| and in |Paragraph|
new_fr_id = 1
# If paragraph already contains footnotes
# and it's the last paragraph with footnotes, then
# append the new footnote and the end with the next reference id.
if p.footnote_reference_ids is not None:
if len(p.footnote_reference_ids) > 0:
new_fr_id = p.footnote_reference_ids[-1] + 1
# If the document has footnotes after this paragraph,
# the increment all footnotes pass this paragraph,
# and insert a new footnote at the proper position.
# break the loop when we get to the footnote before the one we are inserting.
# Read the paragraphs containing footnotes and find where the
# new footnote will be. Keeping in mind that the footnotes are
# sorted by id.
# The value of the new footnote id is the value of the first paragraph
# containing the footnote id that is before the new footnote, incremented by one.
# If a paragraph with footnotes is after the new footnote
# then increment thous footnote ids.
has_passed_containing_para = False
for p_i in reversed(range(len(self.paragraphs))):
# mark when we pass the paragraph containing the footnote
if p is self.paragraphs[p_i]._p:
has_passed_containing_para = True
continue
# skip paragraphs without footnotes (they don't impact new id)
if self.paragraphs[p_i]._p.footnote_reference_ids is None:
# Skip paragraphs without footnotes (they don't impact new id).
if len(self.paragraphs[p_i]._p.footnote_reference_ids) == 0:
continue
# update footnote id of paragraph that is after the
# paragraph that is inserting new footnote.
# These footnotes are after the new footnote, so we increment them.
if not has_passed_containing_para:
self.paragraphs[p_i].increment_containing_footnote_reference_ids()
else:
# this is the first paragraph containing footnotes before the
# paragraph that is inserting new footnote, so we get the largest
# reference id and add one
# This is the last footnote before the new footnote, so we use its
# value to determent the value of the new footnote.
new_fr_id = max(self.paragraphs[p_i]._p.footnote_reference_ids)+1
break
return new_fr_id
Expand Down
2 changes: 1 addition & 1 deletion docx/footnotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def add_footnote(self, footnote_reference_id):
"""
elements = self._element # for easy access
new_footnote = None
if elements.get_by_id(footnote_reference_id):
if elements.get_by_id(footnote_reference_id) is not None:
# When adding a footnote it can be inserted
# in front of some other footnotes, so
# we need to sort footnotes by `footnote_reference_id`
Expand Down
9 changes: 9 additions & 0 deletions docx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,28 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
register_element_cls('w:startOverride', CT_DecimalNumber)

from .section import ( # noqa
CT_FtnProps,
CT_FtnPos,
CT_HdrFtr,
CT_HdrFtrRef,
CT_PageMar,
CT_PageSz,
CT_NumFmt,
CT_NumRestart,
CT_SectPr,
CT_SectType,
)
register_element_cls("w:footerReference", CT_HdrFtrRef)
register_element_cls('w:footnotePr', CT_FtnProps)
register_element_cls("w:ftr", CT_HdrFtr)
register_element_cls("w:hdr", CT_HdrFtr)
register_element_cls("w:headerReference", CT_HdrFtrRef)
register_element_cls('w:numFmt', CT_NumFmt)
register_element_cls('w:numStart', CT_DecimalNumber)
register_element_cls('w:numRestart', CT_NumRestart)
register_element_cls("w:pgMar", CT_PageMar)
register_element_cls("w:pgSz", CT_PageSz)
register_element_cls('w:pos', CT_FtnPos)
register_element_cls("w:sectPr", CT_SectPr)
register_element_cls("w:type", CT_SectType)

Expand Down
15 changes: 1 addition & 14 deletions docx/oxml/footnote.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def add_footnote(self, footnote_reference_id):
return new_f

def get_by_id(self, id):
found = self.xpath('w:footnote[@w:id="%s"]' % id)
found = self.xpath(f'w:footnote[@w:id="{id}"]')
if not found:
return None
return found[0]
Expand All @@ -49,16 +49,3 @@ def add_footnote_before(self, footnote_reference_id):
new_footnote.id = footnote_reference_id
self.addprevious(new_footnote)
return new_footnote

@property
def paragraphs(self):
"""
Returns a list of paragraphs |CT_P|, or |None| if none paragraph is present.
"""
paragraphs = []
for child in self:
if child.tag == qn('w:p'):
paragraphs.append(child)
if paragraphs == []:
paragraphs = None
return paragraphs
126 changes: 125 additions & 1 deletion docx/oxml/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

from copy import deepcopy

from warnings import warn

from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START
from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString
from docx.oxml.exceptions import XmlchemyError
from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString, ST_FtnPos, ST_NumberFormat, ST_RestartNumber
from docx.oxml.xmlchemy import (
BaseOxmlElement,
OptionalAttribute,
Expand All @@ -17,6 +20,22 @@
)


class CT_FtnPos(BaseOxmlElement):
"""``<w:pos>`` element, footnote placement"""
val = RequiredAttribute('w:val', ST_FtnPos)


class CT_FtnProps(BaseOxmlElement):
"""``<w:footnotePr>`` element, section wide footnote properties"""
_tag_seq = (
'w:pos', 'w:numFmt', 'w:numStart', 'w:numRestart'
)
pos = ZeroOrOne('w:pos', successors=_tag_seq)
numFmt = ZeroOrOne('w:numFmt', successors=_tag_seq[1:])
numStart = ZeroOrOne('w:numStart', successors=_tag_seq[2:])
numRestart = ZeroOrOne('w:numRestart', successors=_tag_seq[3:])


class CT_HdrFtr(BaseOxmlElement):
"""`w:hdr` and `w:ftr`, the root element for header and footer part respectively"""

Expand All @@ -32,6 +51,16 @@ class CT_HdrFtrRef(BaseOxmlElement):
rId = RequiredAttribute('r:id', XsdString)


class CT_NumFmt(BaseOxmlElement):
"""``<w:numFmt>`` element, footnote numbering format"""
val = RequiredAttribute('w:val', ST_NumberFormat)


class CT_NumRestart(BaseOxmlElement):
"""``<w:numStart>`` element, footnote numbering restart location"""
val = RequiredAttribute('w:val', ST_RestartNumber)


class CT_PageMar(BaseOxmlElement):
"""
``<w:pgMar>`` element, defining page margins.
Expand Down Expand Up @@ -71,6 +100,7 @@ class CT_SectPr(BaseOxmlElement):
pgSz = ZeroOrOne("w:pgSz", successors=_tag_seq[4:])
pgMar = ZeroOrOne("w:pgMar", successors=_tag_seq[5:])
titlePg = ZeroOrOne("w:titlePg", successors=_tag_seq[14:])
footnotePr = ZeroOrOne("w:footnotePr", successors=_tag_seq[1:])
del _tag_seq

def add_footerReference(self, type_, rId):
Expand Down Expand Up @@ -137,6 +167,100 @@ def footer(self, value):
pgMar = self.get_or_add_pgMar()
pgMar.footer = value

@property
def footnote_number_format(self):
"""
The value of the ``w:val`` attribute in the ``<w:numFmt>`` child
element of ``<w:footnotePr>`` element, as a |String|, or |'decimal'| if either the element or the
attribute is not present.
"""
fPr = self.footnotePr
if fPr is None or fPr.numFmt is None:
return 'decimal'
return fPr.numFmt.val

@footnote_number_format.setter
def footnote_number_format(self, value):
if value is None:
value = 'decimal'
fPr = self.get_or_add_footnotePr()
numFmt = fPr.get_or_add_numFmt()
numFmt.val = value

@property
def footnote_numbering_restart_location(self):
"""
The value of the ``w:val`` attribute in the ``<w:numRestart>`` child
element of ``<w:footnotePr>`` element, as a |String|, or |'continuous'| if either the element or the
attribute is not present.
This property is tied with ``<w:numStart>``.
"""
fPr = self.footnotePr
if fPr is None or fPr.numRestart is None:
return 'continuous'
return fPr.numRestart.val

@footnote_numbering_restart_location.setter
def footnote_numbering_restart_location(self, value):
# this property must have an appropriate ``<w:numStart>`` property.
if value is None:
value = 'continuous'
numStartValue = self.footnote_numbering_start_value
if value != 'continuous' and numStartValue != 1:
raise XmlchemyError( "When ``<w:numRestart> is not 'continuous', then ``<w:numStart>`` must be 1.")
fPr = self.get_or_add_footnotePr()
numStart = fPr.get_or_add_numStart()
numRestart = fPr.get_or_add_numRestart()
numStart.val = numStartValue
numRestart.val = value

@property
def footnote_numbering_start_value(self):
"""
The value of the ``w:val`` attribute in the ``<w:numStart>`` child
element of ``<w:footnotePr>`` element, as a |Number|, or |1| if either the element or the
attribute is not present.
This property is tied with ``<w:numRestart>``.
"""
fPr = self.footnotePr
if fPr is None or fPr.numStart is None:
return 1
return fPr.numStart.val

@footnote_numbering_start_value.setter
def footnote_numbering_start_value(self, value):
# this property must have an appropriate ``<w:numRestart>`` property.
if value is None:
value = 1
numRestartValue = self.footnote_numbering_restart_location
if value != 1 and numRestartValue != 'continuous':
raise XmlchemyError( "When ``<w:numStart> is not 1, then ``<w:numRestart>`` must be 'continuous'.")
fPr = self.get_or_add_footnotePr()
numStart = fPr.get_or_add_numStart()
numRestart = fPr.get_or_add_numRestart()
numStart.val = value
numRestart.val = numRestartValue

@property
def footnote_position(self):
"""
The value of the ``w:val`` attribute in the ``<w:pos>`` child
element of ``<w:footnotePr>`` element, as a |String|, or |'pageBottom'| if either the element or the
attribute is not present.
"""
fPr = self.footnotePr
if fPr is None or fPr.pos is None:
return 'pageBottom'
return fPr.pos.val

@footnote_position.setter
def footnote_position(self, value):
if value is None:
value = 'pageBottom'
fPr = self.get_or_add_footnotePr()
pos = fPr.get_or_add_pos()
pos.val = value

def get_footerReference(self, type_):
"""Return footerReference element of *type_* or None if not present."""
path = "./w:footerReference[@w:type='%s']" % WD_HEADER_FOOTER.to_xml(type_)
Expand Down
28 changes: 28 additions & 0 deletions docx/oxml/simpletypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ def validate(cls, value):
)


class ST_FtnPos(XsdString):

@classmethod
def validate(cls, value):
cls.validate_string(value)
valid_values = ('pageBottom', 'beneathText', 'sectEnd', 'docEnd')
if value not in valid_values:
raise ValueError(
"must be one of %s, got '%s'" % (valid_values, value)
)


class ST_HexColor(BaseStringType):

@classmethod
Expand Down Expand Up @@ -301,6 +313,10 @@ def convert_to_xml(cls, value):
return str(half_points)


class ST_NumberFormat(XsdString):
pass


class ST_Merge(XsdStringEnumeration):
"""
Valid values for <w:xMerge val=""> attribute
Expand Down Expand Up @@ -338,6 +354,18 @@ class ST_RelationshipId(XsdString):
pass


class ST_RestartNumber(XsdString):

@classmethod
def validate(cls, value):
cls.validate_string(value)
valid_values = ('continuous', 'eachSect', 'eachPage')
if value not in valid_values:
raise ValueError(
"must be one of %s, got '%s'" % (valid_values, value)
)


class ST_SignedTwipsMeasure(XsdInt):

@classmethod
Expand Down
6 changes: 1 addition & 5 deletions docx/oxml/text/paragraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,7 @@ def footnote_reference_ids(self):
"""
footnote_ids = []
for run in self.r_lst:
new_footnote_ids = run.footnote_reference_ids
if new_footnote_ids:
footnote_ids.extend(new_footnote_ids)
if footnote_ids == []:
footnote_ids = None
footnote_ids.extend([ref_id for ref_id in run.footnote_reference_ids])
return footnote_ids

def lvl_from_para_props(self, numbering_el):
Expand Down
16 changes: 8 additions & 8 deletions docx/oxml/text/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,24 @@ def add_drawing(self, inline_or_anchor):

def clear_content(self):
"""
Remove all child elements except the ``<w:rPr>`` element if present.
Remove all child elements except the ``<w:rPr>`` and ``<w:footnoteReference>`` element if present.
"""
content_child_elms = self[1:] if self.rPr is not None else self[:]
for child in content_child_elms:
# We keep ``w:footnoteReference`` because of the
# platform `replace_special_chars_preprocessor` preprocessor.
if child.tag == qn('w:footnoteReference'):
continue
self.remove(child)

@property
def footnote_reference_ids(self) -> (list[int]|None):
def footnote_reference_ids(self):
"""
Return all footnote reference ids (``<w:footnoteReference>``), or |None| if not present.
Return all footnote reference ids (``<w:footnoteReference>``).
"""
references = []
for child in self:
if child.tag == qn('w:footnoteReference'):
references.append(child.id)
if references == []:
references = None
return references
yield child.id

def increment_containing_footnote_reference_ids(self):
"""
Expand Down
Loading

0 comments on commit a5330dc

Please sign in to comment.