From 5562cd4040346f11257ca83e260e9fbd9653ee03 Mon Sep 17 00:00:00 2001 From: Junhyeong Ahn Date: Fri, 4 Sep 2020 12:33:17 +0900 Subject: [PATCH] provide api to access footnote, endnote --- .gitignore | 2 + docx/__init__.py | 3 + docx/fntent/__init__.py | 0 docx/fntent/endnoteReference.py | 19 ++++ docx/fntent/fntent.py | 129 +++++++++++++++++++++++++++ docx/fntent/footnoteReference.py | 18 ++++ docx/oxml/__init__.py | 9 ++ docx/oxml/fntent.py | 75 ++++++++++++++++ docx/oxml/text/run.py | 2 + docx/parts/document.py | 57 ++++++++++++ docx/parts/fntent.py | 92 +++++++++++++++++++ docx/templates/default-endnotes.xml | 33 +++++++ docx/templates/default-footnotes.xml | 33 +++++++ docx/text/run.py | 18 +++- 14 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 docx/fntent/__init__.py create mode 100644 docx/fntent/endnoteReference.py create mode 100644 docx/fntent/fntent.py create mode 100644 docx/fntent/footnoteReference.py create mode 100644 docx/oxml/fntent.py create mode 100644 docx/parts/fntent.py create mode 100644 docx/templates/default-endnotes.xml create mode 100644 docx/templates/default-footnotes.xml diff --git a/.gitignore b/.gitignore index e24445137..c81beb59f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ _scratch/ Session.vim /.tox/ +.vscode +venv \ No newline at end of file diff --git a/docx/__init__.py b/docx/__init__.py index 4dae2946b..7c527a025 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -17,6 +17,7 @@ from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart from docx.parts.styles import StylesPart +from docx.parts.fntent import FootnotesPart, EndnotesPart def part_class_selector(content_type, reltype): @@ -33,6 +34,8 @@ def part_class_selector(content_type, reltype): PartFactory.part_type_for[CT.WML_NUMBERING] = NumberingPart PartFactory.part_type_for[CT.WML_SETTINGS] = SettingsPart PartFactory.part_type_for[CT.WML_STYLES] = StylesPart +PartFactory.part_type_for[CT.WML_FOOTNOTES] = FootnotesPart +PartFactory.part_type_for[CT.WML_ENDNOTES] = EndnotesPart del ( CT, diff --git a/docx/fntent/__init__.py b/docx/fntent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docx/fntent/endnoteReference.py b/docx/fntent/endnoteReference.py new file mode 100644 index 000000000..f571ac80f --- /dev/null +++ b/docx/fntent/endnoteReference.py @@ -0,0 +1,19 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +from ..shared import Parented + +class EndnoteReference(Parented): + """ + Proxy object wrapping ```` element. + """ + def __init__(self, endnoteReference, parent): + super(EndnoteReference, self).__init__(parent) + self._element = endnoteReference + + @property + def endnote(self): + return self.part.get_endnote(self._element.id) + + diff --git a/docx/fntent/fntent.py b/docx/fntent/fntent.py new file mode 100644 index 000000000..1efa2e6a0 --- /dev/null +++ b/docx/fntent/fntent.py @@ -0,0 +1,129 @@ +# encoding: utf-8 + + +from __future__ import absolute_import, division, print_function, unicode_literals + +from docx.shared import ElementProxy +from ..text.paragraph import Paragraph +from ..shared import Parented + +class Footnotes(ElementProxy): + """ + Footnotes object, container for all objects in the footnotes part + + Accessed using the :attr:`.Document.footnotes` property. Supports ``len()``, iteration, + and dictionary-style access by footnote id. + """ + + def __init__(self, element, part): + super(Footnotes, self).__init__(element) + self._part = part + + @property + def part(self): + """ + The |FootnotesPart| object of this document. + """ + return self._part + + + @property + def footnotes(self): + return [Footnote(footnote, self) for footnote in self._element.footnote_lst] + + def get_by_id(self, footnote_id): + """Return the footnote matching *footnote_id*. + + Returns |None| if not found. + """ + return self._get_by_id(footnote_id) + + def _get_by_id(self, footnote_id): + """ + Return the footnote matching *footnote_id*. + """ + footnote = self._element.get_by_id(footnote_id) + + if footnote is None: + return None + + return Footnote(footnote, self) + + +class Footnote(Parented): + """ + Proxy object wrapping ```` element. + """ + + def __init__(self, footnote, parent): + super(Footnote, self).__init__(parent) + self._element = footnote + + + @property + def paragraphs(self): + """ + Returns a list of paragraph proxy object + """ + + return [Paragraph(p, self) for p in self._element.p_lst] + + +class Endnotes(ElementProxy): + """ + Endnotes object, container for all objects in the endnotes part + + Accessed using the :attr:`.Document.endnotes` property. Supports ``len()``, iteration, + and dictionary-style access by endnote id. + """ + + def __init__(self, element, part): + super(Endnotes, self).__init__(element) + self._part = part + + @property + def part(self): + """ + The |EndnotesPart| object of this document. + """ + return self._part + + @property + def endnotes(self): + return [Endnote(endnote, self) for endnote in self._element.endnote_lst] + + def get_by_id(self, endnote_id): + """Return the endnote matching *endnote_id*. + + Returns |None| if not found. + """ + return self._get_by_id(endnote_id) + + def _get_by_id(self, endnote_id): + """ + Return the endnote matching *endnote_id*. + """ + endnote = self._element.get_by_id(endnote_id) + + if endnote is None: + return None + + return Endnote(endnote, self) + + +class Endnote(Parented): + """ + Proxy object wrapping ```` element. + """ + + def __init__(self, endnote, parent): + super(Endnote, self).__init__(parent) + self._element = endnote + + @property + def paragraphs(self): + """ + Returns a list of paragraph proxy object + """ + + return [Paragraph(p, self) for p in self._element.p_lst] diff --git a/docx/fntent/footnoteReference.py b/docx/fntent/footnoteReference.py new file mode 100644 index 000000000..08584baae --- /dev/null +++ b/docx/fntent/footnoteReference.py @@ -0,0 +1,18 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +from ..shared import Parented + +class FootnoteReference(Parented): + """ + Proxy object wrapping ```` element. + """ + def __init__(self, footnoteReference, parent): + super(FootnoteReference, self).__init__(parent) + self._element = footnoteReference + + @property + def footnote(self): + return self.part.get_footnote(self._element.id) + diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 093c1b45b..a1d14d818 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -246,3 +246,12 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:br', CT_Br) register_element_cls('w:r', CT_R) register_element_cls('w:t', CT_Text) + + +from .fntent import CT_Footnotes, CT_Footnote, CT_Endnotes, CT_Endnote, CT_FootnoteReference, CT_EndnoteReference +register_element_cls('w:footnote', CT_Footnote) +register_element_cls('w:footnotes', CT_Footnotes) +register_element_cls('w:endnote', CT_Endnote) +register_element_cls('w:endnotes', CT_Endnotes) +register_element_cls('w:footnoteReference', CT_FootnoteReference) +register_element_cls('w:endnoteReference', CT_EndnoteReference) diff --git a/docx/oxml/fntent.py b/docx/oxml/fntent.py new file mode 100644 index 000000000..c215fa2eb --- /dev/null +++ b/docx/oxml/fntent.py @@ -0,0 +1,75 @@ +from .xmlchemy import ( + BaseOxmlElement, OneAndOnlyOne, ZeroOrMore, OneOrMore, RequiredAttribute +) +from .simpletypes import ST_DecimalNumber, ST_OnOff, ST_String + +class CT_Footnotes(BaseOxmlElement): + """ + A ```` element, the root element of a footnotes part, i.e. + footnotes.xml + """ + + footnote = ZeroOrMore('w:footnote') + + def get_by_id(self, footnoteId): + """ + Return the ```` child element having ``w:id`` attribute + matching *footnoteId*, or |None| if not found. + """ + xpath = 'w:footnote[@w:id="%s"]' % footnoteId + try: + return self.xpath(xpath)[0] + except IndexError: + return None + + +class CT_Footnote(BaseOxmlElement): + """ + A ```` element, representing a footnote definition + """ + + p = OneOrMore('w:p') + +class CT_Endnotes(BaseOxmlElement): + """ + A ```` element, the root element of a endnotes part, i.e. + endnotes.xml + """ + + endnote = ZeroOrMore('w:endnote') + + def get_by_id(self, endnoteId): + """ + Return the ```` child element having ``w:id`` attribute + matching *endnoteId*, or |None| if not found. + """ + xpath = 'w:endnote[@w:id="%s"]' % endnoteId + try: + return self.xpath(xpath)[0] + except IndexError: + return None + + + +class CT_Endnote(BaseOxmlElement): + """ + A ```` element, representing a endnote definition + """ + + p = OneOrMore('w:p') + + +class CT_FootnoteReference(BaseOxmlElement): + """ + A ```` element. provide access to footnote proxy object. + """ + + id = RequiredAttribute('w:id', ST_String) + + +class CT_EndnoteReference(BaseOxmlElement): + """ + A ```` element. provide access to endnote proxy object. + """ + + id = RequiredAttribute('w:id', ST_String) \ No newline at end of file diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index 8f0a62e82..98c79ee28 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -29,6 +29,8 @@ class CT_R(BaseOxmlElement): cr = ZeroOrMore('w:cr') tab = ZeroOrMore('w:tab') drawing = ZeroOrMore('w:drawing') + footnoteReference = ZeroOrMore('w:footnoteReference') + endnoteReference = ZeroOrMore('w:endnoteReference') def _insert_rPr(self, rPr): self.insert(0, rPr) diff --git a/docx/parts/document.py b/docx/parts/document.py index 59d0b7a71..ee293d8b0 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -7,6 +7,7 @@ from docx.document import Document from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.parts.hdrftr import FooterPart, HeaderPart +from docx.parts.fntent import FootnotesPart, EndnotesPart from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart from docx.parts.story import BaseStoryPart @@ -125,7 +126,37 @@ def styles(self): of this document. """ return self._styles_part.styles + + @property + def footnotes(self): + """ + A |Footnotes| object providing access to the footnotes in the footnotes part + of this document. + """ + return self._footnotes_part.footnotes + + def get_footnote(self, footnote_id): + """ + Return the footnote matching *footnote_id*. + Returns |None| if no footnote matches *footnote_id* + """ + return self.footnotes.get_by_id(footnote_id) + @property + def endnotes(self): + """ + A |Endnotes| object providing access to the endnotes in the endnotes part + of this document. + """ + return self._endnotes_part.endnotes + + def get_endnote(self, endnote_id): + """ + Return the endnote matching *endnote_id*. + Returns |None| if no endnote matches *endnote_id* + """ + return self.endnotes.get_by_id(endnote_id) + @property def _settings_part(self): """ @@ -152,3 +183,29 @@ def _styles_part(self): styles_part = StylesPart.default(self.package) self.relate_to(styles_part, RT.STYLES) return styles_part + + @property + def _footnotes_part(self): + """ + Instance of |FootnotesPart| for this document. Creates an empty footnotes + part if one is not present. + """ + try: + return self.part_related_by(RT.FOOTNOTES) + except KeyError: + footnotes_part = FootnotesPart.default(self.package) + self.relate_to(footnotes_part, RT.FOOTNOTES) + return footnotes_part + + @property + def _endnotes_part(self): + """ + Instance of |EndnotesPart| for this document. Creates an empty endnotes + part if one is not present. + """ + try: + return self.part_related_by(RT.ENDNOTES) + except KeyError: + endnotes_part = EndnotesPart.default(self.package) + self.relate_to(endnotes_part, RT.ENDNOTES) + return endnotes_part diff --git a/docx/parts/fntent.py b/docx/parts/fntent.py new file mode 100644 index 000000000..197c7599e --- /dev/null +++ b/docx/parts/fntent.py @@ -0,0 +1,92 @@ +# encoding: utf-8 + +""" +Footnotes and endnotes part objects +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import os + +from ..opc.constants import CONTENT_TYPE as CT +from ..opc.packuri import PackURI +from ..opc.part import XmlPart +from ..oxml import parse_xml +from ..fntent.fntent import Footnotes, Endnotes +from .story import BaseStoryPart + + +class FootnotesPart(BaseStoryPart): + """ + Proxy for the footnotes.xml part containing footnote definitions for a document. + """ + @classmethod + def default(cls, package): + """ + Return a newly created footnote part, containing a default set of + elements. + """ + partname = PackURI('/word/footnotes.xml') + content_type = CT.WML_FOOTNOTES + element = parse_xml(cls._default_footnotes_xml()) + return cls(partname, content_type, element, package) + + @property + def footnotes(self): + """ + The |_Footnotes| instance containing the footnotes ( element + proxies) for this footnotes part. + """ + return Footnotes(self.element, self) + + @classmethod + def _default_footnotes_xml(cls): + """ + Return a bytestream containing XML for a default footnotes part. + """ + path = os.path.join( + os.path.split(__file__)[0], '..', 'templates', + 'default-footnotes.xml' + ) + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes + + +class EndnotesPart(BaseStoryPart): + """ + Proxy for the endnotes.xml part containing endnote definitions for a document. + """ + @classmethod + def default(cls, package): + """ + Return a newly created endnote part, containing a default set of + elements. + """ + partname = PackURI('/word/endnotes.xml') + content_type = CT.WML_FOOTNOTES + element = parse_xml(cls._default_endnotes_xml()) + return cls(partname, content_type, element, package) + + @property + def endnotes(self): + """ + The |_Endnotes| instance containing the endnotes ( element + proxies) for this endnotes part. + """ + return Endnotes(self.element, self) + + @classmethod + def _default_endnotes_xml(cls): + """ + Return a bytestream containing XML for a default endnotes part. + """ + path = os.path.join( + os.path.split(__file__)[0], '..', 'templates', + 'default-endnotes.xml' + ) + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes diff --git a/docx/templates/default-endnotes.xml b/docx/templates/default-endnotes.xml new file mode 100644 index 000000000..da6ee65d6 --- /dev/null +++ b/docx/templates/default-endnotes.xml @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/docx/templates/default-footnotes.xml b/docx/templates/default-footnotes.xml new file mode 100644 index 000000000..223f23645 --- /dev/null +++ b/docx/templates/default-footnotes.xml @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/docx/text/run.py b/docx/text/run.py index 97d6da7db..fc6f7a8e8 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -11,7 +11,8 @@ from .font import Font from ..shape import InlineShape from ..shared import Parented - +from ..fntent.footnoteReference import FootnoteReference +from ..fntent.endnoteReference import EndnoteReference class Run(Parented): """ @@ -181,6 +182,21 @@ def underline(self): def underline(self, value): self.font.underline = value + @property + def footnotes(self): + """ + Return a list of footnote proxy elements. + """ + + return [FootnoteReference(footnoteReference, self) for footnoteReference in self._r.footnoteReference_lst] + + @property + def endnotes(self): + """ + Return a list of endnote proxy elements. + """ + + return [EndnoteReference(endnoteReference, self) for endnoteReference in self._r.endnoteReference_lst] class _Text(object): """