From acc9ef63c5a2d177220b17e1014cce72e99b9004 Mon Sep 17 00:00:00 2001 From: Tibor Date: Fri, 24 Nov 2023 20:55:44 +0100 Subject: [PATCH 1/9] footnote: update comments with more clarification for `_calculate_next_footnote_reference_id` --- docx/document.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docx/document.py b/docx/document.py index 3b0c3bf09..a10642463 100644 --- a/docx/document.py +++ b/docx/document.py @@ -209,31 +209,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: 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) + # Skip paragraphs without footnotes (they don't impact new id). if self.paragraphs[p_i]._p.footnote_reference_ids is None: 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 From 71899acb12c3e6a04c3d151b228fa10f6d6b588b Mon Sep 17 00:00:00 2001 From: Tibor Date: Thu, 22 Feb 2024 21:50:00 +0100 Subject: [PATCH 2/9] footnote: add footnote properties --- docx/oxml/__init__.py | 9 +++ docx/oxml/section.py | 117 ++++++++++++++++++++++++++++++++++++++- docx/oxml/simpletypes.py | 28 ++++++++++ docx/section.py | 50 +++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index a786b495f..52d9adf6d 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -87,19 +87,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) diff --git a/docx/oxml/section.py b/docx/oxml/section.py index fc953e74d..c90e3458d 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -6,8 +6,10 @@ 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.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString, ST_FtnPos, ST_NumberFormat, ST_RestartNumber from docx.oxml.xmlchemy import ( BaseOxmlElement, OptionalAttribute, @@ -17,6 +19,22 @@ ) +class CT_FtnPos(BaseOxmlElement): + """```` element, footnote placement""" + val = RequiredAttribute('w:val', ST_FtnPos) + + +class CT_FtnProps(BaseOxmlElement): + """```` 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""" @@ -31,6 +49,16 @@ class CT_HdrFtrRef(BaseOxmlElement): rId = RequiredAttribute('r:id', XsdString) +class CT_NumFmt(BaseOxmlElement): + """```` element, footnote numbering format""" + val = RequiredAttribute('w:val', ST_NumberFormat) + + +class CT_NumRestart(BaseOxmlElement): + """```` element, footnote numbering restart location""" + val = RequiredAttribute('w:val', ST_RestartNumber) + + class CT_PageMar(BaseOxmlElement): """ ```` element, defining page margins. @@ -70,6 +98,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): @@ -136,6 +165,92 @@ 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 ```` child + element of ```` element, as a |String|, or |None| if either the element or the + attribute is not present. + """ + fPr = self.footnotePr + if fPr is None or fPr.numFmt: + return None + return fPr.numFmt.val + + @footnote_number_format.setter + def footnote_number_format(self, value): + 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 ```` child + element of ```` element, as a |String|, or |None| if either the element or the + attribute is not present. + """ + fPr = self.footnotePr + if fPr is None or fPr.numRestart: + return None + return fPr.numRestart.val + + @footnote_numbering_restart_location.setter + def footnote_numbering_restart_location(self, value): + fPr = self.get_or_add_footnotePr() + numStart = fPr.get_or_add_numStart() + numRestart = fPr.get_or_add_numRestart() + numRestart.val = value + if numStart is None or len(numStart.values()) == 0: + numStart.val = 1 + elif value != 'continuous': + numStart.val = 1 + msg = "When `` is not 'continuous', then ```` must be 1." + warn(msg, UserWarning, stacklevel=2) + + @property + def footnote_numbering_start_value(self): + """ + The value of the ``w:val`` attribute in the ```` child + element of ```` element, as a |Number|, or |None| if either the element or the + attribute is not present. + """ + fPr = self.footnotePr + if fPr is None or fPr.numStart: + return None + return fPr.numStart.val + + @footnote_numbering_start_value.setter + def footnote_numbering_start_value(self, value): + fPr = self.get_or_add_footnotePr() + numStart = fPr.get_or_add_numStart() + numRestart = fPr.get_or_add_numRestart() + numStart.val = value + if numRestart is None or len(numRestart.values()) == 0: + numRestart.val = 'continuous' + elif value != 1: + numRestart.val = 'continuous' + msg = "When `` is not 1, then ```` must be 'continuous'." + warn(msg, UserWarning, stacklevel=2) + + @property + def footnote_position(self): + """ + The value of the ``w:val`` attribute in the ```` child + element of ```` element, as a |String|, or |None| if either the element or the + attribute is not present. + """ + fPr = self.footnotePr + if fPr is None or fPr.pos is None: + return None + return fPr.pos.val + + @footnote_position.setter + def footnote_position(self, value): + 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_) diff --git a/docx/oxml/simpletypes.py b/docx/oxml/simpletypes.py index 400a23700..6016177f9 100644 --- a/docx/oxml/simpletypes.py +++ b/docx/oxml/simpletypes.py @@ -237,6 +237,18 @@ class ST_DrawingElementId(XsdUnsignedInt): pass +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 @@ -289,6 +301,10 @@ def convert_to_xml(cls, value): return str(half_points) +class ST_NumberFormat(XsdString): + pass + + class ST_Merge(XsdStringEnumeration): """ Valid values for attribute @@ -326,6 +342,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 diff --git a/docx/section.py b/docx/section.py index 32ceec7da..37f2f8a7b 100644 --- a/docx/section.py +++ b/docx/section.py @@ -131,6 +131,56 @@ def footer_distance(self): def footer_distance(self, value): self._sectPr.footer = value + @property + def footnote_number_format(self): + """The number format property for the |Footnotes|. + + Read/write. |None| if no setting is present in the XML. + """ + return self._sectPr.footnote_number_format + + @footnote_number_format.setter + def footnote_number_format(self, value): + self._sectPr.footnote_number_format = value + + @property + def footnote_numbering_restart_location(self): + """The number restart location property for the |Footnotes|. + + If the value is not |continuous| then the footnote number start value property is set to |1|. + Read/write. |None| if no setting is present in the XML. + """ + return self._sectPr.footnote_numbering_restart_location + + @footnote_numbering_restart_location.setter + def footnote_numbering_restart_location(self, value): + self._sectPr.footnote_numbering_restart_location = value + + @property + def footnote_numbering_start_value(self): + """The number start value property for the |Footnotes|. + + If the value is not |1| then footnote number restart position property is set to |continuous|. + Read/write. |None| if no setting is present in the XML. + """ + return self._sectPr.footnote_numbering_start_value + + @footnote_numbering_start_value.setter + def footnote_numbering_start_value(self, value): + self._sectPr.footnote_numbering_start_value = value + + @property + def footnote_position(self): + """The position property for the |Footnotes|. + + Read/write. |None| if no setting is present in the XML. + """ + return self._sectPr.footnote_position + + @footnote_position.setter + def footnote_position(self, value): + self._sectPr.footnote_position = value + @property def gutter(self): """ From 5aff55efa5cda3c9bbb24960fa683cbb8ac14e9f Mon Sep 17 00:00:00 2001 From: Tibor Date: Fri, 23 Feb 2024 10:06:53 +0100 Subject: [PATCH 3/9] footnote: small improvments --- docx/document.py | 4 ++-- docx/footnotes.py | 2 +- docx/oxml/footnote.py | 15 +-------------- docx/oxml/section.py | 6 +++--- docx/oxml/text/paragraph.py | 6 +----- docx/oxml/text/run.py | 7 ++----- docx/text/paragraph.py | 4 +--- 7 files changed, 11 insertions(+), 33 deletions(-) diff --git a/docx/document.py b/docx/document.py index a10642463..71724fd79 100644 --- a/docx/document.py +++ b/docx/document.py @@ -210,7 +210,7 @@ def _calculate_next_footnote_reference_id(self, p): new_fr_id = 1 # If paragraph already contains footnotes # 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 # Read the paragraphs containing footnotes and find where the # new footnote will be. Keeping in mind that the footnotes are @@ -226,7 +226,7 @@ def _calculate_next_footnote_reference_id(self, 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: + if len(self.paragraphs[p_i]._p.footnote_reference_ids) == 0: continue # These footnotes are after the new footnote, so we increment them. if not has_passed_containing_para: diff --git a/docx/footnotes.py b/docx/footnotes.py index 29361fdae..9d246608e 100644 --- a/docx/footnotes.py +++ b/docx/footnotes.py @@ -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` diff --git a/docx/oxml/footnote.py b/docx/oxml/footnote.py index 67779f7bd..69c4a876f 100644 --- a/docx/oxml/footnote.py +++ b/docx/oxml/footnote.py @@ -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] @@ -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 diff --git a/docx/oxml/section.py b/docx/oxml/section.py index c90e3458d..da60d6bbb 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -173,7 +173,7 @@ def footnote_number_format(self): attribute is not present. """ fPr = self.footnotePr - if fPr is None or fPr.numFmt: + if fPr is None or fPr.numFmt is None: return None return fPr.numFmt.val @@ -191,7 +191,7 @@ def footnote_numbering_restart_location(self): attribute is not present. """ fPr = self.footnotePr - if fPr is None or fPr.numRestart: + if fPr is None or fPr.numRestart is None: return None return fPr.numRestart.val @@ -216,7 +216,7 @@ def footnote_numbering_start_value(self): attribute is not present. """ fPr = self.footnotePr - if fPr is None or fPr.numStart: + if fPr is None or fPr.numStart is None: return None return fPr.numStart.val diff --git a/docx/oxml/text/paragraph.py b/docx/oxml/text/paragraph.py index 46ded8af0..f76070e0d 100644 --- a/docx/oxml/text/paragraph.py +++ b/docx/oxml/text/paragraph.py @@ -60,11 +60,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 set_sectPr(self, sectPr): diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index bef203331..09534a282 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -73,17 +73,14 @@ def clear_content(self): self.remove(child) @property - def footnote_reference_ids(self) -> (list[int]|None): + def footnote_reference_ids(self): """ Return all footnote reference ids (````), or |None| if not present. """ 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): """ diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index 4fb82a4ee..e2188225b 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -81,11 +81,9 @@ def footnotes(self): Returns a list of |Footnote| instances that refers to the footnotes in this paragraph, or |None| if none footnote is defined. """ + footnote_list = [] reference_ids = self._p.footnote_reference_ids - if reference_ids == None: - return None footnotes = self._parent._parent.footnotes - footnote_list = [] for ref_id in reference_ids: footnote_list.append(footnotes[ref_id]) return footnote_list From 4a82b0ce48f8135920c78cbfafa5098fcf21b35e Mon Sep 17 00:00:00 2001 From: Tibor Date: Fri, 23 Feb 2024 18:06:27 +0100 Subject: [PATCH 4/9] fix(footnote): added default values for footnote properties --- docx/oxml/section.py | 49 ++++++++++++++++++++++++++------------------ docx/section.py | 10 ++++----- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/docx/oxml/section.py b/docx/oxml/section.py index da60d6bbb..78fb12e44 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -9,6 +9,7 @@ from warnings import warn from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START +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, @@ -169,16 +170,18 @@ def footer(self, value): def footnote_number_format(self): """ The value of the ``w:val`` attribute in the ```` child - element of ```` element, as a |String|, or |None| if either the element or the + element of ```` 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 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 @@ -187,66 +190,72 @@ def footnote_number_format(self, value): def footnote_numbering_restart_location(self): """ The value of the ``w:val`` attribute in the ```` child - element of ```` element, as a |String|, or |None| if either the element or the + element of ```` element, as a |String|, or |'continuous'| if either the element or the attribute is not present. + This property is tied with ````. """ fPr = self.footnotePr if fPr is None or fPr.numRestart is None: - return 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 ```` property. + if value is None: + value = 'continuous' + numStartValue = self.footnote_numbering_start_value + if value != 'continuous' and numStartValue != 1: + raise XmlchemyError( "When `` is not 'continuous', then ```` 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 - if numStart is None or len(numStart.values()) == 0: - numStart.val = 1 - elif value != 'continuous': - numStart.val = 1 - msg = "When `` is not 'continuous', then ```` must be 1." - warn(msg, UserWarning, stacklevel=2) @property def footnote_numbering_start_value(self): """ The value of the ``w:val`` attribute in the ```` child - element of ```` element, as a |Number|, or |None| if either the element or the + element of ```` element, as a |Number|, or |1| if either the element or the attribute is not present. + This property is tied with ````. """ fPr = self.footnotePr if fPr is None or fPr.numStart is None: - return 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 ```` property. + if value is None: + value = 1 + numRestartValue = self.footnote_numbering_restart_location + if value != 1 and numRestartValue != 'continuous': + raise XmlchemyError( "When `` is not 1, then ```` must be 'continuous'.") fPr = self.get_or_add_footnotePr() numStart = fPr.get_or_add_numStart() numRestart = fPr.get_or_add_numRestart() numStart.val = value - if numRestart is None or len(numRestart.values()) == 0: - numRestart.val = 'continuous' - elif value != 1: - numRestart.val = 'continuous' - msg = "When `` is not 1, then ```` must be 'continuous'." - warn(msg, UserWarning, stacklevel=2) + numRestart.val = numRestartValue @property def footnote_position(self): """ The value of the ``w:val`` attribute in the ```` child - element of ```` element, as a |String|, or |None| if either the element or the + element of ```` 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 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 diff --git a/docx/section.py b/docx/section.py index 37f2f8a7b..1d0b09928 100644 --- a/docx/section.py +++ b/docx/section.py @@ -135,7 +135,7 @@ def footer_distance(self, value): def footnote_number_format(self): """The number format property for the |Footnotes|. - Read/write. |None| if no setting is present in the XML. + Read/write. |'decimal'| if no setting is present in the XML. """ return self._sectPr.footnote_number_format @@ -147,8 +147,8 @@ def footnote_number_format(self, value): def footnote_numbering_restart_location(self): """The number restart location property for the |Footnotes|. - If the value is not |continuous| then the footnote number start value property is set to |1|. - Read/write. |None| if no setting is present in the XML. + If the value is not |'continuous'| then the footnote number start value property is set to |1|. + Read/write. |'continuous'| if no setting is present in the XML. """ return self._sectPr.footnote_numbering_restart_location @@ -161,7 +161,7 @@ def footnote_numbering_start_value(self): """The number start value property for the |Footnotes|. If the value is not |1| then footnote number restart position property is set to |continuous|. - Read/write. |None| if no setting is present in the XML. + Read/write. |1| if no setting is present in the XML. """ return self._sectPr.footnote_numbering_start_value @@ -173,7 +173,7 @@ def footnote_numbering_start_value(self, value): def footnote_position(self): """The position property for the |Footnotes|. - Read/write. |None| if no setting is present in the XML. + Read/write. |'pageBottom'| if no setting is present in the XML. """ return self._sectPr.footnote_position From 69fea23989e1fb10c35e3a4de152e59df1007bf8 Mon Sep 17 00:00:00 2001 From: Tibor Date: Tue, 12 Mar 2024 13:50:46 +0100 Subject: [PATCH 5/9] fix(footnote): small typo fix and return Run.footnote_reference_ids as a list --- docx/oxml/text/run.py | 3 +-- docx/text/run.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index 09534a282..c334a6277 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -75,9 +75,8 @@ def clear_content(self): @property def footnote_reference_ids(self): """ - Return all footnote reference ids (````), or |None| if not present. + Return all footnote reference ids (````). """ - references = [] for child in self: if child.tag == qn('w:footnoteReference'): yield child.id diff --git a/docx/text/run.py b/docx/text/run.py index cdebbc63a..7aca4ae0b 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -108,11 +108,11 @@ def font(self): return Font(self._element) @property - def footnote_reference_ids(self) -> (list[int]|None): + def footnote_reference_ids(self): """ - Returns all footnote reference ids from the run, or |None| if none found. + Returns all footnote reference ids from the run as a list. """ - return self._r.footnote_reference_ids + return [ref_id for ref_id in self._r.footnote_reference_ids] @property def italic(self): From cf7b28a592b981c37f36a5b37bb8a156273b31c9 Mon Sep 17 00:00:00 2001 From: Tibor Date: Tue, 12 Mar 2024 14:09:59 +0100 Subject: [PATCH 6/9] footnote: use `find_containing_document` to get Document object --- docx/text/paragraph.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index d50822a29..d5ed0eed2 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -47,7 +47,7 @@ def add_footnote(self): The footnotes are kept in order by `footnote_reference_id`, so the appropriate id is calculated based on the current state. """ - document = self._parent._parent + document = find_containing_document(self) new_fr_id = document._calculate_next_footnote_reference_id(self._p) r = self._p.add_r() r.add_footnoteReference(new_fr_id) @@ -190,12 +190,14 @@ def runs_and_hyperlinks(self): @property def footnotes(self): """ - Returns a list of |Footnote| instances that refers to the footnotes in this paragraph, - or |None| if none footnote is defined. + Returns a list of |Footnote| instances that refers to the footnotes in this paragraph. """ footnote_list = [] reference_ids = self._p.footnote_reference_ids - footnotes = self._parent._parent.footnotes + document = find_containing_document(self) + if document is None: + return footnote_list + footnotes = document.footnotes for ref_id in reference_ids: footnote_list.append(footnotes[ref_id]) return footnote_list From edc1d6bc2e82975fe08d9356a1af80ebf235df3b Mon Sep 17 00:00:00 2001 From: Tibor Date: Tue, 12 Mar 2024 14:51:49 +0100 Subject: [PATCH 7/9] footnote: for parser preprocessor do not remove footnote reference ids from runs --- docx/oxml/text/run.py | 4 +++- docx/text/paragraph.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index 526b27ed2..b530214ea 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -79,10 +79,12 @@ def add_drawing(self, inline_or_anchor): def clear_content(self): """ - Remove all child elements except the ```` element if present. + Remove all child elements except the ```` and ```` element if present. """ content_child_elms = self[1:] if self.rPr is not None else self[:] for child in content_child_elms: + if child.tag == qn('w:footnoteReference'): + continue self.remove(child) @property diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index d5ed0eed2..ed2e8a3bf 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -571,7 +571,7 @@ def lstrip(self, chars=None): while self.runs: run = self.runs[0] run.text = run.text.lstrip(chars) - if not run.text: + if not run.text and len(run.footnote_reference_ids) == 0: run._r.getparent().remove(run._r) else: break @@ -585,7 +585,7 @@ def rstrip(self, chars=None): while self.runs: run = self.runs[len(self.runs) - 1] run.text = run.text.rstrip(chars) - if not run.text: + if not run.text and len(run.footnote_reference_ids) == 0: run._r.getparent().remove(run._r) else: break From 80ac69bbae82a885e9533778ecc61ec461f808bd Mon Sep 17 00:00:00 2001 From: Tibor Date: Wed, 13 Mar 2024 21:18:28 +0100 Subject: [PATCH 8/9] chore: bump version to v.0.8.10.31 --- docx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docx/__init__.py b/docx/__init__.py index 08fabcda8..b5ade25ca 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -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 From 910f163199c54018c54bd89cf15c2e9fc80b2b28 Mon Sep 17 00:00:00 2001 From: Tibor Date: Sat, 16 Mar 2024 15:04:52 +0100 Subject: [PATCH 9/9] footnote: `find_containing_document` function improvment for platform parser and some additional comments --- docx/oxml/text/run.py | 2 ++ docx/shared.py | 18 +++++++++++++----- docx/text/paragraph.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index b530214ea..2db3b3ea7 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -83,6 +83,8 @@ def clear_content(self): """ 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) diff --git a/docx/shared.py b/docx/shared.py index d11ed2184..e2f8fa913 100644 --- a/docx/shared.py +++ b/docx/shared.py @@ -286,12 +286,20 @@ def find_containing_document(element): """ from .document import Document while True: - if not hasattr(element, '_parent'): - raise PythonDocxError(f'{type(element)} has no `_parent` property.') - if isinstance(element._parent, Document): - return element._parent + if hasattr(element, '_document_part'): + return element._document_part.document + elif hasattr(element, '_parent'): + if isinstance(element._parent, Document): + return element._parent + else: + element = element._parent + elif hasattr(element, 'parent'): + if isinstance(element.parent, Document): + return element.parent + else: + element = element.parent else: - element = element._parent + raise PythonDocxError(f'{type(element)} couldn\'t find root Document.') def is_valid_url(url): diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index ed2e8a3bf..669f9eea4 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -780,7 +780,7 @@ def image_parts(self): """ Return all image parts related to this paragraph. """ - doc = self.part.document + doc = find_containing_document(self) drawings = [] parts = []