diff --git a/.gitignore b/.gitignore index e24445137..9fed3b1fc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ _scratch/ Session.vim /.tox/ +.vscode +venv diff --git a/README.md b/README.md new file mode 100644 index 000000000..fc120361d --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# python docx + +## Summary + +This repository custom modification from [python-openxml/python-docx](https://github.com/python-openxml/python-docx) + +You can referencing [official API document](https://python-docx.readthedocs.org/en/latest/), but not fully compatible with. diff --git a/docs/dev/analysis/features/text/hyperlink.rst b/docs/dev/analysis/features/text/hyperlink.rst new file mode 100644 index 000000000..aa8788da3 --- /dev/null +++ b/docs/dev/analysis/features/text/hyperlink.rst @@ -0,0 +1,301 @@ + +Hyperlink +========= + +Word allows hyperlinks to be placed in a document. + +The target of a hyperlink may be external, such as a web site, or internal, +to another location in the document. + +A hyperlink can contain multiple runs of text, each with its own distinct +text formatting (font). + + +Candidate protocol +------------------ + +An external hyperlink has an address and an optional anchor. An internal +hyperlink has only an anchor. + +.. highlight:: python + +**Add the external hyperlink** `http://us.com#about`:: + + >>> hyperlink = paragraph.add_hyperlink('About', address='http://us.com', anchor='about') + >>> hyperlink + + >>> hyperlink.text + 'About' + >>> hyperlink.address + 'http://us.com' + >>> hyperlink.anchor + 'about' + +**Add an internal hyperlink (to a bookmark)**:: + + >>> hyperlink = paragraph.add_hyperlink('Section 1', anchor='Section_1') + >>> hyperlink.text + 'Section 1' + >>> hyperlink.anchor + 'Section_1' + >>> hyperlink.address + None + +**Modify hyperlink properties**:: + + >>> hyperlink.text = 'Froogle' + >>> hyperlink.text + 'Froogle' + >>> hyperlink.address = 'mailto:info@froogle.com?subject=sup dawg?' + >>> hyperlink.address + 'mailto:info@froogle.com?subject=sup%20dawg%3F' + >>> hyperlink.anchor = None + >>> hyperlink.anchor + None + +**Add additional runs to a hyperlink**:: + + >>> hyperlink.text = 'A ' + >>> # .insert_run inserts a new run at idx, defaults to idx=-1 + >>> hyperlink.insert_run(' link').bold = True + >>> hyperlink.insert_run('formatted', idx=1).bold = True + >>> hyperlink.text + 'A formatted link' + >>> [r for r in hyperlink.iter_runs()] + [, + , + ] + +**Iterate over the run-level items a paragraph contains**:: + + >>> paragraph = document.add_paragraph('A paragraph having a link to: ') + >>> paragraph.add_hyperlink(text='github', address='http://github.com') + >>> [item for item in paragraph.iter_run_level_items()]: + [, ] + +**Paragraph.text now includes text contained in a hyperlink**:: + + >>> paragraph.text + 'A paragraph having a link to: github' + + +Word Behaviors +-------------- + +* What are the semantics of the w:history attribute on w:hyperlink? I'm + suspecting this indicates whether the link should show up blue (unvisited) + or purple (visited). I'm inclined to think we need that as a read/write + property on hyperlink. We should see what the MS API does on this count. + +* We probably need to enforce some character-set restrictions on w:anchor. + Word doesn't seem to like spaces or hyphens, for example. The simple type + ST_String doesn't look like it takes care of this. + +* We'll need to test URL escaping of special characters like spaces and + question marks in Hyperlink.address. + +* What does Word do when loading a document containing an internal hyperlink + having an anchor value that doesn't match an existing bookmark? We'll want + to know because we're sure to get support inquiries from folks who don't + match those up and wonder why they get a repair error or whatever. + + +Specimen XML +------------ + +.. highlight:: xml + + +External links +~~~~~~~~~~~~~~ + +The address (URL) of an external hyperlink is stored in the document.xml.rels +file, keyed by the w:hyperlink@r:id attribute:: + + + + This is an external link to + + + + + + + Google + + + + +... mapping to relationship in document.xml.rels:: + + + + + +A hyperlink can contain multiple runs of text (and a whole lot of other +stuff, including nested hyperlinks, at least as far as the schema indicates):: + + + + + + + + A hyperlink containing an + + + + + + + italicized + + + + + + word + + + + + +Internal links +~~~~~~~~~~~~~~ + +An internal link provides "jump to another document location" behavior in the +Word UI. An internal link is distinguished by the absence of an r:id +attribute. In this case, the w:anchor attribute is required. The value of the +anchor attribute is the name of a bookmark in the document. + +Example:: + + + + See + + + + + + + Section 4 + + + + for more details. + + + +... referring to this bookmark elsewhere in the document:: + + + + + Section 4 + + + + + +Schema excerpt +-------------- + +.. highlight:: xml + +:: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/dev/analysis/features/text/index.rst b/docs/dev/analysis/features/text/index.rst index 2fff03924..7b60a1c65 100644 --- a/docs/dev/analysis/features/text/index.rst +++ b/docs/dev/analysis/features/text/index.rst @@ -5,7 +5,11 @@ Text .. toctree:: :titlesonly: +<<<<<<< HEAD tab-stops +======= + hyperlink +>>>>>>> docs: document hyperlink analysis font-highlight-color paragraph-format font @@ -13,3 +17,5 @@ Text underline run-content breaks + hyperlink + diff --git a/docx/__init__.py b/docx/__init__.py index 4dae2946b..d08fac4ca 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -17,6 +17,9 @@ 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 +from docx.parts.theme import ThemePart +from docx.parts.fnttbl import FontTablePart def part_class_selector(content_type, reltype): @@ -33,6 +36,10 @@ 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 +PartFactory.part_type_for[CT.OFC_THEME] = ThemePart +PartFactory.part_type_for[CT.WML_FONT_TABLE] = FontTablePart 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/fnttbl.py b/docx/fnttbl.py new file mode 100644 index 000000000..08d525de1 --- /dev/null +++ b/docx/fnttbl.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +from docx.shared import ElementProxy + +class FontTable(ElementProxy): + """ + FontTable object, container for all objects in the font table part + """ + def __init__(self, element, part): + super(FontTable, self).__init__(element) + self._part = part \ No newline at end of file diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 093c1b45b..37d5e2be1 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -242,7 +242,19 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:tabs', CT_TabStops) register_element_cls('w:widowControl', CT_OnOff) +from .text.hyperlink import CT_Hyperlink +register_element_cls('w:hyperlink', CT_Hyperlink) + from .text.run import CT_Br, CT_R, CT_Text # noqa 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/ns.py b/docx/oxml/ns.py index 6b0861284..fd12660df 100644 --- a/docx/oxml/ns.py +++ b/docx/oxml/ns.py @@ -24,6 +24,9 @@ "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", "xml": "http://www.w3.org/XML/1998/namespace", "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", + "v": "urn:schemas-microsoft-com:vml" } pfxmap = dict((value, key) for key, value in nsmap.items()) diff --git a/docx/oxml/text/font.py b/docx/oxml/text/font.py index 810ec2b30..7b3286e9a 100644 --- a/docx/oxml/text/font.py +++ b/docx/oxml/text/font.py @@ -32,6 +32,7 @@ class CT_Fonts(BaseOxmlElement): """ ascii = OptionalAttribute('w:ascii', ST_String) hAnsi = OptionalAttribute('w:hAnsi', ST_String) + eastAsia = OptionalAttribute('w:eastAsia', ST_String) class CT_Highlight(BaseOxmlElement): @@ -155,6 +156,23 @@ def rFonts_hAnsi(self, value): rFonts = self.get_or_add_rFonts() rFonts.hAnsi = value + @property + def rFonts_eastAsia(self): + """ + The value of `w:rFonts/@w:eastAsia` or |None| if not present. + """ + rFonts = self.rFonts + if rFonts is None: + return None + return rFonts.eastAsia + + @rFonts_eastAsia.setter + def rFonts_eastAsia(self, value): + if value is None and self.rFonts is None: + return + rFonts = self.get_or_add_rFonts() + rFonts.eastAsia = value + @property def style(self): """ diff --git a/docx/oxml/text/hyperlink.py b/docx/oxml/text/hyperlink.py new file mode 100644 index 000000000..b0748a379 --- /dev/null +++ b/docx/oxml/text/hyperlink.py @@ -0,0 +1,25 @@ + +""" +Custom element classes related to hyperlinks (CT_Hyperlink). +""" + +from ..ns import qn +from ..xmlchemy import BaseOxmlElement, OxmlElement, ZeroOrMore + +class CT_Hyperlink(BaseOxmlElement): + """ + ```` element, containing the properties and text for a hyperlink. + """ + r = ZeroOrMore('w:r') + + def clear_content(self): + """ + Remove all child elements + """ + for child in self[:]: + self.remove(child) + + + + + diff --git a/docx/oxml/text/paragraph.py b/docx/oxml/text/paragraph.py index 5e4213776..f380219bc 100644 --- a/docx/oxml/text/paragraph.py +++ b/docx/oxml/text/paragraph.py @@ -76,3 +76,13 @@ def style(self): def style(self, style): pPr = self.get_or_add_pPr() pPr.style = style + + @property + def inline_items(self): + return self.xpath('./w:r | ./w:hyperlink') + + inline_items.__doc__ = ( + 'A list containing each of the `` | `` child elements, in the o' + 'rder they appear.' + ) + 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..73ca4d514 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -7,6 +7,9 @@ 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.theme import ThemePart +from docx.parts.fnttbl import FontTablePart from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart from docx.parts.story import BaseStoryPart @@ -125,6 +128,52 @@ 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 theme(self): + """ + A |Theme| object providing access to the theme in the theme part + of this document. + """ + return self._theme_part.theme + + @property + def font_table(self): + """ + A |FontTable| object providing access to the font table in the fonttable part + of this document. + """ + return self._font_table_part.font_table @property def _settings_part(self): @@ -152,3 +201,55 @@ 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 + + @property + def _theme_part(self): + """ + Instance of |ThemePart| for this document. Creates an default theme + part if one is not present. + """ + try: + return self.part_related_by(RT.THEME) + except KeyError: + theme_part = ThemePart.default(self.package) + self.relate_to(theme_part, RT.THEME) + return theme_part + + @property + def _font_table_part(self): + """ + Instance of |FontTablePart| for this document. Creates an default font table + part if one is not present. + """ + try: + return self.part_related_by(RT.FONT_TABLE) + except KeyError: + font_table_part = FontTablePart.default(self.package) + self.relate_to(font_table_part, RT.FONT_TABLE) + return font_table_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/parts/fnttbl.py b/docx/parts/fnttbl.py new file mode 100644 index 000000000..acdd45ab2 --- /dev/null +++ b/docx/parts/fnttbl.py @@ -0,0 +1,47 @@ +""" +Theme 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 ..oxml import parse_xml +from ..fnttbl import FontTable +from .story import BaseStoryPart + +class FontTablePart(BaseStoryPart): + """ + Proxy for the fontTable.xml part containing fontTable definitions for a document. + """ + @classmethod + def default(cls, package): + """ + Return a newly created fontTable part, containing a default set of + elements. + """ + partname = PackURI('/word/fontTable.xml') + content_type = CT.WML_FONT_TABLE + element = parse_xml(cls._default_font_table_xml()) + return cls(partname, content_type, element, package) + + @property + def font_table(self): + return FontTable(self.element, self) + + @classmethod + def _default_font_table_xml(cls): + """ + Return a bytestream containing XML for a default fontTable part. + """ + path = os.path.join( + os.path.split(__file__)[0], '..', 'templates', + 'default-fontTable.xml' + ) + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes \ No newline at end of file diff --git a/docx/parts/theme.py b/docx/parts/theme.py new file mode 100644 index 000000000..28824b81f --- /dev/null +++ b/docx/parts/theme.py @@ -0,0 +1,47 @@ +""" +Theme 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 ..oxml import parse_xml +from ..theme import Theme +from .story import BaseStoryPart + +class ThemePart(BaseStoryPart): + """ + Proxy for the theme.xml part containing theme definitions for a document. + """ + @classmethod + def default(cls, package): + """ + Return a newly created theme part, containing a default set of + elements. + """ + partname = PackURI('/word/theme/theme1.xml') + content_type = CT.OFC_THEME + element = parse_xml(cls._default_theme_xml()) + return cls(partname, content_type, element, package) + + @property + def theme(self): + return Theme(self.element, self) + + @classmethod + def _default_theme_xml(cls): + """ + Return a bytestream containing XML for a default theme part. + """ + path = os.path.join( + os.path.split(__file__)[0], '..', 'templates', + 'default-theme.xml' + ) + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes \ No newline at end of file diff --git a/docx/runcntnr.py b/docx/runcntnr.py new file mode 100644 index 000000000..aca2f7c4b --- /dev/null +++ b/docx/runcntnr.py @@ -0,0 +1,63 @@ +""" +Run item container, used by paragraph, hyperlink. +""" + +from __future__ import absolute_import, print_function + +from .shared import Parented +from .text.run import Run + + +class RunItemContainer(Parented): + def __init__(self, element, parent): + super(RunItemContainer, self).__init__(parent) + self._element = element + + def add_run(self, text=None, style=None): + """ + Append a run to this container containing *text* and having character + style identified by style ID *style*. *text* can contain tab + (``\\t``) characters, which are converted to the appropriate XML form + for a tab. *text* can also include newline (``\\n``) or carriage + return (``\\r``) characters, each of which is converted to a line + break. + """ + r = self._element.add_r() + run = Run(r, self) + if text: + run.text = text + if style: + run.style = style + return run + + @property + def runs(self): + """ + Sequence of |Run| instances corresponding to the elements in + this container. + """ + return [Run(r, self) for r in self._element.r_lst] + + @property + def text(self): + """ + String formed by concatenating the text of each run in the paragraph. + Tabs and line breaks in the XML are mapped to ``\\t`` and ``\\n`` + characters respectively. + + Assigning text to this property causes all existing paragraph content + to be replaced with a single run containing the assigned text. + A ``\\t`` character in the text is mapped to a ```` element + and each ``\\n`` or ``\\r`` character is mapped to a line break. + Paragraph-level formatting, such as style, is preserved. All + run-level formatting, such as bold or italic, is removed. + """ + text = '' + for run in self.runs: + text += run.text + return text + + @text.setter + def text(self, text): + self.clear() + self.add_run(text) 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-fontTable.xml b/docx/templates/default-fontTable.xml new file mode 100644 index 000000000..7e42774c3 --- /dev/null +++ b/docx/templates/default-fontTable.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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/templates/default-theme.xml b/docx/templates/default-theme.xml new file mode 100644 index 000000000..ce0c84324 --- /dev/null +++ b/docx/templates/default-theme.xml @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docx/text/hyperlink.py b/docx/text/hyperlink.py new file mode 100644 index 000000000..59b8093db --- /dev/null +++ b/docx/text/hyperlink.py @@ -0,0 +1,32 @@ + +""" +Hyperlink-related proxy types. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .run import Run +from ..shared import Parented +from ..runcntnr import RunItemContainer + +class Hyperlink(RunItemContainer): + """ + Proxy object wrapping ```` element. + """ + def __init__(self, h, parent): + super(Hyperlink, self).__init__(h, parent) + self._h = self._element = h + + def clear(self): + """ + Return this same paragraph after removing all its content. + Paragraph-level formatting, such as style, is preserved. + """ + self._h.clear_content() + return self + + @property + def runs(self): + return super(Hyperlink, self).runs \ No newline at end of file diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index 4fb583b94..ad870cb26 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -12,32 +12,20 @@ from .parfmt import ParagraphFormat from .run import Run from ..shared import Parented +from ..runcntnr import RunItemContainer +from .hyperlink import Hyperlink +from ..oxml.ns import qn - -class Paragraph(Parented): +class Paragraph(RunItemContainer): """ Proxy object wrapping ```` element. """ def __init__(self, p, parent): - super(Paragraph, self).__init__(parent) + super(Paragraph, self).__init__(p, parent) self._p = self._element = p def add_run(self, text=None, style=None): - """ - Append a run to this paragraph containing *text* and having character - style identified by style ID *style*. *text* can contain tab - (``\\t``) characters, which are converted to the appropriate XML form - for a tab. *text* can also include newline (``\\n``) or carriage - return (``\\r``) characters, each of which is converted to a line - break. - """ - r = self._p.add_r() - run = Run(r, self) - if text: - run.text = text - if style: - run.style = style - return run + return super(Paragraph, self).add_run(text, style) @property def alignment(self): @@ -86,11 +74,11 @@ def paragraph_format(self): @property def runs(self): - """ - Sequence of |Run| instances corresponding to the elements in - this paragraph. - """ - return [Run(r, self) for r in self._p.r_lst] + return super(Paragraph, self).runs + + @property + def inline_items(self): + return [self._get_inline_item_class(t.tag)(t, self) for t in self._p.inline_items] @property def style(self): @@ -127,8 +115,8 @@ def text(self): run-level formatting, such as bold or italic, is removed. """ text = '' - for run in self.runs: - text += run.text + for inline in self.inline_items: + text += inline.text return text @text.setter @@ -143,3 +131,10 @@ def _insert_paragraph_before(self): """ p = self._p.add_p_before() return Paragraph(p, self._parent) + + def _get_inline_item_class(self, tag): + if tag == qn('w:r'): + return Run + + if tag == qn('w:hyperlink'): + return Hyperlink 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): """ diff --git a/docx/theme.py b/docx/theme.py new file mode 100644 index 000000000..af8d09859 --- /dev/null +++ b/docx/theme.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +from docx.shared import ElementProxy + +class Theme(ElementProxy): + """ + Theme object, container for all objects in the theme part + """ + def __init__(self, element, part): + super(Theme, self).__init__(element) + self._part = part \ No newline at end of file