Skip to content

Reference

Classes:

Name Description
AnimPar

Container for SMIL animations, “anim:par”.

AnimSeq

Container for SMIL Presentation Animations, “anim:seq”.

AnimTransFilter

Transition between two frames, “anim:transitionFilter”.

Annotation

An annotation (private note), “text:annotation”.

AnnotationEnd

End of annotation marker, “office:annotation-end”.

BackgroundImage

Style for a background image, “style:background-image”.

Body

Root of the document content, “office:body”.

Bookmark

Bookmark class, “text:bookmark” tag.

BookmarkEnd

Bookmark end marker, “text:bookmark-end”.

BookmarkStart

Bookmark start marker, “text:bookmark-start”.

Cell

A cell of a table, “table:table-cell” and “table:covered-table-cell”.

ChangeInfo

Representation of informations of a change, “office:change-info”.

Chart

Root of the Chart document content, “office:chart”.

Column

A Column of a table, “table:table-column”.

ConnectorShape

A connector shape, “draw:connector”.

Container

Storage of the ODF document, as zip or other format.

Content

Representation of the “content.xml” part.

Database

Root of the Database document content, “office:database”.

DcCreatorMixin

Creator of the document, “dc:creator”.

DcDateMixin

Date of the document, “dc:date”.

Document

Abstraction of the ODF document.

DrawFillImage

A link to a bitmap resource, “draw:fill-image”.

DrawGroup

Representation of a group of drawing shapes, “draw:g”.

DrawImage

An ODF image, “draw:image”.

DrawPage

ODF draw page for presentations and drawings, “draw:page”.

Drawing

Root of the Drawing document content, “office:drawing”.

EText

Representation of an XML text node (internal).

Element

Base class of all ODF classes, abstraction of the underlying XML.

ElementTyped

Subclass of Element for classes managing typed values.

EllipseShape

An ellipse shape, “draw:ellipse”.

Frame

ODF Frame, “draw:frame”.

Header

A title, a specialized paragraph, “text:h”.

HeaderRows

A header row of a table, “table:table-header-rows”.

Image

Root of the Image document content, “office:image”.

IndexTitle

The title of an index, “text:index-title”.

IndexTitleTemplate

Template style for title, “text:index-title-template”.

LineBreak

Representation of a line break, “text:line-break”.

LineShape

A line shape, “draw:line”.

Link

Representation of a link (URL), “text:a”.

List

A list of elements, “text:list”.

ListItem

An item of a list, “text:list-item”.

Manifest

Representation of the “manifest.xml” part.

Meta

Representation of the “meta.xml” part.

MetaAutoReload

Container for auto-reload properties, “meta:auto-reload”.

MetaHyperlinkBehaviour

Container for hyperlink-behaviour properties, “meta:hyperlink-behaviour”.

MetaTemplate

Container for the meta template properties, “meta:template”.

NamedRange

Named range of cells in a table, “table:named-range”.

Note

A note (footnote or endnote), “text:note”.

ParaMixin

Mixin for Paragraph methods.

Paragraph

An ODF paragraph, “text:p”.

Presentation

Root of the Presentation document content, “office:presentation”.

RectangleShape

A rectangle shape, “draw:rect”.

Reference

A reference to a content marked by a reference mark, “text:reference-ref”.”

ReferenceMark

A point reference, “text:reference-mark”.

ReferenceMarkEnd

End of a range reference, “text:reference-mark-end”.

ReferenceMarkStart

Start of a range reference, “text:reference-mark-start”.

Row

A row of a table, “table:table-row”.

RowGroup

A group of rows with common properties, “table:table-row-group”.

Section

Section of the text document, “text:section”.

Spacer

Representation of several spaces, “text:s”.

Span

A span tag (syled text in paragraph), “text:span”.

Spreadsheet

Root of the Spreadsheet document content, “office:spreadsheet”.

Style

Style class for many ODF tags, “style:style”, “number:date-style”,…

Styles

Representation of the “styles.xml” part.

TOC

A table of content, “text:table-of-content”.

Tab

Representation of a tabulation, “text:tab”.

TabStopStyle

Base style for a TOC entryBase style for a TOC entry, “style:tab-stop”.

Table

A table, in a spreadsheet or other document, “table:table”.

Text

Root of the Text document content, “office:text”.

TextChange

A text change position, “text:change”.

TextChangeEnd

End of a changed region, “text:change-end”.

TextChangeStart

Start of a changed region, “text:change-start”.

TextChangedRegion

A changed region of text, “text:changed-region”.

TextDeletion

Informations on a text deletion, “text:deletion”.

TextFormatChange

A change in text formatting, “text:format-change”.

TextInsertion

Informations on a text insertion, “text:insertion”.

TocEntryTemplate

Template for entry of TOC, “text:table-of-content-entry-template”.

TrackedChanges

A tracked change, “text:tracked-changes”.

UserDefined

A user defined field, “text:user-defined”.

UserFieldDecl

Declaration of a user field, “text:user-field-decl”.

UserFieldDecls

Container of user fields declarations, “text:user-field-decls”.

UserFieldGet

Representation of user field getter, “text:user-field-get”.

UserFieldInput

Representation of user field input, “text:user-field-input”.

VarChapter

Variable for a chapter, “text:chapter”.

VarCreationDate

Variable for the creation date, “text:creation-date”.

VarCreationTime

Variable for the creation time, “text:creation-time”.

VarDate

Variable for a date, “text:date”.

VarDecl

A variable declaration, “text:variable-decl”.

VarDecls

Container of variables declarations, “text:variable-decls”.

VarDescription

Variable for the text description, “text:description”.

VarFileName

Variable for the file name, “text:file-name”.

VarGet

Representation of a variable getter, “text:variable-get”.

VarInitialCreator

Variable for the initial creator, “text:initial-creator”.

VarKeywords

Variable for the keywords, “text:keywords”.

VarPageCount

Variable for page count, “text:page-count”.

VarPageNumber

Variable for page number, “text:page-number”.

VarSet

Representation of a variable setter, “text:variable-set”.

VarSubject

Variable for the subject, “text:subject”.

VarTime

Variable for a time, “text:time”.

VarTitle

Variable for the title, “text:title”.

XmlPart

Representation of an XML part.

Functions:

Name Description
PageBreak

Return an empty paragraph with a manual page break.

create_table_cell_style

Return a cell style.

default_boolean_style

Return a default boolean style.

default_currency_style

Return a default currency style (€).

default_date_style

Return a default time style Y-M-D.

default_frame_position_style

Generate a style for positioning frames in desktop applications.

default_number_style

Return a default number style with two decimals.

default_percentage_style

Return a default percentage style with two decimals.

default_time_style

Return a default time style.

default_toc_level_style

Generate an automatic default style for the given TOC level.

hex2rgb

Convert “#RRGGBB” hexadecimal representation into (R, G, B) tuple.

hexa_color

Safe conversion from color tuple or string to hexadecimal representation.

make_table_cell_border_string

Returns a string for “style:table-cell-properties” “fo:border”.

rgb2hex

Convert color name or (R, G, B) tuple into “#RRGGBB” hexadecimal.

Attributes:

Name Type Description
FIRST_CHILD
LAST_CHILD
NEXT_SIBLING
PREV_SIBLING

FIRST_CHILD module-attribute

FIRST_CHILD = 0

LAST_CHILD module-attribute

LAST_CHILD = 1

NEXT_SIBLING module-attribute

NEXT_SIBLING = 2

PREV_SIBLING module-attribute

PREV_SIBLING = 3

AnimPar

Bases: Element

Container for SMIL animations, “anim:par”.

Methods:

Name Description
__init__

Create a container for SMIL presentation animations “anim:par”.

Attributes:

Name Type Description
presentation_node_type
smil_begin
Source code in odfdo/smil.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class AnimPar(Element):
    """Container for SMIL animations, "anim:par"."""

    _tag = "anim:par"
    _properties = (
        PropDef("presentation_node_type", "presentation:node-type"),
        PropDef("smil_begin", "smil:begin"),
    )

    def __init__(
        self,
        presentation_node_type: str | None = None,
        smil_begin: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a container for SMIL presentation animations "anim:par".

        Arguments:

            presentation_node_type -- default, on-click, with-previous,
                                      after-previous, timing-root, main-sequence
                                      and interactive-sequence

            smil_begin -- indefinite, 10s, [id].click, [id].begin
        """
        super().__init__(**kwargs)
        if self._do_init:
            if presentation_node_type:
                self.presentation_node_type = presentation_node_type
            if smil_begin:
                self.smil_begin = smil_begin

presentation_node_type instance-attribute

presentation_node_type = presentation_node_type

smil_begin instance-attribute

smil_begin = smil_begin

__init__

__init__(
    presentation_node_type: str | None = None,
    smil_begin: str | None = None,
    **kwargs: Any,
) -> None

Create a container for SMIL presentation animations “anim:par”.

Arguments:

presentation_node_type -- default, on-click, with-previous,
                          after-previous, timing-root, main-sequence
                          and interactive-sequence

smil_begin -- indefinite, 10s, [id].click, [id].begin
Source code in odfdo/smil.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    presentation_node_type: str | None = None,
    smil_begin: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a container for SMIL presentation animations "anim:par".

    Arguments:

        presentation_node_type -- default, on-click, with-previous,
                                  after-previous, timing-root, main-sequence
                                  and interactive-sequence

        smil_begin -- indefinite, 10s, [id].click, [id].begin
    """
    super().__init__(**kwargs)
    if self._do_init:
        if presentation_node_type:
            self.presentation_node_type = presentation_node_type
        if smil_begin:
            self.smil_begin = smil_begin

AnimSeq

Bases: Element

Container for SMIL Presentation Animations, “anim:seq”.

Animations inside this block are executed after the slide has executed its initial transition.

Methods:

Name Description
__init__

Create a container for SMIL Presentation Animations “anim:seq”.

Attributes:

Name Type Description
presentation_node_type
Source code in odfdo/smil.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class AnimSeq(Element):
    """Container for SMIL Presentation Animations, "anim:seq".

    Animations inside this block are executed after the slide
    has executed its initial transition.
    """

    _tag = "anim:seq"
    _properties = (PropDef("presentation_node_type", "presentation:node-type"),)

    def __init__(
        self,
        presentation_node_type: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a container for SMIL Presentation Animations "anim:seq".

        Animations inside this block are executed after the slide
        has executed its initial transition.

        Arguments:

            presentation_node_type -- default, on-click, with-previous,
                                      after-previous, timing-root, main-sequence
                                      and interactive-sequence
        """
        super().__init__(**kwargs)
        if self._do_init and presentation_node_type:
            self.presentation_node_type = presentation_node_type

presentation_node_type instance-attribute

presentation_node_type = presentation_node_type

__init__

__init__(
    presentation_node_type: str | None = None, **kwargs: Any
) -> None

Create a container for SMIL Presentation Animations “anim:seq”.

Animations inside this block are executed after the slide has executed its initial transition.

Arguments:

presentation_node_type -- default, on-click, with-previous,
                          after-previous, timing-root, main-sequence
                          and interactive-sequence
Source code in odfdo/smil.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(
    self,
    presentation_node_type: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a container for SMIL Presentation Animations "anim:seq".

    Animations inside this block are executed after the slide
    has executed its initial transition.

    Arguments:

        presentation_node_type -- default, on-click, with-previous,
                                  after-previous, timing-root, main-sequence
                                  and interactive-sequence
    """
    super().__init__(**kwargs)
    if self._do_init and presentation_node_type:
        self.presentation_node_type = presentation_node_type

AnimTransFilter

Bases: Element

Transition between two frames, “anim:transitionFilter”.

Methods:

Name Description
__init__

Create a transition between two frames “anim:transitionFilter”.

Attributes:

Name Type Description
smil_direction
smil_dur
smil_fadeColor
smil_mode
smil_subtype
smil_type
Source code in odfdo/smil.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class AnimTransFilter(Element):
    """Transition between two frames, "anim:transitionFilter"."""

    _tag = "anim:transitionFilter"
    _properties = (
        PropDef("smil_dur", "smil:dur"),
        PropDef("smil_type", "smil:type"),
        PropDef("smil_subtype", "smil:subtype"),
        PropDef("smil_direction", "smil:direction"),
        PropDef("smil_fadeColor", "smil:fadeColor"),
        PropDef("smil_mode", "smil:mode"),
    )

    def __init__(
        self,
        smil_dur: str | None = None,
        smil_type: str | None = None,
        smil_subtype: str | None = None,
        smil_direction: str | None = None,
        smil_fadeColor: str | None = None,
        smil_mode: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a transition between two frames "anim:transitionFilter".

        Arguments:

          smil_dur -- str, duration

          smil_type and smil_subtype -- see http://www.w3.org/TR/SMIL20/
                        smil-transitions.html#TransitionEffects-Appendix
                                        to get a list of all types/subtypes

          smil_direction -- forward, reverse

          smil_fadeColor -- forward, reverse

          smil_mode -- in, out
        """
        super().__init__(**kwargs)
        if self._do_init:
            if smil_dur:
                self.smil_dur = smil_dur
            if smil_type:
                self.smil_type = smil_type
            if smil_subtype:
                self.smil_subtype = smil_subtype
            if smil_direction:
                self.smil_direction = smil_direction
            if smil_fadeColor:
                self.smil_fadeColor = smil_fadeColor
            if smil_mode:
                self.smil_mode = smil_mode

smil_direction instance-attribute

smil_direction = smil_direction

smil_dur instance-attribute

smil_dur = smil_dur

smil_fadeColor instance-attribute

smil_fadeColor = smil_fadeColor

smil_mode instance-attribute

smil_mode = smil_mode

smil_subtype instance-attribute

smil_subtype = smil_subtype

smil_type instance-attribute

smil_type = smil_type

__init__

__init__(
    smil_dur: str | None = None,
    smil_type: str | None = None,
    smil_subtype: str | None = None,
    smil_direction: str | None = None,
    smil_fadeColor: str | None = None,
    smil_mode: str | None = None,
    **kwargs: Any,
) -> None

Create a transition between two frames “anim:transitionFilter”.

Arguments:

smil_dur – str, duration

smil_type and smil_subtype – see http://www.w3.org/TR/SMIL20/ smil-transitions.html#TransitionEffects-Appendix to get a list of all types/subtypes

smil_direction – forward, reverse

smil_fadeColor – forward, reverse

smil_mode – in, out

Source code in odfdo/smil.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __init__(
    self,
    smil_dur: str | None = None,
    smil_type: str | None = None,
    smil_subtype: str | None = None,
    smil_direction: str | None = None,
    smil_fadeColor: str | None = None,
    smil_mode: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a transition between two frames "anim:transitionFilter".

    Arguments:

      smil_dur -- str, duration

      smil_type and smil_subtype -- see http://www.w3.org/TR/SMIL20/
                    smil-transitions.html#TransitionEffects-Appendix
                                    to get a list of all types/subtypes

      smil_direction -- forward, reverse

      smil_fadeColor -- forward, reverse

      smil_mode -- in, out
    """
    super().__init__(**kwargs)
    if self._do_init:
        if smil_dur:
            self.smil_dur = smil_dur
        if smil_type:
            self.smil_type = smil_type
        if smil_subtype:
            self.smil_subtype = smil_subtype
        if smil_direction:
            self.smil_direction = smil_direction
        if smil_fadeColor:
            self.smil_fadeColor = smil_fadeColor
        if smil_mode:
            self.smil_mode = smil_mode

Annotation

Bases: MDTail, Element, DcCreatorMixin, DcDateMixin

An annotation (private note), “text:annotation”.

Methods:

Name Description
__init__

An annotation (private note), “text:annotation”.

check_validity
delete

Delete the given element from the XML tree. If no element is given,

get_annotated

Returns the annotated content from an annotation.

Attributes:

Name Type Description
creator
date
dc_creator str | None

Alias for self.creator property.

dc_date datetime | None

Alias for self.date property.

end Element | None

Return the corresponding annotation-end tag or None.

name
note_body str
start Element

Return self.

Source code in odfdo/annotation.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
class Annotation(MDTail, Element, DcCreatorMixin, DcDateMixin):
    """An annotation (private note), "text:annotation"."""

    _tag = "office:annotation"
    _properties = (
        PropDef("name", "office:name"),
        PropDef("note_id", "text:id"),
    )

    def __init__(
        self,
        text_or_element: Element | str | None = None,
        creator: str | None = None,
        date: datetime | None = None,
        name: str | None = None,
        parent: Element | None = None,
        **kwargs: Any,
    ) -> None:
        """An annotation (private note), "text:annotation".

        Annotation element credited to the given creator with the
        given text, optionally dated (current date by default).
        If name not provided and some parent is provided, the name is
        autogenerated.

        Arguments:

            text -- str or odf_element

            creator -- str

            date -- datetime

            name -- str

            parent -- Element
        """
        # fixme : use offset
        # TODO allow paragraph and text styles
        super().__init__(**kwargs)

        if self._do_init:
            self.note_body = text_or_element  # type:ignore
            if creator:
                self.creator = creator
            if date is None:
                date = datetime.now()
            self.date = date
            if not name:
                name = get_unique_office_name(parent)
            self.name = name

    @property
    def dc_creator(self) -> str | None:
        """Alias for self.creator property."""
        return self.creator

    @dc_creator.setter
    def dc_creator(self, creator: str) -> None:
        self.creator = creator

    @property
    def dc_date(self) -> datetime | None:
        """Alias for self.date property."""
        return self.date

    @dc_date.setter
    def dc_date(self, dtdate: datetime) -> None:
        self.date = dtdate

    @property
    def note_body(self) -> str:
        return self.text_content

    @note_body.setter
    def note_body(self, text_or_element: Element | str | None) -> None:
        if text_or_element is None:
            self.text_content = ""
        elif isinstance(text_or_element, str):
            self.text_content = text_or_element
        elif isinstance(text_or_element, Element):
            self.clear()
            self.append(text_or_element)
        else:
            raise TypeError(f'Unexpected type for body: "{type(text_or_element)}"')

    @property
    def start(self) -> Element:
        """Return self."""
        return self

    @property
    def end(self) -> Element | None:
        """Return the corresponding annotation-end tag or None."""
        name = self.name
        parent = self.parent
        if parent is None:  # pragma: nocover
            raise ValueError("Can't find end tag: no parent available")
        body = self.document_body
        if not body:
            body = parent
        return body.get_annotation_end(name=name)

    def get_annotated(
        self,
        as_text: bool = False,
        no_header: bool = True,
        clean: bool = True,
    ) -> Element | list | str | None:
        """Returns the annotated content from an annotation.

        If no content exists (single position annotation or annotation-end not
        found), returns [] (or "" if text flag is True).
        If as_text is True: returns the text content.
        If clean is True: suppress unwanted tags (deletions marks, ...)
        If no_header is True: existing text:h are changed in text:p
        By default: returns a list of odf_element, cleaned and without headers.

        Arguments:

            as_text -- boolean

            clean -- boolean

            no_header -- boolean

        Return: list or Element or text or None
        """
        end = self.end
        if end is None:
            if as_text:
                return ""
            return None
        body = self.document_body
        if not body:
            body = self.root  # pragma: nocover
        return body.get_between(
            self, end, as_text=as_text, no_header=no_header, clean=clean
        )

    def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
        """Delete the given element from the XML tree. If no element is given,
        "self" is deleted. The XML library may allow to continue to use an
        element now "orphan" as long as you have a reference to it.

        For Annotation : delete the annotation-end tag if exists.

        Arguments:

            child -- Element or None
        """
        if child is not None:  # act like normal delete
            super().delete(child)
            return
        end = self.end
        if end:
            end.delete()
        # act like normal delete
        super().delete()

    def check_validity(self) -> None:
        if not self.note_body:
            raise ValueError("Annotation must have a body")
        if not self.dc_creator:
            raise ValueError("Annotation must have a creator")
        if not self.dc_date:
            self.dc_date = datetime.now()

    def __str__(self) -> str:
        return f"{self.note_body}\n{self.dc_creator} {self.dc_date}"

creator instance-attribute

creator = creator

date instance-attribute

date = date

dc_creator property writable

dc_creator: str | None

Alias for self.creator property.

dc_date property writable

dc_date: datetime | None

Alias for self.date property.

end property

end: Element | None

Return the corresponding annotation-end tag or None.

name instance-attribute

name = name

note_body property writable

note_body: str

start property

start: Element

Return self.

__init__

__init__(
    text_or_element: Element | str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    name: str | None = None,
    parent: Element | None = None,
    **kwargs: Any,
) -> None

An annotation (private note), “text:annotation”.

Annotation element credited to the given creator with the given text, optionally dated (current date by default). If name not provided and some parent is provided, the name is autogenerated.

Arguments:

text -- str or odf_element

creator -- str

date -- datetime

name -- str

parent -- Element
Source code in odfdo/annotation.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def __init__(
    self,
    text_or_element: Element | str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    name: str | None = None,
    parent: Element | None = None,
    **kwargs: Any,
) -> None:
    """An annotation (private note), "text:annotation".

    Annotation element credited to the given creator with the
    given text, optionally dated (current date by default).
    If name not provided and some parent is provided, the name is
    autogenerated.

    Arguments:

        text -- str or odf_element

        creator -- str

        date -- datetime

        name -- str

        parent -- Element
    """
    # fixme : use offset
    # TODO allow paragraph and text styles
    super().__init__(**kwargs)

    if self._do_init:
        self.note_body = text_or_element  # type:ignore
        if creator:
            self.creator = creator
        if date is None:
            date = datetime.now()
        self.date = date
        if not name:
            name = get_unique_office_name(parent)
        self.name = name

check_validity

check_validity() -> None
Source code in odfdo/annotation.py
219
220
221
222
223
224
225
def check_validity(self) -> None:
    if not self.note_body:
        raise ValueError("Annotation must have a body")
    if not self.dc_creator:
        raise ValueError("Annotation must have a creator")
    if not self.dc_date:
        self.dc_date = datetime.now()

delete

delete(
    child: Element | None = None, keep_tail: bool = True
) -> None

Delete the given element from the XML tree. If no element is given, “self” is deleted. The XML library may allow to continue to use an element now “orphan” as long as you have a reference to it.

For Annotation : delete the annotation-end tag if exists.

Arguments:

child -- Element or None
Source code in odfdo/annotation.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
    """Delete the given element from the XML tree. If no element is given,
    "self" is deleted. The XML library may allow to continue to use an
    element now "orphan" as long as you have a reference to it.

    For Annotation : delete the annotation-end tag if exists.

    Arguments:

        child -- Element or None
    """
    if child is not None:  # act like normal delete
        super().delete(child)
        return
    end = self.end
    if end:
        end.delete()
    # act like normal delete
    super().delete()

get_annotated

get_annotated(
    as_text: bool = False,
    no_header: bool = True,
    clean: bool = True,
) -> Element | list | str | None

Returns the annotated content from an annotation.

If no content exists (single position annotation or annotation-end not found), returns [] (or “” if text flag is True). If as_text is True: returns the text content. If clean is True: suppress unwanted tags (deletions marks, …) If no_header is True: existing text:h are changed in text:p By default: returns a list of odf_element, cleaned and without headers.

Arguments:

as_text -- boolean

clean -- boolean

no_header -- boolean

Return: list or Element or text or None

Source code in odfdo/annotation.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def get_annotated(
    self,
    as_text: bool = False,
    no_header: bool = True,
    clean: bool = True,
) -> Element | list | str | None:
    """Returns the annotated content from an annotation.

    If no content exists (single position annotation or annotation-end not
    found), returns [] (or "" if text flag is True).
    If as_text is True: returns the text content.
    If clean is True: suppress unwanted tags (deletions marks, ...)
    If no_header is True: existing text:h are changed in text:p
    By default: returns a list of odf_element, cleaned and without headers.

    Arguments:

        as_text -- boolean

        clean -- boolean

        no_header -- boolean

    Return: list or Element or text or None
    """
    end = self.end
    if end is None:
        if as_text:
            return ""
        return None
    body = self.document_body
    if not body:
        body = self.root  # pragma: nocover
    return body.get_between(
        self, end, as_text=as_text, no_header=no_header, clean=clean
    )

AnnotationEnd

Bases: MDTail, Element

End of annotation marker, “office:annotation-end”.

AnnotationEnd: the “office:annotation-end” element may be used to define the end of a text range of document content that spans element boundaries. In that case, an “office:annotation” element shall precede the “office:annotation-end” element. Both elements shall have the same value for their office:name attribute. The “office:annotation-end” element shall be preceded by an “office:annotation” element that has the same value for its office:name attribute as the “office:annotation-end” element. An “office:annotation-end” element without a preceding “office:annotation” element that has the same name assigned is ignored.

Methods:

Name Description
__init__

An annotation (private note), “text:annotation”.

Attributes:

Name Type Description
end Element

Return self.

name
start Element | None

Return the corresponding annotation starting tag or None.

Source code in odfdo/annotation.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
class AnnotationEnd(MDTail, Element):
    """End of annotation marker, "office:annotation-end".

    AnnotationEnd: the "office:annotation-end" element may be used to
    define the end of a text range of document content that spans element
    boundaries. In that case, an "office:annotation" element shall precede
    the "office:annotation-end" element. Both elements shall have the same
    value for their office:name attribute. The "office:annotation-end" element
    shall be preceded by an "office:annotation" element that has the same
    value for its office:name attribute as the "office:annotation-end"
    element. An "office:annotation-end" element without a preceding
    "office:annotation" element that has the same name assigned is ignored.
    """

    _tag = "office:annotation-end"
    _properties = (PropDef("name", "office:name"),)

    def __init__(
        self,
        annotation: Element | None = None,
        name: str | None = None,
        **kwargs: Any,
    ) -> None:
        """An annotation (private note), "text:annotation".

        Annotation element credited to the given creator with the
        given text, optionally dated (current date by default).
        If name not provided and some parent is provided, the name is
        autogenerated.

        Arguments:

            text -- str or odf_element

            creator -- str

            date -- datetime

            name -- str

            parent -- Element
        """
        # fixme : use offset
        # TODO allow paragraph and text styles
        super().__init__(**kwargs)
        if self._do_init:
            if annotation:
                name = annotation.name  # type: ignore
            if not name:
                raise ValueError("Annotation-end must have a name")
            self.name = name

    @property
    def start(self) -> Element | None:
        """Return the corresponding annotation starting tag or None."""
        name = self.name
        parent = self.parent
        if parent is None:
            raise ValueError(
                "Can't find start tag: no parent available"
            )  # pragma:nocover
        body = self.document_body
        if not body:
            body = parent  # pragma:nocover
        return body.get_annotation(name=name)

    @property
    def end(self) -> Element:
        """Return self."""
        return self

end property

end: Element

Return self.

name instance-attribute

name = name

start property

start: Element | None

Return the corresponding annotation starting tag or None.

__init__

__init__(
    annotation: Element | None = None,
    name: str | None = None,
    **kwargs: Any,
) -> None

An annotation (private note), “text:annotation”.

Annotation element credited to the given creator with the given text, optionally dated (current date by default). If name not provided and some parent is provided, the name is autogenerated.

Arguments:

text -- str or odf_element

creator -- str

date -- datetime

name -- str

parent -- Element
Source code in odfdo/annotation.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def __init__(
    self,
    annotation: Element | None = None,
    name: str | None = None,
    **kwargs: Any,
) -> None:
    """An annotation (private note), "text:annotation".

    Annotation element credited to the given creator with the
    given text, optionally dated (current date by default).
    If name not provided and some parent is provided, the name is
    autogenerated.

    Arguments:

        text -- str or odf_element

        creator -- str

        date -- datetime

        name -- str

        parent -- Element
    """
    # fixme : use offset
    # TODO allow paragraph and text styles
    super().__init__(**kwargs)
    if self._do_init:
        if annotation:
            name = annotation.name  # type: ignore
        if not name:
            raise ValueError("Annotation-end must have a name")
        self.name = name

BackgroundImage

Bases: Style, DrawImage

Style for a background image, “style:background-image”.

Methods:

Name Description
__init__

Create style for a background image “style:background-image”.

Attributes:

Name Type Description
display_name
family
name
position
Source code in odfdo/style.py
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
class BackgroundImage(Style, DrawImage):
    """Style for a background image, "style:background-image"."""

    _tag = "style:background-image"
    _properties: tuple[PropDef, ...] = (
        PropDef("name", "style:name"),
        PropDef("display_name", "style:display-name"),
        PropDef("svg_font_family", "svg:font-family"),
        PropDef("font_family_generic", "style:font-family-generic"),
        PropDef("font_pitch", "style:font-pitch"),
        PropDef("position", "style:position", "background-image"),
        PropDef("repeat", "style:repeat", "background-image"),
        PropDef("opacity", "draw:opacity", "background-image"),
        PropDef("filter", "style:filter-name", "background-image"),
        PropDef("text_style", "text:style-name"),
    )

    def __init__(
        self,
        name: str | None = None,
        display_name: str | None = None,
        position: str | None = None,
        repeat: str | None = None,
        opacity: str | None = None,
        filter: str | None = None,  # noqa: A002
        # Every other property
        **kwargs: Any,
    ):
        """Create style for a background image "style:background-image"."""
        kwargs["family"] = "background-image"
        super().__init__(**kwargs)
        if self._do_init:
            kwargs.pop("tag", None)
            kwargs.pop("tag_or_elem", None)
            self.family = "background-image"
            if name:
                self.name = name
            if display_name:
                self.display_name = display_name
            if position:
                self.position = position
            if repeat:
                self.position = repeat
            if opacity:
                self.position = opacity
            if filter:
                self.position = filter
            # Every other properties
            for prop in BackgroundImage._properties:
                if prop.name in kwargs:
                    self.set_style_attribute(prop.attr, kwargs[prop.name])

display_name instance-attribute

display_name = display_name

family instance-attribute

family = 'background-image'

name instance-attribute

name = name

position instance-attribute

position = position

__init__

__init__(
    name: str | None = None,
    display_name: str | None = None,
    position: str | None = None,
    repeat: str | None = None,
    opacity: str | None = None,
    filter: str | None = None,
    **kwargs: Any,
)

Create style for a background image “style:background-image”.

Source code in odfdo/style.py
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
def __init__(
    self,
    name: str | None = None,
    display_name: str | None = None,
    position: str | None = None,
    repeat: str | None = None,
    opacity: str | None = None,
    filter: str | None = None,  # noqa: A002
    # Every other property
    **kwargs: Any,
):
    """Create style for a background image "style:background-image"."""
    kwargs["family"] = "background-image"
    super().__init__(**kwargs)
    if self._do_init:
        kwargs.pop("tag", None)
        kwargs.pop("tag_or_elem", None)
        self.family = "background-image"
        if name:
            self.name = name
        if display_name:
            self.display_name = display_name
        if position:
            self.position = position
        if repeat:
            self.position = repeat
        if opacity:
            self.position = opacity
        if filter:
            self.position = filter
        # Every other properties
        for prop in BackgroundImage._properties:
            if prop.name in kwargs:
                self.set_style_attribute(prop.attr, kwargs[prop.name])

Body

Bases: Element

Root of the document content, “office:body”.

Methods:

Name Description
get_table

Return the table that matches the criteria.

get_tables

Return all the tables that match the criteria.

Attributes:

Name Type Description
tables list[Table]

Return all the tables.

Source code in odfdo/body.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class Body(Element):
    """Root of the document content, "office:body"."""

    _tag: str = "office:body"
    _properties: tuple[PropDef, ...] = ()

    def get_tables(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Table]:
        """Return all the tables that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Table
        """
        return self._filtered_elements(
            "descendant::table:table", table_style=style, content=content
        )

    @property
    def tables(self) -> list[Table]:
        """Return all the tables.

        Return: list of Table
        """
        return self.get_elements("descendant::table:table")

    def get_table(
        self,
        position: int = 0,
        name: str | None = None,
        content: str | None = None,
    ) -> Table | None:
        """Return the table that matches the criteria.

        Arguments:

            position -- int

            name -- str

            content -- str regex

        Return: Table or None if not found
        """
        if name is None and content is None:
            result = self._filtered_element("descendant::table:table", position)
        else:
            result = self._filtered_element(
                "descendant::table:table",
                position,
                table_name=name,
                content=content,
            )
        return result

tables property

tables: list[Table]

Return all the tables.

Return: list of Table

get_table

get_table(
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> Table | None

Return the table that matches the criteria.

Arguments:

position -- int

name -- str

content -- str regex

Return: Table or None if not found

Source code in odfdo/body.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def get_table(
    self,
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> Table | None:
    """Return the table that matches the criteria.

    Arguments:

        position -- int

        name -- str

        content -- str regex

    Return: Table or None if not found
    """
    if name is None and content is None:
        result = self._filtered_element("descendant::table:table", position)
    else:
        result = self._filtered_element(
            "descendant::table:table",
            position,
            table_name=name,
            content=content,
        )
    return result

get_tables

get_tables(
    style: str | None = None, content: str | None = None
) -> list[Table]

Return all the tables that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Table

Source code in odfdo/body.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def get_tables(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Table]:
    """Return all the tables that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Table
    """
    return self._filtered_elements(
        "descendant::table:table", table_style=style, content=content
    )

Bookmark

Bases: Element

Bookmark class, “text:bookmark” tag.

Methods:

Name Description
__init__

Bookmark class, “text:bookmark” tag.

Attributes:

Name Type Description
name
Source code in odfdo/bookmark.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Bookmark(Element):
    """Bookmark class, "text:bookmark" tag."""

    _tag = "text:bookmark"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        """Bookmark class, "text:bookmark" tag.

        Arguments:

            name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None

Bookmark class, “text:bookmark” tag.

Arguments:

name -- str
Source code in odfdo/bookmark.py
36
37
38
39
40
41
42
43
44
45
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """Bookmark class, "text:bookmark" tag.

    Arguments:

        name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

BookmarkEnd

Bases: Element

Bookmark end marker, “text:bookmark-end”.

Arguments:

name -- str

Methods:

Name Description
__init__

Attributes:

Name Type Description
name
Source code in odfdo/bookmark.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class BookmarkEnd(Element):
    """Bookmark end marker, "text:bookmark-end".

    Arguments:

        name -- str
    """

    _tag = "text:bookmark-end"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None
Source code in odfdo/bookmark.py
78
79
80
81
def __init__(self, name: str = "", **kwargs: Any) -> None:
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

BookmarkStart

Bases: Element

Bookmark start marker, “text:bookmark-start”.

Methods:

Name Description
__init__

Bookmark start marker, “text:bookmark-start”.

Attributes:

Name Type Description
name
Source code in odfdo/bookmark.py
51
52
53
54
55
56
57
58
59
60
61
class BookmarkStart(Element):
    """Bookmark start marker, "text:bookmark-start"."""

    _tag = "text:bookmark-start"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        """Bookmark start marker, "text:bookmark-start"."""
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None

Bookmark start marker, “text:bookmark-start”.

Source code in odfdo/bookmark.py
57
58
59
60
61
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """Bookmark start marker, "text:bookmark-start"."""
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

Cell

Bases: ElementTyped

A cell of a table, “table:table-cell” and “table:covered-table-cell”.

Methods:

Name Description
__init__

Cell of a Table, “table:table-cell”.

is_covered

Return whether the cell is covered (tag table:covered-table-cell).

is_empty

Return whether the cell has no value or the value evaluates

is_spanned

Return whether the cell is spanned over several cells.

set_value

Set the cell state from the Python value type.

span_area

Return the tuple (nb_columns, nb_rows) of the zone covered

Attributes:

Name Type Description
bool bool

Set / get the value of the cell as a boolean.

clone Cell
currency str | None

Get / set the currency used for monetary values.

date date

Set / get the value of the cell as a date.

datetime datetime

Set / get the value of the cell as a datetime.

decimal Decimal

Set / get the value of the cell as a Decimal (or 0.0).

duration timedelta

Set / get the value of the cell as a duration (Python timedelta).

float Float

Set / get the value of the cell as a float (or 0.0).

formula str | None

Get / set the formula of the cell, or None if undefined.

int int

Set / get the value of the cell as a integer (or 0).

repeated int | None

Get / set the number of times the cell is repeated.

string str

Set / get the value of the cell as a string (or ‘’).

style str | None

Get / set the style of the cell itself.

type str | None

Get / set the type of the cell: boolean, float, date, string

value str | bool | int | Float | Decimal | date | datetime | timedelta | None

Set / get the value of the cell. The type is read from the

x
y
Source code in odfdo/cell.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
class Cell(ElementTyped):
    """A cell of a table, "table:table-cell" and "table:covered-table-cell"."""

    _tag = "table:table-cell"

    def __init__(
        self,
        value: Any = None,
        text: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
        formula: str | None = None,
        repeated: int | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Cell of a Table, "table:table-cell".

        Create a cell element containing the given value. The textual
        representation is automatically formatted but can be provided. Cell
        type can be deduced as well, unless the number is a percentage or
        currency. If cell type is "currency", the currency must be given.
        The cell can be repeated on the given number of columns.

        Arguments:

            value -- bool, int, float, Decimal, date, datetime, str,
                     timedelta

            text -- str

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                         'string' or 'time'

            currency -- three-letter str

            repeated -- int

            style -- str
        """
        super().__init__(**kwargs)
        self.x = None
        self.y = None
        if self._do_init:
            self.set_value(
                value,
                text=text,
                cell_type=cell_type,
                currency=currency,
                formula=formula,
            )
            if repeated and repeated > 1:
                self.repeated = repeated
            if style is not None:
                self.style = style

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} x={self.x} y={self.y}>"

    @property
    def clone(self) -> Cell:
        clone = Element.clone.fget(self)  # type: ignore
        clone.y = self.y
        clone.x = self.x
        return clone

    @property
    def value(
        self,
    ) -> str | bool | int | Float | Decimal | date | datetime | timedelta | None:
        """Set / get the value of the cell. The type is read from the
        'office:value-type' attribute of the cell. When setting the value,
        the type of the value will determine the new value_type of the cell.

        Warning:
            - for date, datetime and timedelta, a default text value is generated.
            - for boolean type, the text value is either 'True' or 'False'.
            - for numeric types, the return value is either Decimal or in, use
              the float, decimal or int properties to force the type.
            - Use the method Cell.set_value() to customize the text value.
        """
        value_type = self.get_attribute_string("office:value-type")
        if value_type == "boolean":
            return self.bool
        if value_type in {"float", "percentage", "currency"}:
            value_decimal = Decimal(str(self.get_attribute_string("office:value")))
            # Return 3 instead of 3.0 if possible
            if int(value_decimal) == value_decimal:
                return int(value_decimal)
            return value_decimal
        if value_type == "date":
            value_str = str(self.get_attribute_string("office:date-value"))
            if "T" in value_str:
                return DateTime.decode(value_str)
            return Date.decode(value_str)
        if value_type == "time":
            return Duration.decode(str(self.get_attribute_string("office:time-value")))
        if value_type == "string":
            value = self.get_attribute_string("office:string-value")
            if value is not None:
                return value
            value_list = []
            for para in self.get_elements("text:p"):
                value_list.append(para.inner_text)
            return "\n".join(value_list)
        return None

    @value.setter
    def value(
        self,
        value: (
            str
            | bytes
            | bool
            | int
            | Float
            | Decimal
            | timedelta
            | datetime
            | date
            | None
        ),
    ) -> None:
        if value is None:
            self.clear()
        elif isinstance(value, (str, bytes)):
            self.string = value
        elif isinstance(value, bool):
            self.bool = value
        elif isinstance(value, Float):
            self.float = value
        elif isinstance(value, Decimal):
            self.decimal = value
        elif isinstance(value, int):
            self.int = value
        elif isinstance(value, timedelta):
            self.duration = value
        elif isinstance(value, datetime):
            self.datetime = value
        elif isinstance(value, date):
            self.date = value
        else:
            raise TypeError(f"Unknown value type, try with set_value() : {value!r}")

    @property
    def _bool_string(self) -> str:
        value = self.get_attribute_string("office:boolean-value")
        if not isinstance(value, str):
            return "0"
        return "1" if value == "true" else "0"

    @property
    def float(self) -> Float:
        """Set / get the value of the cell as a float (or 0.0)."""
        for tag in ("office:value", "office:string-value"):
            read_attr = self.get_attribute(tag)
            if isinstance(read_attr, str):
                with contextlib.suppress(ValueError, TypeError):
                    return Float(read_attr)
        return Float(self._bool_string)

    @float.setter
    def float(self, value: str | Float | int | Decimal | None) -> None:
        try:
            value_float = Float(value)
        except (ValueError, TypeError, ConversionSyntax):
            value_float = 0.0
        value_str = str(value_float)
        self.clear()
        self.set_attribute("office:value", value_str)
        self.set_attribute("office:value-type", "float")
        self.text = value_str

    @property
    def decimal(self) -> Decimal:
        """Set / get the value of the cell as a Decimal (or 0.0)."""
        for tag in ("office:value", "office:string-value"):
            read_attr = self.get_attribute(tag)
            if isinstance(read_attr, str):
                with contextlib.suppress(ValueError, TypeError, ConversionSyntax):
                    return Decimal(read_attr)
        return Decimal(self._bool_string)

    @decimal.setter
    def decimal(self, value: str | Float | int | Decimal | None) -> None:
        try:
            value_decimal = Decimal(value)
        except (ValueError, TypeError, ConversionSyntax, InvalidOperation):
            value_decimal = Decimal("0.0")
        value_str = str(value_decimal)
        self.clear()
        self.set_attribute("office:value", value_str)
        self.set_attribute("office:value-type", "float")
        self.text = value_str

    @property
    def int(self) -> int:
        """Set / get the value of the cell as a integer (or 0)."""
        for tag in ("office:value", "office:string-value"):
            read_attr = self.get_attribute(tag)
            if isinstance(read_attr, str):
                with contextlib.suppress(ValueError, TypeError):
                    return int(float(read_attr))
        return int(self._bool_string)

    @int.setter
    def int(self, value: str | Float | int | Decimal | None) -> None:
        try:
            value_int = int(value)  # type:ignore
        except (ValueError, TypeError, ConversionSyntax):
            value_int = 0
        value_str = str(value_int)
        self.clear()
        self.set_attribute("office:value", value_str)
        self.set_attribute("office:value-type", "float")
        self.text = value_str

    @property
    def string(self) -> str:
        """Set / get the value of the cell as a string (or '')."""
        value = self.get_attribute_string("office:string-value")
        if isinstance(value, str):
            return value
        return ""

    @string.setter
    def string(
        self,
        value: str | bytes | int | Float | Decimal | bool | None,
    ) -> None:
        self.clear()
        if value is None:
            value_str = ""
        elif isinstance(value, bytes):
            value_str = value.decode()
        else:
            value_str = str(value)
        self.set_attribute("office:value-type", "string")
        self.set_attribute("office:string-value", value_str)
        self.text = value_str

    @property
    def bool(self) -> bool:
        """Set / get the value of the cell as a boolean."""
        value = self.get_attribute_string("office:boolean-value")
        if isinstance(value, str):
            return value == "true"
        return bool(self.int)

    @bool.setter
    def bool(
        self,
        value: str | bytes | int | Float | Decimal | bool | None,
    ) -> None:
        self.clear()
        self.set_attribute("office:value-type", "boolean")
        if isinstance(value, (bool, str, bytes)):
            bvalue = Boolean.encode(value)
        else:
            bvalue = Boolean.encode(bool(value))
        self.set_attribute("office:boolean-value", bvalue)
        self.text = bvalue

    @property
    def duration(self) -> timedelta:
        """Set / get the value of the cell as a duration (Python timedelta)."""
        value = self.get_attribute("office:time-value")
        if isinstance(value, str):
            return Duration.decode(value)
        return timedelta(0)

    @duration.setter
    def duration(self, value: timedelta) -> None:
        self.clear()
        self.set_attribute("office:value-type", "time")
        dvalue = Duration.encode(value)
        self.set_attribute("office:time-value", dvalue)
        self.text = dvalue

    @property
    def datetime(self) -> datetime:
        """Set / get the value of the cell as a datetime."""
        value = self.get_attribute("office:date-value")
        if isinstance(value, str):
            return DateTime.decode(value)
        return datetime.fromtimestamp(0)

    @datetime.setter
    def datetime(self, value: datetime) -> None:
        self.clear()
        self.set_attribute("office:value-type", "date")
        dvalue = DateTime.encode(value)
        self.set_attribute("office:date-value", dvalue)
        self.text = dvalue

    @property
    def date(self) -> date:
        """Set / get the value of the cell as a date."""
        value = self.get_attribute("office:date-value")
        if isinstance(value, str):
            return Date.decode(value).date()
        return date.fromtimestamp(0)

    @date.setter
    def date(self, value: date) -> None:
        self.clear()
        self.set_attribute("office:value-type", "date")
        dvalue = Date.encode(value)
        self.set_attribute("office:date-value", dvalue)
        self.text = dvalue

    def set_value(
        self,
        value: (
            str
            | bytes
            | Float
            | int
            | Decimal
            | bool
            | datetime
            | date
            | timedelta
            | None
        ),
        text: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
        formula: str | None = None,
    ) -> None:
        """Set the cell state from the Python value type.

        Text is how the cell is displayed. Cell type is guessed,
        unless provided.

        For monetary values, provide the name of the currency.

        Arguments:

            value -- Python type

            text -- str

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                        'currency' or 'percentage'

            currency -- str
        """
        self.clear()
        text = self.set_value_and_type(
            value=value,
            text=text,
            value_type=cell_type,
            currency=currency,
        )
        if text is not None:
            self.text_content = text
        if formula is not None:
            self.formula = formula

    @property
    def type(self) -> str | None:
        """Get / set the type of the cell: boolean, float, date, string
        or time.

        Return: str | None
        """
        return self.get_attribute_string("office:value-type")

    @type.setter
    def type(self, cell_type: str) -> None:
        self.set_attribute("office:value-type", cell_type)

    @property
    def currency(self) -> str | None:
        """Get / set the currency used for monetary values.

        Return: str | None
        """
        return self.get_attribute_string("office:currency")

    @currency.setter
    def currency(self, currency: str) -> None:
        self.set_attribute("office:currency", currency)

    def _set_repeated(self, repeated: int | None) -> None:
        """Internal only. Set the numnber of times the cell is repeated, or
        None to delete. Without changing cache.
        """
        if repeated is None or repeated < 2:
            with contextlib.suppress(KeyError):
                self.del_attribute("table:number-columns-repeated")
            return
        self.set_attribute("table:number-columns-repeated", str(repeated))

    @property
    def repeated(self) -> int | None:
        """Get / set the number of times the cell is repeated.

        Always None when using the table API.

        Return: int or None
        """
        repeated = self.get_attribute("table:number-columns-repeated")
        if repeated is None:
            return None
        return int(repeated)

    @repeated.setter
    def repeated(self, repeated: int | None) -> None:
        self._set_repeated(repeated)
        # update cache
        child: Element = self
        while True:
            # look for Row, parent may be group of rows
            upper = child.parent
            if not upper:
                # lonely cell
                return
            # parent may be group of rows, not table
            if isinstance(upper, Element) and upper._tag == "table:table-row":
                upper._compute_row_cache()
                return
            child = upper

    @property
    def style(self) -> str | None:
        """Get / set the style of the cell itself.

        Return: str | None
        """
        return self.get_attribute_string("table:style-name")

    @style.setter
    def style(self, style: str | Element) -> None:
        self.set_style_attribute("table:style-name", style)

    @property
    def formula(self) -> str | None:
        """Get / set the formula of the cell, or None if undefined.

        The formula is not interpreted in any way.

        Return: str | None
        """
        return self.get_attribute_string("table:formula")

    @formula.setter
    def formula(self, formula: str | None) -> None:
        self.set_attribute("table:formula", formula)

    def is_empty(self, aggressive: bool = False) -> bool:
        """Return whether the cell has no value or the value evaluates
        to False (empty string), and no style.

        If aggressive is True, empty cells with style are considered empty.

        Arguments:

            aggressive -- bool

        Return: bool
        """
        if (
            self.value is not None
            or self.children
            or self.is_covered()
            or self.is_spanned()
        ):
            return False
        if not aggressive and self.style is not None:  # noqa: SIM103
            return False
        return True

    def is_covered(self) -> bool:
        """Return whether the cell is covered (tag table:covered-table-cell).

        Returns: True | False
        """
        return self.tag == "table:covered-table-cell"

    def is_spanned(self, covered: bool = True) -> bool:
        """Return whether the cell is spanned over several cells.

        If covered is True (the default), covered cells are considered as
        spanned, else only the top left cell. The top left contains the
        attributes "table:number-columns-spanned" and
        "table:number-rows-spanned".

        Arguments:

            covered -- bool

        Return: True | False
        """
        if self.is_covered():
            return covered
        if self.get_attribute("table:number-columns-spanned") is not None:
            return True
        if self.get_attribute("table:number-rows-spanned") is not None:  # noqa: SIM103
            return True
        return False

    _is_spanned = is_spanned  # compatibility

    def span_area(self) -> tuple[int, int]:
        """Return the tuple (nb_columns, nb_rows) of the zone covered
        by a spanned cell.

        If the cell is not spanned, return (0,0).

        Return: tuple[int, int]
        """
        columns = self.get_attribute_integer("table:number-columns-spanned") or 0
        rows = self.get_attribute_integer("table:number-rows-spanned") or 0
        return (columns, rows)

bool property writable

bool: bool

Set / get the value of the cell as a boolean.

clone property

clone: Cell

currency property writable

currency: str | None

Get / set the currency used for monetary values.

Return: str | None

date property writable

date: date

Set / get the value of the cell as a date.

datetime property writable

datetime: datetime

Set / get the value of the cell as a datetime.

decimal property writable

decimal: Decimal

Set / get the value of the cell as a Decimal (or 0.0).

duration property writable

duration: timedelta

Set / get the value of the cell as a duration (Python timedelta).

float property writable

float: Float

Set / get the value of the cell as a float (or 0.0).

formula property writable

formula: str | None

Get / set the formula of the cell, or None if undefined.

The formula is not interpreted in any way.

Return: str | None

int property writable

int: int

Set / get the value of the cell as a integer (or 0).

repeated property writable

repeated: int | None

Get / set the number of times the cell is repeated.

Always None when using the table API.

Return: int or None

string property writable

string: str

Set / get the value of the cell as a string (or ‘’).

style property writable

style: str | None

Get / set the style of the cell itself.

Return: str | None

type property writable

type: str | None

Get / set the type of the cell: boolean, float, date, string or time.

Return: str | None

value property writable

value: (
    str
    | bool
    | int
    | Float
    | Decimal
    | date
    | datetime
    | timedelta
    | None
)

Set / get the value of the cell. The type is read from the ‘office:value-type’ attribute of the cell. When setting the value, the type of the value will determine the new value_type of the cell.

Warning
  • for date, datetime and timedelta, a default text value is generated.
  • for boolean type, the text value is either ‘True’ or ‘False’.
  • for numeric types, the return value is either Decimal or in, use the float, decimal or int properties to force the type.
  • Use the method Cell.set_value() to customize the text value.

x instance-attribute

x = None

y instance-attribute

y = None

__init__

__init__(
    value: Any = None,
    text: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
    formula: str | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Cell of a Table, “table:table-cell”.

Create a cell element containing the given value. The textual representation is automatically formatted but can be provided. Cell type can be deduced as well, unless the number is a percentage or currency. If cell type is “currency”, the currency must be given. The cell can be repeated on the given number of columns.

Arguments:

value -- bool, int, float, Decimal, date, datetime, str,
         timedelta

text -- str

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
             'string' or 'time'

currency -- three-letter str

repeated -- int

style -- str
Source code in odfdo/cell.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def __init__(
    self,
    value: Any = None,
    text: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
    formula: str | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Cell of a Table, "table:table-cell".

    Create a cell element containing the given value. The textual
    representation is automatically formatted but can be provided. Cell
    type can be deduced as well, unless the number is a percentage or
    currency. If cell type is "currency", the currency must be given.
    The cell can be repeated on the given number of columns.

    Arguments:

        value -- bool, int, float, Decimal, date, datetime, str,
                 timedelta

        text -- str

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

        currency -- three-letter str

        repeated -- int

        style -- str
    """
    super().__init__(**kwargs)
    self.x = None
    self.y = None
    if self._do_init:
        self.set_value(
            value,
            text=text,
            cell_type=cell_type,
            currency=currency,
            formula=formula,
        )
        if repeated and repeated > 1:
            self.repeated = repeated
        if style is not None:
            self.style = style

is_covered

is_covered() -> bool

Return whether the cell is covered (tag table:covered-table-cell).

Returns: True | False

Source code in odfdo/cell.py
515
516
517
518
519
520
def is_covered(self) -> bool:
    """Return whether the cell is covered (tag table:covered-table-cell).

    Returns: True | False
    """
    return self.tag == "table:covered-table-cell"

is_empty

is_empty(aggressive: bool = False) -> bool

Return whether the cell has no value or the value evaluates to False (empty string), and no style.

If aggressive is True, empty cells with style are considered empty.

Arguments:

aggressive -- bool

Return: bool

Source code in odfdo/cell.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
def is_empty(self, aggressive: bool = False) -> bool:
    """Return whether the cell has no value or the value evaluates
    to False (empty string), and no style.

    If aggressive is True, empty cells with style are considered empty.

    Arguments:

        aggressive -- bool

    Return: bool
    """
    if (
        self.value is not None
        or self.children
        or self.is_covered()
        or self.is_spanned()
    ):
        return False
    if not aggressive and self.style is not None:  # noqa: SIM103
        return False
    return True

is_spanned

is_spanned(covered: bool = True) -> bool

Return whether the cell is spanned over several cells.

If covered is True (the default), covered cells are considered as spanned, else only the top left cell. The top left contains the attributes “table:number-columns-spanned” and “table:number-rows-spanned”.

Arguments:

covered -- bool

Return: True | False

Source code in odfdo/cell.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def is_spanned(self, covered: bool = True) -> bool:
    """Return whether the cell is spanned over several cells.

    If covered is True (the default), covered cells are considered as
    spanned, else only the top left cell. The top left contains the
    attributes "table:number-columns-spanned" and
    "table:number-rows-spanned".

    Arguments:

        covered -- bool

    Return: True | False
    """
    if self.is_covered():
        return covered
    if self.get_attribute("table:number-columns-spanned") is not None:
        return True
    if self.get_attribute("table:number-rows-spanned") is not None:  # noqa: SIM103
        return True
    return False

set_value

set_value(
    value: str
    | bytes
    | Float
    | int
    | Decimal
    | bool
    | datetime
    | date
    | timedelta
    | None,
    text: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
    formula: str | None = None,
) -> None

Set the cell state from the Python value type.

Text is how the cell is displayed. Cell type is guessed, unless provided.

For monetary values, provide the name of the currency.

Arguments:

value -- Python type

text -- str

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
            'currency' or 'percentage'

currency -- str
Source code in odfdo/cell.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def set_value(
    self,
    value: (
        str
        | bytes
        | Float
        | int
        | Decimal
        | bool
        | datetime
        | date
        | timedelta
        | None
    ),
    text: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
    formula: str | None = None,
) -> None:
    """Set the cell state from the Python value type.

    Text is how the cell is displayed. Cell type is guessed,
    unless provided.

    For monetary values, provide the name of the currency.

    Arguments:

        value -- Python type

        text -- str

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                    'currency' or 'percentage'

        currency -- str
    """
    self.clear()
    text = self.set_value_and_type(
        value=value,
        text=text,
        value_type=cell_type,
        currency=currency,
    )
    if text is not None:
        self.text_content = text
    if formula is not None:
        self.formula = formula

span_area

span_area() -> tuple[int, int]

Return the tuple (nb_columns, nb_rows) of the zone covered by a spanned cell.

If the cell is not spanned, return (0,0).

Return: tuple[int, int]

Source code in odfdo/cell.py
546
547
548
549
550
551
552
553
554
555
556
def span_area(self) -> tuple[int, int]:
    """Return the tuple (nb_columns, nb_rows) of the zone covered
    by a spanned cell.

    If the cell is not spanned, return (0,0).

    Return: tuple[int, int]
    """
    columns = self.get_attribute_integer("table:number-columns-spanned") or 0
    rows = self.get_attribute_integer("table:number-rows-spanned") or 0
    return (columns, rows)

ChangeInfo

Bases: Element, DcCreatorMixin, DcDateMixin

Representation of informations of a change, “office:change-info”.

The “office:change-info” element represents who made a change and when. It may also contain a comment (one or more Paragrah “text:p” elements) on the change.

The comments available in the ChangeInfo are available through:

  • paragraphs property, get_paragraphs and get_paragraph methods for actual Paragraph.

  • get_comments for a plain text version

Methods:

Name Description
__init__

Create informations of a change “office:change-info”.

get_comments

Get text content of the comments. If joined is True (default), the

set_comments

Set the text content of the comments. If replace is True (default),

Attributes:

Name Type Description
creator
date
Source code in odfdo/tracked_changes.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class ChangeInfo(Element, DcCreatorMixin, DcDateMixin):
    """Representation of informations of a change, "office:change-info".

    The "office:change-info" element represents who made a change and when.
    It may also contain a comment (one or more Paragrah "text:p" elements)
    on the change.

    The comments available in the ChangeInfo are available through:

       - paragraphs property, get_paragraphs and get_paragraph methods for actual Paragraph.

       - get_comments for a plain text version
    """

    _tag = "office:change-info"

    def __init__(
        self,
        creator: str | None = None,
        date: datetime | None = None,
        **kwargs: Any,
    ) -> None:
        """Create informations of a change "office:change-info".

        Arguments:

           creator -- str (or None)

           date -- datetime (or None)
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.creator = creator or "Unknown"
            self.date = date

    def get_comments(self, joined: bool = True) -> str | list[str]:
        """Get text content of the comments. If joined is True (default), the
        text of different paragraphs is concatenated, else a list of strings,
        one per paragraph, is returned.

        Arguments:

            joined -- boolean (default is True)

        Return: str or list of str.
        """
        content = self.paragraphs
        text = [para.get_formatted_text(simple=True) for para in content]  # type: ignore
        if joined:
            return "\n".join(text)
        return text

    def set_comments(self, text: str = "", replace: bool = True) -> None:
        """Set the text content of the comments. If replace is True (default),
        the new text replace old comments, else it is added at the end.

        Arguments:

            text -- str

            replace -- boolean
        """
        if replace:
            for para in self.paragraphs:
                self.delete(para)
        para = Paragraph()
        para.append_plain_text(text)
        self.insert(para, xmlposition=LAST_CHILD)

creator instance-attribute

creator = creator or 'Unknown'

date instance-attribute

date = date

__init__

__init__(
    creator: str | None = None,
    date: datetime | None = None,
    **kwargs: Any,
) -> None

Create informations of a change “office:change-info”.

Arguments:

creator – str (or None)

date – datetime (or None)

Source code in odfdo/tracked_changes.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    creator: str | None = None,
    date: datetime | None = None,
    **kwargs: Any,
) -> None:
    """Create informations of a change "office:change-info".

    Arguments:

       creator -- str (or None)

       date -- datetime (or None)
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.creator = creator or "Unknown"
        self.date = date

get_comments

get_comments(joined: bool = True) -> str | list[str]

Get text content of the comments. If joined is True (default), the text of different paragraphs is concatenated, else a list of strings, one per paragraph, is returned.

Arguments:

joined -- boolean (default is True)

Return: str or list of str.

Source code in odfdo/tracked_changes.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def get_comments(self, joined: bool = True) -> str | list[str]:
    """Get text content of the comments. If joined is True (default), the
    text of different paragraphs is concatenated, else a list of strings,
    one per paragraph, is returned.

    Arguments:

        joined -- boolean (default is True)

    Return: str or list of str.
    """
    content = self.paragraphs
    text = [para.get_formatted_text(simple=True) for para in content]  # type: ignore
    if joined:
        return "\n".join(text)
    return text

set_comments

set_comments(text: str = '', replace: bool = True) -> None

Set the text content of the comments. If replace is True (default), the new text replace old comments, else it is added at the end.

Arguments:

text -- str

replace -- boolean
Source code in odfdo/tracked_changes.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def set_comments(self, text: str = "", replace: bool = True) -> None:
    """Set the text content of the comments. If replace is True (default),
    the new text replace old comments, else it is added at the end.

    Arguments:

        text -- str

        replace -- boolean
    """
    if replace:
        for para in self.paragraphs:
            self.delete(para)
    para = Paragraph()
    para.append_plain_text(text)
    self.insert(para, xmlposition=LAST_CHILD)

Chart

Bases: Body

Root of the Chart document content, “office:chart”.

Source code in odfdo/body.py
95
96
97
98
99
class Chart(Body):
    """Root of the Chart document content, "office:chart"."""

    _tag: str = "office:chart"
    _properties: tuple[PropDef, ...] = ()

Column

Bases: Element

A Column of a table, “table:table-column”.

Methods:

Name Description
__init__

A Column of a table, “table:table-column”.

get_default_cell_style

Get or set the default cell style for column.

set_default_cell_style

Get or set the default cell style for column.

Attributes:

Name Type Description
clone Column
default_cell_style str | None

Get or set the default cell style for column.

repeated int | None

Get /set the number of times the column is repeated.

style str | None

Get /set the style of the column itself.

x
Source code in odfdo/column.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
class Column(Element):
    """A Column of a table, "table:table-column"."""

    _tag = "table:table-column"

    def __init__(
        self,
        default_cell_style: str | None = None,
        repeated: int | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """A Column of a table, "table:table-column".

        Create a column group element of the optionally given style. Cell
        style can be set for the whole column. If the properties apply to
        several columns, give the number of repeated columns.

        Columns don't contain cells, just style information.

        You don't generally have to create columns by hand, use the Table API.

        Arguments:

            default_cell_style -- str or None

            repeated -- int or None

            style -- str or None
        """
        super().__init__(**kwargs)
        self.x = None
        if self._do_init:
            if default_cell_style:
                self.default_cell_style = default_cell_style
            if repeated and repeated > 1:
                self.repeated = repeated
            if style:
                self.style = style

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} x={self.x}>"

    @property
    def clone(self) -> Column:
        clone: Column = Element.clone.fget(self)  # type: ignore[attr-defined]
        clone.x = self.x
        return clone

    def get_default_cell_style(self) -> str | None:
        """Get or set the default cell style for column.

        (See also self.default_cell_style property.)
        """
        return self.get_attribute_string("table:default-cell-style-name")

    def set_default_cell_style(self, style: Style | str | None) -> None:
        """Get or set the default cell style for column.

        (See also self.default_cell_style property.)
        """
        self.set_style_attribute("table:default-cell-style-name", style)

    @property
    def default_cell_style(self) -> str | None:
        """Get or set the default cell style for column."""
        return self.get_attribute_string("table:default-cell-style-name")

    @default_cell_style.setter
    def default_cell_style(self, style: Style | str | None) -> None:
        self.set_style_attribute("table:default-cell-style-name", style)

    def _set_repeated(self, repeated: int | None) -> None:
        """Internal only. Set the number of times the column is repeated, or
        None to delete it. Without changing cache.

        Arguments:

            repeated -- int or None
        """
        if repeated is None or repeated < 2:
            with contextlib.suppress(KeyError):
                self.del_attribute("table:number-columns-repeated")
            return
        self.set_attribute("table:number-columns-repeated", str(repeated))

    @property
    def repeated(self) -> int | None:
        """Get /set the number of times the column is repeated.

        Always None when using the table API.

        Return: int or None
        """
        repeated = self.get_attribute("table:number-columns-repeated")
        if repeated is None:
            return None
        return int(repeated)

    @repeated.setter
    def repeated(self, repeated: int | None) -> None:
        self._set_repeated(repeated)
        # update cache
        current: Element = self
        while True:
            # look for Table, parent may be group of rows
            upper = current.parent
            if not upper:
                # lonely column
                return
            # parent may be group of rows, not table
            if upper.tag == "table:table":
                break
            current = upper

    @property
    def style(self) -> str | None:
        """Get /set the style of the column itself.

        Return: str or None
        """
        return self.get_attribute_string("table:style-name")

    @style.setter
    def style(self, style: str | Style | None) -> None:
        self.set_style_attribute("table:style-name", style)

clone property

clone: Column

default_cell_style property writable

default_cell_style: str | None

Get or set the default cell style for column.

repeated property writable

repeated: int | None

Get /set the number of times the column is repeated.

Always None when using the table API.

Return: int or None

style property writable

style: str | None

Get /set the style of the column itself.

Return: str or None

x instance-attribute

x = None

__init__

__init__(
    default_cell_style: str | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

A Column of a table, “table:table-column”.

Create a column group element of the optionally given style. Cell style can be set for the whole column. If the properties apply to several columns, give the number of repeated columns.

Columns don’t contain cells, just style information.

You don’t generally have to create columns by hand, use the Table API.

Arguments:

default_cell_style -- str or None

repeated -- int or None

style -- str or None
Source code in odfdo/column.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    default_cell_style: str | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """A Column of a table, "table:table-column".

    Create a column group element of the optionally given style. Cell
    style can be set for the whole column. If the properties apply to
    several columns, give the number of repeated columns.

    Columns don't contain cells, just style information.

    You don't generally have to create columns by hand, use the Table API.

    Arguments:

        default_cell_style -- str or None

        repeated -- int or None

        style -- str or None
    """
    super().__init__(**kwargs)
    self.x = None
    if self._do_init:
        if default_cell_style:
            self.default_cell_style = default_cell_style
        if repeated and repeated > 1:
            self.repeated = repeated
        if style:
            self.style = style

get_default_cell_style

get_default_cell_style() -> str | None

Get or set the default cell style for column.

(See also self.default_cell_style property.)

Source code in odfdo/column.py
84
85
86
87
88
89
def get_default_cell_style(self) -> str | None:
    """Get or set the default cell style for column.

    (See also self.default_cell_style property.)
    """
    return self.get_attribute_string("table:default-cell-style-name")

set_default_cell_style

set_default_cell_style(style: Style | str | None) -> None

Get or set the default cell style for column.

(See also self.default_cell_style property.)

Source code in odfdo/column.py
91
92
93
94
95
96
def set_default_cell_style(self, style: Style | str | None) -> None:
    """Get or set the default cell style for column.

    (See also self.default_cell_style property.)
    """
    self.set_style_attribute("table:default-cell-style-name", style)

ConnectorShape

Bases: ShapeBase

A connector shape, “draw:connector”.

Methods:

Name Description
__init__

Create a Connector shape “draw:connector”.

Attributes:

Name Type Description
end_glue_point
end_shape
start_glue_point
start_shape
x1
x2
y1
y2
Source code in odfdo/shapes.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
class ConnectorShape(ShapeBase):
    """A connector shape, "draw:connector"."""

    _tag = "draw:connector"
    _properties: tuple[PropDef, ...] = (
        PropDef("start_shape", "draw:start-shape"),
        PropDef("end_shape", "draw:end-shape"),
        PropDef("start_glue_point", "draw:start-glue-point"),
        PropDef("end_glue_point", "draw:end-glue-point"),
        PropDef("x1", "svg:x1"),
        PropDef("y1", "svg:y1"),
        PropDef("x2", "svg:x2"),
        PropDef("y2", "svg:y2"),
    )

    def __init__(
        self,
        style: str | None = None,
        text_style: str | None = None,
        draw_id: str | None = None,
        layer: str | None = None,
        connected_shapes: tuple | None = None,
        glue_points: tuple | None = None,
        p1: tuple | None = None,
        p2: tuple | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a Connector shape "draw:connector".

        Arguments:

            style -- str

            text_style -- str

            draw_id -- str

            layer -- str

            connected_shapes -- (shape, shape)

            glue_points -- (point, point)

            p1 -- (str, str)

            p2 -- (str, str)
        """
        kwargs.update(
            {
                "style": style,
                "text_style": text_style,
                "draw_id": draw_id,
                "layer": layer,
            }
        )
        super().__init__(**kwargs)
        if self._do_init:
            if connected_shapes:
                self.start_shape = connected_shapes[0].draw_id
                self.end_shape = connected_shapes[1].draw_id
            if glue_points:
                self.start_glue_point = glue_points[0]
                self.end_glue_point = glue_points[1]
            if p1:
                self.x1 = p1[0]
                self.y1 = p1[1]
            if p2:
                self.x2 = p2[0]
                self.y2 = p2[1]

end_glue_point instance-attribute

end_glue_point = glue_points[1]

end_shape instance-attribute

end_shape = draw_id

start_glue_point instance-attribute

start_glue_point = glue_points[0]

start_shape instance-attribute

start_shape = draw_id

x1 instance-attribute

x1 = p1[0]

x2 instance-attribute

x2 = p2[0]

y1 instance-attribute

y1 = p1[1]

y2 instance-attribute

y2 = p2[1]

__init__

__init__(
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    connected_shapes: tuple | None = None,
    glue_points: tuple | None = None,
    p1: tuple | None = None,
    p2: tuple | None = None,
    **kwargs: Any,
) -> None

Create a Connector shape “draw:connector”.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

connected_shapes -- (shape, shape)

glue_points -- (point, point)

p1 -- (str, str)

p2 -- (str, str)
Source code in odfdo/shapes.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def __init__(
    self,
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    connected_shapes: tuple | None = None,
    glue_points: tuple | None = None,
    p1: tuple | None = None,
    p2: tuple | None = None,
    **kwargs: Any,
) -> None:
    """Create a Connector shape "draw:connector".

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        connected_shapes -- (shape, shape)

        glue_points -- (point, point)

        p1 -- (str, str)

        p2 -- (str, str)
    """
    kwargs.update(
        {
            "style": style,
            "text_style": text_style,
            "draw_id": draw_id,
            "layer": layer,
        }
    )
    super().__init__(**kwargs)
    if self._do_init:
        if connected_shapes:
            self.start_shape = connected_shapes[0].draw_id
            self.end_shape = connected_shapes[1].draw_id
        if glue_points:
            self.start_glue_point = glue_points[0]
            self.end_glue_point = glue_points[1]
        if p1:
            self.x1 = p1[0]
            self.y1 = p1[1]
        if p2:
            self.x2 = p2[0]
            self.y2 = p2[1]

Container

Storage of the ODF document, as zip or other format.

Methods:

Name Description
__init__

Storage of the ODF document, as zip or other format.

del_part

Mark a part for deletion.

get_part

Get the bytes of a part of the ODF.

get_parts

Get the list of members.

open

Load the content of an ODF file.

save

Save the container to the given target, a path or a file-like

set_part

Replace or add a new part.

Attributes:

Name Type Description
clone Container

Make a copy of this container with no path.

default_manifest_rdf str
mimetype str

Return str value of mimetype of the document.

parts list[str]

Get the list of members.

path Path | None
Source code in odfdo/container.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
class Container:
    """Storage of the ODF document, as zip or other format."""

    def __init__(self, path: Path | str | io.BytesIO | None = None) -> None:
        """Storage of the ODF document, as zip or other format.

        Arguments:

        path -- path like, io.BytesIO or None
        """
        self.__parts: dict[str, bytes | None] = {}
        self.__parts_ts: dict[str, int] = {}
        self.__path_like: Path | str | io.BytesIO | None = None
        self.__packaging: str = ZIP
        self.path: Path | None = None  # or Path
        if path:
            self.open(path)

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} type={self.mimetype} path={self.path}>"

    def open(self, path_or_file: Path | str | io.BytesIO) -> None:
        """Load the content of an ODF file."""
        self.__path_like = path_or_file
        if isinstance(path_or_file, (str, Path)):
            self.path = Path(path_or_file).expanduser()
            if not self.path.exists():
                raise FileNotFoundError(str(self.path))
            self.__path_like = self.path
        if (self.path or isinstance(self.__path_like, io.BytesIO)) and is_zipfile(
            self.__path_like
        ):
            self.__packaging = ZIP
            return self._read_zip()
        if self.path:
            is_folder = False
            with contextlib.suppress(OSError):
                is_folder = self.path.is_dir()
            if is_folder:
                self.__packaging = FOLDER
                return self._read_folder()
        raise TypeError(f"Document format not managed by odfdo: {type(path_or_file)}.")

    def _read_zip(self) -> None:
        if isinstance(self.__path_like, io.BytesIO):
            self.__path_like.seek(0)
        with ZipFile(self.__path_like) as zf:  # type: ignore
            mimetype = bytes_to_str(zf.read("mimetype"))
            if mimetype not in ODF_MIMETYPES:
                raise ValueError(f"Document of unknown type {mimetype}")
            self.__parts["mimetype"] = str_to_bytes(mimetype)
        if self.path is None:
            if isinstance(self.__path_like, io.BytesIO):
                self.__path_like.seek(0)
            # read the full file at once and forget file
            with ZipFile(self.__path_like) as zf:  # type: ignore
                for name in zf.namelist():
                    upath = normalize_path(name)
                    self.__parts[upath] = zf.read(name)
            self.__path_like = None

    def _read_folder(self) -> None:
        try:
            mimetype, timestamp = self._get_folder_part("mimetype")
        except OSError:
            printwarn("Corrupted or not an OpenDocument folder (missing mimetype)")
            mimetype = b""
            timestamp = int(time.time())
        if bytes_to_str(mimetype) not in ODF_MIMETYPES:
            message = f"Document of unknown type {mimetype!r}, try with ODF Text."
            printwarn(message)
            self.__parts["mimetype"] = str_to_bytes(ODF_EXTENSIONS["odt"])
            self.__parts_ts["mimetype"] = timestamp

    def _parse_folder(self, folder: str) -> list[str]:
        parts = []
        if self.path is None:
            raise ValueError("Document path is not defined")
        root = self.path / folder
        for path in root.iterdir():
            if path.name.startswith("."):  # no hidden files
                continue
            relative_path = path.relative_to(self.path)
            if path.is_file():
                parts.append(relative_path.as_posix())
            if path.is_dir():
                sub_parts = self._parse_folder(str(relative_path))
                if sub_parts:
                    parts.extend(sub_parts)
                else:
                    # store leaf directories
                    parts.append(relative_path.as_posix() + "/")
        return parts

    def _get_folder_parts(self) -> list[str]:
        """Get the list of members in the ODF folder."""
        return self._parse_folder("")

    def _get_folder_part(self, name: str) -> tuple[bytes, int]:
        """Get bytes of a part from the ODF folder, with timestamp."""
        if self.path is None:
            raise ValueError("Document path is not defined")
        path = self.path / name
        timestamp = int(path.stat().st_mtime)
        if path.is_dir():
            return (b"", timestamp)
        return (path.read_bytes(), timestamp)

    def _get_folder_part_timestamp(self, name: str) -> int:
        if self.path is None:
            raise ValueError("Document path is not defined")
        path = self.path / name
        try:
            timestamp = int(path.stat().st_mtime)
        except OSError:
            timestamp = -1
        return timestamp

    def _get_zip_part(self, name: str) -> bytes | None:
        """Get bytes of a part from the Zip ODF file. No cache."""
        if self.path is None:
            raise ValueError("Document path is not defined")
        try:
            with ZipFile(self.path) as zf:
                upath = normalize_path(name)
                self.__parts[upath] = zf.read(name)
                return self.__parts[upath]
        except BadZipfile:
            return None

    def _get_all_zip_part(self) -> None:
        """Read all parts. No cache."""
        if self.path is None:
            raise ValueError("Document path is not defined")
        try:
            with ZipFile(self.path) as zf:
                for name in zf.namelist():
                    upath = normalize_path(name)
                    self.__parts[upath] = zf.read(name)
        except BadZipfile:
            pass

    def _save_zip(self, target: str | Path | io.BytesIO) -> None:
        """Save a Zip ODF from the available parts."""
        parts = self.__parts
        with ZipFile(target, "w", compression=ZIP_DEFLATED) as filezip:
            # Parts to save, except manifest at the end
            part_names = list(parts.keys())
            try:
                part_names.remove(ODF_MANIFEST)
            except ValueError:
                printwarn(f"Missing '{ODF_MANIFEST}'")
            # "Pretty-save" parts in some order
            # mimetype requires to be first and uncompressed
            mimetype = parts.get("mimetype")
            if mimetype is None:
                raise ValueError("Mimetype is not defined")
            try:
                filezip.writestr("mimetype", mimetype, ZIP_STORED)
                part_names.remove("mimetype")
            except (ValueError, KeyError):
                printwarn("Missing 'mimetype'")
            # XML parts
            for path in ODF_CONTENT, ODF_META, ODF_SETTINGS, ODF_STYLES:
                if path not in parts:
                    printwarn(f"Missing '{path}'")
                    continue
                part = parts[path]
                if part is None:
                    continue
                filezip.writestr(path, part)
                part_names.remove(path)
            # Everything else
            for path in part_names:
                data = parts[path]
                if data is None:
                    # Deleted
                    continue
                filezip.writestr(path, data)
            with contextlib.suppress(KeyError):
                part = parts[ODF_MANIFEST]
                if part is not None:
                    filezip.writestr(ODF_MANIFEST, part)

    def _save_folder(self, folder: Path | str) -> None:
        """Save a folder ODF from the available parts."""

        def dump(part_path: str, content: bytes) -> None:
            if part_path.endswith("/"):  # folder
                is_folder = True
                pure_path = PurePath(folder, part_path[:-1])
            else:
                is_folder = False
                pure_path = PurePath(folder, part_path)
            path = Path(pure_path)
            if is_folder:
                path.mkdir(parents=True, exist_ok=True)
            else:
                path.parent.mkdir(parents=True, exist_ok=True)
                path.write_bytes(content)
                path.chmod(0o666)

        for part_path, data in self.__parts.items():
            if data is None:
                # Deleted
                continue
            dump(part_path, data)

    def _encoded_image(self, elem: _Element) -> _Element | None:
        mime_type = elem.get(
            "{urn:oasis:names:tc:opendocument:xmlns:drawing:1.0}mime-type"
        )
        path = elem.get("{http://www.w3.org/1999/xlink}href")
        if not path:
            return None
        content = self.__parts[path]
        if not content:
            return None
        text = base64.standard_b64encode(content).decode()
        ebytes = (
            f'<draw:image draw:mime-type="{mime_type}">'
            f"<office:binary-data>{text}\n</office:binary-data></draw:image>"
        ).encode()
        root = fromstring(NAMESPACES_XML % ebytes)
        return root[0]

    def _xml_content(self, pretty: bool = True) -> bytes:
        mimetype = self.__parts["mimetype"].decode("utf8")
        doc_xml = (
            OFFICE_PREFIX.decode("utf8")
            + f'office:version="{OFFICE_VERSION}"\n'
            + f'office:mimetype="{mimetype}">'
            + "</office:document>"
        )
        root = fromstring(doc_xml.encode("utf8"))
        for path in ODF_META, ODF_SETTINGS, ODF_STYLES, ODF_CONTENT:
            if path not in self.__parts:
                printwarn(f"Missing '{path}'")
                continue
            part = self.__parts[path]
            if part is None:
                continue
            if isinstance(part, bytes):
                xpart = fromstring(part)
            else:
                xpart = part
            if path == ODF_CONTENT:
                xpath = xpath_compile("descendant::draw:image")
                images = xpath(xpart)
                if images:
                    for elem in images:
                        encoded = self._encoded_image(elem)
                        elem.getparent().replace(elem, encoded)
            for child in xpart:
                root.append(child)
        if pretty:
            xml_header = b'<?xml version="1.0" encoding="UTF-8"?>\n'
            bytes_tree = tostring(
                pretty_indent(root),
                encoding="unicode",
            ).encode("utf8")
            return xml_header + bytes_tree
        else:
            return tostring(root, encoding="UTF-8", xml_declaration=True)

    def _save_xml(self, target: Path | str | io.BytesIO, pretty: bool = True) -> None:
        """Save a XML flat ODF format from the available parts."""
        if isinstance(target, (Path, str)):
            target = Path(target).with_suffix(".xml")
            target.write_bytes(self._xml_content(pretty))
        else:
            target.write(self._xml_content(pretty))

    # Public API

    def get_parts(self) -> list[str]:
        """Get the list of members."""
        if not self.path:
            # maybe a file like zip archive
            return list(self.__parts.keys())
        if self.__packaging == ZIP:
            parts = []
            with ZipFile(self.path) as zf:
                for name in zf.namelist():
                    upath = normalize_path(name)
                    parts.append(upath)
            return parts
        elif self.__packaging == FOLDER:
            return self._get_folder_parts()
        else:
            raise ValueError("Unable to provide parts of the document")

    @property
    def parts(self) -> list[str]:
        """Get the list of members."""
        return self.get_parts()

    def get_part(self, path: str) -> str | bytes | None:
        """Get the bytes of a part of the ODF."""
        path = str(path)
        if path in self.__parts:
            part = self.__parts[path]
            if part is None:
                raise ValueError(f'Part "{path}" is deleted')
            if self.__packaging == FOLDER:
                cache_ts = self.__parts_ts.get(path, -1)
                current_ts = self._get_folder_part_timestamp(path)
                if current_ts != cache_ts:
                    part, timestamp = self._get_folder_part(path)
                    self.__parts[path] = part
                    self.__parts_ts[path] = timestamp
            return part
        if self.__packaging == ZIP:
            return self._get_zip_part(path)
        if self.__packaging == FOLDER:
            part, timestamp = self._get_folder_part(path)
            self.__parts[path] = part
            self.__parts_ts[path] = timestamp
            return part
        return None

    @property
    def default_manifest_rdf(self) -> str:
        return (
            '<?xml version="1.0" encoding="utf-8"?>\n'
            '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n'
            '  <rdf:Description rdf:about="styles.xml">\n'
            f'    <rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/{OFFICE_VERSION}/meta/odf#StylesFile"/>\n'
            "  </rdf:Description>\n"
            '  <rdf:Description rdf:about="">\n'
            f'    <ns0:hasPart xmlns:ns0="http://docs.oasis-open.org/ns/office/{OFFICE_VERSION}/meta/pkg#" rdf:resource="styles.xml"/>\n'
            "  </rdf:Description>\n"
            '  <rdf:Description rdf:about="content.xml">\n'
            f'    <rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/{OFFICE_VERSION}/meta/odf#ContentFile"/>\n'
            "  </rdf:Description>\n"
            '  <rdf:Description rdf:about="">\n'
            f'    <ns0:hasPart xmlns:ns0="http://docs.oasis-open.org/ns/office/{OFFICE_VERSION}/meta/pkg#" rdf:resource="content.xml"/>\n'
            "  </rdf:Description>\n"
            '  <rdf:Description rdf:about="">\n'
            f'    <rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/{OFFICE_VERSION}/meta/pkg#Document"/>\n'
            "  </rdf:Description>\n"
            "</rdf:RDF>\n"
        )

    @property
    def mimetype(self) -> str:
        """Return str value of mimetype of the document."""
        with contextlib.suppress(Exception):
            b_mimetype = self.get_part("mimetype")
            if isinstance(b_mimetype, bytes):
                return bytes_to_str(b_mimetype)
        return ""

    @mimetype.setter
    def mimetype(self, mimetype: str | bytes) -> None:
        """Set mimetype value of the document."""
        if isinstance(mimetype, str):
            self.__parts["mimetype"] = str_to_bytes(mimetype)
        elif isinstance(mimetype, bytes):
            self.__parts["mimetype"] = mimetype
        else:
            raise TypeError(f'Wrong mimetype "{mimetype!r}"')

    def set_part(self, path: str, data: bytes) -> None:
        """Replace or add a new part."""
        self.__parts[path] = data

    def del_part(self, path: str) -> None:
        """Mark a part for deletion."""
        self.__parts[path] = None

    @property
    def clone(self) -> Container:
        """Make a copy of this container with no path."""
        if self.path and self.__packaging == ZIP:
            self._get_all_zip_part()
        clone = deepcopy(self)
        clone.path = None
        return clone

    def _backup_or_unlink(self, backup: bool, target: str | Path) -> None:
        if backup:
            self._do_backup(target)
        else:
            self._do_unlink(target)

    @staticmethod
    def _do_backup(target: str | Path) -> None:
        path = Path(target)
        if not path.exists():
            return
        back_file = Path(path.stem + ".backup" + path.suffix)
        if back_file.is_dir():
            try:
                shutil.rmtree(back_file)
            except OSError as e:
                printwarn(str(e))
        try:
            shutil.move(target, back_file)
        except OSError as e:
            printwarn(str(e))

    @staticmethod
    def _do_unlink(target: str | Path) -> None:
        path = Path(target)
        if path.exists():
            try:
                shutil.rmtree(path)
            except OSError as e:
                printwarn(str(e))

    def _clean_save_packaging(self, packaging: str | None) -> str:
        if not packaging:
            packaging = self.__packaging if self.__packaging else ZIP
        packaging = packaging.strip().lower()
        if packaging not in PACKAGING:
            raise ValueError(f'Packaging of type "{packaging}" is not supported')
        return packaging

    def _clean_save_target(
        self,
        target: str | Path | io.BytesIO | None,
    ) -> str | io.BytesIO:
        if target is None:
            target = self.path
        if isinstance(target, Path):
            target = str(target)
        if isinstance(target, str):
            while target.endswith(os.sep):
                target = target[:-1]
            while target.endswith(".folder"):
                target = target.split(".folder", 1)[0]
        return target  # type: ignore

    def _save_as_zip(self, target: str | Path | io.BytesIO, backup: bool) -> None:
        if isinstance(target, (str, Path)) and backup:
            self._do_backup(target)
        self._save_zip(target)

    def _save_as_folder(self, target: str | Path, backup: bool) -> None:
        if not isinstance(target, (str, Path)):
            raise TypeError(
                f"Saving in folder format requires a folder name, not '{target!r}'"
            )
        if not str(target).endswith(".folder"):
            target = str(target) + ".folder"
        self._backup_or_unlink(backup, target)
        self._save_folder(target)

    def _save_as_xml(
        self,
        target: str | Path | io.BytesIO,
        backup: bool,
        pretty: bool = True,
    ) -> None:
        if not isinstance(target, (str, Path, io.BytesIO)):
            raise TypeError(
                f"Saving in XML format requires a path name, not '{target!r}'"
            )
        if isinstance(target, (str, Path)):
            if not str(target).endswith(".xml"):
                target = str(target) + ".xml"
            if backup:
                self._do_backup(target)
        self._save_xml(target, pretty)

    def save(
        self,
        target: str | Path | io.BytesIO | None,
        packaging: str | None = None,
        backup: bool = False,
        pretty: bool = False,
    ) -> None:
        """Save the container to the given target, a path or a file-like
        object.

        Package the output document in the same format than current document,
        unless "packaging" is different.

        Arguments:

            target -- str or file-like or Path

            packaging -- 'zip', or for debugging purpose 'xml' or 'folder'

            backup -- boolean
        """
        parts = self.__parts
        packaging = self._clean_save_packaging(packaging)
        # Load parts else they will be considered deleted
        for path in self.parts:
            if path not in parts:
                self.get_part(path)
        target = self._clean_save_target(target)
        if packaging == FOLDER:
            if isinstance(target, io.BytesIO):
                raise TypeError(
                    "Impossible to save on io.BytesIO with 'folder' packaging"
                )
            self._save_as_folder(target, backup)
        elif packaging == XML:
            self._save_as_xml(target, backup, pretty)
        else:
            # default:
            self._save_as_zip(target, backup)

clone property

clone: Container

Make a copy of this container with no path.

default_manifest_rdf property

default_manifest_rdf: str

mimetype property writable

mimetype: str

Return str value of mimetype of the document.

parts property

parts: list[str]

Get the list of members.

path instance-attribute

path: Path | None = None

__init__

__init__(path: Path | str | BytesIO | None = None) -> None

Storage of the ODF document, as zip or other format.

Arguments:

path – path like, io.BytesIO or None

Source code in odfdo/container.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def __init__(self, path: Path | str | io.BytesIO | None = None) -> None:
    """Storage of the ODF document, as zip or other format.

    Arguments:

    path -- path like, io.BytesIO or None
    """
    self.__parts: dict[str, bytes | None] = {}
    self.__parts_ts: dict[str, int] = {}
    self.__path_like: Path | str | io.BytesIO | None = None
    self.__packaging: str = ZIP
    self.path: Path | None = None  # or Path
    if path:
        self.open(path)

del_part

del_part(path: str) -> None

Mark a part for deletion.

Source code in odfdo/container.py
611
612
613
def del_part(self, path: str) -> None:
    """Mark a part for deletion."""
    self.__parts[path] = None

get_part

get_part(path: str) -> str | bytes | None

Get the bytes of a part of the ODF.

Source code in odfdo/container.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
def get_part(self, path: str) -> str | bytes | None:
    """Get the bytes of a part of the ODF."""
    path = str(path)
    if path in self.__parts:
        part = self.__parts[path]
        if part is None:
            raise ValueError(f'Part "{path}" is deleted')
        if self.__packaging == FOLDER:
            cache_ts = self.__parts_ts.get(path, -1)
            current_ts = self._get_folder_part_timestamp(path)
            if current_ts != cache_ts:
                part, timestamp = self._get_folder_part(path)
                self.__parts[path] = part
                self.__parts_ts[path] = timestamp
        return part
    if self.__packaging == ZIP:
        return self._get_zip_part(path)
    if self.__packaging == FOLDER:
        part, timestamp = self._get_folder_part(path)
        self.__parts[path] = part
        self.__parts_ts[path] = timestamp
        return part
    return None

get_parts

get_parts() -> list[str]

Get the list of members.

Source code in odfdo/container.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
def get_parts(self) -> list[str]:
    """Get the list of members."""
    if not self.path:
        # maybe a file like zip archive
        return list(self.__parts.keys())
    if self.__packaging == ZIP:
        parts = []
        with ZipFile(self.path) as zf:
            for name in zf.namelist():
                upath = normalize_path(name)
                parts.append(upath)
        return parts
    elif self.__packaging == FOLDER:
        return self._get_folder_parts()
    else:
        raise ValueError("Unable to provide parts of the document")

open

open(path_or_file: Path | str | BytesIO) -> None

Load the content of an ODF file.

Source code in odfdo/container.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def open(self, path_or_file: Path | str | io.BytesIO) -> None:
    """Load the content of an ODF file."""
    self.__path_like = path_or_file
    if isinstance(path_or_file, (str, Path)):
        self.path = Path(path_or_file).expanduser()
        if not self.path.exists():
            raise FileNotFoundError(str(self.path))
        self.__path_like = self.path
    if (self.path or isinstance(self.__path_like, io.BytesIO)) and is_zipfile(
        self.__path_like
    ):
        self.__packaging = ZIP
        return self._read_zip()
    if self.path:
        is_folder = False
        with contextlib.suppress(OSError):
            is_folder = self.path.is_dir()
        if is_folder:
            self.__packaging = FOLDER
            return self._read_folder()
    raise TypeError(f"Document format not managed by odfdo: {type(path_or_file)}.")

save

save(
    target: str | Path | BytesIO | None,
    packaging: str | None = None,
    backup: bool = False,
    pretty: bool = False,
) -> None

Save the container to the given target, a path or a file-like object.

Package the output document in the same format than current document, unless “packaging” is different.

Arguments:

target -- str or file-like or Path

packaging -- 'zip', or for debugging purpose 'xml' or 'folder'

backup -- boolean
Source code in odfdo/container.py
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
def save(
    self,
    target: str | Path | io.BytesIO | None,
    packaging: str | None = None,
    backup: bool = False,
    pretty: bool = False,
) -> None:
    """Save the container to the given target, a path or a file-like
    object.

    Package the output document in the same format than current document,
    unless "packaging" is different.

    Arguments:

        target -- str or file-like or Path

        packaging -- 'zip', or for debugging purpose 'xml' or 'folder'

        backup -- boolean
    """
    parts = self.__parts
    packaging = self._clean_save_packaging(packaging)
    # Load parts else they will be considered deleted
    for path in self.parts:
        if path not in parts:
            self.get_part(path)
    target = self._clean_save_target(target)
    if packaging == FOLDER:
        if isinstance(target, io.BytesIO):
            raise TypeError(
                "Impossible to save on io.BytesIO with 'folder' packaging"
            )
        self._save_as_folder(target, backup)
    elif packaging == XML:
        self._save_as_xml(target, backup, pretty)
    else:
        # default:
        self._save_as_zip(target, backup)

set_part

set_part(path: str, data: bytes) -> None

Replace or add a new part.

Source code in odfdo/container.py
607
608
609
def set_part(self, path: str, data: bytes) -> None:
    """Replace or add a new part."""
    self.__parts[path] = data

Content

Bases: XmlPart

Representation of the “content.xml” part.

Methods:

Name Description
get_style

Return the style uniquely identified by the name/family pair. If

get_styles

Return the list of styles in the Content part, optionally limited

Source code in odfdo/content.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class Content(XmlPart):
    """Representation of the "content.xml" part."""

    def _get_style_contexts(self, family: str | None) -> tuple:
        if family == "font-face":
            return (self.get_element("//office:font-face-decls"),)
        return (
            self.get_element("//office:font-face-decls"),
            self.get_element("//office:automatic-styles"),
        )

    def __str__(self) -> str:
        return str(self.body)

    # Public API

    def get_styles(self, family: str | None = None) -> list[Style]:
        """Return the list of styles in the Content part, optionally limited
        to the given family.

        Arguments:

            family -- str or None

        Return: list of Style
        """
        result: list[Style] = []
        for context in self._get_style_contexts(family):
            if context is None:
                continue
            result.extend(context.get_styles(family=family))
        return result

    def get_style(
        self,
        family: str,
        name_or_element: str | Element | None = None,
        display_name: str | None = None,
    ) -> Style | None:
        """Return the style uniquely identified by the name/family pair. If
        the argument is already a style object, it will return it.

        If the name is None, the default style is fetched.

        If the name is not the internal name but the name you gave in the
        desktop application, use display_name instead.

        Arguments:

            family -- 'paragraph', 'text', 'graphic', 'table', 'list',
                      'number', ...
            name_or_element -- str or Style

            display_name -- str

        Return: Style or None if not found
        """
        for context in self._get_style_contexts(family):
            if context is None:
                continue
            style = context.get_style(
                family,
                name_or_element=name_or_element,
                display_name=display_name,
            )
            if style is not None:
                return style
        return None

get_style

get_style(
    family: str,
    name_or_element: str | Element | None = None,
    display_name: str | None = None,
) -> Style | None

Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it.

If the name is None, the default style is fetched.

If the name is not the internal name but the name you gave in the desktop application, use display_name instead.

Arguments:

family -- 'paragraph', 'text', 'graphic', 'table', 'list',
          'number', ...
name_or_element -- str or Style

display_name -- str

Return: Style or None if not found

Source code in odfdo/content.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def get_style(
    self,
    family: str,
    name_or_element: str | Element | None = None,
    display_name: str | None = None,
) -> Style | None:
    """Return the style uniquely identified by the name/family pair. If
    the argument is already a style object, it will return it.

    If the name is None, the default style is fetched.

    If the name is not the internal name but the name you gave in the
    desktop application, use display_name instead.

    Arguments:

        family -- 'paragraph', 'text', 'graphic', 'table', 'list',
                  'number', ...
        name_or_element -- str or Style

        display_name -- str

    Return: Style or None if not found
    """
    for context in self._get_style_contexts(family):
        if context is None:
            continue
        style = context.get_style(
            family,
            name_or_element=name_or_element,
            display_name=display_name,
        )
        if style is not None:
            return style
    return None

get_styles

get_styles(family: str | None = None) -> list[Style]

Return the list of styles in the Content part, optionally limited to the given family.

Arguments:

family -- str or None

Return: list of Style

Source code in odfdo/content.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def get_styles(self, family: str | None = None) -> list[Style]:
    """Return the list of styles in the Content part, optionally limited
    to the given family.

    Arguments:

        family -- str or None

    Return: list of Style
    """
    result: list[Style] = []
    for context in self._get_style_contexts(family):
        if context is None:
            continue
        result.extend(context.get_styles(family=family))
    return result

Database

Bases: Body

Root of the Database document content, “office:database”.

Source code in odfdo/body.py
102
103
104
105
106
class Database(Body):
    """Root of the Database document content, "office:database"."""

    _tag: str = "office:database"
    _properties: tuple[PropDef, ...] = ()

DcCreatorMixin

Creator of the document, “dc:creator”.

Methods:

Name Description
get_creator

Get the creator of the document.

set_creator

Set the creator of the document.

Attributes:

Name Type Description
creator str | None

Get or set the element.

Source code in odfdo/mixin_dc_creator.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class DcCreatorMixin:
    """Creator of the document, "dc:creator"."""

    def get_creator(self) -> str | None:
        """Get the creator of the document.

        (Also available as "self.creator" property.)

        Return: str (or None if inexistant)

        Example::

            >>> document.meta.get_creator()
            Unknown
        """
        element = self.clone.get_element("//dc:creator")
        if element is None:
            return None
        return element.text

    def set_creator(self, creator: str) -> None:
        """Set the creator of the document.

        (Also available as "self.creator" property.)

        Arguments:

            creator -- str

        Example::

            >>> document.meta.set_creator("Plato")
        """
        element = self.get_element("//dc:creator")
        if element is None:
            element = Element.from_tag("dc:creator")
            if hasattr(self, "get_meta_body"):
                self.get_meta_body().append(element)
            else:
                self.append(element)
        element.text = creator

    @property
    def creator(self) -> str | None:
        """Get or set the <dc:creator> element.

        The <dc:creator> element specifies the name of the person who last
        modified a document (<office:meta>), who created an annotation
        (<office:annotation>), who authored a change (<office:change-info>).

        The <dc:creator> element is usable within the following elements:
        <office:annotation>, <office:change-info> and <office:meta>.
        """
        return self.get_creator()

    @creator.setter
    def creator(self, creator: str) -> None:
        self.set_creator(creator)

creator property writable

creator: str | None

Get or set the element.

The element specifies the name of the person who last modified a document (), who created an annotation (), who authored a change ().

The element is usable within the following elements: , and .

get_creator

get_creator() -> str | None

Get the creator of the document.

(Also available as “self.creator” property.)

Return: str (or None if inexistant)

Example::

>>> document.meta.get_creator()
Unknown
Source code in odfdo/mixin_dc_creator.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def get_creator(self) -> str | None:
    """Get the creator of the document.

    (Also available as "self.creator" property.)

    Return: str (or None if inexistant)

    Example::

        >>> document.meta.get_creator()
        Unknown
    """
    element = self.clone.get_element("//dc:creator")
    if element is None:
        return None
    return element.text

set_creator

set_creator(creator: str) -> None

Set the creator of the document.

(Also available as “self.creator” property.)

Arguments:

creator -- str

Example::

>>> document.meta.set_creator("Plato")
Source code in odfdo/mixin_dc_creator.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def set_creator(self, creator: str) -> None:
    """Set the creator of the document.

    (Also available as "self.creator" property.)

    Arguments:

        creator -- str

    Example::

        >>> document.meta.set_creator("Plato")
    """
    element = self.get_element("//dc:creator")
    if element is None:
        element = Element.from_tag("dc:creator")
        if hasattr(self, "get_meta_body"):
            self.get_meta_body().append(element)
        else:
            self.append(element)
    element.text = creator

DcDateMixin

Date of the document, “dc:date”.

Methods:

Name Description
get_modification_date

Get the last modified date of the document.

set_modification_date

Set the last modified date of the document.

Attributes:

Name Type Description
date datetime | None

Get or set the element.

set_dc_date
Source code in odfdo/mixin_dc_date.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class DcDateMixin:
    """Date of the document, "dc:date"."""

    def get_modification_date(self) -> datetime | None:
        """Get the last modified date of the document.

        (Also available as "self.date" property.)

        Return: datetime (or None if inexistant)
        """
        element = self.clone.get_element("//dc:date")
        if element is None:
            return None
        dtdate = element.text
        return DateTime.decode(dtdate)

    def set_modification_date(self, date: datetime | None = None) -> None:
        """Set the last modified date of the document.

        If provided datetime is None, use current time.

        (Also available as "self.date" property.)

        Arguments:

            dtdate -- datetime
        """
        element = self.get_element("//dc:date")
        if element is None:
            element = Element.from_tag("dc:date")
            if hasattr(self, "get_meta_body"):
                self.get_meta_body().append(element)
            else:
                self.append(element)
        if date is None:
            date = datetime.now()
        element.text = DateTime.encode(date)

    # alias for ChangeInfo class
    set_dc_date = set_modification_date

    @property
    def date(self) -> datetime | None:
        """Get or set the <dc:date> element.

        If provided datetime is None, use current time.

        The <dc:date> element specifies the date and time when the
        document was last modified (<office:meta>), when an annotation
        was created (<office:annotation>), when a change was made
        (<office:change-info>).
        """
        return self.get_modification_date()

    @date.setter
    def date(self, date: datetime | None = None) -> None:
        self.set_modification_date(date)

date property writable

date: datetime | None

Get or set the element.

If provided datetime is None, use current time.

The element specifies the date and time when the document was last modified (), when an annotation was created (), when a change was made ().

set_dc_date class-attribute instance-attribute

set_dc_date = set_modification_date

get_modification_date

get_modification_date() -> datetime | None

Get the last modified date of the document.

(Also available as “self.date” property.)

Return: datetime (or None if inexistant)

Source code in odfdo/mixin_dc_date.py
33
34
35
36
37
38
39
40
41
42
43
44
def get_modification_date(self) -> datetime | None:
    """Get the last modified date of the document.

    (Also available as "self.date" property.)

    Return: datetime (or None if inexistant)
    """
    element = self.clone.get_element("//dc:date")
    if element is None:
        return None
    dtdate = element.text
    return DateTime.decode(dtdate)

set_modification_date

set_modification_date(date: datetime | None = None) -> None

Set the last modified date of the document.

If provided datetime is None, use current time.

(Also available as “self.date” property.)

Arguments:

dtdate -- datetime
Source code in odfdo/mixin_dc_date.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def set_modification_date(self, date: datetime | None = None) -> None:
    """Set the last modified date of the document.

    If provided datetime is None, use current time.

    (Also available as "self.date" property.)

    Arguments:

        dtdate -- datetime
    """
    element = self.get_element("//dc:date")
    if element is None:
        element = Element.from_tag("dc:date")
        if hasattr(self, "get_meta_body"):
            self.get_meta_body().append(element)
        else:
            self.append(element)
    if date is None:
        date = datetime.now()
    element.text = DateTime.encode(date)

Document

Bases: MDDocument

Abstraction of the ODF document.

Methods:

Name Description
__init__

Abstraction of the ODF document.

add_file

Insert a file from a path or a file-like object in the container.

add_page_break_style

Ensure that the document contains the style required for a manual page break.

del_part

Mark a part for deletion. The path is relative to the archive,

delete_styles

Remove all style information from content and all styles.

get_cell_background_color

Return the background color of a table cell of a .ods document,

get_cell_style_properties

Return the style properties of a table cell of a .ods document,

get_formated_meta

Return meta informations as text, with some formatting.

get_formatted_text

Return content as text, with some formatting.

get_list_style
get_parent_style
get_part

Return the bytes of the given part. The path is relative to the

get_parts

Return available part names with path inside the archive, e.g.

get_style

Return the style uniquely identified by the name/family pair. If

get_style_properties

Return the properties of the required style as a dict.

get_styled_elements

Brute-force to find paragraphs, tables, etc. using the given style

get_styles
get_table_displayed

Return the table:display property of the style of the table, ie if

get_table_style

Return the Style instance the table.

get_type

Get the ODF type (also called class) of this document.

insert_style

Insert the given style object in the document, as required by the

merge_styles_from

Copy all the styles of a document into ourself.

new

Create a Document from a template.

save

Save the document, at the same place it was opened or at the given

set_part

Set the bytes of the given part. The path is relative to the

set_table_displayed

Set the table:display property of the style of the table, ie if

show_styles
to_markdown

Attributes:

Name Type Description
body Element

Return the body element of the content part, where actual content

clone Document

Return an exact copy of the document.

container Container | None
content Content
manifest Manifest

Return the manifest part (manifest.xml) of the document.

meta Meta

Return the meta part (meta.xml) of the document, where meta data

mimetype str
parts list[str]

Return available part names with path inside the archive, e.g.

path Path | None

Shortcut to Document.Container.path.

styles Styles
Source code in odfdo/document.py
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
class Document(MDDocument):
    """Abstraction of the ODF document."""

    def __init__(
        self,
        target: str | bytes | Path | Container | io.BytesIO | None = "text",
    ) -> None:
        """Abstraction of the ODF document.

        To create a new Document, several possibilities:

            - Document() or Document("text") or Document("odt")
                -> an "empty" document of type text

            - Document("spreadsheet") or Document("ods")
                -> an "empty" document of type spreadsheet

            - Document("presentation") or Document("odp")
                -> an "empty" document of type presentation

            - Document("drawing") or Document("odg")
                -> an "empty" document of type drawing

            Meaning of “empty”: these documents are copies of the default
            templates documents provided with this library, which, as templates,
            are not really empty. It may be useful to clear the newly created
            document: document.body.clear(), or adjust meta informations like
            description or default language: document.meta.language = 'fr-FR'

        If the argument is not a known template type, or is a Path,
        Document(file) will load the content of the ODF file.

        To explicitly create a document from a custom template, use the
        Document.new(path) method whose argument is the path to the template file.
        """
        # Cache of XML parts
        self.__xmlparts: dict[str, XmlPart] = {}
        # Cache of the body
        self.__body: Element | None = None
        self.container: Container | None = None
        if isinstance(target, bytes):
            # eager conversion
            target = bytes_to_str(target)
        if target is None:
            # empty document, you probably don't wnat this.
            self.container = Container()
            return
        if isinstance(target, Path):
            # let's assume we open a container on existing file
            self.container = Container(target)
            return
        if isinstance(target, Container):
            # special internal case, use an existing container
            self.container = target
            return
        if isinstance(target, str):
            if target in ODF_TEMPLATES:
                # assuming a new document from templates
                self.container = container_from_template(target)
                return
            # let's assume we open a container on existing file
            self.container = Container(target)
            return
        if isinstance(target, io.BytesIO):
            self.container = Container(target)
            return
        raise TypeError(f"Unknown Document source type: '{target!r}'")

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} type={self.get_type()} path={self.path}>"

    def __str__(self) -> str:
        try:
            return str(self.get_formatted_text())
        except NotImplementedError:
            return str(self.body)

    @classmethod
    def new(cls, template: str | Path | io.BytesIO = "text") -> Document:
        """Create a Document from a template.

        The template argument is expected to be the path to a ODF template.

        Arguments:

            template -- str or Path or file-like (io.BytesIO)

        Return : ODF document -- Document
        """
        container = container_from_template(template)
        return cls(container)

    # Public API

    @property
    def path(self) -> Path | None:
        """Shortcut to Document.Container.path."""
        if not self.container:
            return None
        return self.container.path

    @path.setter
    def path(self, path_or_str: str | Path) -> None:
        """Shortcut to Document.Container.path

        Only accepting str or Path."""
        if not self.container:
            return
        self.container.path = Path(path_or_str)

    def get_parts(self) -> list[str]:
        """Return available part names with path inside the archive, e.g.
        ['content.xml', ..., 'Pictures/100000000000032000000258912EB1C3.jpg']
        """
        if not self.container:
            raise ValueError("Empty Container")
        return self.container.parts

    @property
    def parts(self) -> list[str]:
        """Return available part names with path inside the archive, e.g.
        ['content.xml', ..., 'Pictures/100000000000032000000258912EB1C3.jpg']
        """
        return self.get_parts()

    def get_part(self, path: str) -> XmlPart | str | bytes | None:
        """Return the bytes of the given part. The path is relative to the
        archive, e.g. "Pictures/1003200258912EB1C3.jpg".

        'content', 'meta', 'settings', 'styles' and 'manifest' are shortcuts
        to the real path, e.g. content.xml, and return a dedicated object with
        its own API.

        path formated as URI, so always use '/' separator
        """
        if not self.container:
            raise ValueError("Empty Container")
        # "./ObjectReplacements/Object 1"
        path = path.lstrip("./")
        path = _get_part_path(path)
        cls = _get_part_class(path)
        # Raw bytes
        if cls is None:
            return self.container.get_part(path)
        # XML part
        part = self.__xmlparts.get(path)
        if part is None:
            self.__xmlparts[path] = part = cls(path, self.container)
        return part

    def set_part(self, path: str, data: bytes) -> None:
        """Set the bytes of the given part. The path is relative to the
        archive, e.g. "Pictures/1003200258912EB1C3.jpg".

        path formated as URI, so always use '/' separator
        """
        if not self.container:
            raise ValueError("Empty Container")
        # "./ObjectReplacements/Object 1"
        path = path.lstrip("./")
        path = _get_part_path(path)
        cls = _get_part_class(path)
        # XML part overwritten
        if cls is not None:
            with suppress(KeyError):
                self.__xmlparts[path]
        self.container.set_part(path, data)

    def del_part(self, path: str) -> None:
        """Mark a part for deletion. The path is relative to the archive,
        e.g. "Pictures/1003200258912EB1C3.jpg"
        """
        if not self.container:
            raise ValueError("Empty Container")
        path = _get_part_path(path)
        cls = _get_part_class(path)
        if path == ODF_MANIFEST or cls is not None:
            raise ValueError(f"part '{path}' is mandatory")
        self.container.del_part(path)

    @property
    def mimetype(self) -> str:
        if not self.container:
            raise ValueError("Empty Container")
        return self.container.mimetype

    @mimetype.setter
    def mimetype(self, mimetype: str) -> None:
        if not self.container:
            raise ValueError("Empty Container")
        self.container.mimetype = mimetype

    def get_type(self) -> str:
        """Get the ODF type (also called class) of this document.

        Return: 'chart', 'database', 'formula', 'graphics',
            'graphics-template', 'image', 'presentation',
            'presentation-template', 'spreadsheet', 'spreadsheet-template',
            'text', 'text-master', 'text-template' or 'text-web'
        """
        # The mimetype must be with the form:
        # application/vnd.oasis.opendocument.text

        # Isolate and return the last part
        return self.mimetype.rsplit(".", 1)[-1]

    @property
    def body(self) -> Element:
        """Return the body element of the content part, where actual content
        is stored.
        """
        if self.__body is None:
            self.__body = self.content.body
        return self.__body

    @property
    def meta(self) -> Meta:
        """Return the meta part (meta.xml) of the document, where meta data
        are stored."""
        metadata = self.get_part(ODF_META)
        if metadata is None or not isinstance(metadata, Meta):
            raise ValueError("Empty Meta")
        return metadata

    @property
    def manifest(self) -> Manifest:
        """Return the manifest part (manifest.xml) of the document."""
        manifest = self.get_part(ODF_MANIFEST)
        if manifest is None or not isinstance(manifest, Manifest):
            raise ValueError("Empty Manifest")
        return manifest

    def _get_formatted_text_footnotes(
        self,
        result: list[str],
        context: dict,
        rst_mode: bool,
    ) -> None:
        # Separate text from notes
        if rst_mode:
            result.append("\n")
        else:
            result.append("----\n")
        for citation, body in context["footnotes"]:
            if rst_mode:
                result.append(f".. [#] {body}\n")
            else:
                result.append(f"[{citation}] {body}\n")
        # Append a \n after the notes
        result.append("\n")
        # Reset for the next paragraph
        context["footnotes"] = []

    def _get_formatted_text_annotations(
        self,
        result: list[str],
        context: dict,
        rst_mode: bool,
    ) -> None:
        # Insert the annotations
        # With a separation
        if rst_mode:
            result.append("\n")
        else:
            result.append("----\n")
        for annotation in context["annotations"]:
            if rst_mode:
                result.append(f".. [#] {annotation}\n")
            else:
                result.append(f"[*] {annotation}\n")
        context["annotations"] = []

    def _get_formatted_text_images(
        self,
        result: list[str],
        context: dict,
        rst_mode: bool,
    ) -> None:
        # Insert the images ref, only in rst mode
        result.append("\n")
        for ref, filename, (width, height) in context["images"]:
            result.append(f".. {ref} image:: {filename}\n")
            if width is not None:
                result.append(f"   :width: {width}\n")
            if height is not None:
                result.append(f"   :height: {height}\n")
        context["images"] = []

    def _get_formatted_text_endnotes(
        self,
        result: list[str],
        context: dict,
        rst_mode: bool,
    ) -> None:
        # Append the end notes
        if rst_mode:
            result.append("\n\n")
        else:
            result.append("\n========\n")
        for citation, body in context["endnotes"]:
            if rst_mode:
                result.append(f".. [*] {body}\n")
            else:
                result.append(f"({citation}) {body}\n")

    def get_formatted_text(self, rst_mode: bool = False) -> str:
        """Return content as text, with some formatting."""
        # For the moment, only "type='text'"
        doc_type = self.get_type()
        if doc_type == "spreadsheet":
            return self._tables_csv()
        if doc_type in {
            "text",
            "text-template",
            "presentation",
            "presentation-template",
        }:
            return self._formatted_text(rst_mode)
        raise NotImplementedError(f"Type of document '{doc_type}' not supported yet")

    def _tables_csv(self) -> str:
        return "\n\n".join(str(table) for table in self.body.tables)

    def _formatted_text(self, rst_mode: bool) -> str:
        # Initialize an empty context
        context = {
            "document": self,
            "footnotes": [],
            "endnotes": [],
            "annotations": [],
            "rst_mode": rst_mode,
            "img_counter": 0,
            "images": [],
            "no_img_level": 0,
        }
        body = self.body
        # Get the text
        result = []
        for child in body.children:
            # self._get_formatted_text_child(result, element, context, rst_mode)
            # if child.tag == "table:table":
            #     result.append(child.get_formatted_text(context))
            #     return
            result.append(child.get_formatted_text(context))
            if context["footnotes"]:
                self._get_formatted_text_footnotes(result, context, rst_mode)
            if context["annotations"]:
                self._get_formatted_text_annotations(result, context, rst_mode)
            # Insert the images ref, only in rst mode
            if context["images"]:
                self._get_formatted_text_images(result, context, rst_mode)
        if context["endnotes"]:
            self._get_formatted_text_endnotes(result, context, rst_mode)
        return "".join(result)

    def get_formated_meta(self) -> str:
        """Return meta informations as text, with some formatting.

        (Redirection to new implementation for compatibility.)"""
        return self.meta.as_text()

    def to_markdown(self) -> str:
        doc_type = self.get_type()
        if doc_type not in {
            "text",
        }:
            raise NotImplementedError(
                f"Type of document '{doc_type}' not supported yet"
            )
        return self._markdown_export()

    def _add_binary_part(self, blob: Blob) -> str:
        if not self.container:
            raise ValueError("Empty Container")
        manifest = self.manifest
        if manifest.get_media_type("Pictures/") is None:
            manifest.add_full_path("Pictures/")
        path = posixpath.join("Pictures", blob.name)
        self.container.set_part(path, blob.content)
        manifest.add_full_path(path, blob.mime_type)
        return path

    def add_file(self, path_or_file: str | Path | BinaryIO) -> str:
        """Insert a file from a path or a file-like object in the container.

        Return the full path to reference in the content. The internal name
        of the file in the Picture/ folder is gnerated by a hash function.

        Arguments:

            path_or_file -- str or Path or file-like

        Return: str (URI)
        """
        if not self.container:
            raise ValueError("Empty Container")
        if isinstance(path_or_file, (str, Path)):
            blob = Blob.from_path(path_or_file)
        else:
            blob = Blob.from_io(path_or_file)
        return self._add_binary_part(blob)

    @property
    def clone(self) -> Document:
        """Return an exact copy of the document.

        Return: Document
        """
        clone = object.__new__(self.__class__)
        for name in self.__dict__:
            if name == "_Document__body":
                setattr(clone, name, None)
            elif name == "_Document__xmlparts":
                setattr(clone, name, {})
            elif name == "container":
                if not self.container:
                    raise ValueError("Empty Container")
                setattr(clone, name, self.container.clone)
            else:
                value = deepcopy(getattr(self, name))
                setattr(clone, name, value)
        return clone

    def _check_manifest_rdf(self) -> None:
        manifest = self.manifest
        parts = self.container.parts
        if manifest.get_media_type(ODF_MANIFEST_RDF):
            if ODF_MANIFEST_RDF not in parts:
                self.container.set_part(
                    ODF_MANIFEST_RDF, self.container.default_manifest_rdf.encode("utf8")
                )
        else:
            if ODF_MANIFEST_RDF in parts:
                self.container.del_part(ODF_MANIFEST_RDF)

    def save(
        self,
        target: str | Path | io.BytesIO | None = None,
        packaging: str = ZIP,
        pretty: bool | None = None,
        backup: bool = False,
    ) -> None:
        """Save the document, at the same place it was opened or at the given
        target path. Target can also be a file-like object. It can be saved
        as a Zip file (default), flat XML format or as files in a folder
        (for debugging purpose). XML parts can be pretty printed (the default
        for 'folder' and 'xml' packaging).

        Note: 'xml' packaging is an experimental work in progress.

        Arguments:

            target -- str or file-like object

            packaging -- 'zip', 'folder', 'xml'

            pretty -- bool | None

            backup -- bool
        """
        if not self.container:
            raise ValueError("Empty Container")
        if packaging not in PACKAGING:
            raise ValueError(f'Packaging of type "{packaging}" is not supported')
        # Some advertising
        self.meta.set_generator_default()
        # Synchronize data with container
        container = self.container
        if pretty is None:
            pretty = packaging in {"folder", "xml"}
        pretty = bool(pretty)
        backup = bool(backup)
        self._check_manifest_rdf()
        if pretty and packaging != XML:
            for path, part in self.__xmlparts.items():
                if part is not None:
                    container.set_part(path, part.pretty_serialize())
            for path in (ODF_CONTENT, ODF_META, ODF_SETTINGS, ODF_STYLES):
                if path in self.__xmlparts:
                    continue
                cls = _get_part_class(path)
                # XML part
                self.__xmlparts[path] = part = cls(path, container)
                container.set_part(path, part.pretty_serialize())
        else:
            for path, part in self.__xmlparts.items():
                if part is not None:
                    container.set_part(path, part.serialize())
        container.save(target, packaging=packaging, backup=backup, pretty=pretty)

    @property
    def content(self) -> Content:
        content: Content | None = self.get_part(ODF_CONTENT)  # type:ignore
        if content is None:
            raise ValueError("Empty Content")
        return content

    @property
    def styles(self) -> Styles:
        styles: Styles | None = self.get_part(ODF_STYLES)  # type:ignore
        if styles is None:
            raise ValueError("Empty Styles")
        return styles

    # Styles over several parts

    def get_styles(
        self,
        family: str | bytes = "",
        automatic: bool = False,
    ) -> list[Style | Element]:
        # compatibility with old versions:

        if isinstance(family, bytes):
            family = bytes_to_str(family)
        return self.content.get_styles(family=family) + self.styles.get_styles(
            family=family, automatic=automatic
        )

    def get_style(
        self,
        family: str,
        name_or_element: str | Style | None = None,
        display_name: str | None = None,
    ) -> Style | None:
        """Return the style uniquely identified by the name/family pair. If
        the argument is already a style object, it will return it.

        If the name is None, the default style is fetched.

        If the name is not the internal name but the name you gave in a
        desktop application, use display_name instead.

        Arguments:

            family -- 'paragraph', 'text',  'graphic', 'table', 'list',
                      'number', 'page-layout', 'master-page', ...

            name -- str or Element or None

            display_name -- str

        Return: Style or None if not found.
        """
        # 1. content.xml
        element = self.content.get_style(
            family, name_or_element=name_or_element, display_name=display_name
        )
        if element is not None:
            return element
        # 2. styles.xml
        return self.styles.get_style(
            family,
            name_or_element=name_or_element,
            display_name=display_name,
        )

    def get_parent_style(self, style: Style) -> Style | None:
        family = style.family
        parent_style_name = style.parent_style
        if not parent_style_name:
            return None
        return self.get_style(family, parent_style_name)

    def get_list_style(self, style: Style) -> Style | None:
        list_style_name = style.list_style_name
        if not list_style_name:
            return None
        return self.get_style("list", list_style_name)

    @staticmethod
    def _pseudo_style_attribute(style_element: Style | Element, attribute: str) -> Any:
        if hasattr(style_element, attribute):
            return getattr(style_element, attribute)
        return ""

    def _set_automatic_name(self, style: Style, family: str) -> None:
        """Generate a name for the new automatic style."""
        if not hasattr(style, "name"):
            # do nothing
            return
        styles = self.get_styles(family=family, automatic=True)
        max_index = 0
        for existing_style in styles:
            if not hasattr(existing_style, "name"):
                continue
            if not existing_style.name.startswith(AUTOMATIC_PREFIX):
                continue
            try:
                index = int(existing_style.name[len(AUTOMATIC_PREFIX) :])
            except ValueError:
                continue
            max_index = max(max_index, index)

        style.name = f"{AUTOMATIC_PREFIX}{max_index + 1}"

    def _insert_style_get_common_styles(
        self,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.styles.get_element("office:styles")
        existing = self.styles.get_style(family, name)
        return existing, style_container

    def _insert_style_get_automatic_styles(
        self,
        style: Style,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.content.get_element("office:automatic-styles")
        # A name ?
        if name:
            if hasattr(style, "name"):
                style.name = name
            existing = self.content.get_style(family, name)
        else:
            self._set_automatic_name(style, family)
            existing = None
        return existing, style_container

    def _insert_style_get_default_styles(
        self,
        style: Style,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.styles.get_element("office:styles")
        style.tag = "style:default-style"
        if name:
            style.del_attribute("style:name")
        existing = self.styles.get_style(family)
        return existing, style_container

    def _insert_style_get_master_page(
        self,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.styles.get_element("office:master-styles")
        existing = self.styles.get_style(family, name)
        return existing, style_container

    def _insert_style_get_font_face_default(
        self,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.styles.get_element("office:font-face-decls")
        existing = self.styles.get_style(family, name)
        return existing, style_container

    def _insert_style_get_font_face(
        self,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        style_container = self.content.get_element("office:font-face-decls")
        existing = self.content.get_style(family, name)
        return existing, style_container

    def _insert_style_get_page_layout(
        self,
        family: str,
        name: str,
    ) -> tuple[Any, Any]:
        # force to automatic
        style_container = self.styles.get_element("office:automatic-styles")
        existing = self.styles.get_style(family, name)
        return existing, style_container

    def _insert_style_get_draw_fill_image(
        self,
        name: str,
    ) -> tuple[Any, Any]:
        # special case for 'draw:fill-image' pseudo style
        # not family and style_element.__class__.__name__ == "DrawFillImage"
        style_container = self.styles.get_element("office:styles")
        existing = self.styles.get_style("", name)
        return existing, style_container

    def _insert_style_standard(
        self,
        style: Style,
        name: str,
        family: str,
        automatic: bool,
        default: bool,
    ) -> tuple[Any, Any]:
        # Common style
        if name and automatic is False and default is False:
            return self._insert_style_get_common_styles(family, name)
        # Automatic style
        elif automatic is True and default is False:
            return self._insert_style_get_automatic_styles(style, family, name)
        # Default style
        elif automatic is False and default is True:
            return self._insert_style_get_default_styles(style, family, name)
        else:
            raise AttributeError("Invalid combination of arguments")

    def insert_style(
        self,
        style: Style | str,
        name: str = "",
        automatic: bool = False,
        default: bool = False,
    ) -> Any:
        """Insert the given style object in the document, as required by the
        style family and type.

        The style is expected to be a common style with a name. In case it
        was created with no name, the given can be set on the fly.

        If automatic is True, the style will be inserted as an automatic
        style.

        If default is True, the style will be inserted as a default style and
        would replace any existing default style of the same family. Any name
        or display name would be ignored.

        Automatic and default arguments are mutually exclusive.

        All styles can't be used as default styles. Default styles are
        allowed for the following families: paragraph, text, section, table,
        table-column, table-row, table-cell, table-page, chart, drawing-page,
        graphic, presentation, control and ruby.

        Arguments:

            style -- Style or str

            name -- str

            automatic -- bool

            default -- bool

        Return : style name -- str
        """

        # if style is a str, assume it is the Style definition
        if isinstance(style, str):
            style_element: Style = Element.from_tag(style)  # type: ignore
        else:
            style_element = style
        if not isinstance(style_element, Element):
            raise TypeError(f"Unknown Style type: '{style!r}'")

        # Get family and name
        family = style_element.family
        if not name:
            name = self._pseudo_style_attribute(style_element, "name")

        # Master page style
        if family == "master-page":
            existing, style_container = self._insert_style_get_master_page(family, name)
        # Font face declarations
        elif family == "font-face":
            if default:
                existing, style_container = self._insert_style_get_font_face_default(
                    family, name
                )
            else:
                existing, style_container = self._insert_style_get_font_face(
                    family, name
                )
        # page layout style
        elif family == "page-layout":
            existing, style_container = self._insert_style_get_page_layout(family, name)
        # Common style
        elif family in FAMILY_MAPPING:
            existing, style_container = self._insert_style_standard(
                style_element, name, family, automatic, default
            )
        elif not family and style_element.__class__.__name__ == "DrawFillImage":
            # special case for 'draw:fill-image' pseudo style
            existing, style_container = self._insert_style_get_draw_fill_image(name)
        # Invalid style
        else:
            raise ValueError(
                "Invalid style: "
                f"{style_element}, tag:{style_element.tag}, family:{family}"
            )

        # Insert it!
        if existing is not None:
            style_container.delete(existing)
        style_container.append(style_element)
        return self._pseudo_style_attribute(style_element, "name")

    def get_styled_elements(self, name: str = "") -> list[Element]:
        """Brute-force to find paragraphs, tables, etc. using the given style
        name (or all by default).

        Arguments:

            name -- str

        Return: list
        """
        # Header, footer, etc. have styles too
        return self.content.root.get_styled_elements(
            name
        ) + self.styles.root.get_styled_elements(name)

    def show_styles(
        self,
        automatic: bool = True,
        common: bool = True,
        properties: bool = False,
    ) -> str:
        infos = []
        for style in self.get_styles():
            try:
                name = style.name  # type: ignore
            except AttributeError:
                print("--------------")
                print(style.__class__)
                print(style.serialize())
                raise
            if style.__class__.__name__ == "DrawFillImage":
                family = ""
            else:
                family = str(style.family)  # type: ignore
            parent = style.parent
            is_auto = parent and parent.tag == "office:automatic-styles"
            if (is_auto and automatic is False) or (not is_auto and common is False):
                continue
            is_used = bool(self.get_styled_elements(name))
            infos.append(
                {
                    "type": "auto  " if is_auto else "common",
                    "used": "y" if is_used else "n",
                    "family": family,
                    "parent": self._pseudo_style_attribute(style, "parent_style") or "",
                    "name": name or "",
                    "display_name": self._pseudo_style_attribute(style, "display_name")
                    or "",
                    "properties": style.get_properties() if properties else None,  # type: ignore
                }
            )
        if not infos:
            return ""
        # Sort by family and name
        infos.sort(key=itemgetter("family", "name"))
        # Show common and used first
        infos.sort(key=itemgetter("type", "used"), reverse=True)
        max_family = str(max(len(x["family"]) for x in infos))  # type: ignore
        max_parent = str(max(len(x["parent"]) for x in infos))  # type: ignore
        formater = (
            "%(type)s used:%(used)s family:%(family)-0"
            + max_family
            + "s parent:%(parent)-0"
            + max_parent
            + "s name:%(name)s"
        )
        output = []
        for info in infos:
            line = formater % info
            if info["display_name"]:
                line += " display_name:" + info["display_name"]  # type: ignore
            output.append(line)
            if info["properties"]:
                for name, value in info["properties"].items():  # type: ignore
                    output.append(f"   - {name}: {value}")
        output.append("")
        return "\n".join(output)

    def delete_styles(self) -> int:
        """Remove all style information from content and all styles.

        Return: number of deleted styles
        """
        # First remove references to styles
        for element in self.get_styled_elements():
            for attribute in (
                "text:style-name",
                "draw:style-name",
                "draw:text-style-name",
                "table:style-name",
                "style:page-layout-name",
            ):
                try:
                    element.del_attribute(attribute)
                except KeyError:
                    continue
        # Then remove supposedly orphaned styles
        deleted = 0
        for style in self.get_styles():
            if style.name is None:  # type: ignore
                # Don't delete default styles
                continue
            # elif type(style) is odf_master_page:
            #    # Don't suppress header and footer, just styling was removed
            #    continue
            style.delete()
            deleted += 1
        return deleted

    def merge_styles_from(self, document: Document) -> None:
        """Copy all the styles of a document into ourself.

        Styles with the same type and name will be replaced, so only unique
        styles will be preserved.
        """
        manifest = self.manifest
        document_manifest = document.manifest
        for style in document.get_styles():
            tagname = style.tag
            family = style.family
            stylename = style.name  # type: ignore
            container = style.parent
            container_name = container.tag  # type: ignore
            partname = container.parent.tag  # type: ignore
            # The destination part
            if partname == "office:document-styles":
                part: Content | Styles = self.styles
            elif partname == "office:document-content":
                part = self.content
            else:
                raise NotImplementedError(partname)
            # Implemented containers
            if container_name not in {
                "office:styles",
                "office:automatic-styles",
                "office:master-styles",
                "office:font-face-decls",
            }:
                raise NotImplementedError(container_name)
            dest = part.get_element(f"//{container_name}")
            # Implemented style types
            # if tagname not in registered_styles:
            #    raise NotImplementedError(tagname)
            duplicate = part.get_style(family, stylename)
            if duplicate is not None:
                duplicate.delete()
            dest.append(style)
            # Copy images from the header/footer
            if tagname == "style:master-page":
                query = "descendant::draw:image"
                for image in style.get_elements(query):
                    url = image.url  # type: ignore
                    part_url = document.get_part(url)
                    # Manually add the part to keep the name
                    self.set_part(url, part_url)  # type: ignore
                    media_type = document_manifest.get_media_type(url)
                    manifest.add_full_path(url, media_type)  # type: ignore
            # Copy images from the fill-image
            elif tagname == "draw:fill-image":
                url = style.url  # type: ignore
                part_url = document.get_part(url)
                self.set_part(url, part_url)  # type: ignore
                media_type = document_manifest.get_media_type(url)
                manifest.add_full_path(url, media_type)  # type: ignore

    def add_page_break_style(self) -> None:
        """Ensure that the document contains the style required for a manual page break.

        Then a manual page break can be added to the document with:
            from paragraph import PageBreak
            ...
            document.body.append(PageBreak())

        Note: this style uses the property 'fo:break-after', another
        possibility could be the property 'fo:break-before'
        """
        if existing := self.get_style(  # noqa: SIM102
            family="paragraph",
            name_or_element="odfdopagebreak",
        ):
            if properties := existing.get_properties():  # noqa: SIM102
                if properties["fo:break-after"] == "page":
                    return
        style = (
            '<style:style style:family="paragraph" style:parent-style-name="Standard" '
            'style:name="odfdopagebreak">'
            '<style:paragraph-properties fo:break-after="page"/></style:style>'
        )
        self.insert_style(style, automatic=False)

    def get_style_properties(
        self, family: str, name: str, area: str | None = None
    ) -> dict[str, str] | None:
        """Return the properties of the required style as a dict."""
        style = self.get_style(family, name)
        if style is None:
            return None
        return style.get_properties(area=area)  # type: ignore

    def _get_table(self, table: int | str) -> Table | None:
        if not isinstance(table, (int, str)):
            raise TypeError(f"Table parameter must be int or str: {table!r}")
        if isinstance(table, int):
            return self.body.get_table(position=table)  # type: ignore
        return self.body.get_table(name=table)  # type: ignore

    def get_cell_style_properties(
        self, table: str | int, coord: tuple | list | str
    ) -> dict[str, str]:
        """Return the style properties of a table cell of a .ods document,
        from the cell style or from the row style."""

        if not (sheet := self._get_table(table)):
            return {}
        cell = sheet.get_cell(coord, clone=False)
        if cell.style:
            return (
                self.get_style_properties("table-cell", cell.style, "table-cell") or {}
            )
        try:
            row = sheet.get_row(cell.y, clone=False, create=False)  # type: ignore
            if row.style:  # noqa: SIM102
                if props := self.get_style_properties(
                    "table-row", row.style, "table-cell"
                ):
                    return props
            column = sheet.get_column(cell.x)  # type: ignore
            style = column.default_cell_style
            if style:  # noqa: SIM102
                if props := self.get_style_properties(
                    "table-cell", style, "table-cell"
                ):
                    return props
        except ValueError:
            pass
        return {}

    def get_cell_background_color(
        self,
        table: str | int,
        coord: tuple | list | str,
        default: str = "#ffffff",
    ) -> str:
        """Return the background color of a table cell of a .ods document,
        from the cell style, or from the row or column.

        If color is not defined, return default value."""
        found = self.get_cell_style_properties(table, coord).get("fo:background-color")
        return found or default

    def get_table_style(
        self,
        table: str | int,
    ) -> Style | None:
        """Return the Style instance the table.

        Arguments:

            table -- name or index of the table
        """
        if not (sheet := self._get_table(table)):
            return None
        return self.get_style("table", sheet.style)

    def get_table_displayed(self, table: str | int) -> bool:
        """Return the table:display property of the style of the table, ie if
        the table should be displayed in a graphical interface.

        Note: that method replaces the broken Table.displayd() method from previous
        odfdo versions.

        Arguments:

            table -- name or index of the table
        """
        style = self.get_table_style(table)
        if not style:
            # should not happen, but assume that a table without style is
            # displayed by default
            return True
        properties = style.get_properties() or {}
        property_str = str(properties.get("table:display", "true"))
        return Boolean.decode(property_str)

    def _unique_style_name(self, base: str) -> str:
        current = {style.name for style in self.get_styles()}
        idx = 0
        while True:
            name = f"{base}_{idx}"
            if name in current:
                idx += 1
                continue
            return name

    def set_table_displayed(self, table: str | int, displayed: bool) -> None:
        """Set the table:display property of the style of the table, ie if
        the table should be displayed in a graphical interface.

        Note: that method replaces the broken Table.displayd() method from previous
        odfdo versions.

        Arguments:

            table -- name or index of the table

            displayed -- boolean flag
        """
        orig_style = self.get_table_style(table)
        if not orig_style:
            name = self._unique_style_name("ta")
            orig_style = Element.from_tag(
                f'<style:style style:name="{name}" style:family="table" '
                'style:master-page-name="Default">'
                '<style:table-properties table:display="true" '
                'style:writing-mode="lr-tb"/></style:style>'
            )
            self.insert_style(orig_style, automatic=True)  # type:ignore
        new_style = orig_style.clone
        new_name = self._unique_style_name("ta")
        new_style.name = new_name  # type:ignore
        self.insert_style(new_style, automatic=True)  # type:ignore
        sheet = self._get_table(table)
        sheet.style = new_name  # type: ignore
        properties = {"table:display": Boolean.encode(displayed)}
        new_style.set_properties(properties)  # type: ignore

body property

body: Element

Return the body element of the content part, where actual content is stored.

clone property

clone: Document

Return an exact copy of the document.

Return: Document

container instance-attribute

container: Container | None = None

content property

content: Content

manifest property

manifest: Manifest

Return the manifest part (manifest.xml) of the document.

meta property

meta: Meta

Return the meta part (meta.xml) of the document, where meta data are stored.

mimetype property writable

mimetype: str

parts property

parts: list[str]

Return available part names with path inside the archive, e.g. [‘content.xml’, …, ‘Pictures/100000000000032000000258912EB1C3.jpg’]

path property writable

path: Path | None

Shortcut to Document.Container.path.

styles property

styles: Styles

__init__

__init__(
    target: str
    | bytes
    | Path
    | Container
    | BytesIO
    | None = "text",
) -> None

Abstraction of the ODF document.

To create a new Document, several possibilities:

- Document() or Document("text") or Document("odt")
    -> an "empty" document of type text

- Document("spreadsheet") or Document("ods")
    -> an "empty" document of type spreadsheet

- Document("presentation") or Document("odp")
    -> an "empty" document of type presentation

- Document("drawing") or Document("odg")
    -> an "empty" document of type drawing

Meaning of “empty”: these documents are copies of the default
templates documents provided with this library, which, as templates,
are not really empty. It may be useful to clear the newly created
document: document.body.clear(), or adjust meta informations like
description or default language: document.meta.language = 'fr-FR'

If the argument is not a known template type, or is a Path, Document(file) will load the content of the ODF file.

To explicitly create a document from a custom template, use the Document.new(path) method whose argument is the path to the template file.

Source code in odfdo/document.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def __init__(
    self,
    target: str | bytes | Path | Container | io.BytesIO | None = "text",
) -> None:
    """Abstraction of the ODF document.

    To create a new Document, several possibilities:

        - Document() or Document("text") or Document("odt")
            -> an "empty" document of type text

        - Document("spreadsheet") or Document("ods")
            -> an "empty" document of type spreadsheet

        - Document("presentation") or Document("odp")
            -> an "empty" document of type presentation

        - Document("drawing") or Document("odg")
            -> an "empty" document of type drawing

        Meaning of “empty”: these documents are copies of the default
        templates documents provided with this library, which, as templates,
        are not really empty. It may be useful to clear the newly created
        document: document.body.clear(), or adjust meta informations like
        description or default language: document.meta.language = 'fr-FR'

    If the argument is not a known template type, or is a Path,
    Document(file) will load the content of the ODF file.

    To explicitly create a document from a custom template, use the
    Document.new(path) method whose argument is the path to the template file.
    """
    # Cache of XML parts
    self.__xmlparts: dict[str, XmlPart] = {}
    # Cache of the body
    self.__body: Element | None = None
    self.container: Container | None = None
    if isinstance(target, bytes):
        # eager conversion
        target = bytes_to_str(target)
    if target is None:
        # empty document, you probably don't wnat this.
        self.container = Container()
        return
    if isinstance(target, Path):
        # let's assume we open a container on existing file
        self.container = Container(target)
        return
    if isinstance(target, Container):
        # special internal case, use an existing container
        self.container = target
        return
    if isinstance(target, str):
        if target in ODF_TEMPLATES:
            # assuming a new document from templates
            self.container = container_from_template(target)
            return
        # let's assume we open a container on existing file
        self.container = Container(target)
        return
    if isinstance(target, io.BytesIO):
        self.container = Container(target)
        return
    raise TypeError(f"Unknown Document source type: '{target!r}'")

add_file

add_file(path_or_file: str | Path | BinaryIO) -> str

Insert a file from a path or a file-like object in the container.

Return the full path to reference in the content. The internal name of the file in the Picture/ folder is gnerated by a hash function.

Arguments:

path_or_file -- str or Path or file-like

Return: str (URI)

Source code in odfdo/document.py
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
def add_file(self, path_or_file: str | Path | BinaryIO) -> str:
    """Insert a file from a path or a file-like object in the container.

    Return the full path to reference in the content. The internal name
    of the file in the Picture/ folder is gnerated by a hash function.

    Arguments:

        path_or_file -- str or Path or file-like

    Return: str (URI)
    """
    if not self.container:
        raise ValueError("Empty Container")
    if isinstance(path_or_file, (str, Path)):
        blob = Blob.from_path(path_or_file)
    else:
        blob = Blob.from_io(path_or_file)
    return self._add_binary_part(blob)

add_page_break_style

add_page_break_style() -> None

Ensure that the document contains the style required for a manual page break.

Then a manual page break can be added to the document with

from paragraph import PageBreak … document.body.append(PageBreak())

Note: this style uses the property ‘fo:break-after’, another possibility could be the property ‘fo:break-before’

Source code in odfdo/document.py
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
def add_page_break_style(self) -> None:
    """Ensure that the document contains the style required for a manual page break.

    Then a manual page break can be added to the document with:
        from paragraph import PageBreak
        ...
        document.body.append(PageBreak())

    Note: this style uses the property 'fo:break-after', another
    possibility could be the property 'fo:break-before'
    """
    if existing := self.get_style(  # noqa: SIM102
        family="paragraph",
        name_or_element="odfdopagebreak",
    ):
        if properties := existing.get_properties():  # noqa: SIM102
            if properties["fo:break-after"] == "page":
                return
    style = (
        '<style:style style:family="paragraph" style:parent-style-name="Standard" '
        'style:name="odfdopagebreak">'
        '<style:paragraph-properties fo:break-after="page"/></style:style>'
    )
    self.insert_style(style, automatic=False)

del_part

del_part(path: str) -> None

Mark a part for deletion. The path is relative to the archive, e.g. “Pictures/1003200258912EB1C3.jpg”

Source code in odfdo/document.py
373
374
375
376
377
378
379
380
381
382
383
def del_part(self, path: str) -> None:
    """Mark a part for deletion. The path is relative to the archive,
    e.g. "Pictures/1003200258912EB1C3.jpg"
    """
    if not self.container:
        raise ValueError("Empty Container")
    path = _get_part_path(path)
    cls = _get_part_class(path)
    if path == ODF_MANIFEST or cls is not None:
        raise ValueError(f"part '{path}' is mandatory")
    self.container.del_part(path)

delete_styles

delete_styles() -> int

Remove all style information from content and all styles.

Return: number of deleted styles

Source code in odfdo/document.py
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
def delete_styles(self) -> int:
    """Remove all style information from content and all styles.

    Return: number of deleted styles
    """
    # First remove references to styles
    for element in self.get_styled_elements():
        for attribute in (
            "text:style-name",
            "draw:style-name",
            "draw:text-style-name",
            "table:style-name",
            "style:page-layout-name",
        ):
            try:
                element.del_attribute(attribute)
            except KeyError:
                continue
    # Then remove supposedly orphaned styles
    deleted = 0
    for style in self.get_styles():
        if style.name is None:  # type: ignore
            # Don't delete default styles
            continue
        # elif type(style) is odf_master_page:
        #    # Don't suppress header and footer, just styling was removed
        #    continue
        style.delete()
        deleted += 1
    return deleted

get_cell_background_color

get_cell_background_color(
    table: str | int,
    coord: tuple | list | str,
    default: str = "#ffffff",
) -> str

Return the background color of a table cell of a .ods document, from the cell style, or from the row or column.

If color is not defined, return default value.

Source code in odfdo/document.py
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
def get_cell_background_color(
    self,
    table: str | int,
    coord: tuple | list | str,
    default: str = "#ffffff",
) -> str:
    """Return the background color of a table cell of a .ods document,
    from the cell style, or from the row or column.

    If color is not defined, return default value."""
    found = self.get_cell_style_properties(table, coord).get("fo:background-color")
    return found or default

get_cell_style_properties

get_cell_style_properties(
    table: str | int, coord: tuple | list | str
) -> dict[str, str]

Return the style properties of a table cell of a .ods document, from the cell style or from the row style.

Source code in odfdo/document.py
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
def get_cell_style_properties(
    self, table: str | int, coord: tuple | list | str
) -> dict[str, str]:
    """Return the style properties of a table cell of a .ods document,
    from the cell style or from the row style."""

    if not (sheet := self._get_table(table)):
        return {}
    cell = sheet.get_cell(coord, clone=False)
    if cell.style:
        return (
            self.get_style_properties("table-cell", cell.style, "table-cell") or {}
        )
    try:
        row = sheet.get_row(cell.y, clone=False, create=False)  # type: ignore
        if row.style:  # noqa: SIM102
            if props := self.get_style_properties(
                "table-row", row.style, "table-cell"
            ):
                return props
        column = sheet.get_column(cell.x)  # type: ignore
        style = column.default_cell_style
        if style:  # noqa: SIM102
            if props := self.get_style_properties(
                "table-cell", style, "table-cell"
            ):
                return props
    except ValueError:
        pass
    return {}

get_formated_meta

get_formated_meta() -> str

Return meta informations as text, with some formatting.

(Redirection to new implementation for compatibility.)

Source code in odfdo/document.py
560
561
562
563
564
def get_formated_meta(self) -> str:
    """Return meta informations as text, with some formatting.

    (Redirection to new implementation for compatibility.)"""
    return self.meta.as_text()

get_formatted_text

get_formatted_text(rst_mode: bool = False) -> str

Return content as text, with some formatting.

Source code in odfdo/document.py
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def get_formatted_text(self, rst_mode: bool = False) -> str:
    """Return content as text, with some formatting."""
    # For the moment, only "type='text'"
    doc_type = self.get_type()
    if doc_type == "spreadsheet":
        return self._tables_csv()
    if doc_type in {
        "text",
        "text-template",
        "presentation",
        "presentation-template",
    }:
        return self._formatted_text(rst_mode)
    raise NotImplementedError(f"Type of document '{doc_type}' not supported yet")

get_list_style

get_list_style(style: Style) -> Style | None
Source code in odfdo/document.py
769
770
771
772
773
def get_list_style(self, style: Style) -> Style | None:
    list_style_name = style.list_style_name
    if not list_style_name:
        return None
    return self.get_style("list", list_style_name)

get_parent_style

get_parent_style(style: Style) -> Style | None
Source code in odfdo/document.py
762
763
764
765
766
767
def get_parent_style(self, style: Style) -> Style | None:
    family = style.family
    parent_style_name = style.parent_style
    if not parent_style_name:
        return None
    return self.get_style(family, parent_style_name)

get_part

get_part(path: str) -> XmlPart | str | bytes | None

Return the bytes of the given part. The path is relative to the archive, e.g. “Pictures/1003200258912EB1C3.jpg”.

‘content’, ‘meta’, ‘settings’, ‘styles’ and ‘manifest’ are shortcuts to the real path, e.g. content.xml, and return a dedicated object with its own API.

path formated as URI, so always use ‘/’ separator

Source code in odfdo/document.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def get_part(self, path: str) -> XmlPart | str | bytes | None:
    """Return the bytes of the given part. The path is relative to the
    archive, e.g. "Pictures/1003200258912EB1C3.jpg".

    'content', 'meta', 'settings', 'styles' and 'manifest' are shortcuts
    to the real path, e.g. content.xml, and return a dedicated object with
    its own API.

    path formated as URI, so always use '/' separator
    """
    if not self.container:
        raise ValueError("Empty Container")
    # "./ObjectReplacements/Object 1"
    path = path.lstrip("./")
    path = _get_part_path(path)
    cls = _get_part_class(path)
    # Raw bytes
    if cls is None:
        return self.container.get_part(path)
    # XML part
    part = self.__xmlparts.get(path)
    if part is None:
        self.__xmlparts[path] = part = cls(path, self.container)
    return part

get_parts

get_parts() -> list[str]

Return available part names with path inside the archive, e.g. [‘content.xml’, …, ‘Pictures/100000000000032000000258912EB1C3.jpg’]

Source code in odfdo/document.py
315
316
317
318
319
320
321
def get_parts(self) -> list[str]:
    """Return available part names with path inside the archive, e.g.
    ['content.xml', ..., 'Pictures/100000000000032000000258912EB1C3.jpg']
    """
    if not self.container:
        raise ValueError("Empty Container")
    return self.container.parts

get_style

get_style(
    family: str,
    name_or_element: str | Style | None = None,
    display_name: str | None = None,
) -> Style | None

Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it.

If the name is None, the default style is fetched.

If the name is not the internal name but the name you gave in a desktop application, use display_name instead.

Arguments:

family -- 'paragraph', 'text',  'graphic', 'table', 'list',
          'number', 'page-layout', 'master-page', ...

name -- str or Element or None

display_name -- str

Return: Style or None if not found.

Source code in odfdo/document.py
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
def get_style(
    self,
    family: str,
    name_or_element: str | Style | None = None,
    display_name: str | None = None,
) -> Style | None:
    """Return the style uniquely identified by the name/family pair. If
    the argument is already a style object, it will return it.

    If the name is None, the default style is fetched.

    If the name is not the internal name but the name you gave in a
    desktop application, use display_name instead.

    Arguments:

        family -- 'paragraph', 'text',  'graphic', 'table', 'list',
                  'number', 'page-layout', 'master-page', ...

        name -- str or Element or None

        display_name -- str

    Return: Style or None if not found.
    """
    # 1. content.xml
    element = self.content.get_style(
        family, name_or_element=name_or_element, display_name=display_name
    )
    if element is not None:
        return element
    # 2. styles.xml
    return self.styles.get_style(
        family,
        name_or_element=name_or_element,
        display_name=display_name,
    )

get_style_properties

get_style_properties(
    family: str, name: str, area: str | None = None
) -> dict[str, str] | None

Return the properties of the required style as a dict.

Source code in odfdo/document.py
1187
1188
1189
1190
1191
1192
1193
1194
def get_style_properties(
    self, family: str, name: str, area: str | None = None
) -> dict[str, str] | None:
    """Return the properties of the required style as a dict."""
    style = self.get_style(family, name)
    if style is None:
        return None
    return style.get_properties(area=area)  # type: ignore

get_styled_elements

get_styled_elements(name: str = '') -> list[Element]

Brute-force to find paragraphs, tables, etc. using the given style name (or all by default).

Arguments:

name -- str

Return: list

Source code in odfdo/document.py
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
def get_styled_elements(self, name: str = "") -> list[Element]:
    """Brute-force to find paragraphs, tables, etc. using the given style
    name (or all by default).

    Arguments:

        name -- str

    Return: list
    """
    # Header, footer, etc. have styles too
    return self.content.root.get_styled_elements(
        name
    ) + self.styles.root.get_styled_elements(name)

get_styles

get_styles(
    family: str | bytes = "", automatic: bool = False
) -> list[Style | Element]
Source code in odfdo/document.py
711
712
713
714
715
716
717
718
719
720
721
722
def get_styles(
    self,
    family: str | bytes = "",
    automatic: bool = False,
) -> list[Style | Element]:
    # compatibility with old versions:

    if isinstance(family, bytes):
        family = bytes_to_str(family)
    return self.content.get_styles(family=family) + self.styles.get_styles(
        family=family, automatic=automatic
    )

get_table_displayed

get_table_displayed(table: str | int) -> bool

Return the table:display property of the style of the table, ie if the table should be displayed in a graphical interface.

Note: that method replaces the broken Table.displayd() method from previous odfdo versions.

Arguments:

table -- name or index of the table
Source code in odfdo/document.py
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
def get_table_displayed(self, table: str | int) -> bool:
    """Return the table:display property of the style of the table, ie if
    the table should be displayed in a graphical interface.

    Note: that method replaces the broken Table.displayd() method from previous
    odfdo versions.

    Arguments:

        table -- name or index of the table
    """
    style = self.get_table_style(table)
    if not style:
        # should not happen, but assume that a table without style is
        # displayed by default
        return True
    properties = style.get_properties() or {}
    property_str = str(properties.get("table:display", "true"))
    return Boolean.decode(property_str)

get_table_style

get_table_style(table: str | int) -> Style | None

Return the Style instance the table.

Arguments:

table -- name or index of the table
Source code in odfdo/document.py
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
def get_table_style(
    self,
    table: str | int,
) -> Style | None:
    """Return the Style instance the table.

    Arguments:

        table -- name or index of the table
    """
    if not (sheet := self._get_table(table)):
        return None
    return self.get_style("table", sheet.style)

get_type

get_type() -> str

Get the ODF type (also called class) of this document.

‘chart’, ‘database’, ‘formula’, ‘graphics’,

‘graphics-template’, ‘image’, ‘presentation’, ‘presentation-template’, ‘spreadsheet’, ‘spreadsheet-template’, ‘text’, ‘text-master’, ‘text-template’ or ‘text-web’

Source code in odfdo/document.py
397
398
399
400
401
402
403
404
405
406
407
408
409
def get_type(self) -> str:
    """Get the ODF type (also called class) of this document.

    Return: 'chart', 'database', 'formula', 'graphics',
        'graphics-template', 'image', 'presentation',
        'presentation-template', 'spreadsheet', 'spreadsheet-template',
        'text', 'text-master', 'text-template' or 'text-web'
    """
    # The mimetype must be with the form:
    # application/vnd.oasis.opendocument.text

    # Isolate and return the last part
    return self.mimetype.rsplit(".", 1)[-1]

insert_style

insert_style(
    style: Style | str,
    name: str = "",
    automatic: bool = False,
    default: bool = False,
) -> Any

Insert the given style object in the document, as required by the style family and type.

The style is expected to be a common style with a name. In case it was created with no name, the given can be set on the fly.

If automatic is True, the style will be inserted as an automatic style.

If default is True, the style will be inserted as a default style and would replace any existing default style of the same family. Any name or display name would be ignored.

Automatic and default arguments are mutually exclusive.

All styles can’t be used as default styles. Default styles are allowed for the following families: paragraph, text, section, table, table-column, table-row, table-cell, table-page, chart, drawing-page, graphic, presentation, control and ruby.

Arguments:

style -- Style or str

name -- str

automatic -- bool

default -- bool

Return : style name – str

Source code in odfdo/document.py
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
def insert_style(
    self,
    style: Style | str,
    name: str = "",
    automatic: bool = False,
    default: bool = False,
) -> Any:
    """Insert the given style object in the document, as required by the
    style family and type.

    The style is expected to be a common style with a name. In case it
    was created with no name, the given can be set on the fly.

    If automatic is True, the style will be inserted as an automatic
    style.

    If default is True, the style will be inserted as a default style and
    would replace any existing default style of the same family. Any name
    or display name would be ignored.

    Automatic and default arguments are mutually exclusive.

    All styles can't be used as default styles. Default styles are
    allowed for the following families: paragraph, text, section, table,
    table-column, table-row, table-cell, table-page, chart, drawing-page,
    graphic, presentation, control and ruby.

    Arguments:

        style -- Style or str

        name -- str

        automatic -- bool

        default -- bool

    Return : style name -- str
    """

    # if style is a str, assume it is the Style definition
    if isinstance(style, str):
        style_element: Style = Element.from_tag(style)  # type: ignore
    else:
        style_element = style
    if not isinstance(style_element, Element):
        raise TypeError(f"Unknown Style type: '{style!r}'")

    # Get family and name
    family = style_element.family
    if not name:
        name = self._pseudo_style_attribute(style_element, "name")

    # Master page style
    if family == "master-page":
        existing, style_container = self._insert_style_get_master_page(family, name)
    # Font face declarations
    elif family == "font-face":
        if default:
            existing, style_container = self._insert_style_get_font_face_default(
                family, name
            )
        else:
            existing, style_container = self._insert_style_get_font_face(
                family, name
            )
    # page layout style
    elif family == "page-layout":
        existing, style_container = self._insert_style_get_page_layout(family, name)
    # Common style
    elif family in FAMILY_MAPPING:
        existing, style_container = self._insert_style_standard(
            style_element, name, family, automatic, default
        )
    elif not family and style_element.__class__.__name__ == "DrawFillImage":
        # special case for 'draw:fill-image' pseudo style
        existing, style_container = self._insert_style_get_draw_fill_image(name)
    # Invalid style
    else:
        raise ValueError(
            "Invalid style: "
            f"{style_element}, tag:{style_element.tag}, family:{family}"
        )

    # Insert it!
    if existing is not None:
        style_container.delete(existing)
    style_container.append(style_element)
    return self._pseudo_style_attribute(style_element, "name")

merge_styles_from

merge_styles_from(document: Document) -> None

Copy all the styles of a document into ourself.

Styles with the same type and name will be replaced, so only unique styles will be preserved.

Source code in odfdo/document.py
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
def merge_styles_from(self, document: Document) -> None:
    """Copy all the styles of a document into ourself.

    Styles with the same type and name will be replaced, so only unique
    styles will be preserved.
    """
    manifest = self.manifest
    document_manifest = document.manifest
    for style in document.get_styles():
        tagname = style.tag
        family = style.family
        stylename = style.name  # type: ignore
        container = style.parent
        container_name = container.tag  # type: ignore
        partname = container.parent.tag  # type: ignore
        # The destination part
        if partname == "office:document-styles":
            part: Content | Styles = self.styles
        elif partname == "office:document-content":
            part = self.content
        else:
            raise NotImplementedError(partname)
        # Implemented containers
        if container_name not in {
            "office:styles",
            "office:automatic-styles",
            "office:master-styles",
            "office:font-face-decls",
        }:
            raise NotImplementedError(container_name)
        dest = part.get_element(f"//{container_name}")
        # Implemented style types
        # if tagname not in registered_styles:
        #    raise NotImplementedError(tagname)
        duplicate = part.get_style(family, stylename)
        if duplicate is not None:
            duplicate.delete()
        dest.append(style)
        # Copy images from the header/footer
        if tagname == "style:master-page":
            query = "descendant::draw:image"
            for image in style.get_elements(query):
                url = image.url  # type: ignore
                part_url = document.get_part(url)
                # Manually add the part to keep the name
                self.set_part(url, part_url)  # type: ignore
                media_type = document_manifest.get_media_type(url)
                manifest.add_full_path(url, media_type)  # type: ignore
        # Copy images from the fill-image
        elif tagname == "draw:fill-image":
            url = style.url  # type: ignore
            part_url = document.get_part(url)
            self.set_part(url, part_url)  # type: ignore
            media_type = document_manifest.get_media_type(url)
            manifest.add_full_path(url, media_type)  # type: ignore

new classmethod

new(template: str | Path | BytesIO = 'text') -> Document

Create a Document from a template.

The template argument is expected to be the path to a ODF template.

Arguments:

template -- str or Path or file-like (io.BytesIO)

Return : ODF document – Document

Source code in odfdo/document.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@classmethod
def new(cls, template: str | Path | io.BytesIO = "text") -> Document:
    """Create a Document from a template.

    The template argument is expected to be the path to a ODF template.

    Arguments:

        template -- str or Path or file-like (io.BytesIO)

    Return : ODF document -- Document
    """
    container = container_from_template(template)
    return cls(container)

save

save(
    target: str | Path | BytesIO | None = None,
    packaging: str = ZIP,
    pretty: bool | None = None,
    backup: bool = False,
) -> None

Save the document, at the same place it was opened or at the given target path. Target can also be a file-like object. It can be saved as a Zip file (default), flat XML format or as files in a folder (for debugging purpose). XML parts can be pretty printed (the default for ‘folder’ and ‘xml’ packaging).

Note: ‘xml’ packaging is an experimental work in progress.

Arguments:

target -- str or file-like object

packaging -- 'zip', 'folder', 'xml'

pretty -- bool | None

backup -- bool
Source code in odfdo/document.py
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
def save(
    self,
    target: str | Path | io.BytesIO | None = None,
    packaging: str = ZIP,
    pretty: bool | None = None,
    backup: bool = False,
) -> None:
    """Save the document, at the same place it was opened or at the given
    target path. Target can also be a file-like object. It can be saved
    as a Zip file (default), flat XML format or as files in a folder
    (for debugging purpose). XML parts can be pretty printed (the default
    for 'folder' and 'xml' packaging).

    Note: 'xml' packaging is an experimental work in progress.

    Arguments:

        target -- str or file-like object

        packaging -- 'zip', 'folder', 'xml'

        pretty -- bool | None

        backup -- bool
    """
    if not self.container:
        raise ValueError("Empty Container")
    if packaging not in PACKAGING:
        raise ValueError(f'Packaging of type "{packaging}" is not supported')
    # Some advertising
    self.meta.set_generator_default()
    # Synchronize data with container
    container = self.container
    if pretty is None:
        pretty = packaging in {"folder", "xml"}
    pretty = bool(pretty)
    backup = bool(backup)
    self._check_manifest_rdf()
    if pretty and packaging != XML:
        for path, part in self.__xmlparts.items():
            if part is not None:
                container.set_part(path, part.pretty_serialize())
        for path in (ODF_CONTENT, ODF_META, ODF_SETTINGS, ODF_STYLES):
            if path in self.__xmlparts:
                continue
            cls = _get_part_class(path)
            # XML part
            self.__xmlparts[path] = part = cls(path, container)
            container.set_part(path, part.pretty_serialize())
    else:
        for path, part in self.__xmlparts.items():
            if part is not None:
                container.set_part(path, part.serialize())
    container.save(target, packaging=packaging, backup=backup, pretty=pretty)

set_part

set_part(path: str, data: bytes) -> None

Set the bytes of the given part. The path is relative to the archive, e.g. “Pictures/1003200258912EB1C3.jpg”.

path formated as URI, so always use ‘/’ separator

Source code in odfdo/document.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def set_part(self, path: str, data: bytes) -> None:
    """Set the bytes of the given part. The path is relative to the
    archive, e.g. "Pictures/1003200258912EB1C3.jpg".

    path formated as URI, so always use '/' separator
    """
    if not self.container:
        raise ValueError("Empty Container")
    # "./ObjectReplacements/Object 1"
    path = path.lstrip("./")
    path = _get_part_path(path)
    cls = _get_part_class(path)
    # XML part overwritten
    if cls is not None:
        with suppress(KeyError):
            self.__xmlparts[path]
    self.container.set_part(path, data)

set_table_displayed

set_table_displayed(
    table: str | int, displayed: bool
) -> None

Set the table:display property of the style of the table, ie if the table should be displayed in a graphical interface.

Note: that method replaces the broken Table.displayd() method from previous odfdo versions.

Arguments:

table -- name or index of the table

displayed -- boolean flag
Source code in odfdo/document.py
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
def set_table_displayed(self, table: str | int, displayed: bool) -> None:
    """Set the table:display property of the style of the table, ie if
    the table should be displayed in a graphical interface.

    Note: that method replaces the broken Table.displayd() method from previous
    odfdo versions.

    Arguments:

        table -- name or index of the table

        displayed -- boolean flag
    """
    orig_style = self.get_table_style(table)
    if not orig_style:
        name = self._unique_style_name("ta")
        orig_style = Element.from_tag(
            f'<style:style style:name="{name}" style:family="table" '
            'style:master-page-name="Default">'
            '<style:table-properties table:display="true" '
            'style:writing-mode="lr-tb"/></style:style>'
        )
        self.insert_style(orig_style, automatic=True)  # type:ignore
    new_style = orig_style.clone
    new_name = self._unique_style_name("ta")
    new_style.name = new_name  # type:ignore
    self.insert_style(new_style, automatic=True)  # type:ignore
    sheet = self._get_table(table)
    sheet.style = new_name  # type: ignore
    properties = {"table:display": Boolean.encode(displayed)}
    new_style.set_properties(properties)  # type: ignore

show_styles

show_styles(
    automatic: bool = True,
    common: bool = True,
    properties: bool = False,
) -> str
Source code in odfdo/document.py
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
def show_styles(
    self,
    automatic: bool = True,
    common: bool = True,
    properties: bool = False,
) -> str:
    infos = []
    for style in self.get_styles():
        try:
            name = style.name  # type: ignore
        except AttributeError:
            print("--------------")
            print(style.__class__)
            print(style.serialize())
            raise
        if style.__class__.__name__ == "DrawFillImage":
            family = ""
        else:
            family = str(style.family)  # type: ignore
        parent = style.parent
        is_auto = parent and parent.tag == "office:automatic-styles"
        if (is_auto and automatic is False) or (not is_auto and common is False):
            continue
        is_used = bool(self.get_styled_elements(name))
        infos.append(
            {
                "type": "auto  " if is_auto else "common",
                "used": "y" if is_used else "n",
                "family": family,
                "parent": self._pseudo_style_attribute(style, "parent_style") or "",
                "name": name or "",
                "display_name": self._pseudo_style_attribute(style, "display_name")
                or "",
                "properties": style.get_properties() if properties else None,  # type: ignore
            }
        )
    if not infos:
        return ""
    # Sort by family and name
    infos.sort(key=itemgetter("family", "name"))
    # Show common and used first
    infos.sort(key=itemgetter("type", "used"), reverse=True)
    max_family = str(max(len(x["family"]) for x in infos))  # type: ignore
    max_parent = str(max(len(x["parent"]) for x in infos))  # type: ignore
    formater = (
        "%(type)s used:%(used)s family:%(family)-0"
        + max_family
        + "s parent:%(parent)-0"
        + max_parent
        + "s name:%(name)s"
    )
    output = []
    for info in infos:
        line = formater % info
        if info["display_name"]:
            line += " display_name:" + info["display_name"]  # type: ignore
        output.append(line)
        if info["properties"]:
            for name, value in info["properties"].items():  # type: ignore
                output.append(f"   - {name}: {value}")
    output.append("")
    return "\n".join(output)

to_markdown

to_markdown() -> str
Source code in odfdo/document.py
566
567
568
569
570
571
572
573
574
def to_markdown(self) -> str:
    doc_type = self.get_type()
    if doc_type not in {
        "text",
    }:
        raise NotImplementedError(
            f"Type of document '{doc_type}' not supported yet"
        )
    return self._markdown_export()

DrawFillImage

Bases: DrawImage

A link to a bitmap resource, “draw:fill-image”.

Methods:

Name Description
__init__

A link to a bitmap resource, “draw:fill-image”.

Attributes:

Name Type Description
display_name
family
height
name
width
Source code in odfdo/image.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class DrawFillImage(DrawImage):
    """A link to a bitmap resource, "draw:fill-image"."""

    _tag = "draw:fill-image"
    _properties: tuple[PropDef, ...] = (
        PropDef("display_name", "draw:display-name"),
        PropDef("name", "draw:name"),
        PropDef("height", "svg:height"),
        PropDef("width", "svg:width"),
    )

    def __init__(
        self,
        name: str | None = None,
        display_name: str | None = None,
        height: str | None = None,
        width: str | None = None,
        **kwargs: Any,
    ) -> None:
        """
        A link to a bitmap resource, "draw:fill-image".

        The "draw:fill-image" element specifies a link to a bitmap
        resource. Fill image are not available as automatic styles.
        The "draw:fill-image" element is usable within the following element:
        "office:styles"

        Arguments:

            name -- str

            display_name -- str

            height -- str

            width -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name
            self.display_name = display_name
            self.height = height
            self.width = width
        self.family = ""

display_name instance-attribute

display_name = display_name

family instance-attribute

family = ''

height instance-attribute

height = height

name instance-attribute

name = name

width instance-attribute

width = width

__init__

__init__(
    name: str | None = None,
    display_name: str | None = None,
    height: str | None = None,
    width: str | None = None,
    **kwargs: Any,
) -> None

A link to a bitmap resource, “draw:fill-image”.

The “draw:fill-image” element specifies a link to a bitmap resource. Fill image are not available as automatic styles. The “draw:fill-image” element is usable within the following element: “office:styles”

Arguments:

name -- str

display_name -- str

height -- str

width -- str
Source code in odfdo/image.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def __init__(
    self,
    name: str | None = None,
    display_name: str | None = None,
    height: str | None = None,
    width: str | None = None,
    **kwargs: Any,
) -> None:
    """
    A link to a bitmap resource, "draw:fill-image".

    The "draw:fill-image" element specifies a link to a bitmap
    resource. Fill image are not available as automatic styles.
    The "draw:fill-image" element is usable within the following element:
    "office:styles"

    Arguments:

        name -- str

        display_name -- str

        height -- str

        width -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name
        self.display_name = display_name
        self.height = height
        self.width = width
    self.family = ""

DrawGroup

Bases: Element, AnchorMix, ZMix, PosMix

Representation of a group of drawing shapes, “draw:g”.

Warning: implementation is currently minimal.

Drawing shapes contained by a “draw:g” element that is itself contained by a “draw:a” element, act as hyperlinks using the xlink:href attribute of the containing “draw:a” element. If the included drawing shapes are themselves contained within “draw:a” elements, then the xlink:href attributes of those “draw:a” elements act as the hyperlink information for the shapes they contain.

The “draw:g” element has the following attributes: draw:caption-id, draw:class-names, draw:id, draw:name, draw:style-name, draw:z-index, presentation:class-names, presentation:style-name, svg:y, table:end-cell-address, table:end-x, table:end-y, table:table-background, text:anchor-page-number, text:anchor-type, and xml:id.

The “draw:g” element has the following child elements: “dr3d:scene”, “draw:a”, “draw:caption”, “draw:circle”, “draw:connector”, “draw:control”, “draw:custom-shape”, “draw:ellipse”, “draw:frame”, “draw:g”, “draw:glue-point”, “draw:line”, “draw:measure”, “draw:page-thumbnail”, “draw:path”, “draw:polygon”, “draw:polyline”, “draw:rect”, “draw:regular-polygon”, “office:event-listeners”, “svg:desc” and “svg:title”.

Methods:

Name Description
__init__

Create a group of drawing shapes “draw:g”.

Attributes:

Name Type Description
anchor_page
anchor_type
draw_id
name
position
presentation_style
style
z_index
Source code in odfdo/shapes.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
class DrawGroup(Element, AnchorMix, ZMix, PosMix):
    """Representation of a group of drawing shapes, "draw:g".

    Warning: implementation is currently minimal.

    Drawing shapes contained by a "draw:g" element that is itself
    contained by a "draw:a" element, act as hyperlinks using the
    xlink:href attribute of the containing "draw:a" element. If the
    included drawing shapes are themselves contained within "draw:a"
    elements, then the xlink:href attributes of those "draw:a" elements
    act as the hyperlink information for the shapes they contain.

    The "draw:g" element has the following attributes: draw:caption-id,
    draw:class-names, draw:id, draw:name, draw:style-name, draw:z-index,
    presentation:class-names, presentation:style-name, svg:y,
    table:end-cell-address, table:end-x, table:end-y,
    table:table-background, text:anchor-page-number, text:anchor-type,
    and xml:id.

    The "draw:g" element has the following child elements: "dr3d:scene",
    "draw:a", "draw:caption", "draw:circle", "draw:connector",
    "draw:control", "draw:custom-shape", "draw:ellipse", "draw:frame",
    "draw:g", "draw:glue-point", "draw:line", "draw:measure",
    "draw:page-thumbnail", "draw:path", "draw:polygon", "draw:polyline",
    "draw:rect", "draw:regular-polygon", "office:event-listeners",
    "svg:desc" and "svg:title".
    """

    _tag = "draw:g"
    _properties: tuple[PropDef, ...] = (
        PropDef("draw_id", "draw:id"),
        PropDef("caption_id", "draw:caption-id"),
        PropDef("draw_class_names", "draw:class-names"),
        PropDef("name", "draw:name"),
        PropDef("style", "draw:style-name"),
        # ('z_index', 'draw:z-index'),
        PropDef("presentation_class_names", "presentation:class-names"),
        PropDef("presentation_style", "presentation:style-name"),
        PropDef("table_end_cell", "table:end-cell-address"),
        PropDef("table_end_x", "table:end-x"),
        PropDef("table_end_y", "table:end-y"),
        PropDef("table_background", "table:table-background"),
        # ('anchor_page', 'text:anchor-page-number'),
        # ('anchor_type', 'text:anchor-type'),
        PropDef("xml_id", "xml:id"),
        PropDef("pos_x", "svg:x"),
        PropDef("pos_y", "svg:y"),
    )

    def __init__(
        self,
        name: str | None = None,
        draw_id: str | None = None,
        style: str | None = None,
        position: tuple | None = None,
        z_index: int = 0,
        anchor_type: str | None = None,
        anchor_page: int | None = None,
        presentation_style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a group of drawing shapes "draw:g"."""
        super().__init__(**kwargs)
        if self._do_init:
            if z_index is not None:
                self.z_index = z_index
            if name:
                self.name = name
            if draw_id is not None:
                self.draw_id = draw_id
            if style is not None:
                self.style = style
            if position is not None:
                self.position = position
            if anchor_type:
                self.anchor_type = anchor_type
            if anchor_page is not None:
                self.anchor_page = anchor_page
            if presentation_style is not None:
                self.presentation_style = presentation_style

anchor_page instance-attribute

anchor_page = anchor_page

anchor_type instance-attribute

anchor_type = anchor_type

draw_id instance-attribute

draw_id = draw_id

name instance-attribute

name = name

position instance-attribute

position = position

presentation_style instance-attribute

presentation_style = presentation_style

style instance-attribute

style = style

z_index instance-attribute

z_index = z_index

__init__

__init__(
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    z_index: int = 0,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> None

Create a group of drawing shapes “draw:g”.

Source code in odfdo/shapes.py
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def __init__(
    self,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    z_index: int = 0,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a group of drawing shapes "draw:g"."""
    super().__init__(**kwargs)
    if self._do_init:
        if z_index is not None:
            self.z_index = z_index
        if name:
            self.name = name
        if draw_id is not None:
            self.draw_id = draw_id
        if style is not None:
            self.style = style
        if position is not None:
            self.position = position
        if anchor_type:
            self.anchor_type = anchor_type
        if anchor_page is not None:
            self.anchor_page = anchor_page
        if presentation_style is not None:
            self.presentation_style = presentation_style

DrawImage

Bases: Element

An ODF image, “draw:image”.

The “draw:image” element represents an image. An image can be either a link to an external resource or most often embedded into the document.

Methods:

Name Description
__init__

An ODF image, “draw:image”.

Attributes:

Name Type Description
actuate
filter_name
show
type
url
Source code in odfdo/image.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class DrawImage(Element):
    """
    An ODF image, "draw:image".

    The "draw:image" element represents an image. An image can be
    either a link to an external resource or most often embedded into
    the document.
    """

    _tag = "draw:image"
    _properties: tuple[PropDef, ...] = (
        PropDef("url", "xlink:href"),
        PropDef("type", "xlink:type"),
        PropDef("show", "xlink:show"),
        PropDef("actuate", "xlink:actuate"),
        PropDef("filter_name", "draw:filter-name"),
    )

    def __init__(
        self,
        url: str = "",
        xlink_type: str = "simple",
        show: str = "embed",
        actuate: str = "onLoad",
        filter_name: str | None = None,
        **kwargs: Any,
    ) -> None:
        """An ODF image, "draw:image".

        When image is embedded in the document, the url parameter is a
        reference to the local document obtained by copying the source
        image into the document, ie: url = document.add_file(image_path)

        Warning: image elements must be stored in a frame "draw:frame",
        see Frame().

        Initialisation of an DrawImage.

        Arguments:

            url -- str

            type -- str

            show -- str

            actuate -- str

            filter_name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.url = url
            self.type = xlink_type
            self.show = show
            self.actuate = actuate
            self.filter_name = filter_name

actuate instance-attribute

actuate = actuate

filter_name instance-attribute

filter_name = filter_name

show instance-attribute

show = show

type instance-attribute

type = xlink_type

url instance-attribute

url = url

__init__

__init__(
    url: str = "",
    xlink_type: str = "simple",
    show: str = "embed",
    actuate: str = "onLoad",
    filter_name: str | None = None,
    **kwargs: Any,
) -> None

An ODF image, “draw:image”.

When image is embedded in the document, the url parameter is a reference to the local document obtained by copying the source image into the document, ie: url = document.add_file(image_path)

Warning: image elements must be stored in a frame “draw:frame”, see Frame().

Initialisation of an DrawImage.

Arguments:

url -- str

type -- str

show -- str

actuate -- str

filter_name -- str
Source code in odfdo/image.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(
    self,
    url: str = "",
    xlink_type: str = "simple",
    show: str = "embed",
    actuate: str = "onLoad",
    filter_name: str | None = None,
    **kwargs: Any,
) -> None:
    """An ODF image, "draw:image".

    When image is embedded in the document, the url parameter is a
    reference to the local document obtained by copying the source
    image into the document, ie: url = document.add_file(image_path)

    Warning: image elements must be stored in a frame "draw:frame",
    see Frame().

    Initialisation of an DrawImage.

    Arguments:

        url -- str

        type -- str

        show -- str

        actuate -- str

        filter_name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.url = url
        self.type = xlink_type
        self.show = show
        self.actuate = actuate
        self.filter_name = filter_name

DrawPage

Bases: Element

ODF draw page for presentations and drawings, “draw:page”.

Methods:

Name Description
__init__

ODF draw page for presentations and drawings, “draw:page”.

get_formatted_text
get_shapes
get_transition
set_transition

Attributes:

Name Type Description
draw_id
master_page
name
presentation_page_layout
style
Source code in odfdo/draw_page.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class DrawPage(Element):
    """ODF draw page for presentations and drawings, "draw:page"."""

    _tag = "draw:page"
    _properties = (
        PropDef("name", "draw:name"),
        PropDef("draw_id", "draw:id"),
        PropDef("master_page", "draw:master-page-name"),
        PropDef(
            "presentation_page_layout", "presentation:presentation-page-layout-name"
        ),
        PropDef("style", "draw:style-name"),
    )

    def __init__(
        self,
        draw_id: str | None = None,
        name: str | None = None,
        master_page: str | None = None,
        presentation_page_layout: str | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """
        ODF draw page for presentations and drawings, "draw:page".

        Arguments:

            draw_id -- str

            name -- str

            master_page -- str

            presentation_page_layout -- str

            style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            if draw_id:
                self.draw_id = draw_id
            if name:
                self.name = name
            if master_page:
                self.master_page = master_page
            if presentation_page_layout:
                self.presentation_page_layout = presentation_page_layout
            if style:
                self.style = style

    def get_transition(self) -> AnimPar | None:
        return self.get_element("anim:par")  # type: ignore

    def set_transition(
        self,
        smil_type: str,
        subtype: str | None = None,
        dur: str = "2s",
        node_type: str = "default",
    ) -> None:
        # Create the new animation
        anim_page = AnimPar(presentation_node_type=node_type)
        anim_begin = AnimPar(smil_begin=f"{self.draw_id}.begin")
        transition = AnimTransFilter(
            smil_dur=dur, smil_type=smil_type, smil_subtype=subtype
        )
        anim_page.append(anim_begin)
        anim_begin.append(transition)
        # Replace when already a transition:
        #   anim:seq => After the frame's transition
        #   cf page 349 of OpenDocument-v1.0-os.pdf
        #   Conclusion: We must delete the first child 'anim:par'
        previous_transition = self.get_transition()
        if previous_transition:
            self.delete(previous_transition)
        self.append(anim_page)

    def get_shapes(self) -> list[Element]:
        query = "(descendant::" + "|descendant::".join(registered_shapes) + ")"
        return self.get_elements(query)

    def get_formatted_text(self, context: dict | None = None) -> str:
        result: list[str] = []
        for child in self.children:
            if child.tag == "presentation:notes":
                # No need for an advanced odf_notes.get_formatted_text()
                # because the text seems to be only contained in paragraphs
                # and frames, that we already handle
                for sub_child in child.children:
                    result.append(sub_child.get_formatted_text(context))
                result.append("\n")
            result.append(child.get_formatted_text(context))
        result.append("\n")
        return "".join(result)

draw_id instance-attribute

draw_id = draw_id

master_page instance-attribute

master_page = master_page

name instance-attribute

name = name

presentation_page_layout instance-attribute

presentation_page_layout = presentation_page_layout

style instance-attribute

style = style

__init__

__init__(
    draw_id: str | None = None,
    name: str | None = None,
    master_page: str | None = None,
    presentation_page_layout: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

ODF draw page for presentations and drawings, “draw:page”.

Arguments:

draw_id -- str

name -- str

master_page -- str

presentation_page_layout -- str

style -- str
Source code in odfdo/draw_page.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(
    self,
    draw_id: str | None = None,
    name: str | None = None,
    master_page: str | None = None,
    presentation_page_layout: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """
    ODF draw page for presentations and drawings, "draw:page".

    Arguments:

        draw_id -- str

        name -- str

        master_page -- str

        presentation_page_layout -- str

        style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        if draw_id:
            self.draw_id = draw_id
        if name:
            self.name = name
        if master_page:
            self.master_page = master_page
        if presentation_page_layout:
            self.presentation_page_layout = presentation_page_layout
        if style:
            self.style = style

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/draw_page.py
114
115
116
117
118
119
120
121
122
123
124
125
126
def get_formatted_text(self, context: dict | None = None) -> str:
    result: list[str] = []
    for child in self.children:
        if child.tag == "presentation:notes":
            # No need for an advanced odf_notes.get_formatted_text()
            # because the text seems to be only contained in paragraphs
            # and frames, that we already handle
            for sub_child in child.children:
                result.append(sub_child.get_formatted_text(context))
            result.append("\n")
        result.append(child.get_formatted_text(context))
    result.append("\n")
    return "".join(result)

get_shapes

get_shapes() -> list[Element]
Source code in odfdo/draw_page.py
110
111
112
def get_shapes(self) -> list[Element]:
    query = "(descendant::" + "|descendant::".join(registered_shapes) + ")"
    return self.get_elements(query)

get_transition

get_transition() -> AnimPar | None
Source code in odfdo/draw_page.py
83
84
def get_transition(self) -> AnimPar | None:
    return self.get_element("anim:par")  # type: ignore

set_transition

set_transition(
    smil_type: str,
    subtype: str | None = None,
    dur: str = "2s",
    node_type: str = "default",
) -> None
Source code in odfdo/draw_page.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def set_transition(
    self,
    smil_type: str,
    subtype: str | None = None,
    dur: str = "2s",
    node_type: str = "default",
) -> None:
    # Create the new animation
    anim_page = AnimPar(presentation_node_type=node_type)
    anim_begin = AnimPar(smil_begin=f"{self.draw_id}.begin")
    transition = AnimTransFilter(
        smil_dur=dur, smil_type=smil_type, smil_subtype=subtype
    )
    anim_page.append(anim_begin)
    anim_begin.append(transition)
    # Replace when already a transition:
    #   anim:seq => After the frame's transition
    #   cf page 349 of OpenDocument-v1.0-os.pdf
    #   Conclusion: We must delete the first child 'anim:par'
    previous_transition = self.get_transition()
    if previous_transition:
        self.delete(previous_transition)
    self.append(anim_page)

Drawing

Bases: Body

Root of the Drawing document content, “office:drawing”.

Source code in odfdo/body.py
109
110
111
112
113
class Drawing(Body):
    """Root of the Drawing document content, "office:drawing"."""

    _tag: str = "office:drawing"
    _properties: tuple[PropDef, ...] = ()

EText

Bases: str

Representation of an XML text node (internal).

Created to hide the specifics of lxml in searching text nodes using XPath.

Constructed like any str object but only accepts lxml text objects.

Methods:

Name Description
__init__
is_tail
is_text

Attributes:

Name Type Description
parent Element | None
Source code in odfdo/element.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class EText(str):
    """Representation of an XML text node (internal).

    Created to hide the specifics of lxml in searching text nodes using XPath.

    Constructed like any str object but only accepts lxml text objects.
    """

    # There's some black magic in inheriting from str
    def __init__(
        self,
        text_result: _Element,
    ) -> None:
        self.__parent = text_result.getparent()
        self.__is_text: bool = bool(text_result.is_text)
        self.__is_tail: bool = bool(text_result.is_tail)

    @property
    def parent(self) -> Element | None:
        parent = self.__parent
        # XXX happens just because of the unit test
        if parent is None:
            return None
        return Element.from_tag(tag_or_elem=parent)

    def is_text(self) -> bool:
        return self.__is_text

    def is_tail(self) -> bool:
        return self.__is_tail

parent property

parent: Element | None

__init__

__init__(text_result: _Element) -> None
Source code in odfdo/element.py
277
278
279
280
281
282
283
def __init__(
    self,
    text_result: _Element,
) -> None:
    self.__parent = text_result.getparent()
    self.__is_text: bool = bool(text_result.is_text)
    self.__is_tail: bool = bool(text_result.is_tail)

is_tail

is_tail() -> bool
Source code in odfdo/element.py
296
297
def is_tail(self) -> bool:
    return self.__is_tail

is_text

is_text() -> bool
Source code in odfdo/element.py
293
294
def is_text(self) -> bool:
    return self.__is_text

Element

Bases: MDBase

Base class of all ODF classes, abstraction of the underlying XML.

Methods:

Name Description
__init__

Base class of all ODF classes, abstraction of the underlying XML.

append_named_range

Append the named range to the spreadsheet, replacing existing named

clear

Remove text, children and attributes from the element.

del_attribute
delete

Delete the given element from the XML tree. If no element is given,

delete_named_range

Delete the Named Range of specified name from the spreadsheet.

elements_repeated_sequence

Utility method for table module.

extend

Fast append elements at the end of ourself using extend.

from_tag

Element class and subclass factory.

from_tag_for_clone
get_annotation

Return the annotation that matches the criteria.

get_annotation_end

Return the annotation end that matches the criteria.

get_annotation_ends

Return all the annotation ends.

get_annotations

Return all the annotations that match the criteria.

get_attribute

Return the attribute value as type str | bool | None.

get_attribute_integer

Return either the attribute as type int, or None.

get_attribute_string

Return either the attribute as type str, or None.

get_between

Returns elements between tag1 and tag2, tag1 and tag2 shall

get_bookmark

Return the bookmark that matches the criteria.

get_bookmark_end

Return the bookmark end that matches the criteria.

get_bookmark_ends

Return all the bookmark ends.

get_bookmark_start

Return the bookmark start that matches the criteria.

get_bookmark_starts

Return all the bookmark starts.

get_bookmarks

Return all the bookmarks.

get_changes_ids

Return a list of ids that refers to a change region in the tracked

get_draw_connector

Return the draw connector that matches the criteria.

get_draw_connectors

Return all the draw connectors that match the criteria.

get_draw_ellipse

Return the draw ellipse that matches the criteria.

get_draw_ellipses

Return all the draw ellipses that match the criteria.

get_draw_group

Return the draw group that matches the criteria.

get_draw_groups

Return all the draw groups that match the criteria.

get_draw_line

Return the draw line that matches the criteria.

get_draw_lines

Return all the draw lines that match the criteria.

get_draw_page

Return the draw page that matches the criteria.

get_draw_pages

Return all the draw pages that match the criteria.

get_draw_rectangle

Return the draw rectangle that matches the criteria.

get_draw_rectangles

Return all the draw rectangles that match the criteria.

get_element
get_elements
get_formatted_text

This function should return a beautiful version of the text.

get_frame

Return the section that matches the criteria.

get_frames

Return all the frames that match the criteria.

get_header

Return the Header that matches the criteria.

get_headers

Return all the Headers that match the criteria.

get_image

Return the image matching the criteria.

get_images

Return all the images matching the criteria.

get_link

Return the link that matches the criteria.

get_links

Return all the links that match the criteria.

get_list

Return the list that matches the criteria.

get_lists

Return all the lists that match the criteria.

get_named_range

Return the named range of specified name, or None if not found.

get_named_ranges

Return all the tables named ranges.

get_note

Return the note that matches the criteria.

get_notes

Return all the notes that match the criteria.

get_office_names

Return all the used office:name tags values of the element.

get_orphan_draw_connectors

Return a list of connectors which don’t have any shape connected

get_paragraph

Return the paragraph that matches the criteria.

get_paragraphs

Return all the paragraphs that match the criteria.

get_reference_mark

Return the reference mark that match the criteria. Either single

get_reference_mark_end

Return the reference mark end that matches the criteria. Search only

get_reference_mark_ends

Return all the reference mark ends. Search only the tags

get_reference_mark_single

Return the reference mark that matches the criteria. Search only the

get_reference_mark_start

Return the reference mark start that matches the criteria. Search

get_reference_mark_starts

Return all the reference mark starts. Search only the tags

get_reference_marks

Return all the reference marks, either single position reference

get_reference_marks_single

Return all the reference marks. Search only the tags

get_references

Return all the references (text:reference-ref). If name is

get_section

Return the section that matches the criteria.

get_sections

Return all the sections that match the criteria.

get_span

Return the span that matches the criteria.

get_spans

Return all the spans that match the criteria.

get_style

Return the style uniquely identified by the family/name pair. If

get_styled_elements

Brute-force to find paragraphs, tables, etc. using the given style

get_styles
get_text_change

Return the text change that matches the criteria. Either single

get_text_change_deletion

Return the text change of deletion kind that matches the criteria.

get_text_change_deletions

Return all the text changes of deletion kind: the tags text:change.

get_text_change_end

Return the text change-end that matches the criteria. Search only

get_text_change_ends

Return all the text change-end. Search only the tags

get_text_change_start

Return the text change-start that matches the criteria. Search

get_text_change_starts

Return all the text change-start. Search only for the tags

get_text_changes

Return all the text changes, either single deletion

get_toc

Return the table of contents that matches the criteria.

get_tocs

Return all the tables of contents.

get_tracked_changes

Return the tracked-changes part in the text body.

get_user_defined

return the user defined declaration for the given name.

get_user_defined_list

Return all the user defined field declarations.

get_user_defined_value

Return the value of the given user defined field name.

get_user_field_decl

return the user field declaration for the given name.

get_user_field_decl_list

Return all the user field declarations.

get_user_field_decls

Return the container for user field declarations. Created if not

get_user_field_value

Return the value of the given user field name.

get_variable_decl

return the variable declaration for the given name.

get_variable_decl_list

Return all the variable declarations.

get_variable_decls

Return the container for variable declarations. Created if not

get_variable_set

Return the variable set for the given name (last one by default).

get_variable_set_value

Return the last value of the given variable name.

get_variable_sets

Return all the variable sets that match the criteria.

index

Return the position of the child in this element.

insert

Insert an element relatively to ourself.

is_empty

Check if the element is empty : no text, no children, no tail.

make_etree_element
match

return True if the pattern is found one or more times anywhere in

replace

Replace the pattern with the given text, or delete if text is an

replace_element

Replaces in place a sub element with the element passed as second

search

Return the first position of the pattern in the text content of

search_all

Return all start and end positions of the regex pattern in

search_first

Return the start and end position of the first occurence

serialize

Return text serialization of XML element.

set_attribute
set_style_attribute

Shortcut to accept a style object as a value.

strip_elements

Remove the tags of provided elements, keeping inner childs and text.

strip_tags

Remove the tags listed in strip, recursively, keeping inner childs

text_at

Return the text (recursive) content of the element between

xpath

Apply XPath query to the element and its subtree. Return list of

Attributes:

Name Type Description
append
attributes dict[str, str]
children list[Element]
clone Element
document_body Body | None

Return the first children of document body if any: ‘office:body/*[1]’

frames list[Frame]

Return all the frames.

headers list[Header]

Return all the Headers.

images list[DrawImage]

Return all the images.

inner_text str
is_bound bool
lists list[List]

Return all the lists.

paragraphs list[Paragraph]

Return all the paragraphs.

parent Element | None
root Element
sections list[Section]

Return all the sections.

spans list[Span]

Return all the spans.

svg_description str | None
svg_title str | None
tag str

Get/set the underlying xml tag with the given qualified name.

tail str | None

Get / set the text immediately following the element.

text str

Get / set the text content of the element.

text_changes list[TextChange | TextChangeStart]

Return all the text changes, either single deletion

text_content str

Get / set the text of the embedded paragraphs, including embeded

text_recursive str
toc TOC | None

Return the first table of contents.

tocs list[TOC]

Return all the tables of contents.

tracked_changes Element | None

Return the tracked-changes part in the text body.

user_defined_list list[UserDefined]

Return all the user defined field declarations.

Source code in odfdo/element.py
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
class Element(MDBase):
    """Base class of all ODF classes, abstraction of the underlying XML."""

    _tag: str = ""
    _properties: tuple[PropDef, ...] = ()

    def __init__(self, **kwargs: Any) -> None:
        """Base class of all ODF classes, abstraction of the underlying XML."""
        tag_or_elem = kwargs.pop("tag_or_elem", None)
        if tag_or_elem is None:
            # Instance for newly created object: create new lxml element and
            # continue by subclass __init__
            # If the tag key word exists, make a custom element
            self._do_init = True
            tag = kwargs.pop("tag", self._tag)
            self.__element = self.make_etree_element(tag)
        else:
            # called with an existing lxml element, sould be a result of
            # from_tag() casting, do not execute the subclass __init__
            if not isinstance(tag_or_elem, _Element):
                raise TypeError(f'"{type(tag_or_elem)}" is not an element node')
            self._do_init = False
            self.__element = tag_or_elem

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} tag={self.tag}>"

    @classmethod
    def from_tag(cls, tag_or_elem: str | _Element) -> Element:
        """Element class and subclass factory.

        Turn an lxml Element or ODF string tag into an ODF XML Element
        of the relevant class.

        Arguments:

            tag_or_elem -- ODF str tag or lxml.Element

        Return: Element (or subclass) instance
        """
        if isinstance(tag_or_elem, str):
            # assume the argument is a prefix:name tag
            elem = cls.make_etree_element(tag_or_elem)
        else:
            elem = tag_or_elem
        klass = _class_registry.get(elem.tag, cls)
        return klass(tag_or_elem=elem)

    @classmethod
    def from_tag_for_clone(
        cls: Any,  # ABCMeta, type, ...
        tree_element: _Element,
        cache: tuple | None,
    ) -> Element:
        tag = to_str(tree_element.tag)
        klass = _class_registry.get(tag, cls)
        element: Element = klass(tag_or_elem=tree_element)
        if cache:
            element._copy_cache(cache)
        return element

    def _copy_cache(self, cache: tuple | None) -> None:
        """Method eredefined for cahched elements."""
        pass

    @staticmethod
    def make_etree_element(tag: str) -> _Element:
        if not isinstance(tag, str):
            raise TypeError(f"Tag is not str: {tag!r}")
        tag = tag.strip()
        if not tag:
            raise ValueError("Tag is empty")
        if "<" not in tag:
            # Qualified name
            # XXX don't build the element from scratch or lxml will pollute with
            # repeated namespace declarations
            tag = f"<{tag}/>"
        # XML fragment
        root = fromstring(NAMESPACES_XML % str_to_bytes(tag))
        return root[0]

    @staticmethod
    def _generic_attrib_getter(attr_name: str, family: str | None = None) -> Callable:
        name = _get_lxml_tag(attr_name)

        def getter(self: Element) -> str | bool | None:
            try:
                if family and self.family != family:  # type: ignore
                    return None
            except AttributeError:
                return None
            value = self.__element.get(name)
            if value is None:
                return None
            elif value in ("true", "false"):
                return Boolean.decode(value)
            return str(value)

        return getter

    @staticmethod
    def _generic_attrib_setter(attr_name: str, family: str | None = None) -> Callable:
        name = _get_lxml_tag(attr_name)

        def setter(self: Element, value: Any) -> None:
            try:
                if family and self.family != family:  # type: ignore
                    return None
            except AttributeError:
                return None
            if value is None:
                with contextlib.suppress(KeyError):
                    del self.__element.attrib[name]
                return
            if isinstance(value, bool):
                value = Boolean.encode(value)
            self.__element.set(name, str(value))

        return setter

    @classmethod
    def _define_attribut_property(cls: type[Element]) -> None:
        for prop in cls._properties:
            setattr(
                cls,
                prop.name,
                property(
                    cls._generic_attrib_getter(prop.attr, prop.family or None),
                    cls._generic_attrib_setter(prop.attr, prop.family or None),
                    None,
                    f"Get/set the attribute {prop.attr}",
                ),
            )

    @staticmethod
    def _make_before_regex(
        before: str | None,
        after: str | None,
    ) -> re.Pattern:
        # 1) before xor after is not None
        if before is not None:
            return re.compile(before)
        else:
            if after is None:
                raise ValueError("Both 'before' and 'after' are None")
            return re.compile(after)

    @staticmethod
    def _search_negative_position(
        xpath_result: list,
        regex: re.Pattern,
    ) -> tuple[str, re.Match]:
        # Found the last text that matches the regex
        text = None
        for a_text in xpath_result:
            if regex.search(str(a_text)) is not None:
                text = a_text
        if text is None:
            raise ValueError(f"Text not found: '{xpath_result}'")
        if not isinstance(text, str):
            raise TypeError(f"Text not found or text not of type str: '{text}'")
        return text, list(regex.finditer(text))[-1]

    @staticmethod
    def _search_positive_position(
        xpath_result: list,
        regex: re.Pattern,
        position: int,
    ) -> tuple[str, re.Match]:
        # Found the last text that matches the regex
        count = 0
        for text in xpath_result:
            found_nb = len(regex.findall(str(text)))
            if found_nb + count >= position + 1:
                break
            count += found_nb
        else:
            raise ValueError(f"Text not found: '{xpath_result}'")
        if not isinstance(text, str):
            raise TypeError(f"Text not found or text not of type str: '{text}'")
        return text, list(regex.finditer(text))[position - count]

    def _insert_before_after(
        self,
        current: _Element,
        element: _Element,
        before: str | None,
        after: str | None,
        position: int,
        xpath_text: XPath,
    ) -> tuple[int, str]:
        regex = self._make_before_regex(before, after)
        xpath_result = xpath_text(current)
        if not isinstance(xpath_result, list):
            raise TypeError("Bad XPath result")
        # position = -1
        if position < 0:
            text, sre = self._search_negative_position(xpath_result, regex)
        # position >= 0
        else:
            text, sre = self._search_positive_position(xpath_result, regex, position)
        # Compute pos
        if before is None:
            pos = sre.end()
        else:
            pos = sre.start()
        return pos, text

    def _insert_find_text(
        self,
        current: _Element,
        element: _Element,
        before: str | None,
        after: str | None,
        position: int,
        xpath_text: XPath,
    ) -> tuple[int, str]:
        # Find the text
        xpath_result = xpath_text(current)
        if not isinstance(xpath_result, list):
            raise TypeError("Bad XPath result")
        count = 0
        for text in xpath_result:
            if not isinstance(text, str):
                continue
            found_nb = len(text)
            if found_nb + count >= position:
                break
            count += found_nb
        else:
            raise ValueError("Text not found")
        # We insert before the character
        pos = position - count
        return pos, text

    def _insert(
        self,
        element: Element,
        before: str | None = None,
        after: str | None = None,
        position: int = 0,
        main_text: bool = False,
    ) -> None:
        """Insert an element before or after the characters in the text which
        match the regex before/after.

        When the regex matches more of one part of the text, position can be
        set to choice which part must be used. If before and after are None,
        we use only position that is the number of characters. If position is
        positive and before=after=None, we insert before the position
        character. But if position=-1, we insert after the last character.


        Arguments:

        element -- Element

        before -- str regex

        after -- str regex

        position -- int
        """
        # not implemented: if main_text is True, filter out the annotations texts in computation.
        current = self.__element
        xelement = element.__element

        if main_text:
            xpath_text = _xpath_text_main_descendant
        else:
            xpath_text = _xpath_text_descendant

        # 1) before xor after is not None
        if (before is not None) ^ (after is not None):
            pos, text = self._insert_before_after(
                current,
                xelement,
                before,
                after,
                position,
                xpath_text,
            )
        # 2) before=after=None => only with position
        elif before is None and after is None:
            # Hack if position is negative => quickly
            if position < 0:
                current.append(xelement)
                return
            pos, text = self._insert_find_text(
                current,
                xelement,
                before,
                after,
                position,
                xpath_text,
            )
        else:
            raise ValueError("bad combination of arguments")

        # Compute new texts
        text_before = text[:pos] if text[:pos] else None
        text_after = text[pos:] if text[pos:] else None

        # Insert!
        parent = text.getparent()  # type: ignore
        if text.is_text:  # type: ignore
            parent.text = text_before
            element.tail = text_after
            parent.insert(0, xelement)
        else:
            parent.addnext(xelement)
            parent.tail = text_before
            element.tail = text_after

    def _insert_between(
        self,
        element: Element,
        from_: str,
        to: str,
    ) -> None:
        """Insert the given empty element to wrap the text beginning with
        "from_" and ending with "to".

        Example 1: '<p>toto tata titi</p>

        We want to insert a link around "tata".

        Result 1: '<p>toto <a>tata</a> titi</p>

        Example 2: '<p><span>toto</span> tata titi</p>

        We want to insert a link around "tata".

        Result 2: '<p><span>toto</span> <a>tata</a> titi</p>

        Example 3: '<p>toto <span> tata </span> titi</p>'

        We want to insert a link from "tata" to "titi" included.

        Result 3: '<p>toto <span> </span>'
                  '<a><span>tata </span> titi</a></p>'

        Example 4: '<p>toto <span>tata titi</span> tutu</p>'

        We want to insert a link from "titi" to "tutu"

        Result 4: '<p>toto <span>tata </span><a><span>titi</span></a>'
                  '<a> tutu</a></p>'

        Example 5: '<p>toto <span>tata titi</span> '
                   '<span>tutu tyty</span></p>'

        We want to insert a link from "titi" to "tutu"

        Result 5: '<p>toto <span>tata </span><a><span>titi</span><a> '
                  '<a> <span>tutu</span></a><span> tyty</span></p>'
        """
        current = self.__element
        wrapper = element.__element

        xpath_result = _xpath_text_descendant(current)
        if not isinstance(xpath_result, list):
            raise TypeError("Bad XPath result")

        for text in xpath_result:
            if not isinstance(text, str):
                raise TypeError("Text not found or text not of type str")
            if from_ not in text:
                continue
            from_index = text.index(from_)
            text_before = text[:from_index]
            text_after = text[from_index:]
            from_container = text.getparent()  # type: ignore
            if not isinstance(from_container, _Element):
                raise TypeError("Bad XPath result")
            # Include from_index to match a single word
            to_index = text.find(to, from_index)
            if to_index >= 0:
                # Simple case: "from" and "to" in the same element
                to_end = to_index + len(to)
                if text.is_text:  # type: ignore
                    from_container.text = text_before
                    wrapper.text = text[to_index:to_end]
                    wrapper.tail = text[to_end:]
                    from_container.insert(0, wrapper)
                else:
                    from_container.tail = text_before
                    wrapper.text = text[to_index:to_end]
                    wrapper.tail = text[to_end:]
                    parent = from_container.getparent()
                    index = parent.index(from_container)
                    parent.insert(index + 1, wrapper)
                return
            else:
                # Exit to the second part where we search for the end text
                break
        else:
            raise ValueError("Start text not found")

        # The container is split in two
        container2 = deepcopy(from_container)
        if text.is_text:  # type: ignore
            from_container.text = text_before
            from_container.tail = None
            container2.text = text_after
            from_container.tail = None
        else:
            from_container.tail = text_before
            container2.tail = text_after
        # Stack the copy into the surrounding element
        wrapper.append(container2)
        parent = from_container.getparent()
        index = parent.index(from_container)
        parent.insert(index + 1, wrapper)

        xpath_result = _xpath_text_descendant(wrapper)
        if not isinstance(xpath_result, list):
            raise TypeError("Bad XPath result")

        for text in xpath_result:
            if not isinstance(text, str):
                raise TypeError("Text not found or text not of type str")
            if to not in text:
                continue
            to_end = text.index(to) + len(to)
            text_before = text[:to_end]
            text_after = text[to_end:]
            container_to = text.getparent()  # type: ignore
            if not isinstance(container_to, _Element):
                raise TypeError("Bad XPath result")
            if text.is_text:  # type: ignore
                container_to.text = text_before
                container_to.tail = text_after
            else:
                container_to.tail = text_before
                next_one = container_to.getnext()
                if next_one is None:
                    next_one = container_to.getparent()
                next_one.tail = text_after
            return
        raise ValueError("End text not found")

    @property
    def tag(self) -> str:
        """Get/set the underlying xml tag with the given qualified name.

        Warning: direct change of tag does not change the element class.

        Arguments:

            qname -- str (e.g. "text:span")
        """
        return _get_prefixed_name(self.__element.tag)

    @tag.setter
    def tag(self, qname: str) -> None:
        self.__element.tag = _get_lxml_tag(qname)

    def elements_repeated_sequence(
        self,
        xpath_instance: XPath,
        name: str,
    ) -> list[tuple[int, int]]:
        """Utility method for table module."""
        lxml_tag = _get_lxml_tag_or_name(name)
        element = self.__element
        sub_elements = xpath_instance(element)
        if not isinstance(sub_elements, list):
            raise TypeError("Bad XPath result.")
        result: list[tuple[int, int]] = []
        idx = -1
        for sub_element in sub_elements:
            if not isinstance(sub_element, _Element):
                continue
            idx += 1
            value = sub_element.get(lxml_tag)
            if value is None:
                result.append((idx, 1))
                continue
            try:
                int_value = int(value)
            except ValueError:
                int_value = 1
            result.append((idx, max(int_value, 1)))
        return result

    def get_elements(self, xpath_query: XPath | str) -> list[Element]:
        if isinstance(xpath_query, str):
            new_xpath_query = xpath_compile(xpath_query)
            result = new_xpath_query(self.__element)
        else:
            result = xpath_query(self.__element)
        if not isinstance(result, list):
            raise TypeError("Bad XPath result")
        return [
            Element.from_tag_for_clone(e, None)
            for e in result
            if isinstance(e, _Element)
        ]

    def get_element(self, xpath_query: XPath | str) -> Element | None:
        result = self.__element.xpath(f"({xpath_query})[1]", namespaces=ODF_NAMESPACES)
        if result:
            return Element.from_tag(result[0])
        return None

    def _get_element_idx(self, xpath_query: XPath | str, idx: int) -> Element | None:
        element = self.__element
        result = element.xpath(f"({xpath_query})[{idx + 1}]", namespaces=ODF_NAMESPACES)
        if result:
            return Element.from_tag(result[0])
        return None

    def _get_element_idx2(self, xpath_instance: XPath, idx: int) -> Element | None:
        element = self.__element
        result = xpath_instance(element, idx=idx + 1)
        if result:
            return Element.from_tag(result[0])
        return None

    @property
    def attributes(self) -> dict[str, str]:
        return {
            _get_prefixed_name(str(key)): str(value)
            for key, value in self.__element.attrib.items()
        }

    def get_attribute(self, name: str) -> str | bool | None:
        """Return the attribute value as type str | bool | None."""
        element = self.__element
        lxml_tag = _get_lxml_tag_or_name(name)
        value = element.get(lxml_tag)
        if value is None:
            return None
        elif value in ("true", "false"):
            return Boolean.decode(value)
        return str(value)

    def get_attribute_integer(self, name: str) -> int | None:
        """Return either the attribute as type int, or None."""
        element = self.__element
        lxml_tag = _get_lxml_tag_or_name(name)
        value = element.get(lxml_tag)
        if value is None:
            return None
        try:
            return int(value)
        except ValueError:
            return None

    def get_attribute_string(self, name: str) -> str | None:
        """Return either the attribute as type str, or None."""
        element = self.__element
        lxml_tag = _get_lxml_tag_or_name(name)
        value = element.get(lxml_tag)
        if value is None:
            return None
        return str(value)

    def set_attribute(
        self, name: str, value: bool | str | tuple[int, int, int] | None
    ) -> None:
        if name in ODF_COLOR_PROPERTY:
            if isinstance(value, bool):
                raise TypeError(f"Wrong color type {value!r}")
            if value != "transparent":
                value = hexa_color(value)
        element = self.__element
        lxml_tag = _get_lxml_tag_or_name(name)
        if isinstance(value, bool):
            value = Boolean.encode(value)
        elif value is None:
            with contextlib.suppress(KeyError):
                del element.attrib[lxml_tag]
            return
        element.set(lxml_tag, str(value))

    def set_style_attribute(self, name: str, value: Element | str | None) -> None:
        """Shortcut to accept a style object as a value."""
        if isinstance(value, Element):
            value = str(value.name)  # type:ignore
        return self.set_attribute(name, value)

    def del_attribute(self, name: str) -> None:
        element = self.__element
        lxml_tag = _get_lxml_tag_or_name(name)
        del element.attrib[lxml_tag]

    @property
    def text(self) -> str:
        """Get / set the text content of the element."""
        return self.__element.text or ""

    @text.setter
    def text(self, text: str | None) -> None:
        if text is None:
            text = ""
        try:
            self.__element.text = text
        except TypeError as e:
            raise TypeError(f'Str type expected: "{type(text)}"') from e

    def __str__(self) -> str:
        return self.inner_text

    @property
    def _text_tail(self) -> str:
        return str(self) + (self.tail or "")

    # def _elements_descendants(self) -> Iterator[Element]:
    #     for elem in self.__element.iterdescendants():
    #         if isinstance(elem, _Element):
    #             yield Element.from_tag(elem)

    @property
    def inner_text(self) -> str:
        return self.text + "".join(e._text_tail for e in self.children)

    @property
    def text_recursive(self) -> str:
        return self.inner_text + (self.tail or "")

    @property
    def tail(self) -> str | None:
        """Get / set the text immediately following the element."""
        return self.__element.tail  # type: ignore[no-any-return]

    @tail.setter
    def tail(self, text: str | None) -> None:
        self.__element.tail = text or ""

    def search(self, pattern: str) -> int | None:
        """Return the first position of the pattern in the text content of
        the element, or None if not found.

        Python regular expression syntax applies.

        Arguments:

            pattern -- str

        Return: int or None
        """
        match = re.search(pattern, self.text_recursive)
        if match is None:
            return None
        return match.start()

    def search_first(self, pattern: str) -> tuple[int, int] | None:
        """Return the start and end position of the first occurence
        of the regex pattern in the text content of the element.

        Result is tuples of start and end position, or None.
        Python regular expression syntax applies.

        Arguments:

            pattern -- str

        Return: tuple[int,int] or None
        """
        match = re.search(pattern, self.text_recursive)
        if match is None:
            return None
        return match.start(), match.end()

    def search_all(self, pattern: str) -> list[tuple[int, int]]:
        """Return all start and end positions of the regex pattern in
        the text content of the element.

        Result is a list of tuples of start and end position of
        the matches.
        Python regular expression syntax applies.

        Arguments:

            pattern -- str

        Return: list[tuple[int,int]]
        """
        results: list[tuple[int, int]] = []
        for match in re.finditer(pattern, self.text_recursive):
            results.append((match.start(), match.end()))
        return results

    def text_at(self, start: int, end: int | None = None) -> str:
        """Return the text (recursive) content of the element between
        start and end position.

        If the end parameter is not set, return from start to the end
        of the recursive text.

        Arguments:

            start -- int
            end -- int or None

        Return: str
        """
        if start < 0:
            start = 0
        if end is None:
            return self.text_recursive[start:]
        else:
            if end < start:
                end = start
            return self.text_recursive[start:end]

    def match(self, pattern: str) -> bool:
        """return True if the pattern is found one or more times anywhere in
        the text content of the element.

        Python regular expression syntax applies.

        Arguments:

            pattern -- str

        Return: bool
        """
        return self.search(pattern) is not None

    def replace(
        self,
        pattern: str,
        new: str | None = None,
        formatted: bool = False,
    ) -> int:
        """Replace the pattern with the given text, or delete if text is an
        empty string, and return the number of replacements. By default, only
        return the number of occurences that would be replaced.

        It cannot replace patterns found across several element, like a word
        split into two consecutive spans.

        Python regular expression syntax applies.

        If formatted is True, and the target is a Paragraph, Span or Header,
        and the replacement text contains spaces, tabs or newlines, try to
        convert them into actual ODF elements to obtain a formatted result.
        On very complex contents, result may differ of expectations.

        Arguments:

            pattern -- str

            new -- str

            formatted -- bool

        Return: int
        """
        if not isinstance(pattern, str):
            # Fail properly if the pattern is an non-ascii bytestring
            pattern = str(pattern)
        cpattern = re.compile(pattern)
        count = 0
        for text in self.xpath("descendant::text()"):
            if new is None:
                count += len(cpattern.findall(str(text)))
            else:
                new_text, number = cpattern.subn(new, str(text))
                container = text.parent
                if not container:
                    continue
                if text.is_text():  # type: ignore
                    container.text = new_text
                else:
                    container.tail = new_text
                if formatted and container.tag in {  # type; ignore
                    "text:h",
                    "text:p",
                    "text:span",
                }:
                    container.append_plain_text("")  # type: ignore[attr-defined]
                count += number
        return count

    @property
    def root(self) -> Element:
        element = self.__element
        tree = element.getroottree()
        root = tree.getroot()
        return Element.from_tag(root)

    @property
    def parent(self) -> Element | None:
        element = self.__element
        parent = element.getparent()
        if parent is None:
            # Already at root
            return None
        return Element.from_tag(parent)

    @property
    def is_bound(self) -> bool:
        return self.parent is not None

    # def get_next_sibling(self):
    #     element = self.__element
    #     next_one = element.getnext()
    #     if next_one is None:
    #         return None
    #     return Element.from_tag(next_one)
    #
    # def get_prev_sibling(self):
    #     element = self.__element
    #     prev = element.getprevious()
    #     if prev is None:
    #         return None
    #     return Element.from_tag(prev)

    @property
    def children(self) -> list[Element]:
        element = self.__element
        return [
            Element.from_tag(e)
            for e in element.iterchildren()
            if isinstance(e, _Element)
        ]

    def index(self, child: Element) -> int:
        """Return the position of the child in this element.

        Inspired by lxml.
        """
        idx: int = self.__element.index(child.__element)
        return idx

    @property
    def text_content(self) -> str:
        """Get / set the text of the embedded paragraphs, including embeded
        annotations, cells...

        Set does create a paragraph if missing.
        """
        content = "".join(
            str(child) for child in self.get_elements("descendant::text:p")
        )
        if content.endswith("\n"):
            return content[:-1]
        return content

    @text_content.setter
    def text_content(self, text: str | Element | None) -> None:
        paragraphs = self.get_elements("text:p")
        if not paragraphs:
            # E.g., text:p in draw:text-box in draw:frame
            paragraphs = self.get_elements("*/text:p")
        if paragraphs:
            paragraph = paragraphs.pop(0)
            for obsolete in paragraphs:
                obsolete.delete()
        else:
            paragraph = Element.from_tag("text:p")
            self.insert(paragraph, FIRST_CHILD)
        # As "text_content" returned all text nodes, "text_content"
        # will overwrite all text nodes and children that may contain them
        element = paragraph.__element
        # Clear but the attributes
        del element[:]
        if text is None:
            text = ""
        element.text = str(text)

    def _erase_text_content(self) -> None:
        paragraphs = self.get_elements("text:p")
        if not paragraphs:
            # E.g., text:p in draw:text-box in draw:frame
            paragraphs = self.get_elements("*/text:p")
        if paragraphs:
            paragraphs.pop(0)
            for obsolete in paragraphs:
                obsolete.delete()

    def is_empty(self) -> bool:
        """Check if the element is empty : no text, no children, no tail.

        Return: Boolean
        """
        element = self.__element
        if element.tail is not None:
            return False
        if element.text is not None:
            return False
        if list(element.iterchildren()):  # noqa: SIM103
            return False
        return True

    def _get_successor(self, target: Element) -> tuple[Element | None, Element | None]:
        element = self.__element
        next_one = element.getnext()
        if next_one is not None:
            return Element.from_tag(next_one), target
        parent = self.parent
        if parent is None:
            return None, None
        return parent._get_successor(target.parent)  # type:ignore

    def _get_between_base(
        self,
        tag1: Element,
        tag2: Element,
    ) -> list[Element]:
        def find_any_id(elem: Element) -> tuple[str, str, str]:
            elem_tag = elem.tag
            for attribute in (
                "text:id",
                "text:change-id",
                "text:name",
                "office:name",
                "text:ref-name",
                "xml:id",
            ):
                idx = elem.get_attribute(attribute)
                if idx is not None:
                    return elem_tag, attribute, str(idx)
            raise ValueError(f"No Id found in {elem.serialize()}")

        def common_ancestor(
            tag1: str,
            attr1: str,
            val1: str,
            tag2: str,
            attr2: str,
            val2: str,
        ) -> Element | None:
            root = self.root
            request1 = f'descendant::{tag1}[@{attr1}="{val1}"]'
            request2 = f'descendant::{tag2}[@{attr2}="{val2}"]'
            ancestor = root.xpath(request1)[0]
            if ancestor is None:
                return None
            while True:
                # print "up",
                new_ancestor = ancestor.parent
                if new_ancestor is None:
                    return None
                has_tag2 = new_ancestor.xpath(request2)
                ancestor = new_ancestor
                if not has_tag2:
                    continue
                # print 'found'
                break
            # print up.serialize()
            return ancestor

        elem1_tag, elem1_attr, elem1_val = find_any_id(tag1)
        elem2_tag, elem2_attr, elem2_val = find_any_id(tag2)
        ancestor_result = common_ancestor(
            elem1_tag,
            elem1_attr,
            elem1_val,
            elem2_tag,
            elem2_attr,
            elem2_val,
        )
        if ancestor_result is None:
            raise RuntimeError(f"No common ancestor for {elem1_tag} {elem2_tag}")
        ancestor = ancestor_result.clone
        path1 = f'{elem1_tag}[@{elem1_attr}="{elem1_val}"]'
        path2 = f'{elem2_tag}[@{elem2_attr}="{elem2_val}"]'
        result = ancestor.clone
        for child in result.children:
            result.delete(child)
        result.text = ""
        result.tail = ""
        target = result
        current = ancestor.children[0]

        state = 0
        while True:
            if current is None:
                raise RuntimeError(f"No current ancestor for {elem1_tag} {elem2_tag}")
            # print 'current', state, current.serialize()
            if state == 0:  # before tag 1
                if current.xpath(f"descendant-or-self::{path1}"):
                    if current.xpath(f"self::{path1}"):
                        tail = current.tail
                        if tail:
                            # got a tail => the parent should be either t:p or t:h
                            target.text = tail
                        current, target = current._get_successor(target)  # type: ignore
                        state = 1
                        continue
                    # got T1 in chidren, need further analysis
                    new_target = current.clone
                    for child in new_target.children:
                        new_target.delete(child)
                    new_target.text = ""
                    new_target.tail = ""
                    target.__append(new_target)
                    target = new_target
                    current = current.children[0]
                    continue
                else:
                    # before tag1 : forget element, go to next one
                    current, target = current._get_successor(target)  # type: ignore
                    continue
            elif state == 1:  # collect elements
                further = False
                if current.xpath(f"descendant-or-self::{path2}"):
                    if current.xpath(f"self::{path2}"):
                        # end of trip
                        break
                    # got T2 in chidren, need further analysis
                    further = True
                # further analysis needed :
                if further:
                    new_target = current.clone
                    for child in new_target.children:
                        new_target.delete(child)
                    new_target.text = ""
                    new_target.tail = ""
                    target.__append(new_target)
                    target = new_target
                    current = current.children[0]
                    continue
                # collect
                target.__append(current.clone)
                current, target = current._get_successor(target)  # type: ignore
                continue
        # Now resu should be the "parent" of inserted parts
        # - a text:h or text:p sigle item (simple case)
        # - a upper element, with some text:p, text:h in it => need to be
        #   stripped to have a list of text:p, text:h
        if result.tag in {"text:p", "text:h"}:
            inner = [result]
        else:
            inner = result.children
        return inner

    def get_between(
        self,
        tag1: Element,
        tag2: Element,
        as_text: bool = False,
        clean: bool = True,
        no_header: bool = True,
    ) -> list | str:
        """Returns elements between tag1 and tag2, tag1 and tag2 shall
        be unique and having an id attribute.
        (WARN: buggy if tag1/tag2 defines a malformed odf xml.)
        If as_text is True: returns the text content.
        If clean is True: suppress unwanted tags (deletions marks, ...)
        If no_header is True: existing text:h are changed in text:p
        By default: returns a list of Element, cleaned and without headers.

        Implementation and standard retrictions:
        Only text:h and text:p sould be 'cut' by an insert tag, so inner parts
        of insert tags are:

            - any text:h, text:p or sub tag of these

            - some text, part of a parent text:h or text:p

        Arguments:

            tag1 -- Element

            tag2 -- Element

            as_text -- boolean

            clean -- boolean

            no_header -- boolean

        Return: list of odf_paragraph or odf_header
        """
        inner = self._get_between_base(tag1, tag2)

        if clean:
            clean_tags = (
                "text:change",
                "text:change-start",
                "text:change-end",
                "text:reference-mark",
                "text:reference-mark-start",
                "text:reference-mark-end",
            )
            request_self = " | ".join([f"self::{tag}" for tag in clean_tags])
            inner = [e for e in inner if not e.xpath(request_self)]
            request = " | ".join([f"descendant::{tag}" for tag in clean_tags])
            for element in inner:
                to_del = element.xpath(request)
                for elem in to_del:
                    if isinstance(elem, Element):
                        element.delete(elem)
        if no_header:  # crude replace t:h by t:p
            new_inner = []
            for element in inner:
                if element.tag == "text:h":
                    children = element.children
                    text = element.__element.text
                    para = Element.from_tag("text:p")
                    para.text = text or ""
                    for child in children:
                        para.__append(child)
                    new_inner.append(para)
                else:
                    new_inner.append(element)
            inner = new_inner
        if as_text:
            return "\n".join([e.get_formatted_text() for e in inner])
        else:
            return inner

    def insert(
        self,
        element: Element,
        xmlposition: int | None = None,
        position: int | None = None,
        start: bool = False,
    ) -> None:
        """Insert an element relatively to ourself.

        Insert either using DOM vocabulary or by numeric position.
        If text start is True, insert the element before any existing text.

        Position start at 0.

        Arguments:

            element -- Element

            xmlposition -- FIRST_CHILD, LAST_CHILD, NEXT_SIBLING
                           or PREV_SIBLING

            start -- Boolean

            position -- int
        """
        # child_tag = element.tag
        current = self.__element
        lx_element = element.__element
        if start:
            text = current.text
            if text is not None:
                current.text = None
                tail = lx_element.tail
                if tail is None:
                    tail = text
                else:
                    tail = tail + text
                lx_element.tail = tail
            position = 0
        if position is not None:
            current.insert(position, lx_element)
        elif xmlposition is FIRST_CHILD:
            current.insert(0, lx_element)
        elif xmlposition is LAST_CHILD:
            current.append(lx_element)
        elif xmlposition is NEXT_SIBLING:
            parent = current.getparent()
            index = parent.index(current)
            parent.insert(index + 1, lx_element)
        elif xmlposition is PREV_SIBLING:
            parent = current.getparent()
            index = parent.index(current)
            parent.insert(index, lx_element)
        else:
            raise ValueError("(xml)position must be defined")

    def extend(self, odf_elements: Iterable[Element]) -> None:
        """Fast append elements at the end of ourself using extend."""
        if odf_elements:
            current = self.__element
            elements = [element.__element for element in odf_elements]
            current.extend(elements)

    @staticmethod
    def _add_text(text1: str | None, text2: str | None) -> str:
        if text1 is None:
            text1 = ""
        if text2 is None:
            text2 = ""
        return _re_anyspace.sub(" ", text1 + text2)

    def _cut_text_tail(self) -> str:
        removed = ""
        current = self.__element
        children = list(current.iterchildren())
        if children:
            # Append to tail of the last child
            last_child = children[-1]
            if last_child.tail:
                removed = last_child.tail
                last_child.tail = ""
        else:
            removed = current.text or ""
            current.text = ""
        return removed

    def __append(self, str_or_element: str | Element) -> None:
        """Insert element or text in the last position."""
        current = self.__element
        if isinstance(str_or_element, str):
            # Has children ?
            children = list(current.iterchildren())
            if children:
                # Append to tail of the last child
                last_child = children[-1]
                last_child.tail = self._add_text(last_child.tail, str_or_element)
            else:
                # Append to text of the element
                current.text = self._add_text(current.text, str_or_element)
        elif isinstance(str_or_element, Element):
            current.append(str_or_element.__element)
        else:
            raise TypeError(f'Element or string expected, not "{type(str_or_element)}"')

    append = __append

    def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
        """Delete the given element from the XML tree. If no element is given,
        "self" is deleted. The XML library may allow to continue to use an
        element now "orphan" as long as you have a reference to it.

        if keep_tail is True (default), the tail text is not erased.

        Arguments:

            child -- Element

            keep_tail -- boolean (default to True), True for most usages.
        """
        if child is None:
            parent = self.parent
            if parent is None:
                raise ValueError(f"Can't delete the root element\n{self.serialize()}")
            child = self
        else:
            parent = self
        if keep_tail and child.__element.tail is not None:
            current = child.__element
            tail = str(current.tail)
            current.tail = None
            prev = current.getprevious()
            if prev is not None:
                if prev.tail is None:
                    prev.tail = tail
                else:
                    prev.tail += tail
            else:
                if parent.__element.text is None:
                    parent.__element.text = tail
                else:
                    parent.__element.text += tail
        parent.__element.remove(child.__element)

    def replace_element(self, old_element: Element, new_element: Element) -> None:
        """Replaces in place a sub element with the element passed as second
        argument.

        Warning : no clone for old element.
        """
        current = self.__element
        current.replace(old_element.__element, new_element.__element)

    def strip_elements(
        self,
        sub_elements: Element | Iterable[Element],
    ) -> Element | list:
        """Remove the tags of provided elements, keeping inner childs and text.

        Return : the striped element.

        Warning : no clone in sub_elements list.

        Arguments:

            sub_elements -- Element or list of Element
        """
        if not sub_elements:
            return self
        if isinstance(sub_elements, Element):
            sub_elements = (sub_elements,)
        replacer = _get_lxml_tag("text:this-will-be-removed")
        for element in sub_elements:
            element.__element.tag = replacer
        strip = ("text:this-will-be-removed",)
        return self.strip_tags(strip=strip, default=None)

    def strip_tags(
        self,
        strip: Iterable[str] | None = None,
        protect: Iterable[str] | None = None,
        default: str | None = "text:p",
    ) -> Element | list:
        """Remove the tags listed in strip, recursively, keeping inner childs
        and text. Tags listed in protect stop the removal one level depth. If
        the first level element is stripped, default is used to embed the
        content in the default element. If default is None and first level is
        striped, a list of text and children is returned. Return : the striped
        element.

        strip_tags should be used by on purpose methods (strip_span ...)
        (Method name taken from lxml).

        Arguments:

            strip -- iterable list of str odf tags, or None

            protect -- iterable list of str odf tags, or None

            default -- str odf tag, or None

        Return:

            Element.
        """
        if not strip:
            return self
        if not protect:
            protect = ()
        protected = False
        element, modified = Element._strip_tags(self, strip, protect, protected)
        if modified and isinstance(element, list) and default:
            new = Element.from_tag(default)
            for content in element:
                if isinstance(content, Element):
                    new.__append(content)
                else:
                    new.text = content
            element = new
        return element

    @staticmethod
    def _strip_tags(
        element: Element,
        strip: Iterable[str],
        protect: Iterable[str],
        protected: bool,
    ) -> tuple[Element | list, bool]:
        """Sub method for strip_tags()."""
        element_clone = element.clone
        modified = False
        children = []
        if protect and element.tag in protect:
            protect_below = True
        else:
            protect_below = False
        for child in element_clone.children:
            striped_child, is_modified = Element._strip_tags(
                child, strip, protect, protect_below
            )
            if is_modified:
                modified = True
            if isinstance(striped_child, list):
                children.extend(striped_child)
            else:
                children.append(striped_child)

        text = element_clone.text
        tail = element_clone.tail
        if not protected and strip and element.tag in strip:
            element_result: list[Element | str] = []
            if text is not None:
                element_result.append(text)
            for child in children:
                element_result.append(child)
            if tail is not None:
                element_result.append(tail)
            return (element_result, True)
        else:
            if not modified:
                return (element, False)
            element.clear()
            try:
                for key, value in element_clone.attributes.items():
                    element.set_attribute(key, value)
            except ValueError:
                sys.stderr.write(f"strip_tags(): bad attribute in {element_clone}\n")
            if text is not None:
                element.__append(text)
            for child in children:
                element.__append(child)
            if tail is not None:
                element.tail = tail
            return (element, True)

    def xpath(self, xpath_query: str) -> list[Element | EText]:
        """Apply XPath query to the element and its subtree. Return list of
        Element or EText instances translated from the nodes found.
        """
        element = self.__element
        xpath_instance = xpath_compile(xpath_query)
        elements = xpath_instance(element)
        result: list[Element | EText] = []
        if hasattr(elements, "__iter__"):
            for obj in elements:
                if isinstance(obj, (str, bytes)):
                    result.append(EText(obj))
                elif isinstance(obj, _Element):
                    result.append(Element.from_tag(obj))
                # else:
                #     result.append(obj)
        return result

    def clear(self) -> None:
        """Remove text, children and attributes from the element."""
        self.__element.clear()

    @property
    def clone(self) -> Element:
        clone = deepcopy(self.__element)
        root = lxml_Element("ROOT", nsmap=ODF_NAMESPACES)
        root.append(clone)
        return self.from_tag(clone)

        # slow data = tostring(self.__element, encoding='unicode')
        # return self.from_tag(data)

    @staticmethod
    def _strip_namespaces(data: str) -> str:
        """Remove xmlns:* fields from serialized XML."""
        return re.sub(r' xmlns:\w*="[\w:\-\/\.#]*"', "", data)

    def serialize(self, pretty: bool = False, with_ns: bool = False) -> str:
        """Return text serialization of XML element."""
        # This copy bypasses serialization side-effects in lxml
        native = deepcopy(self.__element)
        data: str = tostring(
            native, with_tail=False, pretty_print=pretty, encoding="unicode"
        )
        if with_ns:
            return data
        # Remove namespaces
        return self._strip_namespaces(data)

    # Element helpers usable from any context

    @property
    def document_body(self) -> Body | None:
        """Return the first children of document body if any: 'office:body/*[1]'"""
        return self.get_element("//office:body/*[1]")  # type: ignore[return-value]

    def get_formatted_text(self, context: dict | None = None) -> str:
        """This function should return a beautiful version of the text."""
        return ""

    def get_styled_elements(self, name: str = "") -> list[Element]:
        """Brute-force to find paragraphs, tables, etc. using the given style
        name (or all by default).

        Arguments:

            name -- str

        Return: list of Element
        """
        # FIXME incomplete (and possibly inaccurate)
        return (
            self._filtered_elements("descendant::*", text_style=name)
            + self._filtered_elements("descendant::*", draw_style=name)
            + self._filtered_elements("descendant::*", draw_text_style=name)
            + self._filtered_elements("descendant::*", table_style=name)
            + self._filtered_elements("descendant::*", page_layout=name)
            + self._filtered_elements("descendant::*", master_page=name)
            + self._filtered_elements("descendant::*", parent_style=name)
        )

    # Common attributes

    def _get_inner_text(self, tag: str) -> str | None:
        element = self.get_element(tag)
        if element is None:
            return None
        return element.text

    def _set_inner_text(self, tag: str, text: str) -> None:
        element = self.get_element(tag)
        if element is None:
            element = Element.from_tag(tag)
            self.__append(element)
        element.text = text

    # SVG

    @property
    def svg_title(self) -> str | None:
        return self._get_inner_text("svg:title")

    @svg_title.setter
    def svg_title(self, title: str) -> None:
        self._set_inner_text("svg:title", title)

    @property
    def svg_description(self) -> str | None:
        return self._get_inner_text("svg:desc")

    @svg_description.setter
    def svg_description(self, description: str) -> None:
        self._set_inner_text("svg:desc", description)

    # Sections

    def get_sections(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Section]:
        """Return all the sections that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Section
        """
        return self._filtered_elements(
            "text:section", text_style=style, content=content
        )  # type: ignore[return-value]

    @property
    def sections(
        self,
    ) -> list[Section]:
        """Return all the sections.

        Return: list of Section
        """
        return self.get_elements("text:section")  # type: ignore[return-value]

    def get_section(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Section | None:
        """Return the section that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: Section or None if not found
        """
        return self._filtered_element(
            "descendant::text:section", position, content=content
        )  # type: ignore[return-value]

    # Paragraphs

    def get_paragraphs(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Paragraph]:
        """Return all the paragraphs that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Paragraph
        """
        return self._filtered_elements(
            "descendant::text:p", text_style=style, content=content
        )  # type: ignore[return-value]

    @property
    def paragraphs(self) -> list[Paragraph]:
        """Return all the paragraphs.

        Return: list of Paragraph
        """
        return self.get_elements(
            "descendant::text:p",
        )  # type: ignore[return-value]

    def get_paragraph(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Paragraph | None:
        """Return the paragraph that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: Paragraph or None if not found
        """
        return self._filtered_element(
            "descendant::text:p",
            position,
            content=content,
        )  # type: ignore[return-value]

    # Span

    def get_spans(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Span]:
        """Return all the spans that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Span
        """
        return self._filtered_elements(
            "descendant::text:span", text_style=style, content=content
        )  # type: ignore[return-value]

    @property
    def spans(self) -> list[Span]:
        """Return all the spans.

        Return: list of Span
        """
        return self.get_elements("descendant::text:span")  # type: ignore[return-value]

    def get_span(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Span | None:
        """Return the span that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: Span or None if not found
        """
        return self._filtered_element(
            "descendant::text:span", position, content=content
        )  # type: ignore[return-value]

    # Headers

    def get_headers(
        self,
        style: str | None = None,
        outline_level: str | None = None,
        content: str | None = None,
    ) -> list[Header]:
        """Return all the Headers that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Header
        """
        return self._filtered_elements(
            "descendant::text:h",
            text_style=style,
            outline_level=outline_level,
            content=content,
        )  # type: ignore[return-value]

    @property
    def headers(self) -> list[Header]:
        """Return all the Headers.

        Return: list of Header
        """
        return self.get_elements("descendant::text:h")  # type: ignore[return-value]

    def get_header(
        self,
        position: int = 0,
        outline_level: str | None = None,
        content: str | None = None,
    ) -> Header | None:
        """Return the Header that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: Header or None if not found
        """
        return self._filtered_element(
            "descendant::text:h",
            position,
            outline_level=outline_level,
            content=content,
        )  # type: ignore[return-value]

    # Lists

    def get_lists(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[List]:
        """Return all the lists that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of List
        """
        return self._filtered_elements(
            "descendant::text:list", text_style=style, content=content
        )  # type: ignore[return-value]

    @property
    def lists(self) -> list[List]:
        """Return all the lists.

        Return: list of List
        """
        return self.get_elements("descendant::text:list")  # type: ignore[return-value]

    def get_list(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> List | None:
        """Return the list that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: List or None if not found
        """
        return self._filtered_element(
            "descendant::text:list", position, content=content
        )  # type: ignore[return-value]

    # Frames

    def get_frames(
        self,
        presentation_class: str | None = None,
        style: str | None = None,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> list[Frame]:
        """Return all the frames that match the criteria.

        Arguments:

            presentation_class -- str

            style -- str

            title -- str regex

            description -- str regex

            content -- str regex

        Return: list of Frame
        """
        return self._filtered_elements(
            "descendant::draw:frame",
            presentation_class=presentation_class,
            draw_style=style,
            svg_title=title,
            svg_desc=description,
            content=content,
        )  # type: ignore[return-value]

    @property
    def frames(self) -> list[Frame]:
        """Return all the frames.

        Return: list of Frame
        """
        return self.get_elements("descendant::draw:frame")  # type: ignore[return-value]

    def get_frame(
        self,
        position: int = 0,
        name: str | None = None,
        presentation_class: str | None = None,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> Frame | None:
        """Return the section that matches the criteria.

        Arguments:

            position -- int

            name -- str

            presentation_class -- str

            title -- str regex

            description -- str regex

            content -- str regex

        Return: Frame or None if not found
        """
        return self._filtered_element(
            "descendant::draw:frame",
            position,
            draw_name=name,
            presentation_class=presentation_class,
            svg_title=title,
            svg_desc=description,
            content=content,
        )  # type: ignore[return-value]

    # Images

    def get_images(
        self,
        style: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> list[DrawImage]:
        """Return all the images matching the criteria.

        Arguments:

            style -- str

            url -- str regex

            content -- str regex

        Return: list of DrawImage
        """
        return self._filtered_elements(
            "descendant::draw:image", text_style=style, url=url, content=content
        )  # type: ignore[return-value]

    @property
    def images(self) -> list[DrawImage]:
        """Return all the images.

        Return: list of DrawImage
        """
        return self.get_elements("descendant::draw:image")  # type: ignore[return-value]

    def get_image(
        self,
        position: int = 0,
        name: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> DrawImage | None:
        """Return the image matching the criteria.

        Arguments:

            position -- int

            name -- str

            url -- str regex

            content -- str regex

        Return: DrawImage or None if not found
        """
        # The frame is holding the name
        if name is not None:
            frame = self._filtered_element(
                "descendant::draw:frame", position, draw_name=name
            )
            if frame is None:
                return None
            # The name is supposedly unique
            return frame.get_element("draw:image")  # type: ignore[return-value]
        return self._filtered_element(
            "descendant::draw:image", position, url=url, content=content
        )  # type: ignore[return-value]

    # Named Range

    def get_named_ranges(self) -> list[NamedRange]:
        """Return all the tables named ranges.

        Return: list of NamedRange
        """
        named_ranges = self.get_elements(
            "descendant::table:named-expressions/table:named-range"
        )
        return named_ranges  # type: ignore[return-value]

    def get_named_range(self, name: str) -> NamedRange | None:
        """Return the named range of specified name, or None if not found.

        Arguments:

            name -- str

        Return: NamedRange
        """
        named_range = self.get_elements(
            f'descendant::table:named-expressions/table:named-range[@table:name="{name}"][1]'
        )
        if named_range:
            return named_range[0]  # type: ignore[return-value]
        else:
            return None

    def append_named_range(self, named_range: NamedRange) -> None:
        """Append the named range to the spreadsheet, replacing existing named
        range of same name if any.

        Arguments:

            named_range --  NamedRange
        """
        if self.tag != "office:spreadsheet":
            raise ValueError(f"Element is no 'office:spreadsheet' : {self.tag}")
        named_expressions = self.get_element("table:named-expressions")
        if not named_expressions:
            named_expressions = Element.from_tag("table:named-expressions")
            self.__append(named_expressions)
        # exists ?
        current = named_expressions.get_element(
            f'table:named-range[@table:name="{named_range.name}"][1]'
        )
        if current:
            named_expressions.delete(current)
        named_expressions.__append(named_range)

    def delete_named_range(self, name: str) -> None:
        """Delete the Named Range of specified name from the spreadsheet.

        Arguments:

            name -- str
        """
        if self.tag != "office:spreadsheet":
            raise ValueError(f"Element is no 'office:spreadsheet' : {self.tag}")
        named_range = self.get_named_range(name)
        if not named_range:
            return
        named_range.delete()
        named_expressions = self.get_element("table:named-expressions")
        if not named_expressions:
            return
        element = named_expressions.__element
        children = list(element.iterchildren())
        if not children:
            self.delete(named_expressions)

    # Notes

    def get_notes(
        self,
        note_class: str | None = None,
        content: str | None = None,
    ) -> list[Note]:
        """Return all the notes that match the criteria.

        Arguments:

            note_class -- 'footnote' or 'endnote'

            content -- str regex

        Return: list of Note
        """
        return self._filtered_elements(
            "descendant::text:note", note_class=note_class, content=content
        )  # type: ignore[return-value]

    def get_note(
        self,
        position: int = 0,
        note_id: str | None = None,
        note_class: str | None = None,
        content: str | None = None,
    ) -> Note | None:
        """Return the note that matches the criteria.

        Arguments:

            position -- int

            note_id -- str

            note_class -- 'footnote' or 'endnote'

            content -- str regex

        Return: Note or None if not found
        """
        return self._filtered_element(
            "descendant::text:note",
            position,
            text_id=note_id,
            note_class=note_class,
            content=content,
        )  # type: ignore[return-value]

    # Annotations

    def get_annotations(
        self,
        creator: str | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        content: str | None = None,
    ) -> list[Annotation]:
        """Return all the annotations that match the criteria.

        Arguments:

            creator -- str

            start_date -- datetime instance

            end_date --  datetime instance

            content -- str regex

        Return: list of Annotation
        """
        annotations: list[Annotation] = []
        for annotation in self._filtered_elements(
            "descendant::office:annotation", content=content
        ):
            if creator is not None and creator != annotation.dc_creator:  # type: ignore[attr-defined]
                continue
            date = annotation.date  # type: ignore[attr-defined]
            if date is None:
                continue
            if start_date is not None and date < start_date:
                continue
            if end_date is not None and date >= end_date:
                continue
            annotations.append(annotation)  # type: ignore[arg-type]
        return annotations

    def get_annotation(
        self,
        position: int = 0,
        creator: str | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        content: str | None = None,
        name: str | None = None,
    ) -> Annotation | None:
        """Return the annotation that matches the criteria.

        Arguments:

            position -- int

            creator -- str

            start_date -- datetime instance

            end_date -- datetime instance

            content -- str regex

            name -- str

        Return: Annotation or None if not found
        """
        if name is not None:
            return self._filtered_element(
                "descendant::office:annotation", 0, office_name=name
            )  # type: ignore[return-value]
        annotations: list[Annotation] = self.get_annotations(
            creator=creator, start_date=start_date, end_date=end_date, content=content
        )
        if not annotations:
            return None
        try:
            return annotations[position]
        except IndexError:
            return None

    def get_annotation_ends(self) -> list[AnnotationEnd]:
        """Return all the annotation ends.

        Return: list of AnnotationEnd
        """
        return self._filtered_elements(
            "descendant::office:annotation-end",
        )  # type: ignore[return-value]

    def get_annotation_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> AnnotationEnd | None:
        """Return the annotation end that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: AnnotationEnd or None if not found
        """
        return self._filtered_element(
            "descendant::office:annotation-end", position, office_name=name
        )  # type: ignore[return-value]

    # office:names

    def get_office_names(self) -> list[str]:
        """Return all the used office:name tags values of the element.

        Return: list of unique str
        """
        name_xpath_query = xpath_compile("//@office:name")
        response = name_xpath_query(self.__element)
        if not isinstance(response, list):
            return []
        return list({str(name) for name in response if name})

    # Variables

    def get_variable_decls(self) -> VarDecls:
        """Return the container for variable declarations. Created if not
        found.

        Return: VarDecls
        """
        variable_decls = self.get_element("//text:variable-decls")
        if variable_decls is None:
            body = self.document_body
            if not body:
                raise ValueError("Empty document.body")
            body.insert(Element.from_tag("text:variable-decls"), FIRST_CHILD)
            variable_decls = body.get_element("//text:variable-decls")

        return variable_decls  # type: ignore[return-value]

    def get_variable_decl_list(self) -> list[VarDecls]:
        """Return all the variable declarations.

        Return: list of VarDecls
        """
        return self._filtered_elements(
            "descendant::text:variable-decl",
        )  # type: ignore[return-value]

    def get_variable_decl(self, name: str, position: int = 0) -> VarDecls | None:
        """return the variable declaration for the given name.

        Arguments:

            name -- str

            position -- int

        return: VarDecls or none if not found
        """
        return self._filtered_element(
            "descendant::text:variable-decl", position, text_name=name
        )  # type: ignore[return-value]

    def get_variable_sets(self, name: str | None = None) -> list[VarSet]:
        """Return all the variable sets that match the criteria.

        Arguments:

            name -- str

        Return: list of VarSet
        """
        return self._filtered_elements(
            "descendant::text:variable-set",
            text_name=name,
        )  # type: ignore[return-value]

    def get_variable_set(self, name: str, position: int = -1) -> VarSet | None:
        """Return the variable set for the given name (last one by default).

        Arguments:

            name -- str

            position -- int

        Return: VarSet or None if not found
        """
        return self._filtered_element(
            "descendant::text:variable-set", position, text_name=name
        )  # type: ignore[return-value]

    def get_variable_set_value(
        self,
        name: str,
        value_type: str | None = None,
    ) -> bool | str | int | float | Decimal | datetime | timedelta | None:
        """Return the last value of the given variable name.

        Arguments:

            name -- str

            value_type -- 'boolean', 'currency', 'date', 'float',
                          'percentage', 'string', 'time' or automatic

        Return: most appropriate Python type
        """
        variable_set = self.get_variable_set(name)
        if not variable_set:
            return None
        return variable_set.get_value(value_type)  # type: ignore[return-value]

    # User fields

    def get_user_field_decls(self) -> UserFieldDecls | None:
        """Return the container for user field declarations. Created if not
        found.

        Return: UserFieldDecls
        """
        user_field_decls = self.get_element("//text:user-field-decls")
        if user_field_decls is None:
            body = self.document_body
            if not body:
                raise ValueError("Empty document.body")
            body.insert(Element.from_tag("text:user-field-decls"), FIRST_CHILD)
            user_field_decls = body.get_element("//text:user-field-decls")

        return user_field_decls  # type: ignore[return-value]

    def get_user_field_decl_list(self) -> list[UserFieldDecls]:
        """Return all the user field declarations.

        Return: list of UserFieldDecls
        """
        return self._filtered_elements(
            "descendant::text:user-field-decl",
        )  # type: ignore[return-value]

    def get_user_field_decl(
        self, name: str, position: int = 0
    ) -> UserFieldDecls | None:
        """return the user field declaration for the given name.

        return: Element or none if not found
        """
        return self._filtered_element(
            "descendant::text:user-field-decl", position, text_name=name
        )  # type: ignore[return-value]

    def get_user_field_value(
        self, name: str, value_type: str | None = None
    ) -> bool | str | int | float | Decimal | datetime | timedelta | None:
        """Return the value of the given user field name.

        Arguments:

            name -- str

            value_type -- 'boolean', 'currency', 'date', 'float',
                          'percentage', 'string', 'time' or automatic

        Return: most appropriate Python type
        """
        user_field_decl = self.get_user_field_decl(name)
        if user_field_decl is None:
            return None
        value = user_field_decl.get_value(value_type)  # type: ignore[attr-defined]
        return value  # type: ignore[no-any-return]

    # User defined fields
    # They are fields who should contain a copy of a user defined medtadata

    def get_user_defined_list(self) -> list[UserDefined]:
        """Return all the user defined field declarations.

        Return: list of UserDefined
        """
        return self._filtered_elements(
            "descendant::text:user-defined",
        )  # type: ignore[return-value]

    @property
    def user_defined_list(self) -> list[UserDefined]:
        """Return all the user defined field declarations.

        Return: list of UserDefined
        """
        return self.get_user_defined_list()

    def get_user_defined(self, name: str, position: int = 0) -> UserDefined | None:
        """return the user defined declaration for the given name.

        return: UserDefined or none if not found
        """
        return self._filtered_element(
            "descendant::text:user-defined", position, text_name=name
        )  # type: ignore[return-value]

    def get_user_defined_value(
        self, name: str, value_type: str | None = None
    ) -> bool | str | int | float | Decimal | datetime | timedelta | None:
        """Return the value of the given user defined field name.

        Arguments:

            name -- str

            value_type -- 'boolean', 'date', 'float',
                          'string', 'time' or automatic

        Return: most appropriate Python type
        """
        user_defined = self.get_user_defined(name)
        if user_defined is None:
            return None
        return user_defined.get_value(value_type)  # type: ignore[return-value]

    # Draw Pages

    def get_draw_pages(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[DrawPage]:
        """Return all the draw pages that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of DrawPage
        """
        return self._filtered_elements(
            "descendant::draw:page", draw_style=style, content=content
        )  # type: ignore[return-value]

    def get_draw_page(
        self,
        position: int = 0,
        name: str | None = None,
        content: str | None = None,
    ) -> DrawPage | None:
        """Return the draw page that matches the criteria.

        Arguments:

            position -- int

            name -- str

            content -- str regex

        Return: DrawPage or None if not found
        """
        return self._filtered_element(
            "descendant::draw:page", position, draw_name=name, content=content
        )  # type: ignore[return-value]

    # Links

    def get_links(
        self,
        name: str | None = None,
        title: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> list[Link]:
        """Return all the links that match the criteria.

        Arguments:

            name -- str

            title -- str

            url -- str regex

            content -- str regex

        Return: list of Link
        """
        return self._filtered_elements(
            "descendant::text:a",
            office_name=name,
            office_title=title,
            url=url,
            content=content,
        )  # type: ignore[return-value]

    def get_link(
        self,
        position: int = 0,
        name: str | None = None,
        title: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> Link | None:
        """Return the link that matches the criteria.

        Arguments:

            position -- int

            name -- str

            title -- str

            url -- str regex

            content -- str regex

        Return: Link or None if not found
        """
        return self._filtered_element(
            "descendant::text:a",
            position,
            office_name=name,
            office_title=title,
            url=url,
            content=content,
        )  # type: ignore[return-value]

    # Bookmarks

    def get_bookmarks(self) -> list[Bookmark]:
        """Return all the bookmarks.

        Return: list of Bookmark
        """
        return self._filtered_elements(
            "descendant::text:bookmark",
        )  # type: ignore[return-value]

    def get_bookmark(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Bookmark | None:
        """Return the bookmark that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: Bookmark or None if not found
        """
        return self._filtered_element(
            "descendant::text:bookmark", position, text_name=name
        )  # type: ignore[return-value]

    def get_bookmark_starts(self) -> list[BookmarkStart]:
        """Return all the bookmark starts.

        Return: list of BookmarkStart
        """
        return self._filtered_elements(
            "descendant::text:bookmark-start",
        )  # type: ignore[return-value]

    def get_bookmark_start(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> BookmarkStart | None:
        """Return the bookmark start that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: BookmarkStart or None if not found
        """
        return self._filtered_element(
            "descendant::text:bookmark-start", position, text_name=name
        )  # type: ignore[return-value]

    def get_bookmark_ends(self) -> list[BookmarkEnd]:
        """Return all the bookmark ends.

        Return: list of BookmarkEnd
        """
        return self._filtered_elements(
            "descendant::text:bookmark-end",
        )  # type: ignore[return-value]

    def get_bookmark_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> BookmarkEnd | None:
        """Return the bookmark end that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: BookmarkEnd or None if not found
        """
        return self._filtered_element(
            "descendant::text:bookmark-end", position, text_name=name
        )  # type: ignore[return-value]

    # Reference marks

    def get_reference_marks_single(self) -> list[ReferenceMark]:
        """Return all the reference marks. Search only the tags
        text:reference-mark.
        Consider using : get_reference_marks()

        Return: list of ReferenceMark
        """
        return self._filtered_elements(
            "descendant::text:reference-mark",
        )  # type: ignore[return-value]

    def get_reference_mark_single(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> ReferenceMark | None:
        """Return the reference mark that matches the criteria. Search only the
        tags text:reference-mark.
        Consider using : get_reference_mark()

        Arguments:

            position -- int

            name -- str

        Return: ReferenceMark or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark", position, text_name=name
        )  # type: ignore[return-value]

    def get_reference_mark_starts(self) -> list[ReferenceMarkStart]:
        """Return all the reference mark starts. Search only the tags
        text:reference-mark-start.
        Consider using : get_reference_marks()

        Return: list of ReferenceMarkStart
        """
        return self._filtered_elements(
            "descendant::text:reference-mark-start",
        )  # type: ignore[return-value]

    def get_reference_mark_start(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> ReferenceMarkStart | None:
        """Return the reference mark start that matches the criteria. Search
        only the tags text:reference-mark-start.
        Consider using : get_reference_mark()

        Arguments:

            position -- int

            name -- str

        Return: ReferenceMarkStart or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark-start", position, text_name=name
        )  # type: ignore[return-value]

    def get_reference_mark_ends(self) -> list[ReferenceMarkEnd]:
        """Return all the reference mark ends. Search only the tags
        text:reference-mark-end.
        Consider using : get_reference_marks()

        Return: list of ReferenceMarkEnd
        """
        return self._filtered_elements(
            "descendant::text:reference-mark-end",
        )  # type: ignore[return-value]

    def get_reference_mark_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> ReferenceMarkEnd | None:
        """Return the reference mark end that matches the criteria. Search only
        the tags text:reference-mark-end.
        Consider using : get_reference_marks()

        Arguments:

            position -- int

            name -- str

        Return: ReferenceMarkEnd or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark-end", position, text_name=name
        )  # type: ignore[return-value]

    def get_reference_marks(self) -> list[ReferenceMark | ReferenceMarkStart]:
        """Return all the reference marks, either single position reference
        (text:reference-mark) or start of range reference
        (text:reference-mark-start).

        Return: list of ReferenceMark or ReferenceMarkStart
        """
        return self._filtered_elements(
            "descendant::text:reference-mark-start | descendant::text:reference-mark"
        )  # type: ignore[return-value]

    def get_reference_mark(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> ReferenceMark | ReferenceMarkStart | None:
        """Return the reference mark that match the criteria. Either single
        position reference mark (text:reference-mark) or start of range
        reference (text:reference-mark-start).

        Arguments:

            position -- int

            name -- str

        Return: ReferenceMark or ReferenceMarkStart or None if not found
        """
        if name:
            request = (
                f"descendant::text:reference-mark-start"
                f'[@text:name="{name}"] '
                f"| descendant::text:reference-mark"
                f'[@text:name="{name}"]'
            )
            return self._filtered_element(
                request,
                position=0,
            )  # type: ignore[return-value]
        request = (
            "descendant::text:reference-mark-start | descendant::text:reference-mark"
        )
        return self._filtered_element(request, position)  # type: ignore[return-value]

    def get_references(self, name: str | None = None) -> list[Reference]:
        """Return all the references (text:reference-ref). If name is
        provided, returns the references of that name.

        Arguments:

            name -- str or None

        Return: list of Reference
        """
        if name is None:
            return self._filtered_elements(
                "descendant::text:reference-ref",
            )  # type: ignore[return-value]
        request = f'descendant::text:reference-ref[@text:ref-name="{name}"]'
        return self._filtered_elements(request)  # type: ignore[return-value]

    # Shapes elements

    # Groups

    def get_draw_groups(
        self,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> list[DrawGroup]:
        """Return all the draw groups that match the criteria.

        Arguments:

            title -- str or None

            description -- str regex or None

            content -- str regex or None

        Return: list of DrawGroup
        """
        return self._filtered_elements(
            "descendant::draw:g",
            svg_title=title,
            svg_desc=description,
            content=content,
        )  # type: ignore[return-value]

    def get_draw_group(
        self,
        position: int = 0,
        name: str | None = None,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> DrawGroup | None:
        """Return the  draw group that matches the criteria.

        Arguments:

            position -- int

            name  -- str or None

            title -- str or None

            description -- str regex or None

            content -- str regex or None

        Return: DrawGroup or None if not found
        """
        return self._filtered_element(
            "descendant::draw:g",
            position,
            draw_name=name,
            svg_title=title,
            svg_desc=description,
            content=content,
        )  # type: ignore[return-value]

    # Lines

    def get_draw_lines(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[LineShape]:
        """Return all the draw lines that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of LineShape
        """
        return self._filtered_elements(
            "descendant::draw:line",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )  # type: ignore[return-value]

    def get_draw_line(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> LineShape | None:
        """Return the draw line that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: LineShape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:line", position, draw_id=id, content=content
        )  # type: ignore[return-value]

    # Rectangles

    def get_draw_rectangles(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[RectangleShape]:
        """Return all the draw rectangles that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of RectangleShape
        """
        return self._filtered_elements(
            "descendant::draw:rect",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )  # type: ignore[return-value]

    def get_draw_rectangle(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> RectangleShape | None:
        """Return the draw rectangle that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: RectangleShape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:rect", position, draw_id=id, content=content
        )  # type: ignore[return-value]

    # Ellipse

    def get_draw_ellipses(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[EllipseShape]:
        """Return all the draw ellipses that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of EllipseShape
        """
        return self._filtered_elements(
            "descendant::draw:ellipse",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )  # type: ignore[return-value]

    def get_draw_ellipse(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> EllipseShape | None:
        """Return the draw ellipse that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: EllipseShape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:ellipse", position, draw_id=id, content=content
        )  # type: ignore[return-value]

    # Connectors

    def get_draw_connectors(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[ConnectorShape]:
        """Return all the draw connectors that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of ConnectorShape
        """
        return self._filtered_elements(
            "descendant::draw:connector",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )  # type: ignore[return-value]

    def get_draw_connector(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> ConnectorShape | None:
        """Return the draw connector that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: ConnectorShape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:connector", position, draw_id=id, content=content
        )  # type: ignore[return-value]

    def get_orphan_draw_connectors(self) -> list[ConnectorShape]:
        """Return a list of connectors which don't have any shape connected
        to them.

        Return: list of ConnectorShape
        """
        connectors = []
        for connector in self.get_draw_connectors():
            start_shape = connector.get_attribute("draw:start-shape")
            end_shape = connector.get_attribute("draw:end-shape")
            if start_shape is None and end_shape is None:
                connectors.append(connector)
        return connectors

    # Tracked changes and text change

    def get_tracked_changes(self) -> TrackedChanges | None:
        """Return the tracked-changes part in the text body.

        Return: TrackedChanges or None
        """
        return self.get_element("//text:tracked-changes")  # type: ignore[return-value]

    @property
    def tracked_changes(self) -> Element | None:
        """Return the tracked-changes part in the text body.

        Return: Element or None
        """
        return self.get_tracked_changes()

    def get_changes_ids(self) -> list[Element | EText]:
        """Return a list of ids that refers to a change region in the tracked
        changes list.
        """
        # Insertion changes
        xpath_query = "descendant::text:change-start/@text:change-id"
        # Deletion changes
        xpath_query += " | descendant::text:change/@text:change-id"
        return self.xpath(xpath_query)

    def get_text_change_deletions(self) -> list[TextChange]:
        """Return all the text changes of deletion kind: the tags text:change.
        Consider using : get_text_changes()

        Return: list of TextChange
        """
        return self._filtered_elements(
            "descendant::text:text:change",
        )  # type: ignore[return-value]

    def get_text_change_deletion(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> TextChange | None:
        """Return the text change of deletion kind that matches the criteria.
        Search only for the tags text:change.
        Consider using : get_text_change()

        Arguments:

            position -- int

            idx -- str

        Return: TextChange or None if not found
        """
        return self._filtered_element(
            "descendant::text:change", position, change_id=idx
        )  # type: ignore[return-value]

    def get_text_change_starts(self) -> list[TextChangeStart]:
        """Return all the text change-start. Search only for the tags
        text:change-start.
        Consider using : get_text_changes()

        Return: list of TextChangeStart
        """
        return self._filtered_elements(
            "descendant::text:change-start",
        )  # type: ignore[return-value]

    def get_text_change_start(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> TextChangeStart | None:
        """Return the text change-start that matches the criteria. Search
        only the tags text:change-start.
        Consider using : get_text_change()

        Arguments:

            position -- int

            idx -- str

        Return: TextChangeStart or None if not found
        """
        return self._filtered_element(
            "descendant::text:change-start", position, change_id=idx
        )  # type: ignore[return-value]

    def get_text_change_ends(self) -> list[TextChangeEnd]:
        """Return all the text change-end. Search only the tags
        text:change-end.
        Consider using : get_text_changes()

        Return: list of TextChangeEnd
        """
        return self._filtered_elements(
            "descendant::text:change-end",
        )  # type: ignore[return-value]

    def get_text_change_end(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> TextChangeEnd | None:
        """Return the text change-end that matches the criteria. Search only
        the tags text:change-end.
        Consider using : get_text_change()

        Arguments:

            position -- int

            idx -- str

        Return: TextChangeEnd or None if not found
        """
        return self._filtered_element(
            "descendant::text:change-end", position, change_id=idx
        )  # type: ignore[return-value]

    def get_text_changes(self) -> list[TextChange | TextChangeStart]:
        """Return all the text changes, either single deletion
        (text:change) or start of range of changes (text:change-start).

        Return: list of TextChange or TextChangeStart
        """
        request = "descendant::text:change-start | descendant::text:change"
        return self._filtered_elements(request)  # type: ignore[return-value]

    @property
    def text_changes(self) -> list[TextChange | TextChangeStart]:
        """Return all the text changes, either single deletion
        (text:change) or start of range of changes (text:change-start).

        Return: list of Element
        """
        return self.get_text_changes()

    def get_text_change(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> TextChange | TextChangeStart | None:
        """Return the text change that matches the criteria. Either single
        deletion (text:change) or start of range of changes (text:change-start).
        position : index of the element to retrieve if several matches, default
        is 0.
        idx : change-id of the element.

        Arguments:

            position -- int

            idx -- str

        Return: Element or None if not found
        """
        if idx:
            request = (
                f'descendant::text:change-start[@text:change-id="{idx}"] '
                f'| descendant::text:change[@text:change-id="{idx}"]'
            )
            return self._filtered_element(request, 0)  # type: ignore[return-value]

        request = "descendant::text:change-start | descendant::text:change"
        return self._filtered_element(request, position)  # type: ignore[return-value]

    # Table Of Content

    def get_tocs(self) -> list[TOC]:
        """Return all the tables of contents.

        Return: list of TOC
        """
        return self.get_elements("text:table-of-content")  # type: ignore[return-value]

    @property
    def tocs(self) -> list[TOC]:
        """Return all the tables of contents.

        Return: list of TOC
        """
        return self.get_elements("text:table-of-content")  # type: ignore[return-value]

    def get_toc(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> TOC | None:
        """Return the table of contents that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: TOC or None if not found
        """
        return self._filtered_element(
            "text:table-of-content", position, content=content
        )  # type: ignore[return-value]

    @property
    def toc(self) -> TOC | None:
        """Return the first table of contents.

        Return: odf_toc or None if not found
        """
        return self.get_toc()

    # Styles

    @staticmethod
    def _get_style_tagname(family: str | None, is_default: bool = False) -> str:
        """Widely match possible tag names given the family (or not)."""
        if not family:
            tagname = "(style:default-style|*[@style:name]|draw:fill-image|draw:marker)"
        elif is_default:
            # Default style
            tagname = "style:default-style"
        else:
            tagname = _family_style_tagname(family)
            # if famattr:
            #    # Include family default style
            #    tagname = '(%s|style:default-style)' % tagname
            if family in FAMILY_ODF_STD:
                # Include family default style
                tagname = f"({tagname}|style:default-style)"
        return tagname

    def get_styles(self, family: str | None = None) -> list[Element]:
        # Both common and default styles
        tagname = self._get_style_tagname(family)
        return self._filtered_elements(tagname, family=family)

    def get_style(
        self,
        family: str,
        name_or_element: str | Element | None = None,
        display_name: str | None = None,
    ) -> Style | None:
        """Return the style uniquely identified by the family/name pair. If
        the argument is already a style object, it will return it.

        If the name is not the internal name but the name you gave in the
        desktop application, use display_name instead.

        Arguments:

            family -- 'paragraph', 'text', 'graphic', 'table', 'list',
                      'number'

            name_or_element -- str or Style

            display_name -- str

        Return: Style or None if not found
        """
        if isinstance(name_or_element, Element):
            name = self.get_attribute("style:name")
            if name is not None:
                return name_or_element  # type: ignore[return-value]
            else:
                raise ValueError(f"Not a odf_style ? {name_or_element!r}")
        style_name = name_or_element
        is_default = not (style_name or display_name)
        tagname = self._get_style_tagname(family, is_default=is_default)
        # famattr became None if no "style:family" attribute
        if family:
            return self._filtered_element(
                tagname,
                0,
                style_name=style_name,
                display_name=display_name,
                family=family,
            )  # type: ignore[return-value]
        else:
            return self._filtered_element(
                tagname,
                0,
                draw_name=style_name or display_name,
                family=family,
            )  # type: ignore[return-value]

    def _filtered_element(
        self,
        query_string: str,
        position: int,
        **kwargs: Any,
    ) -> Element | None:
        results = self._filtered_elements(query_string, **kwargs)
        try:
            return results[position]
        except IndexError:
            return None

    def _filtered_elements(
        self,
        query_string: str,
        content: str | None = None,
        url: str | None = None,
        svg_title: str | None = None,
        svg_desc: str | None = None,
        dc_creator: str | None = None,
        dc_date: datetime | None = None,
        **kwargs: Any,
    ) -> list[Element]:
        query = make_xpath_query(query_string, **kwargs)
        elements = self.get_elements(query)
        # Filter the elements with the regex (TODO use XPath)
        if content is not None:
            elements = [element for element in elements if element.match(content)]
        if url is not None:
            filtered = []
            for element in elements:
                url_attr = element.get_attribute("xlink:href")
                if isinstance(url_attr, str) and search(url, url_attr) is not None:
                    filtered.append(element)
            elements = filtered
        if dc_date is None:
            dt_dc_date = None
        else:
            dt_dc_date = DateTime.encode(dc_date)
        for variable, childname in [
            (svg_title, "svg:title"),
            (svg_desc, "svg:desc"),
            (dc_creator, "descendant::dc:creator"),
            (dt_dc_date, "descendant::dc:date"),
        ]:
            if not variable:
                continue
            filtered = []
            for element in elements:
                child = element.get_element(childname)
                if child and child.match(variable):
                    filtered.append(element)
            elements = filtered
        return elements

append class-attribute instance-attribute

append = __append

attributes property

attributes: dict[str, str]

children property

children: list[Element]

clone property

clone: Element

document_body property

document_body: Body | None

Return the first children of document body if any: ‘office:body/*[1]’

frames property

frames: list[Frame]

Return all the frames.

Return: list of Frame

headers property

headers: list[Header]

Return all the Headers.

Return: list of Header

images property

images: list[DrawImage]

Return all the images.

Return: list of DrawImage

inner_text property

inner_text: str

is_bound property

is_bound: bool

lists property

lists: list[List]

Return all the lists.

Return: list of List

paragraphs property

paragraphs: list[Paragraph]

Return all the paragraphs.

Return: list of Paragraph

parent property

parent: Element | None

root property

root: Element

sections property

sections: list[Section]

Return all the sections.

Return: list of Section

spans property

spans: list[Span]

Return all the spans.

Return: list of Span

svg_description property writable

svg_description: str | None

svg_title property writable

svg_title: str | None

tag property writable

tag: str

Get/set the underlying xml tag with the given qualified name.

Warning: direct change of tag does not change the element class.

Arguments:

qname -- str (e.g. "text:span")

tail property writable

tail: str | None

Get / set the text immediately following the element.

text property writable

text: str

Get / set the text content of the element.

text_changes property

text_changes: list[TextChange | TextChangeStart]

Return all the text changes, either single deletion (text:change) or start of range of changes (text:change-start).

Return: list of Element

text_content property writable

text_content: str

Get / set the text of the embedded paragraphs, including embeded annotations, cells…

Set does create a paragraph if missing.

text_recursive property

text_recursive: str

toc property

toc: TOC | None

Return the first table of contents.

Return: odf_toc or None if not found

tocs property

tocs: list[TOC]

Return all the tables of contents.

Return: list of TOC

tracked_changes property

tracked_changes: Element | None

Return the tracked-changes part in the text body.

Return: Element or None

user_defined_list property

user_defined_list: list[UserDefined]

Return all the user defined field declarations.

Return: list of UserDefined

__init__

__init__(**kwargs: Any) -> None

Base class of all ODF classes, abstraction of the underlying XML.

Source code in odfdo/element.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def __init__(self, **kwargs: Any) -> None:
    """Base class of all ODF classes, abstraction of the underlying XML."""
    tag_or_elem = kwargs.pop("tag_or_elem", None)
    if tag_or_elem is None:
        # Instance for newly created object: create new lxml element and
        # continue by subclass __init__
        # If the tag key word exists, make a custom element
        self._do_init = True
        tag = kwargs.pop("tag", self._tag)
        self.__element = self.make_etree_element(tag)
    else:
        # called with an existing lxml element, sould be a result of
        # from_tag() casting, do not execute the subclass __init__
        if not isinstance(tag_or_elem, _Element):
            raise TypeError(f'"{type(tag_or_elem)}" is not an element node')
        self._do_init = False
        self.__element = tag_or_elem

append_named_range

append_named_range(named_range: NamedRange) -> None

Append the named range to the spreadsheet, replacing existing named range of same name if any.

Arguments:

named_range --  NamedRange
Source code in odfdo/element.py
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
def append_named_range(self, named_range: NamedRange) -> None:
    """Append the named range to the spreadsheet, replacing existing named
    range of same name if any.

    Arguments:

        named_range --  NamedRange
    """
    if self.tag != "office:spreadsheet":
        raise ValueError(f"Element is no 'office:spreadsheet' : {self.tag}")
    named_expressions = self.get_element("table:named-expressions")
    if not named_expressions:
        named_expressions = Element.from_tag("table:named-expressions")
        self.__append(named_expressions)
    # exists ?
    current = named_expressions.get_element(
        f'table:named-range[@table:name="{named_range.name}"][1]'
    )
    if current:
        named_expressions.delete(current)
    named_expressions.__append(named_range)

clear

clear() -> None

Remove text, children and attributes from the element.

Source code in odfdo/element.py
1700
1701
1702
def clear(self) -> None:
    """Remove text, children and attributes from the element."""
    self.__element.clear()

del_attribute

del_attribute(name: str) -> None
Source code in odfdo/element.py
883
884
885
886
def del_attribute(self, name: str) -> None:
    element = self.__element
    lxml_tag = _get_lxml_tag_or_name(name)
    del element.attrib[lxml_tag]

delete

delete(
    child: Element | None = None, keep_tail: bool = True
) -> None

Delete the given element from the XML tree. If no element is given, “self” is deleted. The XML library may allow to continue to use an element now “orphan” as long as you have a reference to it.

if keep_tail is True (default), the tail text is not erased.

Arguments:

child -- Element

keep_tail -- boolean (default to True), True for most usages.
Source code in odfdo/element.py
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
    """Delete the given element from the XML tree. If no element is given,
    "self" is deleted. The XML library may allow to continue to use an
    element now "orphan" as long as you have a reference to it.

    if keep_tail is True (default), the tail text is not erased.

    Arguments:

        child -- Element

        keep_tail -- boolean (default to True), True for most usages.
    """
    if child is None:
        parent = self.parent
        if parent is None:
            raise ValueError(f"Can't delete the root element\n{self.serialize()}")
        child = self
    else:
        parent = self
    if keep_tail and child.__element.tail is not None:
        current = child.__element
        tail = str(current.tail)
        current.tail = None
        prev = current.getprevious()
        if prev is not None:
            if prev.tail is None:
                prev.tail = tail
            else:
                prev.tail += tail
        else:
            if parent.__element.text is None:
                parent.__element.text = tail
            else:
                parent.__element.text += tail
    parent.__element.remove(child.__element)

delete_named_range

delete_named_range(name: str) -> None

Delete the Named Range of specified name from the spreadsheet.

Arguments:

name -- str
Source code in odfdo/element.py
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
def delete_named_range(self, name: str) -> None:
    """Delete the Named Range of specified name from the spreadsheet.

    Arguments:

        name -- str
    """
    if self.tag != "office:spreadsheet":
        raise ValueError(f"Element is no 'office:spreadsheet' : {self.tag}")
    named_range = self.get_named_range(name)
    if not named_range:
        return
    named_range.delete()
    named_expressions = self.get_element("table:named-expressions")
    if not named_expressions:
        return
    element = named_expressions.__element
    children = list(element.iterchildren())
    if not children:
        self.delete(named_expressions)

elements_repeated_sequence

elements_repeated_sequence(
    xpath_instance: XPath, name: str
) -> list[tuple[int, int]]

Utility method for table module.

Source code in odfdo/element.py
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def elements_repeated_sequence(
    self,
    xpath_instance: XPath,
    name: str,
) -> list[tuple[int, int]]:
    """Utility method for table module."""
    lxml_tag = _get_lxml_tag_or_name(name)
    element = self.__element
    sub_elements = xpath_instance(element)
    if not isinstance(sub_elements, list):
        raise TypeError("Bad XPath result.")
    result: list[tuple[int, int]] = []
    idx = -1
    for sub_element in sub_elements:
        if not isinstance(sub_element, _Element):
            continue
        idx += 1
        value = sub_element.get(lxml_tag)
        if value is None:
            result.append((idx, 1))
            continue
        try:
            int_value = int(value)
        except ValueError:
            int_value = 1
        result.append((idx, max(int_value, 1)))
    return result

extend

extend(odf_elements: Iterable[Element]) -> None

Fast append elements at the end of ourself using extend.

Source code in odfdo/element.py
1464
1465
1466
1467
1468
1469
def extend(self, odf_elements: Iterable[Element]) -> None:
    """Fast append elements at the end of ourself using extend."""
    if odf_elements:
        current = self.__element
        elements = [element.__element for element in odf_elements]
        current.extend(elements)

from_tag classmethod

from_tag(tag_or_elem: str | _Element) -> Element

Element class and subclass factory.

Turn an lxml Element or ODF string tag into an ODF XML Element of the relevant class.

Arguments:

tag_or_elem -- ODF str tag or lxml.Element

Return: Element (or subclass) instance

Source code in odfdo/element.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
@classmethod
def from_tag(cls, tag_or_elem: str | _Element) -> Element:
    """Element class and subclass factory.

    Turn an lxml Element or ODF string tag into an ODF XML Element
    of the relevant class.

    Arguments:

        tag_or_elem -- ODF str tag or lxml.Element

    Return: Element (or subclass) instance
    """
    if isinstance(tag_or_elem, str):
        # assume the argument is a prefix:name tag
        elem = cls.make_etree_element(tag_or_elem)
    else:
        elem = tag_or_elem
    klass = _class_registry.get(elem.tag, cls)
    return klass(tag_or_elem=elem)

from_tag_for_clone classmethod

from_tag_for_clone(
    tree_element: _Element, cache: tuple | None
) -> Element
Source code in odfdo/element.py
348
349
350
351
352
353
354
355
356
357
358
359
@classmethod
def from_tag_for_clone(
    cls: Any,  # ABCMeta, type, ...
    tree_element: _Element,
    cache: tuple | None,
) -> Element:
    tag = to_str(tree_element.tag)
    klass = _class_registry.get(tag, cls)
    element: Element = klass(tag_or_elem=tree_element)
    if cache:
        element._copy_cache(cache)
    return element

get_annotation

get_annotation(
    position: int = 0,
    creator: str | None = None,
    start_date: datetime | None = None,
    end_date: datetime | None = None,
    content: str | None = None,
    name: str | None = None,
) -> Annotation | None

Return the annotation that matches the criteria.

Arguments:

position -- int

creator -- str

start_date -- datetime instance

end_date -- datetime instance

content -- str regex

name -- str

Return: Annotation or None if not found

Source code in odfdo/element.py
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
def get_annotation(
    self,
    position: int = 0,
    creator: str | None = None,
    start_date: datetime | None = None,
    end_date: datetime | None = None,
    content: str | None = None,
    name: str | None = None,
) -> Annotation | None:
    """Return the annotation that matches the criteria.

    Arguments:

        position -- int

        creator -- str

        start_date -- datetime instance

        end_date -- datetime instance

        content -- str regex

        name -- str

    Return: Annotation or None if not found
    """
    if name is not None:
        return self._filtered_element(
            "descendant::office:annotation", 0, office_name=name
        )  # type: ignore[return-value]
    annotations: list[Annotation] = self.get_annotations(
        creator=creator, start_date=start_date, end_date=end_date, content=content
    )
    if not annotations:
        return None
    try:
        return annotations[position]
    except IndexError:
        return None

get_annotation_end

get_annotation_end(
    position: int = 0, name: str | None = None
) -> AnnotationEnd | None

Return the annotation end that matches the criteria.

Arguments:

position -- int

name -- str

Return: AnnotationEnd or None if not found

Source code in odfdo/element.py
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
def get_annotation_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> AnnotationEnd | None:
    """Return the annotation end that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: AnnotationEnd or None if not found
    """
    return self._filtered_element(
        "descendant::office:annotation-end", position, office_name=name
    )  # type: ignore[return-value]

get_annotation_ends

get_annotation_ends() -> list[AnnotationEnd]

Return all the annotation ends.

Return: list of AnnotationEnd

Source code in odfdo/element.py
2398
2399
2400
2401
2402
2403
2404
2405
def get_annotation_ends(self) -> list[AnnotationEnd]:
    """Return all the annotation ends.

    Return: list of AnnotationEnd
    """
    return self._filtered_elements(
        "descendant::office:annotation-end",
    )  # type: ignore[return-value]

get_annotations

get_annotations(
    creator: str | None = None,
    start_date: datetime | None = None,
    end_date: datetime | None = None,
    content: str | None = None,
) -> list[Annotation]

Return all the annotations that match the criteria.

Arguments:

creator -- str

start_date -- datetime instance

end_date --  datetime instance

content -- str regex

Return: list of Annotation

Source code in odfdo/element.py
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
def get_annotations(
    self,
    creator: str | None = None,
    start_date: datetime | None = None,
    end_date: datetime | None = None,
    content: str | None = None,
) -> list[Annotation]:
    """Return all the annotations that match the criteria.

    Arguments:

        creator -- str

        start_date -- datetime instance

        end_date --  datetime instance

        content -- str regex

    Return: list of Annotation
    """
    annotations: list[Annotation] = []
    for annotation in self._filtered_elements(
        "descendant::office:annotation", content=content
    ):
        if creator is not None and creator != annotation.dc_creator:  # type: ignore[attr-defined]
            continue
        date = annotation.date  # type: ignore[attr-defined]
        if date is None:
            continue
        if start_date is not None and date < start_date:
            continue
        if end_date is not None and date >= end_date:
            continue
        annotations.append(annotation)  # type: ignore[arg-type]
    return annotations

get_attribute

get_attribute(name: str) -> str | bool | None

Return the attribute value as type str | bool | None.

Source code in odfdo/element.py
827
828
829
830
831
832
833
834
835
836
def get_attribute(self, name: str) -> str | bool | None:
    """Return the attribute value as type str | bool | None."""
    element = self.__element
    lxml_tag = _get_lxml_tag_or_name(name)
    value = element.get(lxml_tag)
    if value is None:
        return None
    elif value in ("true", "false"):
        return Boolean.decode(value)
    return str(value)

get_attribute_integer

get_attribute_integer(name: str) -> int | None

Return either the attribute as type int, or None.

Source code in odfdo/element.py
838
839
840
841
842
843
844
845
846
847
848
def get_attribute_integer(self, name: str) -> int | None:
    """Return either the attribute as type int, or None."""
    element = self.__element
    lxml_tag = _get_lxml_tag_or_name(name)
    value = element.get(lxml_tag)
    if value is None:
        return None
    try:
        return int(value)
    except ValueError:
        return None

get_attribute_string

get_attribute_string(name: str) -> str | None

Return either the attribute as type str, or None.

Source code in odfdo/element.py
850
851
852
853
854
855
856
857
def get_attribute_string(self, name: str) -> str | None:
    """Return either the attribute as type str, or None."""
    element = self.__element
    lxml_tag = _get_lxml_tag_or_name(name)
    value = element.get(lxml_tag)
    if value is None:
        return None
    return str(value)

get_between

get_between(
    tag1: Element,
    tag2: Element,
    as_text: bool = False,
    clean: bool = True,
    no_header: bool = True,
) -> list | str

Returns elements between tag1 and tag2, tag1 and tag2 shall be unique and having an id attribute. (WARN: buggy if tag1/tag2 defines a malformed odf xml.) If as_text is True: returns the text content. If clean is True: suppress unwanted tags (deletions marks, …) If no_header is True: existing text:h are changed in text:p By default: returns a list of Element, cleaned and without headers.

Implementation and standard retrictions: Only text:h and text:p sould be ‘cut’ by an insert tag, so inner parts of insert tags are:

- any text:h, text:p or sub tag of these

- some text, part of a parent text:h or text:p

Arguments:

tag1 -- Element

tag2 -- Element

as_text -- boolean

clean -- boolean

no_header -- boolean

Return: list of odf_paragraph or odf_header

Source code in odfdo/element.py
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def get_between(
    self,
    tag1: Element,
    tag2: Element,
    as_text: bool = False,
    clean: bool = True,
    no_header: bool = True,
) -> list | str:
    """Returns elements between tag1 and tag2, tag1 and tag2 shall
    be unique and having an id attribute.
    (WARN: buggy if tag1/tag2 defines a malformed odf xml.)
    If as_text is True: returns the text content.
    If clean is True: suppress unwanted tags (deletions marks, ...)
    If no_header is True: existing text:h are changed in text:p
    By default: returns a list of Element, cleaned and without headers.

    Implementation and standard retrictions:
    Only text:h and text:p sould be 'cut' by an insert tag, so inner parts
    of insert tags are:

        - any text:h, text:p or sub tag of these

        - some text, part of a parent text:h or text:p

    Arguments:

        tag1 -- Element

        tag2 -- Element

        as_text -- boolean

        clean -- boolean

        no_header -- boolean

    Return: list of odf_paragraph or odf_header
    """
    inner = self._get_between_base(tag1, tag2)

    if clean:
        clean_tags = (
            "text:change",
            "text:change-start",
            "text:change-end",
            "text:reference-mark",
            "text:reference-mark-start",
            "text:reference-mark-end",
        )
        request_self = " | ".join([f"self::{tag}" for tag in clean_tags])
        inner = [e for e in inner if not e.xpath(request_self)]
        request = " | ".join([f"descendant::{tag}" for tag in clean_tags])
        for element in inner:
            to_del = element.xpath(request)
            for elem in to_del:
                if isinstance(elem, Element):
                    element.delete(elem)
    if no_header:  # crude replace t:h by t:p
        new_inner = []
        for element in inner:
            if element.tag == "text:h":
                children = element.children
                text = element.__element.text
                para = Element.from_tag("text:p")
                para.text = text or ""
                for child in children:
                    para.__append(child)
                new_inner.append(para)
            else:
                new_inner.append(element)
        inner = new_inner
    if as_text:
        return "\n".join([e.get_formatted_text() for e in inner])
    else:
        return inner

get_bookmark

get_bookmark(
    position: int = 0, name: str | None = None
) -> Bookmark | None

Return the bookmark that matches the criteria.

Arguments:

position -- int

name -- str

Return: Bookmark or None if not found

Source code in odfdo/element.py
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
def get_bookmark(
    self,
    position: int = 0,
    name: str | None = None,
) -> Bookmark | None:
    """Return the bookmark that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: Bookmark or None if not found
    """
    return self._filtered_element(
        "descendant::text:bookmark", position, text_name=name
    )  # type: ignore[return-value]

get_bookmark_end

get_bookmark_end(
    position: int = 0, name: str | None = None
) -> BookmarkEnd | None

Return the bookmark end that matches the criteria.

Arguments:

position -- int

name -- str

Return: BookmarkEnd or None if not found

Source code in odfdo/element.py
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
def get_bookmark_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> BookmarkEnd | None:
    """Return the bookmark end that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: BookmarkEnd or None if not found
    """
    return self._filtered_element(
        "descendant::text:bookmark-end", position, text_name=name
    )  # type: ignore[return-value]

get_bookmark_ends

get_bookmark_ends() -> list[BookmarkEnd]

Return all the bookmark ends.

Return: list of BookmarkEnd

Source code in odfdo/element.py
2802
2803
2804
2805
2806
2807
2808
2809
def get_bookmark_ends(self) -> list[BookmarkEnd]:
    """Return all the bookmark ends.

    Return: list of BookmarkEnd
    """
    return self._filtered_elements(
        "descendant::text:bookmark-end",
    )  # type: ignore[return-value]

get_bookmark_start

get_bookmark_start(
    position: int = 0, name: str | None = None
) -> BookmarkStart | None

Return the bookmark start that matches the criteria.

Arguments:

position -- int

name -- str

Return: BookmarkStart or None if not found

Source code in odfdo/element.py
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
def get_bookmark_start(
    self,
    position: int = 0,
    name: str | None = None,
) -> BookmarkStart | None:
    """Return the bookmark start that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: BookmarkStart or None if not found
    """
    return self._filtered_element(
        "descendant::text:bookmark-start", position, text_name=name
    )  # type: ignore[return-value]

get_bookmark_starts

get_bookmark_starts() -> list[BookmarkStart]

Return all the bookmark starts.

Return: list of BookmarkStart

Source code in odfdo/element.py
2774
2775
2776
2777
2778
2779
2780
2781
def get_bookmark_starts(self) -> list[BookmarkStart]:
    """Return all the bookmark starts.

    Return: list of BookmarkStart
    """
    return self._filtered_elements(
        "descendant::text:bookmark-start",
    )  # type: ignore[return-value]

get_bookmarks

get_bookmarks() -> list[Bookmark]

Return all the bookmarks.

Return: list of Bookmark

Source code in odfdo/element.py
2746
2747
2748
2749
2750
2751
2752
2753
def get_bookmarks(self) -> list[Bookmark]:
    """Return all the bookmarks.

    Return: list of Bookmark
    """
    return self._filtered_elements(
        "descendant::text:bookmark",
    )  # type: ignore[return-value]

get_changes_ids

get_changes_ids() -> list[Element | EText]

Return a list of ids that refers to a change region in the tracked changes list.

Source code in odfdo/element.py
3278
3279
3280
3281
3282
3283
3284
3285
3286
def get_changes_ids(self) -> list[Element | EText]:
    """Return a list of ids that refers to a change region in the tracked
    changes list.
    """
    # Insertion changes
    xpath_query = "descendant::text:change-start/@text:change-id"
    # Deletion changes
    xpath_query += " | descendant::text:change/@text:change-id"
    return self.xpath(xpath_query)

get_draw_connector

get_draw_connector(
    position: int = 0,
    id: str | None = None,
    content: str | None = None,
) -> ConnectorShape | None

Return the draw connector that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: ConnectorShape or None if not found

Source code in odfdo/element.py
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
def get_draw_connector(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> ConnectorShape | None:
    """Return the draw connector that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: ConnectorShape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:connector", position, draw_id=id, content=content
    )  # type: ignore[return-value]

get_draw_connectors

get_draw_connectors(
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[ConnectorShape]

Return all the draw connectors that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of ConnectorShape

Source code in odfdo/element.py
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
def get_draw_connectors(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[ConnectorShape]:
    """Return all the draw connectors that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of ConnectorShape
    """
    return self._filtered_elements(
        "descendant::draw:connector",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )  # type: ignore[return-value]

get_draw_ellipse

get_draw_ellipse(
    position: int = 0,
    id: str | None = None,
    content: str | None = None,
) -> EllipseShape | None

Return the draw ellipse that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: EllipseShape or None if not found

Source code in odfdo/element.py
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
def get_draw_ellipse(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> EllipseShape | None:
    """Return the draw ellipse that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: EllipseShape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:ellipse", position, draw_id=id, content=content
    )  # type: ignore[return-value]

get_draw_ellipses

get_draw_ellipses(
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[EllipseShape]

Return all the draw ellipses that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of EllipseShape

Source code in odfdo/element.py
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
def get_draw_ellipses(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[EllipseShape]:
    """Return all the draw ellipses that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of EllipseShape
    """
    return self._filtered_elements(
        "descendant::draw:ellipse",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )  # type: ignore[return-value]

get_draw_group

get_draw_group(
    position: int = 0,
    name: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> DrawGroup | None

Return the draw group that matches the criteria.

Arguments:

position -- int

name  -- str or None

title -- str or None

description -- str regex or None

content -- str regex or None

Return: DrawGroup or None if not found

Source code in odfdo/element.py
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
def get_draw_group(
    self,
    position: int = 0,
    name: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> DrawGroup | None:
    """Return the  draw group that matches the criteria.

    Arguments:

        position -- int

        name  -- str or None

        title -- str or None

        description -- str regex or None

        content -- str regex or None

    Return: DrawGroup or None if not found
    """
    return self._filtered_element(
        "descendant::draw:g",
        position,
        draw_name=name,
        svg_title=title,
        svg_desc=description,
        content=content,
    )  # type: ignore[return-value]

get_draw_groups

get_draw_groups(
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> list[DrawGroup]

Return all the draw groups that match the criteria.

Arguments:

title -- str or None

description -- str regex or None

content -- str regex or None

Return: list of DrawGroup

Source code in odfdo/element.py
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
def get_draw_groups(
    self,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> list[DrawGroup]:
    """Return all the draw groups that match the criteria.

    Arguments:

        title -- str or None

        description -- str regex or None

        content -- str regex or None

    Return: list of DrawGroup
    """
    return self._filtered_elements(
        "descendant::draw:g",
        svg_title=title,
        svg_desc=description,
        content=content,
    )  # type: ignore[return-value]

get_draw_line

get_draw_line(
    position: int = 0,
    id: str | None = None,
    content: str | None = None,
) -> LineShape | None

Return the draw line that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: LineShape or None if not found

Source code in odfdo/element.py
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
def get_draw_line(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> LineShape | None:
    """Return the draw line that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: LineShape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:line", position, draw_id=id, content=content
    )  # type: ignore[return-value]

get_draw_lines

get_draw_lines(
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[LineShape]

Return all the draw lines that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of LineShape

Source code in odfdo/element.py
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
def get_draw_lines(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[LineShape]:
    """Return all the draw lines that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of LineShape
    """
    return self._filtered_elements(
        "descendant::draw:line",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )  # type: ignore[return-value]

get_draw_page

get_draw_page(
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> DrawPage | None

Return the draw page that matches the criteria.

Arguments:

position -- int

name -- str

content -- str regex

Return: DrawPage or None if not found

Source code in odfdo/element.py
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
def get_draw_page(
    self,
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> DrawPage | None:
    """Return the draw page that matches the criteria.

    Arguments:

        position -- int

        name -- str

        content -- str regex

    Return: DrawPage or None if not found
    """
    return self._filtered_element(
        "descendant::draw:page", position, draw_name=name, content=content
    )  # type: ignore[return-value]

get_draw_pages

get_draw_pages(
    style: str | None = None, content: str | None = None
) -> list[DrawPage]

Return all the draw pages that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of DrawPage

Source code in odfdo/element.py
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
def get_draw_pages(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[DrawPage]:
    """Return all the draw pages that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of DrawPage
    """
    return self._filtered_elements(
        "descendant::draw:page", draw_style=style, content=content
    )  # type: ignore[return-value]

get_draw_rectangle

get_draw_rectangle(
    position: int = 0,
    id: str | None = None,
    content: str | None = None,
) -> RectangleShape | None

Return the draw rectangle that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: RectangleShape or None if not found

Source code in odfdo/element.py
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
def get_draw_rectangle(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> RectangleShape | None:
    """Return the draw rectangle that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: RectangleShape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:rect", position, draw_id=id, content=content
    )  # type: ignore[return-value]

get_draw_rectangles

get_draw_rectangles(
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[RectangleShape]

Return all the draw rectangles that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of RectangleShape

Source code in odfdo/element.py
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
def get_draw_rectangles(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[RectangleShape]:
    """Return all the draw rectangles that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of RectangleShape
    """
    return self._filtered_elements(
        "descendant::draw:rect",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )  # type: ignore[return-value]

get_element

get_element(xpath_query: XPath | str) -> Element | None
Source code in odfdo/element.py
800
801
802
803
804
def get_element(self, xpath_query: XPath | str) -> Element | None:
    result = self.__element.xpath(f"({xpath_query})[1]", namespaces=ODF_NAMESPACES)
    if result:
        return Element.from_tag(result[0])
    return None

get_elements

get_elements(xpath_query: XPath | str) -> list[Element]
Source code in odfdo/element.py
786
787
788
789
790
791
792
793
794
795
796
797
798
def get_elements(self, xpath_query: XPath | str) -> list[Element]:
    if isinstance(xpath_query, str):
        new_xpath_query = xpath_compile(xpath_query)
        result = new_xpath_query(self.__element)
    else:
        result = xpath_query(self.__element)
    if not isinstance(result, list):
        raise TypeError("Bad XPath result")
    return [
        Element.from_tag_for_clone(e, None)
        for e in result
        if isinstance(e, _Element)
    ]

get_formatted_text

get_formatted_text(context: dict | None = None) -> str

This function should return a beautiful version of the text.

Source code in odfdo/element.py
1738
1739
1740
def get_formatted_text(self, context: dict | None = None) -> str:
    """This function should return a beautiful version of the text."""
    return ""

get_frame

get_frame(
    position: int = 0,
    name: str | None = None,
    presentation_class: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> Frame | None

Return the section that matches the criteria.

Arguments:

position -- int

name -- str

presentation_class -- str

title -- str regex

description -- str regex

content -- str regex

Return: Frame or None if not found

Source code in odfdo/element.py
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
def get_frame(
    self,
    position: int = 0,
    name: str | None = None,
    presentation_class: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> Frame | None:
    """Return the section that matches the criteria.

    Arguments:

        position -- int

        name -- str

        presentation_class -- str

        title -- str regex

        description -- str regex

        content -- str regex

    Return: Frame or None if not found
    """
    return self._filtered_element(
        "descendant::draw:frame",
        position,
        draw_name=name,
        presentation_class=presentation_class,
        svg_title=title,
        svg_desc=description,
        content=content,
    )  # type: ignore[return-value]

get_frames

get_frames(
    presentation_class: str | None = None,
    style: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> list[Frame]

Return all the frames that match the criteria.

Arguments:

presentation_class -- str

style -- str

title -- str regex

description -- str regex

content -- str regex

Return: list of Frame

Source code in odfdo/element.py
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
def get_frames(
    self,
    presentation_class: str | None = None,
    style: str | None = None,
    title: str | None = None,
    description: str | None = None,
    content: str | None = None,
) -> list[Frame]:
    """Return all the frames that match the criteria.

    Arguments:

        presentation_class -- str

        style -- str

        title -- str regex

        description -- str regex

        content -- str regex

    Return: list of Frame
    """
    return self._filtered_elements(
        "descendant::draw:frame",
        presentation_class=presentation_class,
        draw_style=style,
        svg_title=title,
        svg_desc=description,
        content=content,
    )  # type: ignore[return-value]

get_header

get_header(
    position: int = 0,
    outline_level: str | None = None,
    content: str | None = None,
) -> Header | None

Return the Header that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: Header or None if not found

Source code in odfdo/element.py
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
def get_header(
    self,
    position: int = 0,
    outline_level: str | None = None,
    content: str | None = None,
) -> Header | None:
    """Return the Header that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: Header or None if not found
    """
    return self._filtered_element(
        "descendant::text:h",
        position,
        outline_level=outline_level,
        content=content,
    )  # type: ignore[return-value]

get_headers

get_headers(
    style: str | None = None,
    outline_level: str | None = None,
    content: str | None = None,
) -> list[Header]

Return all the Headers that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Header

Source code in odfdo/element.py
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
def get_headers(
    self,
    style: str | None = None,
    outline_level: str | None = None,
    content: str | None = None,
) -> list[Header]:
    """Return all the Headers that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Header
    """
    return self._filtered_elements(
        "descendant::text:h",
        text_style=style,
        outline_level=outline_level,
        content=content,
    )  # type: ignore[return-value]

get_image

get_image(
    position: int = 0,
    name: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> DrawImage | None

Return the image matching the criteria.

Arguments:

position -- int

name -- str

url -- str regex

content -- str regex

Return: DrawImage or None if not found

Source code in odfdo/element.py
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
def get_image(
    self,
    position: int = 0,
    name: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> DrawImage | None:
    """Return the image matching the criteria.

    Arguments:

        position -- int

        name -- str

        url -- str regex

        content -- str regex

    Return: DrawImage or None if not found
    """
    # The frame is holding the name
    if name is not None:
        frame = self._filtered_element(
            "descendant::draw:frame", position, draw_name=name
        )
        if frame is None:
            return None
        # The name is supposedly unique
        return frame.get_element("draw:image")  # type: ignore[return-value]
    return self._filtered_element(
        "descendant::draw:image", position, url=url, content=content
    )  # type: ignore[return-value]

get_images

get_images(
    style: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[DrawImage]

Return all the images matching the criteria.

Arguments:

style -- str

url -- str regex

content -- str regex

Return: list of DrawImage

Source code in odfdo/element.py
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
def get_images(
    self,
    style: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[DrawImage]:
    """Return all the images matching the criteria.

    Arguments:

        style -- str

        url -- str regex

        content -- str regex

    Return: list of DrawImage
    """
    return self._filtered_elements(
        "descendant::draw:image", text_style=style, url=url, content=content
    )  # type: ignore[return-value]
get_link(
    position: int = 0,
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Link | None

Return the link that matches the criteria.

Arguments:

position -- int

name -- str

title -- str

url -- str regex

content -- str regex

Return: Link or None if not found

Source code in odfdo/element.py
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
def get_link(
    self,
    position: int = 0,
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Link | None:
    """Return the link that matches the criteria.

    Arguments:

        position -- int

        name -- str

        title -- str

        url -- str regex

        content -- str regex

    Return: Link or None if not found
    """
    return self._filtered_element(
        "descendant::text:a",
        position,
        office_name=name,
        office_title=title,
        url=url,
        content=content,
    )  # type: ignore[return-value]
get_links(
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[Link]

Return all the links that match the criteria.

Arguments:

name -- str

title -- str

url -- str regex

content -- str regex

Return: list of Link

Source code in odfdo/element.py
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
def get_links(
    self,
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[Link]:
    """Return all the links that match the criteria.

    Arguments:

        name -- str

        title -- str

        url -- str regex

        content -- str regex

    Return: list of Link
    """
    return self._filtered_elements(
        "descendant::text:a",
        office_name=name,
        office_title=title,
        url=url,
        content=content,
    )  # type: ignore[return-value]

get_list

get_list(
    position: int = 0, content: str | None = None
) -> List | None

Return the list that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: List or None if not found

Source code in odfdo/element.py
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
def get_list(
    self,
    position: int = 0,
    content: str | None = None,
) -> List | None:
    """Return the list that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: List or None if not found
    """
    return self._filtered_element(
        "descendant::text:list", position, content=content
    )  # type: ignore[return-value]

get_lists

get_lists(
    style: str | None = None, content: str | None = None
) -> list[List]

Return all the lists that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of List

Source code in odfdo/element.py
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
def get_lists(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[List]:
    """Return all the lists that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of List
    """
    return self._filtered_elements(
        "descendant::text:list", text_style=style, content=content
    )  # type: ignore[return-value]

get_named_range

get_named_range(name: str) -> NamedRange | None

Return the named range of specified name, or None if not found.

Arguments:

name -- str

Return: NamedRange

Source code in odfdo/element.py
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
def get_named_range(self, name: str) -> NamedRange | None:
    """Return the named range of specified name, or None if not found.

    Arguments:

        name -- str

    Return: NamedRange
    """
    named_range = self.get_elements(
        f'descendant::table:named-expressions/table:named-range[@table:name="{name}"][1]'
    )
    if named_range:
        return named_range[0]  # type: ignore[return-value]
    else:
        return None

get_named_ranges

get_named_ranges() -> list[NamedRange]

Return all the tables named ranges.

Return: list of NamedRange

Source code in odfdo/element.py
2198
2199
2200
2201
2202
2203
2204
2205
2206
def get_named_ranges(self) -> list[NamedRange]:
    """Return all the tables named ranges.

    Return: list of NamedRange
    """
    named_ranges = self.get_elements(
        "descendant::table:named-expressions/table:named-range"
    )
    return named_ranges  # type: ignore[return-value]

get_note

get_note(
    position: int = 0,
    note_id: str | None = None,
    note_class: str | None = None,
    content: str | None = None,
) -> Note | None

Return the note that matches the criteria.

Arguments:

position -- int

note_id -- str

note_class -- 'footnote' or 'endnote'

content -- str regex

Return: Note or None if not found

Source code in odfdo/element.py
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
def get_note(
    self,
    position: int = 0,
    note_id: str | None = None,
    note_class: str | None = None,
    content: str | None = None,
) -> Note | None:
    """Return the note that matches the criteria.

    Arguments:

        position -- int

        note_id -- str

        note_class -- 'footnote' or 'endnote'

        content -- str regex

    Return: Note or None if not found
    """
    return self._filtered_element(
        "descendant::text:note",
        position,
        text_id=note_id,
        note_class=note_class,
        content=content,
    )  # type: ignore[return-value]

get_notes

get_notes(
    note_class: str | None = None,
    content: str | None = None,
) -> list[Note]

Return all the notes that match the criteria.

Arguments:

note_class -- 'footnote' or 'endnote'

content -- str regex

Return: list of Note

Source code in odfdo/element.py
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
def get_notes(
    self,
    note_class: str | None = None,
    content: str | None = None,
) -> list[Note]:
    """Return all the notes that match the criteria.

    Arguments:

        note_class -- 'footnote' or 'endnote'

        content -- str regex

    Return: list of Note
    """
    return self._filtered_elements(
        "descendant::text:note", note_class=note_class, content=content
    )  # type: ignore[return-value]

get_office_names

get_office_names() -> list[str]

Return all the used office:name tags values of the element.

Return: list of unique str

Source code in odfdo/element.py
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
def get_office_names(self) -> list[str]:
    """Return all the used office:name tags values of the element.

    Return: list of unique str
    """
    name_xpath_query = xpath_compile("//@office:name")
    response = name_xpath_query(self.__element)
    if not isinstance(response, list):
        return []
    return list({str(name) for name in response if name})

get_orphan_draw_connectors

get_orphan_draw_connectors() -> list[ConnectorShape]

Return a list of connectors which don’t have any shape connected to them.

Return: list of ConnectorShape

Source code in odfdo/element.py
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
def get_orphan_draw_connectors(self) -> list[ConnectorShape]:
    """Return a list of connectors which don't have any shape connected
    to them.

    Return: list of ConnectorShape
    """
    connectors = []
    for connector in self.get_draw_connectors():
        start_shape = connector.get_attribute("draw:start-shape")
        end_shape = connector.get_attribute("draw:end-shape")
        if start_shape is None and end_shape is None:
            connectors.append(connector)
    return connectors

get_paragraph

get_paragraph(
    position: int = 0, content: str | None = None
) -> Paragraph | None

Return the paragraph that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: Paragraph or None if not found

Source code in odfdo/element.py
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
def get_paragraph(
    self,
    position: int = 0,
    content: str | None = None,
) -> Paragraph | None:
    """Return the paragraph that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: Paragraph or None if not found
    """
    return self._filtered_element(
        "descendant::text:p",
        position,
        content=content,
    )  # type: ignore[return-value]

get_paragraphs

get_paragraphs(
    style: str | None = None, content: str | None = None
) -> list[Paragraph]

Return all the paragraphs that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Paragraph

Source code in odfdo/element.py
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
def get_paragraphs(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Paragraph]:
    """Return all the paragraphs that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Paragraph
    """
    return self._filtered_elements(
        "descendant::text:p", text_style=style, content=content
    )  # type: ignore[return-value]

get_reference_mark

get_reference_mark(
    position: int = 0, name: str | None = None
) -> ReferenceMark | ReferenceMarkStart | None

Return the reference mark that match the criteria. Either single position reference mark (text:reference-mark) or start of range reference (text:reference-mark-start).

Arguments:

position -- int

name -- str

Return: ReferenceMark or ReferenceMarkStart or None if not found

Source code in odfdo/element.py
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
def get_reference_mark(
    self,
    position: int = 0,
    name: str | None = None,
) -> ReferenceMark | ReferenceMarkStart | None:
    """Return the reference mark that match the criteria. Either single
    position reference mark (text:reference-mark) or start of range
    reference (text:reference-mark-start).

    Arguments:

        position -- int

        name -- str

    Return: ReferenceMark or ReferenceMarkStart or None if not found
    """
    if name:
        request = (
            f"descendant::text:reference-mark-start"
            f'[@text:name="{name}"] '
            f"| descendant::text:reference-mark"
            f'[@text:name="{name}"]'
        )
        return self._filtered_element(
            request,
            position=0,
        )  # type: ignore[return-value]
    request = (
        "descendant::text:reference-mark-start | descendant::text:reference-mark"
    )
    return self._filtered_element(request, position)  # type: ignore[return-value]

get_reference_mark_end

get_reference_mark_end(
    position: int = 0, name: str | None = None
) -> ReferenceMarkEnd | None

Return the reference mark end that matches the criteria. Search only the tags text:reference-mark-end. Consider using : get_reference_marks()

Arguments:

position -- int

name -- str

Return: ReferenceMarkEnd or None if not found

Source code in odfdo/element.py
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
def get_reference_mark_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> ReferenceMarkEnd | None:
    """Return the reference mark end that matches the criteria. Search only
    the tags text:reference-mark-end.
    Consider using : get_reference_marks()

    Arguments:

        position -- int

        name -- str

    Return: ReferenceMarkEnd or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark-end", position, text_name=name
    )  # type: ignore[return-value]

get_reference_mark_ends

get_reference_mark_ends() -> list[ReferenceMarkEnd]

Return all the reference mark ends. Search only the tags text:reference-mark-end. Consider using : get_reference_marks()

Return: list of ReferenceMarkEnd

Source code in odfdo/element.py
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
def get_reference_mark_ends(self) -> list[ReferenceMarkEnd]:
    """Return all the reference mark ends. Search only the tags
    text:reference-mark-end.
    Consider using : get_reference_marks()

    Return: list of ReferenceMarkEnd
    """
    return self._filtered_elements(
        "descendant::text:reference-mark-end",
    )  # type: ignore[return-value]

get_reference_mark_single

get_reference_mark_single(
    position: int = 0, name: str | None = None
) -> ReferenceMark | None

Return the reference mark that matches the criteria. Search only the tags text:reference-mark. Consider using : get_reference_mark()

Arguments:

position -- int

name -- str

Return: ReferenceMark or None if not found

Source code in odfdo/element.py
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
def get_reference_mark_single(
    self,
    position: int = 0,
    name: str | None = None,
) -> ReferenceMark | None:
    """Return the reference mark that matches the criteria. Search only the
    tags text:reference-mark.
    Consider using : get_reference_mark()

    Arguments:

        position -- int

        name -- str

    Return: ReferenceMark or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark", position, text_name=name
    )  # type: ignore[return-value]

get_reference_mark_start

get_reference_mark_start(
    position: int = 0, name: str | None = None
) -> ReferenceMarkStart | None

Return the reference mark start that matches the criteria. Search only the tags text:reference-mark-start. Consider using : get_reference_mark()

Arguments:

position -- int

name -- str

Return: ReferenceMarkStart or None if not found

Source code in odfdo/element.py
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
def get_reference_mark_start(
    self,
    position: int = 0,
    name: str | None = None,
) -> ReferenceMarkStart | None:
    """Return the reference mark start that matches the criteria. Search
    only the tags text:reference-mark-start.
    Consider using : get_reference_mark()

    Arguments:

        position -- int

        name -- str

    Return: ReferenceMarkStart or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark-start", position, text_name=name
    )  # type: ignore[return-value]

get_reference_mark_starts

get_reference_mark_starts() -> list[ReferenceMarkStart]

Return all the reference mark starts. Search only the tags text:reference-mark-start. Consider using : get_reference_marks()

Return: list of ReferenceMarkStart

Source code in odfdo/element.py
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
def get_reference_mark_starts(self) -> list[ReferenceMarkStart]:
    """Return all the reference mark starts. Search only the tags
    text:reference-mark-start.
    Consider using : get_reference_marks()

    Return: list of ReferenceMarkStart
    """
    return self._filtered_elements(
        "descendant::text:reference-mark-start",
    )  # type: ignore[return-value]

get_reference_marks

get_reference_marks() -> list[
    ReferenceMark | ReferenceMarkStart
]

Return all the reference marks, either single position reference (text:reference-mark) or start of range reference (text:reference-mark-start).

Return: list of ReferenceMark or ReferenceMarkStart

Source code in odfdo/element.py
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
def get_reference_marks(self) -> list[ReferenceMark | ReferenceMarkStart]:
    """Return all the reference marks, either single position reference
    (text:reference-mark) or start of range reference
    (text:reference-mark-start).

    Return: list of ReferenceMark or ReferenceMarkStart
    """
    return self._filtered_elements(
        "descendant::text:reference-mark-start | descendant::text:reference-mark"
    )  # type: ignore[return-value]

get_reference_marks_single

get_reference_marks_single() -> list[ReferenceMark]

Return all the reference marks. Search only the tags text:reference-mark. Consider using : get_reference_marks()

Return: list of ReferenceMark

Source code in odfdo/element.py
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
def get_reference_marks_single(self) -> list[ReferenceMark]:
    """Return all the reference marks. Search only the tags
    text:reference-mark.
    Consider using : get_reference_marks()

    Return: list of ReferenceMark
    """
    return self._filtered_elements(
        "descendant::text:reference-mark",
    )  # type: ignore[return-value]

get_references

get_references(name: str | None = None) -> list[Reference]

Return all the references (text:reference-ref). If name is provided, returns the references of that name.

Arguments:

name -- str or None

Return: list of Reference

Source code in odfdo/element.py
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
def get_references(self, name: str | None = None) -> list[Reference]:
    """Return all the references (text:reference-ref). If name is
    provided, returns the references of that name.

    Arguments:

        name -- str or None

    Return: list of Reference
    """
    if name is None:
        return self._filtered_elements(
            "descendant::text:reference-ref",
        )  # type: ignore[return-value]
    request = f'descendant::text:reference-ref[@text:ref-name="{name}"]'
    return self._filtered_elements(request)  # type: ignore[return-value]

get_section

get_section(
    position: int = 0, content: str | None = None
) -> Section | None

Return the section that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: Section or None if not found

Source code in odfdo/element.py
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
def get_section(
    self,
    position: int = 0,
    content: str | None = None,
) -> Section | None:
    """Return the section that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: Section or None if not found
    """
    return self._filtered_element(
        "descendant::text:section", position, content=content
    )  # type: ignore[return-value]

get_sections

get_sections(
    style: str | None = None, content: str | None = None
) -> list[Section]

Return all the sections that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Section

Source code in odfdo/element.py
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
def get_sections(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Section]:
    """Return all the sections that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Section
    """
    return self._filtered_elements(
        "text:section", text_style=style, content=content
    )  # type: ignore[return-value]

get_span

get_span(
    position: int = 0, content: str | None = None
) -> Span | None

Return the span that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: Span or None if not found

Source code in odfdo/element.py
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
def get_span(
    self,
    position: int = 0,
    content: str | None = None,
) -> Span | None:
    """Return the span that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: Span or None if not found
    """
    return self._filtered_element(
        "descendant::text:span", position, content=content
    )  # type: ignore[return-value]

get_spans

get_spans(
    style: str | None = None, content: str | None = None
) -> list[Span]

Return all the spans that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Span

Source code in odfdo/element.py
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
def get_spans(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Span]:
    """Return all the spans that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Span
    """
    return self._filtered_elements(
        "descendant::text:span", text_style=style, content=content
    )  # type: ignore[return-value]

get_style

get_style(
    family: str,
    name_or_element: str | Element | None = None,
    display_name: str | None = None,
) -> Style | None

Return the style uniquely identified by the family/name pair. If the argument is already a style object, it will return it.

If the name is not the internal name but the name you gave in the desktop application, use display_name instead.

Arguments:

family -- 'paragraph', 'text', 'graphic', 'table', 'list',
          'number'

name_or_element -- str or Style

display_name -- str

Return: Style or None if not found

Source code in odfdo/element.py
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
def get_style(
    self,
    family: str,
    name_or_element: str | Element | None = None,
    display_name: str | None = None,
) -> Style | None:
    """Return the style uniquely identified by the family/name pair. If
    the argument is already a style object, it will return it.

    If the name is not the internal name but the name you gave in the
    desktop application, use display_name instead.

    Arguments:

        family -- 'paragraph', 'text', 'graphic', 'table', 'list',
                  'number'

        name_or_element -- str or Style

        display_name -- str

    Return: Style or None if not found
    """
    if isinstance(name_or_element, Element):
        name = self.get_attribute("style:name")
        if name is not None:
            return name_or_element  # type: ignore[return-value]
        else:
            raise ValueError(f"Not a odf_style ? {name_or_element!r}")
    style_name = name_or_element
    is_default = not (style_name or display_name)
    tagname = self._get_style_tagname(family, is_default=is_default)
    # famattr became None if no "style:family" attribute
    if family:
        return self._filtered_element(
            tagname,
            0,
            style_name=style_name,
            display_name=display_name,
            family=family,
        )  # type: ignore[return-value]
    else:
        return self._filtered_element(
            tagname,
            0,
            draw_name=style_name or display_name,
            family=family,
        )  # type: ignore[return-value]

get_styled_elements

get_styled_elements(name: str = '') -> list[Element]

Brute-force to find paragraphs, tables, etc. using the given style name (or all by default).

Arguments:

name -- str

Return: list of Element

Source code in odfdo/element.py
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
def get_styled_elements(self, name: str = "") -> list[Element]:
    """Brute-force to find paragraphs, tables, etc. using the given style
    name (or all by default).

    Arguments:

        name -- str

    Return: list of Element
    """
    # FIXME incomplete (and possibly inaccurate)
    return (
        self._filtered_elements("descendant::*", text_style=name)
        + self._filtered_elements("descendant::*", draw_style=name)
        + self._filtered_elements("descendant::*", draw_text_style=name)
        + self._filtered_elements("descendant::*", table_style=name)
        + self._filtered_elements("descendant::*", page_layout=name)
        + self._filtered_elements("descendant::*", master_page=name)
        + self._filtered_elements("descendant::*", parent_style=name)
    )

get_styles

get_styles(family: str | None = None) -> list[Element]
Source code in odfdo/element.py
3494
3495
3496
3497
def get_styles(self, family: str | None = None) -> list[Element]:
    # Both common and default styles
    tagname = self._get_style_tagname(family)
    return self._filtered_elements(tagname, family=family)

get_text_change

get_text_change(
    position: int = 0, idx: str | None = None
) -> TextChange | TextChangeStart | None

Return the text change that matches the criteria. Either single deletion (text:change) or start of range of changes (text:change-start). position : index of the element to retrieve if several matches, default is 0. idx : change-id of the element.

Arguments:

position -- int

idx -- str

Return: Element or None if not found

Source code in odfdo/element.py
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
def get_text_change(
    self,
    position: int = 0,
    idx: str | None = None,
) -> TextChange | TextChangeStart | None:
    """Return the text change that matches the criteria. Either single
    deletion (text:change) or start of range of changes (text:change-start).
    position : index of the element to retrieve if several matches, default
    is 0.
    idx : change-id of the element.

    Arguments:

        position -- int

        idx -- str

    Return: Element or None if not found
    """
    if idx:
        request = (
            f'descendant::text:change-start[@text:change-id="{idx}"] '
            f'| descendant::text:change[@text:change-id="{idx}"]'
        )
        return self._filtered_element(request, 0)  # type: ignore[return-value]

    request = "descendant::text:change-start | descendant::text:change"
    return self._filtered_element(request, position)  # type: ignore[return-value]

get_text_change_deletion

get_text_change_deletion(
    position: int = 0, idx: str | None = None
) -> TextChange | None

Return the text change of deletion kind that matches the criteria. Search only for the tags text:change. Consider using : get_text_change()

Arguments:

position -- int

idx -- str

Return: TextChange or None if not found

Source code in odfdo/element.py
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
def get_text_change_deletion(
    self,
    position: int = 0,
    idx: str | None = None,
) -> TextChange | None:
    """Return the text change of deletion kind that matches the criteria.
    Search only for the tags text:change.
    Consider using : get_text_change()

    Arguments:

        position -- int

        idx -- str

    Return: TextChange or None if not found
    """
    return self._filtered_element(
        "descendant::text:change", position, change_id=idx
    )  # type: ignore[return-value]

get_text_change_deletions

get_text_change_deletions() -> list[TextChange]

Return all the text changes of deletion kind: the tags text:change. Consider using : get_text_changes()

Return: list of TextChange

Source code in odfdo/element.py
3288
3289
3290
3291
3292
3293
3294
3295
3296
def get_text_change_deletions(self) -> list[TextChange]:
    """Return all the text changes of deletion kind: the tags text:change.
    Consider using : get_text_changes()

    Return: list of TextChange
    """
    return self._filtered_elements(
        "descendant::text:text:change",
    )  # type: ignore[return-value]

get_text_change_end

get_text_change_end(
    position: int = 0, idx: str | None = None
) -> TextChangeEnd | None

Return the text change-end that matches the criteria. Search only the tags text:change-end. Consider using : get_text_change()

Arguments:

position -- int

idx -- str

Return: TextChangeEnd or None if not found

Source code in odfdo/element.py
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
def get_text_change_end(
    self,
    position: int = 0,
    idx: str | None = None,
) -> TextChangeEnd | None:
    """Return the text change-end that matches the criteria. Search only
    the tags text:change-end.
    Consider using : get_text_change()

    Arguments:

        position -- int

        idx -- str

    Return: TextChangeEnd or None if not found
    """
    return self._filtered_element(
        "descendant::text:change-end", position, change_id=idx
    )  # type: ignore[return-value]

get_text_change_ends

get_text_change_ends() -> list[TextChangeEnd]

Return all the text change-end. Search only the tags text:change-end. Consider using : get_text_changes()

Return: list of TextChangeEnd

Source code in odfdo/element.py
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
def get_text_change_ends(self) -> list[TextChangeEnd]:
    """Return all the text change-end. Search only the tags
    text:change-end.
    Consider using : get_text_changes()

    Return: list of TextChangeEnd
    """
    return self._filtered_elements(
        "descendant::text:change-end",
    )  # type: ignore[return-value]

get_text_change_start

get_text_change_start(
    position: int = 0, idx: str | None = None
) -> TextChangeStart | None

Return the text change-start that matches the criteria. Search only the tags text:change-start. Consider using : get_text_change()

Arguments:

position -- int

idx -- str

Return: TextChangeStart or None if not found

Source code in odfdo/element.py
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
def get_text_change_start(
    self,
    position: int = 0,
    idx: str | None = None,
) -> TextChangeStart | None:
    """Return the text change-start that matches the criteria. Search
    only the tags text:change-start.
    Consider using : get_text_change()

    Arguments:

        position -- int

        idx -- str

    Return: TextChangeStart or None if not found
    """
    return self._filtered_element(
        "descendant::text:change-start", position, change_id=idx
    )  # type: ignore[return-value]

get_text_change_starts

get_text_change_starts() -> list[TextChangeStart]

Return all the text change-start. Search only for the tags text:change-start. Consider using : get_text_changes()

Return: list of TextChangeStart

Source code in odfdo/element.py
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
def get_text_change_starts(self) -> list[TextChangeStart]:
    """Return all the text change-start. Search only for the tags
    text:change-start.
    Consider using : get_text_changes()

    Return: list of TextChangeStart
    """
    return self._filtered_elements(
        "descendant::text:change-start",
    )  # type: ignore[return-value]

get_text_changes

get_text_changes() -> list[TextChange | TextChangeStart]

Return all the text changes, either single deletion (text:change) or start of range of changes (text:change-start).

Return: list of TextChange or TextChangeStart

Source code in odfdo/element.py
3383
3384
3385
3386
3387
3388
3389
3390
def get_text_changes(self) -> list[TextChange | TextChangeStart]:
    """Return all the text changes, either single deletion
    (text:change) or start of range of changes (text:change-start).

    Return: list of TextChange or TextChangeStart
    """
    request = "descendant::text:change-start | descendant::text:change"
    return self._filtered_elements(request)  # type: ignore[return-value]

get_toc

get_toc(
    position: int = 0, content: str | None = None
) -> TOC | None

Return the table of contents that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: TOC or None if not found

Source code in odfdo/element.py
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
def get_toc(
    self,
    position: int = 0,
    content: str | None = None,
) -> TOC | None:
    """Return the table of contents that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: TOC or None if not found
    """
    return self._filtered_element(
        "text:table-of-content", position, content=content
    )  # type: ignore[return-value]

get_tocs

get_tocs() -> list[TOC]

Return all the tables of contents.

Return: list of TOC

Source code in odfdo/element.py
3432
3433
3434
3435
3436
3437
def get_tocs(self) -> list[TOC]:
    """Return all the tables of contents.

    Return: list of TOC
    """
    return self.get_elements("text:table-of-content")  # type: ignore[return-value]

get_tracked_changes

get_tracked_changes() -> TrackedChanges | None

Return the tracked-changes part in the text body.

Return: TrackedChanges or None

Source code in odfdo/element.py
3263
3264
3265
3266
3267
3268
def get_tracked_changes(self) -> TrackedChanges | None:
    """Return the tracked-changes part in the text body.

    Return: TrackedChanges or None
    """
    return self.get_element("//text:tracked-changes")  # type: ignore[return-value]

get_user_defined

get_user_defined(
    name: str, position: int = 0
) -> UserDefined | None

return the user defined declaration for the given name.

return: UserDefined or none if not found

Source code in odfdo/element.py
2609
2610
2611
2612
2613
2614
2615
2616
def get_user_defined(self, name: str, position: int = 0) -> UserDefined | None:
    """return the user defined declaration for the given name.

    return: UserDefined or none if not found
    """
    return self._filtered_element(
        "descendant::text:user-defined", position, text_name=name
    )  # type: ignore[return-value]

get_user_defined_list

get_user_defined_list() -> list[UserDefined]

Return all the user defined field declarations.

Return: list of UserDefined

Source code in odfdo/element.py
2592
2593
2594
2595
2596
2597
2598
2599
def get_user_defined_list(self) -> list[UserDefined]:
    """Return all the user defined field declarations.

    Return: list of UserDefined
    """
    return self._filtered_elements(
        "descendant::text:user-defined",
    )  # type: ignore[return-value]

get_user_defined_value

get_user_defined_value(
    name: str, value_type: str | None = None
) -> (
    bool
    | str
    | int
    | float
    | Decimal
    | datetime
    | timedelta
    | None
)

Return the value of the given user defined field name.

Arguments:

name -- str

value_type -- 'boolean', 'date', 'float',
              'string', 'time' or automatic

Return: most appropriate Python type

Source code in odfdo/element.py
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
def get_user_defined_value(
    self, name: str, value_type: str | None = None
) -> bool | str | int | float | Decimal | datetime | timedelta | None:
    """Return the value of the given user defined field name.

    Arguments:

        name -- str

        value_type -- 'boolean', 'date', 'float',
                      'string', 'time' or automatic

    Return: most appropriate Python type
    """
    user_defined = self.get_user_defined(name)
    if user_defined is None:
        return None
    return user_defined.get_value(value_type)  # type: ignore[return-value]

get_user_field_decl

get_user_field_decl(
    name: str, position: int = 0
) -> UserFieldDecls | None

return the user field declaration for the given name.

return: Element or none if not found

Source code in odfdo/element.py
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
def get_user_field_decl(
    self, name: str, position: int = 0
) -> UserFieldDecls | None:
    """return the user field declaration for the given name.

    return: Element or none if not found
    """
    return self._filtered_element(
        "descendant::text:user-field-decl", position, text_name=name
    )  # type: ignore[return-value]

get_user_field_decl_list

get_user_field_decl_list() -> list[UserFieldDecls]

Return all the user field declarations.

Return: list of UserFieldDecls

Source code in odfdo/element.py
2549
2550
2551
2552
2553
2554
2555
2556
def get_user_field_decl_list(self) -> list[UserFieldDecls]:
    """Return all the user field declarations.

    Return: list of UserFieldDecls
    """
    return self._filtered_elements(
        "descendant::text:user-field-decl",
    )  # type: ignore[return-value]

get_user_field_decls

get_user_field_decls() -> UserFieldDecls | None

Return the container for user field declarations. Created if not found.

Return: UserFieldDecls

Source code in odfdo/element.py
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
def get_user_field_decls(self) -> UserFieldDecls | None:
    """Return the container for user field declarations. Created if not
    found.

    Return: UserFieldDecls
    """
    user_field_decls = self.get_element("//text:user-field-decls")
    if user_field_decls is None:
        body = self.document_body
        if not body:
            raise ValueError("Empty document.body")
        body.insert(Element.from_tag("text:user-field-decls"), FIRST_CHILD)
        user_field_decls = body.get_element("//text:user-field-decls")

    return user_field_decls  # type: ignore[return-value]

get_user_field_value

get_user_field_value(
    name: str, value_type: str | None = None
) -> (
    bool
    | str
    | int
    | float
    | Decimal
    | datetime
    | timedelta
    | None
)

Return the value of the given user field name.

Arguments:

name -- str

value_type -- 'boolean', 'currency', 'date', 'float',
              'percentage', 'string', 'time' or automatic

Return: most appropriate Python type

Source code in odfdo/element.py
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
def get_user_field_value(
    self, name: str, value_type: str | None = None
) -> bool | str | int | float | Decimal | datetime | timedelta | None:
    """Return the value of the given user field name.

    Arguments:

        name -- str

        value_type -- 'boolean', 'currency', 'date', 'float',
                      'percentage', 'string', 'time' or automatic

    Return: most appropriate Python type
    """
    user_field_decl = self.get_user_field_decl(name)
    if user_field_decl is None:
        return None
    value = user_field_decl.get_value(value_type)  # type: ignore[attr-defined]
    return value  # type: ignore[no-any-return]

get_variable_decl

get_variable_decl(
    name: str, position: int = 0
) -> VarDecls | None

return the variable declaration for the given name.

Arguments:

name -- str

position -- int

return: VarDecls or none if not found

Source code in odfdo/element.py
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
def get_variable_decl(self, name: str, position: int = 0) -> VarDecls | None:
    """return the variable declaration for the given name.

    Arguments:

        name -- str

        position -- int

    return: VarDecls or none if not found
    """
    return self._filtered_element(
        "descendant::text:variable-decl", position, text_name=name
    )  # type: ignore[return-value]

get_variable_decl_list

get_variable_decl_list() -> list[VarDecls]

Return all the variable declarations.

Return: list of VarDecls

Source code in odfdo/element.py
2457
2458
2459
2460
2461
2462
2463
2464
def get_variable_decl_list(self) -> list[VarDecls]:
    """Return all the variable declarations.

    Return: list of VarDecls
    """
    return self._filtered_elements(
        "descendant::text:variable-decl",
    )  # type: ignore[return-value]

get_variable_decls

get_variable_decls() -> VarDecls

Return the container for variable declarations. Created if not found.

Return: VarDecls

Source code in odfdo/element.py
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
def get_variable_decls(self) -> VarDecls:
    """Return the container for variable declarations. Created if not
    found.

    Return: VarDecls
    """
    variable_decls = self.get_element("//text:variable-decls")
    if variable_decls is None:
        body = self.document_body
        if not body:
            raise ValueError("Empty document.body")
        body.insert(Element.from_tag("text:variable-decls"), FIRST_CHILD)
        variable_decls = body.get_element("//text:variable-decls")

    return variable_decls  # type: ignore[return-value]

get_variable_set

get_variable_set(
    name: str, position: int = -1
) -> VarSet | None

Return the variable set for the given name (last one by default).

Arguments:

name -- str

position -- int

Return: VarSet or None if not found

Source code in odfdo/element.py
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
def get_variable_set(self, name: str, position: int = -1) -> VarSet | None:
    """Return the variable set for the given name (last one by default).

    Arguments:

        name -- str

        position -- int

    Return: VarSet or None if not found
    """
    return self._filtered_element(
        "descendant::text:variable-set", position, text_name=name
    )  # type: ignore[return-value]

get_variable_set_value

get_variable_set_value(
    name: str, value_type: str | None = None
) -> (
    bool
    | str
    | int
    | float
    | Decimal
    | datetime
    | timedelta
    | None
)

Return the last value of the given variable name.

Arguments:

name -- str

value_type -- 'boolean', 'currency', 'date', 'float',
              'percentage', 'string', 'time' or automatic

Return: most appropriate Python type

Source code in odfdo/element.py
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
def get_variable_set_value(
    self,
    name: str,
    value_type: str | None = None,
) -> bool | str | int | float | Decimal | datetime | timedelta | None:
    """Return the last value of the given variable name.

    Arguments:

        name -- str

        value_type -- 'boolean', 'currency', 'date', 'float',
                      'percentage', 'string', 'time' or automatic

    Return: most appropriate Python type
    """
    variable_set = self.get_variable_set(name)
    if not variable_set:
        return None
    return variable_set.get_value(value_type)  # type: ignore[return-value]

get_variable_sets

get_variable_sets(name: str | None = None) -> list[VarSet]

Return all the variable sets that match the criteria.

Arguments:

name -- str

Return: list of VarSet

Source code in odfdo/element.py
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
def get_variable_sets(self, name: str | None = None) -> list[VarSet]:
    """Return all the variable sets that match the criteria.

    Arguments:

        name -- str

    Return: list of VarSet
    """
    return self._filtered_elements(
        "descendant::text:variable-set",
        text_name=name,
    )  # type: ignore[return-value]

index

index(child: Element) -> int

Return the position of the child in this element.

Inspired by lxml.

Source code in odfdo/element.py
1121
1122
1123
1124
1125
1126
1127
def index(self, child: Element) -> int:
    """Return the position of the child in this element.

    Inspired by lxml.
    """
    idx: int = self.__element.index(child.__element)
    return idx

insert

insert(
    element: Element,
    xmlposition: int | None = None,
    position: int | None = None,
    start: bool = False,
) -> None

Insert an element relatively to ourself.

Insert either using DOM vocabulary or by numeric position. If text start is True, insert the element before any existing text.

Position start at 0.

Arguments:

element -- Element

xmlposition -- FIRST_CHILD, LAST_CHILD, NEXT_SIBLING
               or PREV_SIBLING

start -- Boolean

position -- int
Source code in odfdo/element.py
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
def insert(
    self,
    element: Element,
    xmlposition: int | None = None,
    position: int | None = None,
    start: bool = False,
) -> None:
    """Insert an element relatively to ourself.

    Insert either using DOM vocabulary or by numeric position.
    If text start is True, insert the element before any existing text.

    Position start at 0.

    Arguments:

        element -- Element

        xmlposition -- FIRST_CHILD, LAST_CHILD, NEXT_SIBLING
                       or PREV_SIBLING

        start -- Boolean

        position -- int
    """
    # child_tag = element.tag
    current = self.__element
    lx_element = element.__element
    if start:
        text = current.text
        if text is not None:
            current.text = None
            tail = lx_element.tail
            if tail is None:
                tail = text
            else:
                tail = tail + text
            lx_element.tail = tail
        position = 0
    if position is not None:
        current.insert(position, lx_element)
    elif xmlposition is FIRST_CHILD:
        current.insert(0, lx_element)
    elif xmlposition is LAST_CHILD:
        current.append(lx_element)
    elif xmlposition is NEXT_SIBLING:
        parent = current.getparent()
        index = parent.index(current)
        parent.insert(index + 1, lx_element)
    elif xmlposition is PREV_SIBLING:
        parent = current.getparent()
        index = parent.index(current)
        parent.insert(index, lx_element)
    else:
        raise ValueError("(xml)position must be defined")

is_empty

is_empty() -> bool

Check if the element is empty : no text, no children, no tail.

Return: Boolean

Source code in odfdo/element.py
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
def is_empty(self) -> bool:
    """Check if the element is empty : no text, no children, no tail.

    Return: Boolean
    """
    element = self.__element
    if element.tail is not None:
        return False
    if element.text is not None:
        return False
    if list(element.iterchildren()):  # noqa: SIM103
        return False
    return True

make_etree_element staticmethod

make_etree_element(tag: str) -> _Element
Source code in odfdo/element.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
@staticmethod
def make_etree_element(tag: str) -> _Element:
    if not isinstance(tag, str):
        raise TypeError(f"Tag is not str: {tag!r}")
    tag = tag.strip()
    if not tag:
        raise ValueError("Tag is empty")
    if "<" not in tag:
        # Qualified name
        # XXX don't build the element from scratch or lxml will pollute with
        # repeated namespace declarations
        tag = f"<{tag}/>"
    # XML fragment
    root = fromstring(NAMESPACES_XML % str_to_bytes(tag))
    return root[0]

match

match(pattern: str) -> bool

return True if the pattern is found one or more times anywhere in the text content of the element.

Python regular expression syntax applies.

Arguments:

pattern -- str

Return: bool

Source code in odfdo/element.py
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
def match(self, pattern: str) -> bool:
    """return True if the pattern is found one or more times anywhere in
    the text content of the element.

    Python regular expression syntax applies.

    Arguments:

        pattern -- str

    Return: bool
    """
    return self.search(pattern) is not None

replace

replace(
    pattern: str,
    new: str | None = None,
    formatted: bool = False,
) -> int

Replace the pattern with the given text, or delete if text is an empty string, and return the number of replacements. By default, only return the number of occurences that would be replaced.

It cannot replace patterns found across several element, like a word split into two consecutive spans.

Python regular expression syntax applies.

If formatted is True, and the target is a Paragraph, Span or Header, and the replacement text contains spaces, tabs or newlines, try to convert them into actual ODF elements to obtain a formatted result. On very complex contents, result may differ of expectations.

Arguments:

pattern -- str

new -- str

formatted -- bool

Return: int

Source code in odfdo/element.py
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
def replace(
    self,
    pattern: str,
    new: str | None = None,
    formatted: bool = False,
) -> int:
    """Replace the pattern with the given text, or delete if text is an
    empty string, and return the number of replacements. By default, only
    return the number of occurences that would be replaced.

    It cannot replace patterns found across several element, like a word
    split into two consecutive spans.

    Python regular expression syntax applies.

    If formatted is True, and the target is a Paragraph, Span or Header,
    and the replacement text contains spaces, tabs or newlines, try to
    convert them into actual ODF elements to obtain a formatted result.
    On very complex contents, result may differ of expectations.

    Arguments:

        pattern -- str

        new -- str

        formatted -- bool

    Return: int
    """
    if not isinstance(pattern, str):
        # Fail properly if the pattern is an non-ascii bytestring
        pattern = str(pattern)
    cpattern = re.compile(pattern)
    count = 0
    for text in self.xpath("descendant::text()"):
        if new is None:
            count += len(cpattern.findall(str(text)))
        else:
            new_text, number = cpattern.subn(new, str(text))
            container = text.parent
            if not container:
                continue
            if text.is_text():  # type: ignore
                container.text = new_text
            else:
                container.tail = new_text
            if formatted and container.tag in {  # type; ignore
                "text:h",
                "text:p",
                "text:span",
            }:
                container.append_plain_text("")  # type: ignore[attr-defined]
            count += number
    return count

replace_element

replace_element(
    old_element: Element, new_element: Element
) -> None

Replaces in place a sub element with the element passed as second argument.

Warning : no clone for old element.

Source code in odfdo/element.py
1551
1552
1553
1554
1555
1556
1557
1558
def replace_element(self, old_element: Element, new_element: Element) -> None:
    """Replaces in place a sub element with the element passed as second
    argument.

    Warning : no clone for old element.
    """
    current = self.__element
    current.replace(old_element.__element, new_element.__element)

search

search(pattern: str) -> int | None

Return the first position of the pattern in the text content of the element, or None if not found.

Python regular expression syntax applies.

Arguments:

pattern -- str

Return: int or None

Source code in odfdo/element.py
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
def search(self, pattern: str) -> int | None:
    """Return the first position of the pattern in the text content of
    the element, or None if not found.

    Python regular expression syntax applies.

    Arguments:

        pattern -- str

    Return: int or None
    """
    match = re.search(pattern, self.text_recursive)
    if match is None:
        return None
    return match.start()

search_all

search_all(pattern: str) -> list[tuple[int, int]]

Return all start and end positions of the regex pattern in the text content of the element.

Result is a list of tuples of start and end position of the matches. Python regular expression syntax applies.

Arguments:

pattern -- str

Return: list[tuple[int,int]]

Source code in odfdo/element.py
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
def search_all(self, pattern: str) -> list[tuple[int, int]]:
    """Return all start and end positions of the regex pattern in
    the text content of the element.

    Result is a list of tuples of start and end position of
    the matches.
    Python regular expression syntax applies.

    Arguments:

        pattern -- str

    Return: list[tuple[int,int]]
    """
    results: list[tuple[int, int]] = []
    for match in re.finditer(pattern, self.text_recursive):
        results.append((match.start(), match.end()))
    return results

search_first

search_first(pattern: str) -> tuple[int, int] | None

Return the start and end position of the first occurence of the regex pattern in the text content of the element.

Result is tuples of start and end position, or None. Python regular expression syntax applies.

Arguments:

pattern -- str

Return: tuple[int,int] or None

Source code in odfdo/element.py
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
def search_first(self, pattern: str) -> tuple[int, int] | None:
    """Return the start and end position of the first occurence
    of the regex pattern in the text content of the element.

    Result is tuples of start and end position, or None.
    Python regular expression syntax applies.

    Arguments:

        pattern -- str

    Return: tuple[int,int] or None
    """
    match = re.search(pattern, self.text_recursive)
    if match is None:
        return None
    return match.start(), match.end()

serialize

serialize(
    pretty: bool = False, with_ns: bool = False
) -> str

Return text serialization of XML element.

Source code in odfdo/element.py
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
def serialize(self, pretty: bool = False, with_ns: bool = False) -> str:
    """Return text serialization of XML element."""
    # This copy bypasses serialization side-effects in lxml
    native = deepcopy(self.__element)
    data: str = tostring(
        native, with_tail=False, pretty_print=pretty, encoding="unicode"
    )
    if with_ns:
        return data
    # Remove namespaces
    return self._strip_namespaces(data)

set_attribute

set_attribute(
    name: str,
    value: bool | str | tuple[int, int, int] | None,
) -> None
Source code in odfdo/element.py
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
def set_attribute(
    self, name: str, value: bool | str | tuple[int, int, int] | None
) -> None:
    if name in ODF_COLOR_PROPERTY:
        if isinstance(value, bool):
            raise TypeError(f"Wrong color type {value!r}")
        if value != "transparent":
            value = hexa_color(value)
    element = self.__element
    lxml_tag = _get_lxml_tag_or_name(name)
    if isinstance(value, bool):
        value = Boolean.encode(value)
    elif value is None:
        with contextlib.suppress(KeyError):
            del element.attrib[lxml_tag]
        return
    element.set(lxml_tag, str(value))

set_style_attribute

set_style_attribute(
    name: str, value: Element | str | None
) -> None

Shortcut to accept a style object as a value.

Source code in odfdo/element.py
877
878
879
880
881
def set_style_attribute(self, name: str, value: Element | str | None) -> None:
    """Shortcut to accept a style object as a value."""
    if isinstance(value, Element):
        value = str(value.name)  # type:ignore
    return self.set_attribute(name, value)

strip_elements

strip_elements(
    sub_elements: Element | Iterable[Element],
) -> Element | list

Remove the tags of provided elements, keeping inner childs and text.

Return : the striped element.

Warning : no clone in sub_elements list.

Arguments:

sub_elements -- Element or list of Element
Source code in odfdo/element.py
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
def strip_elements(
    self,
    sub_elements: Element | Iterable[Element],
) -> Element | list:
    """Remove the tags of provided elements, keeping inner childs and text.

    Return : the striped element.

    Warning : no clone in sub_elements list.

    Arguments:

        sub_elements -- Element or list of Element
    """
    if not sub_elements:
        return self
    if isinstance(sub_elements, Element):
        sub_elements = (sub_elements,)
    replacer = _get_lxml_tag("text:this-will-be-removed")
    for element in sub_elements:
        element.__element.tag = replacer
    strip = ("text:this-will-be-removed",)
    return self.strip_tags(strip=strip, default=None)

strip_tags

strip_tags(
    strip: Iterable[str] | None = None,
    protect: Iterable[str] | None = None,
    default: str | None = "text:p",
) -> Element | list

Remove the tags listed in strip, recursively, keeping inner childs and text. Tags listed in protect stop the removal one level depth. If the first level element is stripped, default is used to embed the content in the default element. If default is None and first level is striped, a list of text and children is returned. Return : the striped element.

strip_tags should be used by on purpose methods (strip_span …) (Method name taken from lxml).

Arguments:

strip -- iterable list of str odf tags, or None

protect -- iterable list of str odf tags, or None

default -- str odf tag, or None

Return:

Element.
Source code in odfdo/element.py
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
def strip_tags(
    self,
    strip: Iterable[str] | None = None,
    protect: Iterable[str] | None = None,
    default: str | None = "text:p",
) -> Element | list:
    """Remove the tags listed in strip, recursively, keeping inner childs
    and text. Tags listed in protect stop the removal one level depth. If
    the first level element is stripped, default is used to embed the
    content in the default element. If default is None and first level is
    striped, a list of text and children is returned. Return : the striped
    element.

    strip_tags should be used by on purpose methods (strip_span ...)
    (Method name taken from lxml).

    Arguments:

        strip -- iterable list of str odf tags, or None

        protect -- iterable list of str odf tags, or None

        default -- str odf tag, or None

    Return:

        Element.
    """
    if not strip:
        return self
    if not protect:
        protect = ()
    protected = False
    element, modified = Element._strip_tags(self, strip, protect, protected)
    if modified and isinstance(element, list) and default:
        new = Element.from_tag(default)
        for content in element:
            if isinstance(content, Element):
                new.__append(content)
            else:
                new.text = content
        element = new
    return element

text_at

text_at(start: int, end: int | None = None) -> str

Return the text (recursive) content of the element between start and end position.

If the end parameter is not set, return from start to the end of the recursive text.

Arguments:

start -- int
end -- int or None

Return: str

Source code in odfdo/element.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
def text_at(self, start: int, end: int | None = None) -> str:
    """Return the text (recursive) content of the element between
    start and end position.

    If the end parameter is not set, return from start to the end
    of the recursive text.

    Arguments:

        start -- int
        end -- int or None

    Return: str
    """
    if start < 0:
        start = 0
    if end is None:
        return self.text_recursive[start:]
    else:
        if end < start:
            end = start
        return self.text_recursive[start:end]

xpath

xpath(xpath_query: str) -> list[Element | EText]

Apply XPath query to the element and its subtree. Return list of Element or EText instances translated from the nodes found.

Source code in odfdo/element.py
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
def xpath(self, xpath_query: str) -> list[Element | EText]:
    """Apply XPath query to the element and its subtree. Return list of
    Element or EText instances translated from the nodes found.
    """
    element = self.__element
    xpath_instance = xpath_compile(xpath_query)
    elements = xpath_instance(element)
    result: list[Element | EText] = []
    if hasattr(elements, "__iter__"):
        for obj in elements:
            if isinstance(obj, (str, bytes)):
                result.append(EText(obj))
            elif isinstance(obj, _Element):
                result.append(Element.from_tag(obj))
            # else:
            #     result.append(obj)
    return result

ElementTyped

Bases: Element

Subclass of Element for classes managing typed values.

Methods:

Name Description
get_value

Return Python typed value.

set_value_and_type
Source code in odfdo/element_typed.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class ElementTyped(Element):
    """Subclass of Element for classes managing typed values."""

    def set_value_and_type(
        self,
        value: Any,
        value_type: str | None = None,
        text: str | None = None,
        currency: str | None = None,
    ) -> str | None:
        # Remove possible previous value and type
        for name in (
            "office:value-type",
            "office:boolean-value",
            "office:value",
            "office:date-value",
            "office:string-value",
            "office:time-value",
            "table:formula",
            "office:currency",
            "calcext:value-type",
            "loext:value-type",
        ):
            with contextlib.suppress(KeyError):
                self.del_attribute(name)
        if isinstance(value, bytes):
            value = bytes_to_str(value)
        if isinstance(value_type, bytes):
            value_type = bytes_to_str(value_type)
        if isinstance(text, bytes):
            text = bytes_to_str(text)
        if isinstance(currency, bytes):
            currency = bytes_to_str(currency)
        if value is None:
            self._erase_text_content()
            return text
        if isinstance(value, bool):
            if value_type is None:
                value_type = "boolean"
            if text is None:
                text = "true" if value else "false"
            value = Boolean.encode(value)
        elif isinstance(value, (int, float, Decimal)):
            if value_type == "percentage":
                text = f"{int(value * 100)} %"
            if value_type is None:
                value_type = "float"
            if text is None:
                text = str(value)
            value = str(value)
        elif isinstance(value, datetime):
            if value_type is None:
                value_type = "date"
            if text is None:
                text = str(DateTime.encode(value))
            value = DateTime.encode(value)
        elif isinstance(value, date):
            if value_type is None:
                value_type = "date"
            if text is None:
                text = str(Date.encode(value))
            value = Date.encode(value)
        elif isinstance(value, str):
            if value_type is None:
                value_type = "string"
            if text is None:
                text = value
        elif isinstance(value, timedelta):
            if value_type is None:
                value_type = "time"
            if text is None:
                text = str(Duration.encode(value))
            value = Duration.encode(value)
        else:
            raise TypeError(f"Type unknown: '{value!r}'")

        if isinstance(value_type, str):
            self.set_attribute("office:value-type", value_type)
            self.set_attribute("calcext:value-type", value_type)
        if value_type == "boolean":
            self.set_attribute("office:boolean-value", value)
        elif value_type == "currency":
            self.set_attribute("office:value", value)
            self.set_attribute("office:currency", currency)
        elif value_type == "date":
            self.set_attribute("office:date-value", value)
        elif value_type in ("float", "percentage"):
            self.set_attribute("office:value", value)
            self.set_attribute("calcext:value", value)
        elif value_type == "string":
            self.set_attribute("office:string-value", value)
        elif value_type == "time":
            self.set_attribute("office:time-value", value)

        return text

    def _get_typed_value_boolean(self) -> Any:
        return self.get_attribute("office:boolean-value")

    def _get_typed_value_number_type(self) -> Decimal | int | float:
        read_number = self.get_attribute("office:value")
        value = Decimal(read_number)
        # Return 3 instead of 3.0 if possible
        with contextlib.suppress(ValueError):
            if int(value) == value:
                return int(value)
        return value

    def _get_typed_value_float(self) -> Decimal | int | float:
        return self._get_typed_value_number_type()

    def _get_typed_value_percentage(self) -> Decimal | int | float:
        return self._get_typed_value_number_type()

    def _get_typed_value_currency(self) -> Decimal | int | float:
        return self._get_typed_value_number_type()

    def _get_typed_value_date(self) -> date | datetime:
        read_attribute = self.get_attribute("office:date-value")
        if "T" in read_attribute:
            return DateTime.decode(read_attribute)
        return Date.decode(read_attribute)

    def _get_typed_value_string(self, try_get_text: bool) -> str | None:
        value = self.get_attribute("office:string-value")
        if value is not None:
            return str(value)
        if try_get_text:
            list_value = [para.inner_text for para in self.get_elements("text:p")]
            if list_value:
                return "\n".join(list_value)
        return None

    def _get_typed_value_time(self) -> timedelta:
        read_value = self.get_attribute("office:time-value")
        return Duration.decode(read_value)

    def _get_typed_value(
        self,
        value_type: str = "",
        try_get_text: bool = True,
    ) -> Any:
        """Return Python typed value.

        Only for "with office:value-type" elements, not for meta fields."""
        if value_type == "string":
            return self._get_typed_value_string(try_get_text)
        method = getattr(self, f"_get_typed_value_{value_type}", None)
        if method is None:
            raise TypeError(f"Unexpected value type: {value_type}")
        return method()

    def _get_value_and_type(
        self, value_type: str | None = None, try_get_text: bool = True
    ) -> tuple[Any, str]:
        if value_type is None:
            read_value_type = self.get_attribute("office:value-type")
            if isinstance(read_value_type, bool):
                # for very old versions
                raise TypeError(
                    f'Wrong type for "office:value-type": {type(read_value_type)}'
                )
            if read_value_type is None:
                return None, None
            value_type = read_value_type
        return (
            self._get_typed_value(
                value_type=value_type,
                try_get_text=try_get_text,
            ),
            value_type,
        )

    def get_value(
        self,
        value_type: str | None = None,
        try_get_text: bool = True,
        get_type: bool = False,
    ) -> Any | tuple[Any, str]:
        """Return Python typed value.

        Only for "with office:value-type" elements, not for meta fields."""
        value, actual_type = self._get_value_and_type(
            value_type=value_type, try_get_text=try_get_text
        )
        if get_type:
            return (value, actual_type)
        return value

get_value

get_value(
    value_type: str | None = None,
    try_get_text: bool = True,
    get_type: bool = False,
) -> Any | tuple[Any, str]

Return Python typed value.

Only for “with office:value-type” elements, not for meta fields.

Source code in odfdo/element_typed.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def get_value(
    self,
    value_type: str | None = None,
    try_get_text: bool = True,
    get_type: bool = False,
) -> Any | tuple[Any, str]:
    """Return Python typed value.

    Only for "with office:value-type" elements, not for meta fields."""
    value, actual_type = self._get_value_and_type(
        value_type=value_type, try_get_text=try_get_text
    )
    if get_type:
        return (value, actual_type)
    return value

set_value_and_type

set_value_and_type(
    value: Any,
    value_type: str | None = None,
    text: str | None = None,
    currency: str | None = None,
) -> str | None
Source code in odfdo/element_typed.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def set_value_and_type(
    self,
    value: Any,
    value_type: str | None = None,
    text: str | None = None,
    currency: str | None = None,
) -> str | None:
    # Remove possible previous value and type
    for name in (
        "office:value-type",
        "office:boolean-value",
        "office:value",
        "office:date-value",
        "office:string-value",
        "office:time-value",
        "table:formula",
        "office:currency",
        "calcext:value-type",
        "loext:value-type",
    ):
        with contextlib.suppress(KeyError):
            self.del_attribute(name)
    if isinstance(value, bytes):
        value = bytes_to_str(value)
    if isinstance(value_type, bytes):
        value_type = bytes_to_str(value_type)
    if isinstance(text, bytes):
        text = bytes_to_str(text)
    if isinstance(currency, bytes):
        currency = bytes_to_str(currency)
    if value is None:
        self._erase_text_content()
        return text
    if isinstance(value, bool):
        if value_type is None:
            value_type = "boolean"
        if text is None:
            text = "true" if value else "false"
        value = Boolean.encode(value)
    elif isinstance(value, (int, float, Decimal)):
        if value_type == "percentage":
            text = f"{int(value * 100)} %"
        if value_type is None:
            value_type = "float"
        if text is None:
            text = str(value)
        value = str(value)
    elif isinstance(value, datetime):
        if value_type is None:
            value_type = "date"
        if text is None:
            text = str(DateTime.encode(value))
        value = DateTime.encode(value)
    elif isinstance(value, date):
        if value_type is None:
            value_type = "date"
        if text is None:
            text = str(Date.encode(value))
        value = Date.encode(value)
    elif isinstance(value, str):
        if value_type is None:
            value_type = "string"
        if text is None:
            text = value
    elif isinstance(value, timedelta):
        if value_type is None:
            value_type = "time"
        if text is None:
            text = str(Duration.encode(value))
        value = Duration.encode(value)
    else:
        raise TypeError(f"Type unknown: '{value!r}'")

    if isinstance(value_type, str):
        self.set_attribute("office:value-type", value_type)
        self.set_attribute("calcext:value-type", value_type)
    if value_type == "boolean":
        self.set_attribute("office:boolean-value", value)
    elif value_type == "currency":
        self.set_attribute("office:value", value)
        self.set_attribute("office:currency", currency)
    elif value_type == "date":
        self.set_attribute("office:date-value", value)
    elif value_type in ("float", "percentage"):
        self.set_attribute("office:value", value)
        self.set_attribute("calcext:value", value)
    elif value_type == "string":
        self.set_attribute("office:string-value", value)
    elif value_type == "time":
        self.set_attribute("office:time-value", value)

    return text

EllipseShape

Bases: ShapeBase

An ellipse shape, “draw:ellipse”.

Methods:

Name Description
__init__

Create a ellipse shape “draw:ellipse”.

Source code in odfdo/shapes.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
class EllipseShape(ShapeBase):
    """An ellipse shape, "draw:ellipse"."""

    _tag = "draw:ellipse"
    _properties: tuple[PropDef, ...] = ()

    def __init__(
        self,
        style: str | None = None,
        text_style: str | None = None,
        draw_id: str | None = None,
        layer: str | None = None,
        position: tuple | None = None,
        size: tuple | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a ellipse shape "draw:ellipse".

        Arguments:

            style -- str

            text_style -- str

            draw_id -- str

            layer -- str

            position -- (str, str)

            size -- (str, str)
        """
        kwargs.update(
            {
                "style": style,
                "text_style": text_style,
                "draw_id": draw_id,
                "layer": layer,
                "size": size,
                "position": position,
            }
        )
        super().__init__(**kwargs)

__init__

__init__(
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    position: tuple | None = None,
    size: tuple | None = None,
    **kwargs: Any,
) -> None

Create a ellipse shape “draw:ellipse”.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

position -- (str, str)

size -- (str, str)
Source code in odfdo/shapes.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def __init__(
    self,
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    position: tuple | None = None,
    size: tuple | None = None,
    **kwargs: Any,
) -> None:
    """Create a ellipse shape "draw:ellipse".

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        position -- (str, str)

        size -- (str, str)
    """
    kwargs.update(
        {
            "style": style,
            "text_style": text_style,
            "draw_id": draw_id,
            "layer": layer,
            "size": size,
            "position": position,
        }
    )
    super().__init__(**kwargs)

Frame

Bases: MDDrawFrame, Element, AnchorMix, PosMix, ZMix, SizeMix

ODF Frame, “draw:frame”.

Frames are not useful by themselves. Consider calling Frame.image_frame() or Frame.text_frame directly.

Methods:

Name Description
__init__

ODF Frame, “draw:frame”.

get_formatted_text
get_image
get_text_box
image_frame

Create a ready-to-use image, since image must be embedded in a

set_image
set_text_box
text_frame

Create a ready-to-use text box, since text box must be embedded in

Attributes:

Name Type Description
anchor_page
anchor_type
draw_id
layer
name
position
presentation_class
presentation_style
size
style
text_content str
z_index
Source code in odfdo/frame.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
class Frame(MDDrawFrame, Element, AnchorMix, PosMix, ZMix, SizeMix):
    """ODF Frame, "draw:frame".

    Frames are not useful by themselves. Consider calling
    Frame.image_frame() or Frame.text_frame directly.
    """

    _tag = "draw:frame"
    _properties = (
        PropDef("name", "draw:name"),
        PropDef("draw_id", "draw:id"),
        PropDef("width", "svg:width"),
        PropDef("height", "svg:height"),
        PropDef("style", "draw:style-name"),
        PropDef("pos_x", "svg:x"),
        PropDef("pos_y", "svg:y"),
        PropDef("presentation_class", "presentation:class"),
        PropDef("layer", "draw:layer"),
        PropDef("presentation_style", "presentation:style-name"),
    )

    def __init__(
        self,
        name: str | None = None,
        draw_id: str | None = None,
        style: str | None = None,
        position: tuple | None = None,
        size: tuple = ("1cm", "1cm"),
        z_index: int = 0,
        presentation_class: str | None = None,
        anchor_type: str | None = None,
        anchor_page: int | None = None,
        layer: str | None = None,
        presentation_style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """
        ODF Frame, "draw:frame".

        Frames are not useful by themselves. Consider calling
        Frame.image_frame() or Frame.text_frame directly.

        Create a frame element of the given size. Position is relative to the
        context the frame is inserted in. If positioned by page, give the page
        number and the x, y position.

        Size is a (width, height) tuple and position is a (left, top) tuple; items
        are strings including the unit, e.g. ('10cm', '15cm').

        Frames are not useful by themselves. You should consider calling:
            Frame.image_frame()
        or
            Frame.text_frame()


        Arguments:

            name -- str

            draw_id -- str

            style -- str

            position -- (str, str)

            size -- (str, str)

            z_index -- int (default 0)

            presentation_class -- str

            anchor_type -- 'page', 'frame', 'paragraph', 'char' or 'as-char'

            anchor_page -- int, page number is anchor_type is 'page'

            layer -- str

            presentation_style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.size = size
            self.z_index = z_index
            if name:
                self.name = name
            if draw_id is not None:
                self.draw_id = draw_id
            if style is not None:
                self.style = style
            if position is not None:
                self.position = position
            if presentation_class is not None:
                self.presentation_class = presentation_class
            if anchor_type:
                self.anchor_type = anchor_type
            if position and not anchor_type:
                self.anchor_type = "paragraph"
            if anchor_page is not None:
                self.anchor_page = anchor_page
            if layer is not None:
                self.layer = layer
            if presentation_style is not None:
                self.presentation_style = presentation_style

    @classmethod
    def image_frame(
        cls,
        image: Element | str,
        text: str | None = None,
        name: str | None = None,
        draw_id: str | None = None,
        style: str | None = None,
        position: tuple | None = None,
        size: tuple = ("1cm", "1cm"),
        z_index: int = 0,
        presentation_class: str | None = None,
        anchor_type: str | None = None,
        anchor_page: int | None = None,
        layer: str | None = None,
        presentation_style: str | None = None,
        **kwargs: Any,
    ) -> Element:
        """Create a ready-to-use image, since image must be embedded in a
        frame.

        The optionnal text will appear above the image.

        Arguments:

            image -- DrawImage or str, DrawImage element or URL of the image

            text -- str, text for the image

            See Frame() initialization for the other arguments

        Return: Frame
        """
        frame = cls(
            name=name,
            draw_id=draw_id,
            style=style,
            position=position,
            size=size,
            z_index=z_index,
            presentation_class=presentation_class,
            anchor_type=anchor_type,
            anchor_page=anchor_page,
            layer=layer,
            presentation_style=presentation_style,
            **kwargs,
        )
        image_element = frame.set_image(image)
        if text:
            image_element.text_content = text
        return frame

    @classmethod
    def text_frame(
        cls,
        text_or_element: Iterable[Element] | Element | str,
        text_style: str | None = None,
        name: str | None = None,
        draw_id: str | None = None,
        style: str | None = None,
        position: tuple | None = None,
        size: tuple = ("1cm", "1cm"),
        z_index: int = 0,
        presentation_class: str | None = None,
        anchor_type: str | None = None,
        anchor_page: int | None = None,
        layer: str | None = None,
        presentation_style: str | None = None,
        **kwargs: Any,
    ) -> Element:
        """Create a ready-to-use text box, since text box must be embedded in
        a frame.

        The optionnal text will appear above the image.

        Arguments:

            text_or_element -- str or Element, or list of them, text content
                               of the text box.

            text_style -- str, name of the style for the text

            See Frame() initialization for the other arguments

        Return: Frame
        """
        frame = cls(
            name=name,
            draw_id=draw_id,
            style=style,
            position=position,
            size=size,
            z_index=z_index,
            presentation_class=presentation_class,
            anchor_type=anchor_type,
            anchor_page=anchor_page,
            layer=layer,
            presentation_style=presentation_style,
            **kwargs,
        )
        frame.set_text_box(text_or_element, text_style)
        return frame

    @property
    def text_content(self) -> str:
        text_box = self.get_element("draw:text-box")
        if text_box is None:
            return ""
        return text_box.text_content

    @text_content.setter
    def text_content(self, text: str | Element | None) -> None:
        text_box = self.get_element("draw:text-box")
        if text_box is None:
            text_box = Element.from_tag("draw:text-box")
            self.append(text_box)
        if isinstance(text, Element):
            text_box.clear()
            text_box.append(text)
        else:
            text_box.text_content = text

    def get_image(
        self,
        position: int = 0,
        name: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> Element | None:
        return self.get_element("draw:image")

    def set_image(self, url_or_element: Element | str) -> Element:
        image = self.get_image()
        if image is None:
            if isinstance(url_or_element, Element):
                image = url_or_element
                self.append(image)
            else:
                image = DrawImage(url_or_element)
                self.append(image)
        else:
            if isinstance(url_or_element, Element):
                image.delete()
                image = url_or_element
                self.append(image)
            else:
                image.set_url(url_or_element)  # type: ignore
        return image

    def get_text_box(self) -> Element | None:
        return self.get_element("draw:text-box")

    def set_text_box(
        self,
        text_or_element: Iterable[Element | str] | Element | str,
        text_style: str | None = None,
    ) -> Element:
        text_box = self.get_text_box()
        if text_box is None:
            text_box = Element.from_tag("draw:text-box")
            self.append(text_box)
        else:
            text_box.clear()
        if isinstance(text_or_element, (Element, str)):
            text_or_element_list: Iterable[Element | str] = [text_or_element]
        else:
            text_or_element_list = text_or_element
        for item in text_or_element_list:
            if isinstance(item, str):
                text_box.append(Paragraph(item, style=text_style))
            else:
                text_box.append(item)
        return text_box

    @staticmethod
    def _get_formatted_text_subresult(context: dict, element: Element) -> str:
        str_list = ["  "]
        for child in element.children:
            str_list.append(child.get_formatted_text(context))
        subresult = "".join(str_list)
        subresult = subresult.replace("\n", "\n  ")
        return subresult.rstrip(" ")

    def get_formatted_text(
        self,
        context: dict | None = None,
    ) -> str:
        if not context:
            context = {}
        result = []
        for element in self.children:
            tag = element.tag
            if tag == "draw:image":
                if context["rst_mode"]:
                    filename = element.get_attribute("xlink:href")

                    # Compute width and height
                    width, height = self.size
                    if width is not None:
                        width = Unit(width)
                        width = width.convert("px", DPI)
                    if height is not None:
                        height = Unit(height)
                        height = height.convert("px", DPI)

                    # Insert or not ?
                    if context["no_img_level"]:
                        context["img_counter"] += 1
                        ref = f"|img{context['img_counter']}|"
                        result.append(ref)
                        context["images"].append((ref, filename, (width, height)))
                    else:
                        result.append(f"\n.. image:: {filename}\n")
                        if width is not None:
                            result.append(f"   :width: {width}\n")
                        if height is not None:
                            result.append(f"   :height: {height}\n")
                else:
                    result.append(f"[Image {element.get_attribute('xlink:href')}]\n")
            elif tag == "draw:text-box":
                result.append(self._get_formatted_text_subresult(context, element))
            else:
                result.append(element.get_formatted_text(context))
        result.append("\n")
        return "".join(result)

anchor_page instance-attribute

anchor_page = anchor_page

anchor_type instance-attribute

anchor_type = anchor_type

draw_id instance-attribute

draw_id = draw_id

layer instance-attribute

layer = layer

name instance-attribute

name = name

position instance-attribute

position = position

presentation_class instance-attribute

presentation_class = presentation_class

presentation_style instance-attribute

presentation_style = presentation_style

size instance-attribute

size = size

style instance-attribute

style = style

text_content property writable

text_content: str

z_index instance-attribute

z_index = z_index

__init__

__init__(
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> None

ODF Frame, “draw:frame”.

Frames are not useful by themselves. Consider calling Frame.image_frame() or Frame.text_frame directly.

Create a frame element of the given size. Position is relative to the context the frame is inserted in. If positioned by page, give the page number and the x, y position.

Size is a (width, height) tuple and position is a (left, top) tuple; items are strings including the unit, e.g. (‘10cm’, ‘15cm’).

Frames are not useful by themselves. You should consider calling: Frame.image_frame() or Frame.text_frame()

Arguments:

name -- str

draw_id -- str

style -- str

position -- (str, str)

size -- (str, str)

z_index -- int (default 0)

presentation_class -- str

anchor_type -- 'page', 'frame', 'paragraph', 'char' or 'as-char'

anchor_page -- int, page number is anchor_type is 'page'

layer -- str

presentation_style -- str
Source code in odfdo/frame.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __init__(
    self,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> None:
    """
    ODF Frame, "draw:frame".

    Frames are not useful by themselves. Consider calling
    Frame.image_frame() or Frame.text_frame directly.

    Create a frame element of the given size. Position is relative to the
    context the frame is inserted in. If positioned by page, give the page
    number and the x, y position.

    Size is a (width, height) tuple and position is a (left, top) tuple; items
    are strings including the unit, e.g. ('10cm', '15cm').

    Frames are not useful by themselves. You should consider calling:
        Frame.image_frame()
    or
        Frame.text_frame()


    Arguments:

        name -- str

        draw_id -- str

        style -- str

        position -- (str, str)

        size -- (str, str)

        z_index -- int (default 0)

        presentation_class -- str

        anchor_type -- 'page', 'frame', 'paragraph', 'char' or 'as-char'

        anchor_page -- int, page number is anchor_type is 'page'

        layer -- str

        presentation_style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.size = size
        self.z_index = z_index
        if name:
            self.name = name
        if draw_id is not None:
            self.draw_id = draw_id
        if style is not None:
            self.style = style
        if position is not None:
            self.position = position
        if presentation_class is not None:
            self.presentation_class = presentation_class
        if anchor_type:
            self.anchor_type = anchor_type
        if position and not anchor_type:
            self.anchor_type = "paragraph"
        if anchor_page is not None:
            self.anchor_page = anchor_page
        if layer is not None:
            self.layer = layer
        if presentation_style is not None:
            self.presentation_style = presentation_style

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/frame.py
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
def get_formatted_text(
    self,
    context: dict | None = None,
) -> str:
    if not context:
        context = {}
    result = []
    for element in self.children:
        tag = element.tag
        if tag == "draw:image":
            if context["rst_mode"]:
                filename = element.get_attribute("xlink:href")

                # Compute width and height
                width, height = self.size
                if width is not None:
                    width = Unit(width)
                    width = width.convert("px", DPI)
                if height is not None:
                    height = Unit(height)
                    height = height.convert("px", DPI)

                # Insert or not ?
                if context["no_img_level"]:
                    context["img_counter"] += 1
                    ref = f"|img{context['img_counter']}|"
                    result.append(ref)
                    context["images"].append((ref, filename, (width, height)))
                else:
                    result.append(f"\n.. image:: {filename}\n")
                    if width is not None:
                        result.append(f"   :width: {width}\n")
                    if height is not None:
                        result.append(f"   :height: {height}\n")
            else:
                result.append(f"[Image {element.get_attribute('xlink:href')}]\n")
        elif tag == "draw:text-box":
            result.append(self._get_formatted_text_subresult(context, element))
        else:
            result.append(element.get_formatted_text(context))
    result.append("\n")
    return "".join(result)

get_image

get_image(
    position: int = 0,
    name: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Element | None
Source code in odfdo/frame.py
393
394
395
396
397
398
399
400
def get_image(
    self,
    position: int = 0,
    name: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Element | None:
    return self.get_element("draw:image")

get_text_box

get_text_box() -> Element | None
Source code in odfdo/frame.py
420
421
def get_text_box(self) -> Element | None:
    return self.get_element("draw:text-box")

image_frame classmethod

image_frame(
    image: Element | str,
    text: str | None = None,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> Element

Create a ready-to-use image, since image must be embedded in a frame.

The optionnal text will appear above the image.

Arguments:

image -- DrawImage or str, DrawImage element or URL of the image

text -- str, text for the image

See Frame() initialization for the other arguments

Return: Frame

Source code in odfdo/frame.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@classmethod
def image_frame(
    cls,
    image: Element | str,
    text: str | None = None,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> Element:
    """Create a ready-to-use image, since image must be embedded in a
    frame.

    The optionnal text will appear above the image.

    Arguments:

        image -- DrawImage or str, DrawImage element or URL of the image

        text -- str, text for the image

        See Frame() initialization for the other arguments

    Return: Frame
    """
    frame = cls(
        name=name,
        draw_id=draw_id,
        style=style,
        position=position,
        size=size,
        z_index=z_index,
        presentation_class=presentation_class,
        anchor_type=anchor_type,
        anchor_page=anchor_page,
        layer=layer,
        presentation_style=presentation_style,
        **kwargs,
    )
    image_element = frame.set_image(image)
    if text:
        image_element.text_content = text
    return frame

set_image

set_image(url_or_element: Element | str) -> Element
Source code in odfdo/frame.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
def set_image(self, url_or_element: Element | str) -> Element:
    image = self.get_image()
    if image is None:
        if isinstance(url_or_element, Element):
            image = url_or_element
            self.append(image)
        else:
            image = DrawImage(url_or_element)
            self.append(image)
    else:
        if isinstance(url_or_element, Element):
            image.delete()
            image = url_or_element
            self.append(image)
        else:
            image.set_url(url_or_element)  # type: ignore
    return image

set_text_box

set_text_box(
    text_or_element: Iterable[Element | str]
    | Element
    | str,
    text_style: str | None = None,
) -> Element
Source code in odfdo/frame.py
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
def set_text_box(
    self,
    text_or_element: Iterable[Element | str] | Element | str,
    text_style: str | None = None,
) -> Element:
    text_box = self.get_text_box()
    if text_box is None:
        text_box = Element.from_tag("draw:text-box")
        self.append(text_box)
    else:
        text_box.clear()
    if isinstance(text_or_element, (Element, str)):
        text_or_element_list: Iterable[Element | str] = [text_or_element]
    else:
        text_or_element_list = text_or_element
    for item in text_or_element_list:
        if isinstance(item, str):
            text_box.append(Paragraph(item, style=text_style))
        else:
            text_box.append(item)
    return text_box

text_frame classmethod

text_frame(
    text_or_element: Iterable[Element] | Element | str,
    text_style: str | None = None,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> Element

Create a ready-to-use text box, since text box must be embedded in a frame.

The optionnal text will appear above the image.

Arguments:

text_or_element -- str or Element, or list of them, text content
                   of the text box.

text_style -- str, name of the style for the text

See Frame() initialization for the other arguments

Return: Frame

Source code in odfdo/frame.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
@classmethod
def text_frame(
    cls,
    text_or_element: Iterable[Element] | Element | str,
    text_style: str | None = None,
    name: str | None = None,
    draw_id: str | None = None,
    style: str | None = None,
    position: tuple | None = None,
    size: tuple = ("1cm", "1cm"),
    z_index: int = 0,
    presentation_class: str | None = None,
    anchor_type: str | None = None,
    anchor_page: int | None = None,
    layer: str | None = None,
    presentation_style: str | None = None,
    **kwargs: Any,
) -> Element:
    """Create a ready-to-use text box, since text box must be embedded in
    a frame.

    The optionnal text will appear above the image.

    Arguments:

        text_or_element -- str or Element, or list of them, text content
                           of the text box.

        text_style -- str, name of the style for the text

        See Frame() initialization for the other arguments

    Return: Frame
    """
    frame = cls(
        name=name,
        draw_id=draw_id,
        style=style,
        position=position,
        size=size,
        z_index=z_index,
        presentation_class=presentation_class,
        anchor_type=anchor_type,
        anchor_page=anchor_page,
        layer=layer,
        presentation_style=presentation_style,
        **kwargs,
    )
    frame.set_text_box(text_or_element, text_style)
    return frame

Header

Bases: Paragraph, MDHeader

A title, a specialized paragraph, “text:h”.

Methods:

Name Description
__init__

A title, a specialized paragraph, “text:h”.

get_formatted_text

Attributes:

Name Type Description
level
restart_numbering
start_value
suppress_numbering
text
Source code in odfdo/header.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class Header(Paragraph, MDHeader):
    """A title, a specialized paragraph, "text:h"."""

    _tag = "text:h"
    _properties = (
        PropDef("level", "text:outline-level"),
        PropDef("restart_numbering", "text:restart-numbering"),
        PropDef("start_value", "text:start-value"),
        PropDef("suppress_numbering", "text:suppress-numbering"),
    )

    def __init__(
        self,
        level: int = 1,
        text: str | None = None,
        restart_numbering: bool = False,
        start_value: int | None = None,
        suppress_numbering: bool = False,
        style: str | None = None,
        formatted: bool = True,
        **kwargs: Any,
    ) -> None:
        """A title, a specialized paragraph, "text:h".

        Create a header element of the given style and level, containing the
        optional given text.

        Level count begins at 1.

        If "formatted" is True (the default), the given text is appended with <CR>,
        <TAB> and multiple spaces replaced by ODF corresponding tags.

        Arguments:

            level -- int

            text -- str

            restart_numbering -- bool

            start_value -- int

            style -- str

            formatted -- bool
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.level = int(level)
            if text:
                if formatted:
                    self.text = ""
                    self.append_plain_text(text)
                else:
                    self.text = self._unformatted(text)
            if restart_numbering:
                self.restart_numbering = True
            if start_value is not None:
                self.start_value = start_value
            if suppress_numbering:
                self.suppress_numbering = True
            # if style:
            #     self.style = style

    def get_formatted_text(
        self,
        context: dict | None = None,
        simple: bool = False,
    ) -> str:
        if not context:
            context = {
                "document": None,
                "footnotes": [],
                "endnotes": [],
                "annotations": [],
                "rst_mode": False,
                "img_counter": 0,
                "images": [],
                "no_img_level": 0,
            }
        context["no_img_level"] += 1
        title = super().get_formatted_text(context)
        context["no_img_level"] -= 1
        title = title.strip()
        title = sub(r"\s+", " ", title)

        # No rst_mode ?
        if not context["rst_mode"]:
            return title
        # If here in rst_mode!

        # Get the level, max 5!
        LEVEL_STYLES = "#=-~`+^°'."
        level = int(self.level)
        if level > len(LEVEL_STYLES):
            raise ValueError("Too many levels of heading")

        # And return the result
        result = ["\n", title, "\n", LEVEL_STYLES[level - 1] * len(title), "\n"]
        return "".join(result)

level instance-attribute

level = int(level)

restart_numbering instance-attribute

restart_numbering = True

start_value instance-attribute

start_value = start_value

suppress_numbering instance-attribute

suppress_numbering = True

text instance-attribute

text = ''

__init__

__init__(
    level: int = 1,
    text: str | None = None,
    restart_numbering: bool = False,
    start_value: int | None = None,
    suppress_numbering: bool = False,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
) -> None

A title, a specialized paragraph, “text:h”.

Create a header element of the given style and level, containing the optional given text.

Level count begins at 1.

If “formatted” is True (the default), the given text is appended with , and multiple spaces replaced by ODF corresponding tags.

Arguments:

level -- int

text -- str

restart_numbering -- bool

start_value -- int

style -- str

formatted -- bool
Source code in odfdo/header.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self,
    level: int = 1,
    text: str | None = None,
    restart_numbering: bool = False,
    start_value: int | None = None,
    suppress_numbering: bool = False,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
) -> None:
    """A title, a specialized paragraph, "text:h".

    Create a header element of the given style and level, containing the
    optional given text.

    Level count begins at 1.

    If "formatted" is True (the default), the given text is appended with <CR>,
    <TAB> and multiple spaces replaced by ODF corresponding tags.

    Arguments:

        level -- int

        text -- str

        restart_numbering -- bool

        start_value -- int

        style -- str

        formatted -- bool
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.level = int(level)
        if text:
            if formatted:
                self.text = ""
                self.append_plain_text(text)
            else:
                self.text = self._unformatted(text)
        if restart_numbering:
            self.restart_numbering = True
        if start_value is not None:
            self.start_value = start_value
        if suppress_numbering:
            self.suppress_numbering = True

get_formatted_text

get_formatted_text(
    context: dict | None = None, simple: bool = False
) -> str
Source code in odfdo/header.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def get_formatted_text(
    self,
    context: dict | None = None,
    simple: bool = False,
) -> str:
    if not context:
        context = {
            "document": None,
            "footnotes": [],
            "endnotes": [],
            "annotations": [],
            "rst_mode": False,
            "img_counter": 0,
            "images": [],
            "no_img_level": 0,
        }
    context["no_img_level"] += 1
    title = super().get_formatted_text(context)
    context["no_img_level"] -= 1
    title = title.strip()
    title = sub(r"\s+", " ", title)

    # No rst_mode ?
    if not context["rst_mode"]:
        return title
    # If here in rst_mode!

    # Get the level, max 5!
    LEVEL_STYLES = "#=-~`+^°'."
    level = int(self.level)
    if level > len(LEVEL_STYLES):
        raise ValueError("Too many levels of heading")

    # And return the result
    result = ["\n", title, "\n", LEVEL_STYLES[level - 1] * len(title), "\n"]
    return "".join(result)

HeaderRows

Bases: Element

A header row of a table, “table:table-header-rows”.

Source code in odfdo/header_rows.py
31
32
33
34
class HeaderRows(Element):
    """A header row of a table, "table:table-header-rows"."""

    _tag = "table:table-header-rows"

Image

Bases: Body

Root of the Image document content, “office:image”.

Source code in odfdo/body.py
116
117
118
119
120
class Image(Body):
    """Root of the Image document content, "office:image"."""

    _tag: str = "office:image"
    _properties: tuple[PropDef, ...] = ()

IndexTitle

Bases: Element

The title of an index, “text:index-title”.

The element has the following attributes: text:name, text:protected, text:protection-key, text:protection-key-digest-algorithm, text:style-name, xml:id.

The actual title is stored in a child element

Methods:

Name Description
__init__

Create title of an index “text:index-title”.

set_title_text

Attributes:

Name Type Description
name
style
xml_id
Source code in odfdo/toc.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class IndexTitle(Element):
    """The title of an index, "text:index-title".

    The element has the following attributes:
    text:name, text:protected, text:protection-key,
    text:protection-key-digest-algorithm, text:style-name, xml:id.

    The actual title is stored in a child element
    """

    _tag = "text:index-title"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "text:style-name"),
        PropDef("xml_id", "xml:id"),
        PropDef("protected", "text:protected"),
        PropDef("protection_key", "text:protection-key"),
        PropDef(
            "protection_key_digest_algorithm", "text:protection-key-digest-algorithm"
        ),
    )

    def __init__(
        self,
        name: str | None = None,
        style: str | None = None,
        title_text: str | None = None,
        title_text_style: str | None = None,
        xml_id: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create title of an index "text:index-title".

        The element has the following attributes:
        text:name, text:protected, text:protection-key,
        text:protection-key-digest-algorithm, text:style-name, xml:id.

        The actual title is stored in a child element
        """
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            if style:
                self.style = style
            if xml_id:
                self.xml_id = xml_id
            if title_text:
                self.set_title_text(title_text, title_text_style)

    def set_title_text(
        self,
        title_text: str,
        title_text_style: str | None = None,
    ) -> None:
        title = Paragraph(title_text, style=title_text_style)
        self.append(title)

name instance-attribute

name = name

style instance-attribute

style = style

xml_id instance-attribute

xml_id = xml_id

__init__

__init__(
    name: str | None = None,
    style: str | None = None,
    title_text: str | None = None,
    title_text_style: str | None = None,
    xml_id: str | None = None,
    **kwargs: Any,
) -> None

Create title of an index “text:index-title”.

The element has the following attributes: text:name, text:protected, text:protection-key, text:protection-key-digest-algorithm, text:style-name, xml:id.

The actual title is stored in a child element

Source code in odfdo/toc.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(
    self,
    name: str | None = None,
    style: str | None = None,
    title_text: str | None = None,
    title_text_style: str | None = None,
    xml_id: str | None = None,
    **kwargs: Any,
) -> None:
    """Create title of an index "text:index-title".

    The element has the following attributes:
    text:name, text:protected, text:protection-key,
    text:protection-key-digest-algorithm, text:style-name, xml:id.

    The actual title is stored in a child element
    """
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        if style:
            self.style = style
        if xml_id:
            self.xml_id = xml_id
        if title_text:
            self.set_title_text(title_text, title_text_style)

set_title_text

set_title_text(
    title_text: str, title_text_style: str | None = None
) -> None
Source code in odfdo/toc.py
91
92
93
94
95
96
97
def set_title_text(
    self,
    title_text: str,
    title_text_style: str | None = None,
) -> None:
    title = Paragraph(title_text, style=title_text_style)
    self.append(title)

IndexTitleTemplate

Bases: Element

Template style for title, “text:index-title-template”.

Methods:

Name Description
__init__

Create template style for title “text:index-title-template”.

Attributes:

Name Type Description
style
Source code in odfdo/toc.py
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
class IndexTitleTemplate(Element):
    """Template style for title, "text:index-title-template"."""

    _tag = "text:index-title-template"
    _properties = (PropDef("style", "text:style-name"),)

    def __init__(self, style: str | None = None, **kwargs: Any) -> None:
        """Create template style for title "text:index-title-template".

        Arguments:

            style -- str
        """
        super().__init__(**kwargs)
        if self._do_init and style:
            self.style = style

style instance-attribute

style = style

__init__

__init__(style: str | None = None, **kwargs: Any) -> None

Create template style for title “text:index-title-template”.

Arguments:

style -- str
Source code in odfdo/toc.py
470
471
472
473
474
475
476
477
478
479
def __init__(self, style: str | None = None, **kwargs: Any) -> None:
    """Create template style for title "text:index-title-template".

    Arguments:

        style -- str
    """
    super().__init__(**kwargs)
    if self._do_init and style:
        self.style = style

LineBreak

Bases: MDLineBreak, Element

Representation of a line break, “text:line-break”.

Methods:

Name Description
__init__

Representation of a line break, “text:line-break”.

Attributes:

Name Type Description
text str
Source code in odfdo/line_break.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class LineBreak(MDLineBreak, Element):
    """Representation of a line break, "text:line-break"."""

    _tag = "text:line-break"

    def __init__(self, **kwargs: Any) -> None:
        """Representation of a line break, "text:line-break"."""
        super().__init__(**kwargs)

    def __str__(self) -> str:
        return "\n"

    @property
    def text(self) -> str:
        return "\n"

text property

text: str

__init__

__init__(**kwargs: Any) -> None

Representation of a line break, “text:line-break”.

Source code in odfdo/line_break.py
39
40
41
def __init__(self, **kwargs: Any) -> None:
    """Representation of a line break, "text:line-break"."""
    super().__init__(**kwargs)

LineShape

Bases: ShapeBase

A line shape, “draw:line”.

Methods:

Name Description
__init__

Create a line shape “draw:line”.

Attributes:

Name Type Description
x1
x2
y1
y2
Source code in odfdo/shapes.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class LineShape(ShapeBase):
    """A line shape, "draw:line"."""

    _tag = "draw:line"
    _properties: tuple[PropDef, ...] = (
        PropDef("x1", "svg:x1"),
        PropDef("y1", "svg:y1"),
        PropDef("x2", "svg:x2"),
        PropDef("y2", "svg:y2"),
    )

    def __init__(
        self,
        style: str | None = None,
        text_style: str | None = None,
        draw_id: str | None = None,
        layer: str | None = None,
        p1: tuple | None = None,
        p2: tuple | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a line shape "draw:line".

        Arguments:

            style -- str

            text_style -- str

            draw_id -- str

            layer -- str

            p1 -- (str, str)

            p2 -- (str, str)
        """
        kwargs.update(
            {
                "style": style,
                "text_style": text_style,
                "draw_id": draw_id,
                "layer": layer,
            }
        )
        super().__init__(**kwargs)
        if self._do_init:
            if p1:
                self.x1 = p1[0]
                self.y1 = p1[1]
            if p2:
                self.x2 = p2[0]
                self.y2 = p2[1]

x1 instance-attribute

x1 = p1[0]

x2 instance-attribute

x2 = p2[0]

y1 instance-attribute

y1 = p1[1]

y2 instance-attribute

y2 = p2[1]

__init__

__init__(
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    p1: tuple | None = None,
    p2: tuple | None = None,
    **kwargs: Any,
) -> None

Create a line shape “draw:line”.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

p1 -- (str, str)

p2 -- (str, str)
Source code in odfdo/shapes.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def __init__(
    self,
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    p1: tuple | None = None,
    p2: tuple | None = None,
    **kwargs: Any,
) -> None:
    """Create a line shape "draw:line".

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        p1 -- (str, str)

        p2 -- (str, str)
    """
    kwargs.update(
        {
            "style": style,
            "text_style": text_style,
            "draw_id": draw_id,
            "layer": layer,
        }
    )
    super().__init__(**kwargs)
    if self._do_init:
        if p1:
            self.x1 = p1[0]
            self.y1 = p1[1]
        if p2:
            self.x2 = p2[0]
            self.y2 = p2[1]

Bases: MDLink, ParaFormattedTextMixin, Element

Representation of a link (URL), “text:a”.

Methods:

Name Description
__init__

Representation of a link (URL), “text:a”.

Attributes:

Name Type Description
name
show
style
target_frame
text
title
url
visited_style
Source code in odfdo/link.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class Link(MDLink, ParaFormattedTextMixin, Element):
    """Representation of a link (URL), "text:a"."""

    _tag = "text:a"
    _properties: tuple[PropDef, ...] = (
        PropDef("url", "xlink:href"),
        PropDef("name", "office:name"),
        PropDef("title", "office:title"),
        PropDef("target_frame", "office:target-frame-name"),
        PropDef("show", "xlink:show"),
        PropDef("visited_style", "text:visited-style-name"),
        PropDef("style", "text:style-name"),
    )

    def __init__(
        self,
        url: str | None = "",
        name: str | None = None,
        title: str | None = None,
        text: str | None = None,
        target_frame: str | None = None,
        style: str | None = None,
        visited_style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """
        Representation of a link (URL), "text:a".

        Arguments:

            url -- str

            name -- str

            title -- str

            text -- str

            target_frame -- '_self', '_blank', '_parent', '_top'

            style -- str

            visited_style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.url = url
            if name is not None:
                self.name = name
            if title is not None:
                self.title = title
            if text is not None:
                self.text = text
            if target_frame is not None:
                self.target_frame = target_frame
                # show can be: 'new' or 'replace'"
                if target_frame == "_blank":
                    self.show = "new"
                else:
                    self.show = "replace"
            if style is not None:
                self.style = style
            if visited_style is not None:
                self.visited_style = visited_style

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} tag={self.tag} link={self.url}>"

    def __str__(self) -> str:
        text = self.inner_text.strip()
        if text:
            return f"[{text}]({self.url})"
        return f"({self.url})"

name instance-attribute

name = name

show instance-attribute

show = 'new'

style instance-attribute

style = style

target_frame instance-attribute

target_frame = target_frame

text instance-attribute

text = text

title instance-attribute

title = title

url instance-attribute

url = url

visited_style instance-attribute

visited_style = visited_style

__init__

__init__(
    url: str | None = "",
    name: str | None = None,
    title: str | None = None,
    text: str | None = None,
    target_frame: str | None = None,
    style: str | None = None,
    visited_style: str | None = None,
    **kwargs: Any,
) -> None

Representation of a link (URL), “text:a”.

Arguments:

url -- str

name -- str

title -- str

text -- str

target_frame -- '_self', '_blank', '_parent', '_top'

style -- str

visited_style -- str
Source code in odfdo/link.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(
    self,
    url: str | None = "",
    name: str | None = None,
    title: str | None = None,
    text: str | None = None,
    target_frame: str | None = None,
    style: str | None = None,
    visited_style: str | None = None,
    **kwargs: Any,
) -> None:
    """
    Representation of a link (URL), "text:a".

    Arguments:

        url -- str

        name -- str

        title -- str

        text -- str

        target_frame -- '_self', '_blank', '_parent', '_top'

        style -- str

        visited_style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.url = url
        if name is not None:
            self.name = name
        if title is not None:
            self.title = title
        if text is not None:
            self.text = text
        if target_frame is not None:
            self.target_frame = target_frame
            # show can be: 'new' or 'replace'"
            if target_frame == "_blank":
                self.show = "new"
            else:
                self.show = "replace"
        if style is not None:
            self.style = style
        if visited_style is not None:
            self.visited_style = visited_style

List

Bases: MDList, Element

A list of elements, “text:list”.

Methods:

Name Description
__init__

A list of elements, “text:list”.

append_item
get_formatted_text
get_item

Return the list item that matches the criteria. In nested lists,

get_items

Return all the list items that match the criteria.

insert_item
set_list_header

Attributes:

Name Type Description
style
Source code in odfdo/list.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class List(MDList, Element):
    """A list of elements, "text:list"."""

    _tag = "text:list"
    _properties = (PropDef("style", "text:style-name"),)

    def __init__(
        self,
        list_content: str | Element | Iterable[str | Element] | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """A list of elements, "text:list".

        Create a list element, optionaly loading the list by a list of
        item (str or elements).

        The list_content argument is just a shortcut for the most common case.
        To create more complex lists, first create an empty list, and fill it
        afterwards.

        Arguments:

            list_content -- str or Element, or a list of str or Element

            style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            if list_content:
                if isinstance(list_content, (Element, str)):
                    self.append(ListItem(list_content))
                elif hasattr(list_content, "__iter__"):
                    for item in list_content:
                        self.append(ListItem(item))
            if style is not None:
                self.style = style

    def get_items(self, content: str | None = None) -> list[Element]:
        """Return all the list items that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Element
        """
        return self._filtered_elements("text:list-item", content=content)

    def get_item(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | None:
        """Return the list item that matches the criteria. In nested lists,
        return the list item that really contains that content.

        Arguments:

            position -- int

            content -- str regex

        Return: Element or None if not found
        """
        # Custom implementation because of nested lists
        if content:
            # Don't search recursively but on the very own paragraph(s) of
            # each list item
            for paragraph in self.get_elements("descendant::text:p"):
                if paragraph.match(content):
                    return paragraph.get_element("parent::text:list-item")
            return None
        return self._filtered_element("text:list-item", position)

    def set_list_header(
        self,
        text_or_element: str | Element | Iterable[str | Element],
    ) -> None:
        if isinstance(text_or_element, (str, Element)):
            actual_list: list[str | Element] | tuple = [text_or_element]
        elif isinstance(text_or_element, (list, tuple)):
            actual_list = text_or_element
        else:
            raise TypeError
        # Remove existing header
        for element in self.get_elements("text:p"):
            self.delete(element)
        for paragraph in reversed(actual_list):
            if isinstance(paragraph, str):
                paragraph = Paragraph(paragraph)
            self.insert(paragraph, FIRST_CHILD)

    def insert_item(
        self,
        item: ListItem | str | Element | None,
        position: int | None = None,
        before: Element | None = None,
        after: Element | None = None,
    ) -> None:
        if not isinstance(item, ListItem):
            item = ListItem(item)
        if before is not None:
            before.insert(item, xmlposition=PREV_SIBLING)
        elif after is not None:
            after.insert(item, xmlposition=NEXT_SIBLING)
        elif position is not None:
            self.insert(item, position=position)
        else:
            raise ValueError("Position must be defined")

    def append_item(
        self,
        item: ListItem | str | Element | None,
    ) -> None:
        if not isinstance(item, ListItem):
            item = ListItem(item)
        self.append(item)

    def get_formatted_text(self, context: dict | None = None) -> str:
        if context is None:
            context = {
                "document": None,
                "footnotes": [],
                "endnotes": [],
                "annotations": [],
                "rst_mode": False,
                "img_counter": 0,
                "images": [],
                "no_img_level": 0,
            }
        rst_mode = bool(context.get("rst_mode"))
        result = []
        if rst_mode:
            result.append("\n")
        for list_item in self.get_elements("text:list-item"):
            textbuf = []
            for child in list_item.children:
                text = child.get_formatted_text(context)
                tag = child.tag
                if tag == "text:h":
                    # A title in a list is a bug
                    return text
                if tag == "text:list" and not text.lstrip().startswith("-"):
                    # If the list didn't indent, don't either
                    # (inner title)
                    return text  # pragma: nocover
                textbuf.append(text)
            text_sum = "".join(textbuf)
            text_sum = text_sum.strip("\n")
            # Indent the text
            text_sum = text_sum.replace("\n", "\n  ")
            text_sum = f"- {text_sum}\n"
            result.append(text_sum)
        if rst_mode:
            result.append("\n")
        return "".join(result)

style instance-attribute

style = style

__init__

__init__(
    list_content: str
    | Element
    | Iterable[str | Element]
    | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

A list of elements, “text:list”.

Create a list element, optionaly loading the list by a list of item (str or elements).

The list_content argument is just a shortcut for the most common case. To create more complex lists, first create an empty list, and fill it afterwards.

Arguments:

list_content -- str or Element, or a list of str or Element

style -- str
Source code in odfdo/list.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def __init__(
    self,
    list_content: str | Element | Iterable[str | Element] | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """A list of elements, "text:list".

    Create a list element, optionaly loading the list by a list of
    item (str or elements).

    The list_content argument is just a shortcut for the most common case.
    To create more complex lists, first create an empty list, and fill it
    afterwards.

    Arguments:

        list_content -- str or Element, or a list of str or Element

        style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        if list_content:
            if isinstance(list_content, (Element, str)):
                self.append(ListItem(list_content))
            elif hasattr(list_content, "__iter__"):
                for item in list_content:
                    self.append(ListItem(item))
        if style is not None:
            self.style = style

append_item

append_item(item: ListItem | str | Element | None) -> None
Source code in odfdo/list.py
187
188
189
190
191
192
193
def append_item(
    self,
    item: ListItem | str | Element | None,
) -> None:
    if not isinstance(item, ListItem):
        item = ListItem(item)
    self.append(item)

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/list.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def get_formatted_text(self, context: dict | None = None) -> str:
    if context is None:
        context = {
            "document": None,
            "footnotes": [],
            "endnotes": [],
            "annotations": [],
            "rst_mode": False,
            "img_counter": 0,
            "images": [],
            "no_img_level": 0,
        }
    rst_mode = bool(context.get("rst_mode"))
    result = []
    if rst_mode:
        result.append("\n")
    for list_item in self.get_elements("text:list-item"):
        textbuf = []
        for child in list_item.children:
            text = child.get_formatted_text(context)
            tag = child.tag
            if tag == "text:h":
                # A title in a list is a bug
                return text
            if tag == "text:list" and not text.lstrip().startswith("-"):
                # If the list didn't indent, don't either
                # (inner title)
                return text  # pragma: nocover
            textbuf.append(text)
        text_sum = "".join(textbuf)
        text_sum = text_sum.strip("\n")
        # Indent the text
        text_sum = text_sum.replace("\n", "\n  ")
        text_sum = f"- {text_sum}\n"
        result.append(text_sum)
    if rst_mode:
        result.append("\n")
    return "".join(result)

get_item

get_item(
    position: int = 0, content: str | None = None
) -> Element | None

Return the list item that matches the criteria. In nested lists, return the list item that really contains that content.

Arguments:

position -- int

content -- str regex

Return: Element or None if not found

Source code in odfdo/list.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def get_item(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | None:
    """Return the list item that matches the criteria. In nested lists,
    return the list item that really contains that content.

    Arguments:

        position -- int

        content -- str regex

    Return: Element or None if not found
    """
    # Custom implementation because of nested lists
    if content:
        # Don't search recursively but on the very own paragraph(s) of
        # each list item
        for paragraph in self.get_elements("descendant::text:p"):
            if paragraph.match(content):
                return paragraph.get_element("parent::text:list-item")
        return None
    return self._filtered_element("text:list-item", position)

get_items

get_items(content: str | None = None) -> list[Element]

Return all the list items that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Element

Source code in odfdo/list.py
112
113
114
115
116
117
118
119
120
121
122
123
def get_items(self, content: str | None = None) -> list[Element]:
    """Return all the list items that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Element
    """
    return self._filtered_elements("text:list-item", content=content)

insert_item

insert_item(
    item: ListItem | str | Element | None,
    position: int | None = None,
    before: Element | None = None,
    after: Element | None = None,
) -> None
Source code in odfdo/list.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def insert_item(
    self,
    item: ListItem | str | Element | None,
    position: int | None = None,
    before: Element | None = None,
    after: Element | None = None,
) -> None:
    if not isinstance(item, ListItem):
        item = ListItem(item)
    if before is not None:
        before.insert(item, xmlposition=PREV_SIBLING)
    elif after is not None:
        after.insert(item, xmlposition=NEXT_SIBLING)
    elif position is not None:
        self.insert(item, position=position)
    else:
        raise ValueError("Position must be defined")

set_list_header

set_list_header(
    text_or_element: str
    | Element
    | Iterable[str | Element],
) -> None
Source code in odfdo/list.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def set_list_header(
    self,
    text_or_element: str | Element | Iterable[str | Element],
) -> None:
    if isinstance(text_or_element, (str, Element)):
        actual_list: list[str | Element] | tuple = [text_or_element]
    elif isinstance(text_or_element, (list, tuple)):
        actual_list = text_or_element
    else:
        raise TypeError
    # Remove existing header
    for element in self.get_elements("text:p"):
        self.delete(element)
    for paragraph in reversed(actual_list):
        if isinstance(paragraph, str):
            paragraph = Paragraph(paragraph)
        self.insert(paragraph, FIRST_CHILD)

ListItem

Bases: MDListItem, Element

An item of a list, “text:list-item”.

Methods:

Name Description
__init__

An item of a list, “text:list-item”.

Attributes:

Name Type Description
text_content
Source code in odfdo/list.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class ListItem(MDListItem, Element):
    """An item of a list, "text:list-item"."""

    _tag = "text:list-item"

    def __init__(
        self,
        text_or_element: str | Element | None = None,
        **kwargs: Any,
    ) -> None:
        """An item of a list, "text:list-item".

        Create a list item element, optionaly passing at creation time a
        string or Element as content.

        Arguments:

            text_or_element -- str or ODF Element
        """
        super().__init__(**kwargs)
        if self._do_init:
            if isinstance(text_or_element, str):
                self.text_content = text_or_element
            elif isinstance(text_or_element, Element):
                self.append(text_or_element)
            elif text_or_element is not None:
                raise TypeError(f"Expected str or Element, not {type(text_or_element)}")

    def __str__(self) -> str:
        self._md_initialize_level()
        return "\n".join(self._md_collect())

text_content instance-attribute

text_content = text_or_element

__init__

__init__(
    text_or_element: str | Element | None = None,
    **kwargs: Any,
) -> None

An item of a list, “text:list-item”.

Create a list item element, optionaly passing at creation time a string or Element as content.

Arguments:

text_or_element -- str or ODF Element
Source code in odfdo/list.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    text_or_element: str | Element | None = None,
    **kwargs: Any,
) -> None:
    """An item of a list, "text:list-item".

    Create a list item element, optionaly passing at creation time a
    string or Element as content.

    Arguments:

        text_or_element -- str or ODF Element
    """
    super().__init__(**kwargs)
    if self._do_init:
        if isinstance(text_or_element, str):
            self.text_content = text_or_element
        elif isinstance(text_or_element, Element):
            self.append(text_or_element)
        elif text_or_element is not None:
            raise TypeError(f"Expected str or Element, not {type(text_or_element)}")

Manifest

Bases: XmlPart

Representation of the “manifest.xml” part.

Methods:

Name Description
add_full_path
del_full_path
get_media_type

Get the media type of an existing path.

get_path_medias

Return the list of (full_path, media_type) pairs in the manifest.

get_paths

Return the list of full paths in the manifest.

make_file_entry
set_media_type

Set the media type of an existing path.

Source code in odfdo/manifest.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class Manifest(XmlPart):
    """Representation of the "manifest.xml" part."""

    def get_paths(self) -> list[Element | EText]:
        """Return the list of full paths in the manifest.

        Return: list of str
        """
        xpath_query = "//manifest:file-entry/attribute::manifest:full-path"
        return self.xpath(xpath_query)

    def _file_entry(self, full_path: str) -> Element:
        xpath_query = (
            f'//manifest:file-entry[attribute::manifest:full-path="{full_path}"]'
        )
        result = self.xpath(xpath_query)
        if not result:
            raise KeyError(f"Path not found: '{full_path}'")
        return result[0]  # type: ignore

    def get_path_medias(self) -> list[tuple]:
        """Return the list of (full_path, media_type) pairs in the manifest.

        Return: list of str tuples
        """
        xpath_query = "//manifest:file-entry"
        result = []
        for file_entry in self.xpath(xpath_query):
            if not isinstance(file_entry, Element):  # pragma: no cover
                continue
            result.append(
                (
                    file_entry.get_attribute_string("manifest:full-path"),
                    file_entry.get_attribute_string("manifest:media-type"),
                )
            )
        return result

    def get_media_type(self, full_path: str) -> str | None:
        """Get the media type of an existing path.

        Return: str
        """
        xpath_query = (
            f'//manifest:file-entry[attribute::manifest:full-path="{full_path}"]'
            "/attribute::manifest:media-type"
        )
        result = self.xpath(xpath_query)
        if not result:
            return None
        return str(result[0])

    def set_media_type(self, full_path: str, media_type: str) -> None:
        """Set the media type of an existing path.

        Arguments:

            full_path -- str

            media_type -- str
        """
        file_entry = self._file_entry(full_path)
        file_entry.set_attribute("manifest:media-type", media_type)

    @staticmethod
    def make_file_entry(full_path: str, media_type: str) -> Element:
        tag = (
            f"<manifest:file-entry "
            f'manifest:media-type="{media_type}" '
            f'manifest:full-path="{full_path}"/>'
        )
        return Element.from_tag(tag)

    def add_full_path(self, full_path: str, media_type: str = "") -> None:
        # Existing?
        existing = self.get_media_type(full_path)
        if existing is not None:
            self.set_media_type(full_path, media_type)
        root = self.root
        root.append(self.make_file_entry(full_path, media_type))

    def del_full_path(self, full_path: str) -> None:
        file_entry = self._file_entry(full_path)
        self.root.delete(file_entry)

add_full_path

add_full_path(full_path: str, media_type: str = '') -> None
Source code in odfdo/manifest.py
103
104
105
106
107
108
109
def add_full_path(self, full_path: str, media_type: str = "") -> None:
    # Existing?
    existing = self.get_media_type(full_path)
    if existing is not None:
        self.set_media_type(full_path, media_type)
    root = self.root
    root.append(self.make_file_entry(full_path, media_type))

del_full_path

del_full_path(full_path: str) -> None
Source code in odfdo/manifest.py
111
112
113
def del_full_path(self, full_path: str) -> None:
    file_entry = self._file_entry(full_path)
    self.root.delete(file_entry)

get_media_type

get_media_type(full_path: str) -> str | None

Get the media type of an existing path.

Return: str

Source code in odfdo/manifest.py
68
69
70
71
72
73
74
75
76
77
78
79
80
def get_media_type(self, full_path: str) -> str | None:
    """Get the media type of an existing path.

    Return: str
    """
    xpath_query = (
        f'//manifest:file-entry[attribute::manifest:full-path="{full_path}"]'
        "/attribute::manifest:media-type"
    )
    result = self.xpath(xpath_query)
    if not result:
        return None
    return str(result[0])

get_path_medias

get_path_medias() -> list[tuple]

Return the list of (full_path, media_type) pairs in the manifest.

Return: list of str tuples

Source code in odfdo/manifest.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def get_path_medias(self) -> list[tuple]:
    """Return the list of (full_path, media_type) pairs in the manifest.

    Return: list of str tuples
    """
    xpath_query = "//manifest:file-entry"
    result = []
    for file_entry in self.xpath(xpath_query):
        if not isinstance(file_entry, Element):  # pragma: no cover
            continue
        result.append(
            (
                file_entry.get_attribute_string("manifest:full-path"),
                file_entry.get_attribute_string("manifest:media-type"),
            )
        )
    return result

get_paths

get_paths() -> list[Element | EText]

Return the list of full paths in the manifest.

Return: list of str

Source code in odfdo/manifest.py
33
34
35
36
37
38
39
def get_paths(self) -> list[Element | EText]:
    """Return the list of full paths in the manifest.

    Return: list of str
    """
    xpath_query = "//manifest:file-entry/attribute::manifest:full-path"
    return self.xpath(xpath_query)

make_file_entry staticmethod

make_file_entry(full_path: str, media_type: str) -> Element
Source code in odfdo/manifest.py
 94
 95
 96
 97
 98
 99
100
101
@staticmethod
def make_file_entry(full_path: str, media_type: str) -> Element:
    tag = (
        f"<manifest:file-entry "
        f'manifest:media-type="{media_type}" '
        f'manifest:full-path="{full_path}"/>'
    )
    return Element.from_tag(tag)

set_media_type

set_media_type(full_path: str, media_type: str) -> None

Set the media type of an existing path.

Arguments:

full_path -- str

media_type -- str
Source code in odfdo/manifest.py
82
83
84
85
86
87
88
89
90
91
92
def set_media_type(self, full_path: str, media_type: str) -> None:
    """Set the media type of an existing path.

    Arguments:

        full_path -- str

        media_type -- str
    """
    file_entry = self._file_entry(full_path)
    file_entry.set_attribute("manifest:media-type", media_type)

Meta

Bases: XmlPart, DcCreatorMixin, DcDateMixin

Representation of the “meta.xml” part.

Methods:

Name Description
__init__
as_dict

Return the metadata of the document as a Python dict.

as_json

Return the metadata of the document as a JSON string.

as_text

Return meta informations as text, with some formatting for printing.

clear_user_defined_metadata

Remove all user-defined metadata.

from_dict

Set the metadata of the document from a Python dict.

get_auto_reload

Get the MetaAutoReload element or None.

get_creation_date

Get the creation date of the document.

get_description

Get the description of the document. Also known as comments.

get_editing_cycles

Get the number of times the document was edited, as reported by

get_editing_duration

Get the time the document was edited, as reported by the

get_generator

Get the signature of the software that generated this document.

get_hyperlink_behaviour

Get the MetaHyperlinkBehaviour element or None.

get_initial_creator

Get the first creator of the document.

get_keywords

Get the keywords of the document. Return the field as-is, without

get_language

Get the default language of the document.

get_meta_body
get_statistic

Get the statistics about a document.

get_subject

Get the subject of the document.

get_template

Get the MetaTemplate element or None.

get_title

Get the title of the document.

get_user_defined_metadata

Get all additional user-defined metadata for a document.

get_user_defined_metadata_of_name

Return the content of the user defined metadata of that name.

set_auto_reload

Set the MetaAutoReload element.

set_creation_date

Set the creation date of the document.

set_description

Set the description of the document. Also known as comments.

set_editing_cycles

Set the number of times the document was edited.

set_editing_duration

Set the time the document was edited.

set_generator

Set the signature of the software that generated this document.

set_generator_default

Set the signature of the software that generated this document

set_hyperlink_behaviour

Set the MetaHyperlinkBehaviour element.

set_initial_creator

Set the first creator of the document.

set_keywords

Set the keywords of the document. Although the name is plural, a

set_language

Set the default language of the document.

set_statistic

Set the statistics about a document.

set_subject

Set the subject of the document.

set_template

Set the MetaTemplate element.

set_title

Set the title of the document.

set_user_defined_metadata

Attributes:

Name Type Description
auto_reload MetaAutoReload | None

Get the MetaAutoReload element or None.

creation_date datetime | None

Get or set the date and time when a document was created

description str | None

Get or set the description of a document .

editing_cycles int | None

Get or set the number of times a document has been edited

editing_duration timedelta | None

Get or set the total time spent editing a document

generator str | None

Get or set the signature of the software that generated this

get_comments
hyperlink_behaviour MetaAutoReload | None

Get the MetaHyperlinkBehaviour element or None.

initial_creator str | None

Get or set the initial creator of a document

keyword str | None

Get or set some keyword(s) keyword pertaining to a document

keywords
language str | None

Get or set the default language of the document .

print_date datetime | None

Get or set the date and time when a document when a document was last printed

printed_by str | None

Get or set the name of the last person who printed a document.

set_comments
statistic dict[str, int] | None

Get or set the statistics about a document

subject str | None

Get or set the subject of a document .

template MetaTemplate | None

Get the MetaTemplate element or None.

title str | None

Get or set the title of the document .

user_defined_metadata dict[str, Any]

Get or set all additional user-defined metadata for a document.

Source code in odfdo/meta.py
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
class Meta(XmlPart, DcCreatorMixin, DcDateMixin):
    """Representation of the "meta.xml" part."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self._generator_modified: bool = False

    def get_meta_body(self) -> Element:
        return self.get_element("//office:meta")

    def get_title(self) -> str | None:
        """Get the title of the document.

        This is not the first heading but the title metadata.

        (Also available as "self.title" property.)

        Return: str (or None if inexistant)
        """
        element = self.get_element("//dc:title")
        if element is None:
            return None
        return element.text

    def set_title(self, title: str) -> None:
        """Set the title of the document.

        This is not the first heading but the title metadata.

        (Also available as "self.title" property.)

        Arguments:

            title -- str
        """
        element = self.get_element("//dc:title")
        if element is None:
            element = Element.from_tag("dc:title")
            self.get_meta_body().append(element)
        element.text = title

    @property
    def title(self) -> str | None:
        """Get or set the title of the document <dc:title>.

        Return: str (or None if inexistant)
        """
        return self.get_title()

    @title.setter
    def title(self, title: str) -> None:
        return self.set_title(title)

    def get_description(self) -> str | None:
        """Get the description of the document. Also known as comments.

        (Also available as "self.description" property.)

        Return: str (or None if inexistant)
        """
        element = self.get_element("//dc:description")
        if element is None:
            return None
        return element.text

    # As named in OOo
    get_comments = get_description

    def set_description(self, description: str) -> None:
        """Set the description of the document. Also known as comments.

        (Also available as "self.description" property.)

        Arguments:

            description -- str
        """
        element = self.get_element("//dc:description")
        if element is None:
            element = Element.from_tag("dc:description")
            self.get_meta_body().append(element)
        element.text = description

    set_comments = set_description

    @property
    def description(self) -> str | None:
        """Get or set the description of a document <dc:description>.

        Return: str (or None if inexistant)
        """
        return self.get_description()

    @description.setter
    def description(self, description: str) -> None:
        return self.set_description(description)

    def get_subject(self) -> str | None:
        """Get the subject of the document.

        (Also available as "self.subject" property.)

        Return: str (or None if inexistant)
        """
        element = self.get_element("//dc:subject")
        if element is None:
            return None
        return element.text

    def set_subject(self, subject: str) -> None:
        """Set the subject of the document.

        (Also available as "self.subject" property.)

        Arguments:

            subject -- str
        """
        element = self.get_element("//dc:subject")
        if element is None:
            element = Element.from_tag("dc:subject")
            self.get_meta_body().append(element)
        element.text = subject

    @property
    def subject(self) -> str | None:
        """Get or set the subject of a document <dc:subject>.

        Return: str (or None if inexistant)
        """
        return self.get_subject()

    @subject.setter
    def subject(self, subject: str) -> None:
        return self.set_subject(subject)

    def get_language(self) -> str | None:
        """Get the default language of the document.

        (Also available as "self.language" property.)

        Return: str (or None if inexistant)

        Example::

            >>> document.meta.get_language()
            fr-FR
        """
        element = self.get_element("//dc:language")
        if element is None:
            return None
        return element.text

    def set_language(self, language: str) -> None:
        """Set the default language of the document.

        (Also available as "self.language" property.)

        Arguments:

            language -- str

        Example::

            >>> document.meta.set_language('fr-FR')
        """
        language = str(language)
        if not self._is_RFC3066(language):
            raise TypeError(
                'Language must be "xx" lang or "xx-YY" lang-COUNTRY code (RFC3066)'
            )
        element = self.get_element("//dc:language")
        if element is None:
            element = Element.from_tag("dc:language")
            self.get_meta_body().append(element)
        element.text = language

    @staticmethod
    def _is_RFC3066(lang: str) -> bool:
        def test_part1(part1: str) -> bool:
            if not 2 <= len(part1) <= 3:
                return False
            return all(x in ascii_letters for x in part1)

        def test_part2(part2: str) -> bool:
            return all(x in ascii_letters or x in digits for x in part2)

        if not lang or not isinstance(lang, str):
            return False
        if "-" not in lang:
            return test_part1(lang)
        parts = lang.split("-")
        if len(parts) > 3:
            return False
        if not test_part1(parts[0]):
            return False
        return all(test_part2(p) for p in parts[1:])

    @property
    def language(self) -> str | None:
        """Get or set the default language of the document <dc:language>.

        Return: str (or None if inexistant)
        """
        return self.get_language()

    @language.setter
    def language(self, language: str) -> None:
        return self.set_language(language)

    def get_creation_date(self) -> datetime | None:
        """Get the creation date of the document.

        (Also available as "self.creation_date" property.)

        Return: datetime (or None if inexistant)
        """
        element = self.get_element("//meta:creation-date")
        if element is None:
            return None
        creation_date = element.text
        return DateTime.decode(creation_date)

    def set_creation_date(self, date: datetime | None = None) -> None:
        """Set the creation date of the document.

        If provided datetime is None, use current time.

        (Also available as "self.creation_date" property.)

        Arguments:

            date -- datetime
        """
        element = self.get_element("//meta:creation-date")
        if element is None:
            element = Element.from_tag("meta:creation-date")
            self.get_meta_body().append(element)
        if date is None:
            date = datetime.now()
        element.text = DateTime.encode(date)

    @property
    def creation_date(self) -> datetime | None:
        """Get or set the date and time when a document was created
        <meta:creation-date>.

        If provided datetime is None, use current time.

        Return: datetime (or None if inexistant)
        """
        return self.get_creation_date()

    @creation_date.setter
    def creation_date(self, date: datetime | None = None) -> None:
        return self.set_creation_date(date)

    @property
    def print_date(self) -> datetime | None:
        """Get or set the date and time when a document when a document was last printed
        <meta:print-date>

        If provided datetime is None, use current time.

        Return: datetime (or None if inexistant)
        """
        element = self.get_element("//meta:print-date")
        if element is None:
            return None
        date = element.text
        return DateTime.decode(date)

    @print_date.setter
    def print_date(self, date: datetime | None = None) -> None:
        element = self.get_element("//meta:print-date")
        if element is None:
            element = Element.from_tag("meta:print-date")
            self.get_meta_body().append(element)
        if date is None:
            date = datetime.now()
        element.text = DateTime.encode(date)

    def get_template(self) -> MetaTemplate | None:
        """Get the MetaTemplate <meta:template> element or None."""
        element = self.get_element("//meta:template")
        if element is None:
            return None
        return element

    @property
    def template(self) -> MetaTemplate | None:
        """Get the MetaTemplate <meta:template> element or None."""
        return self.get_template()

    def set_template(
        self,
        date: datetime | None = None,
        href: str = "",
        title: str = "",
    ) -> None:
        """Set the MetaTemplate <meta:template> element."""
        template = MetaTemplate(date=date, href=href, title=title)
        current = self.template
        if isinstance(current, MetaTemplate):
            current.delete()
        self.get_meta_body().append(template)

    def get_auto_reload(self) -> MetaAutoReload | None:
        """Get the MetaAutoReload <meta:auto-reload> element or None."""
        element = self.get_element("//meta:auto-reload")
        if element is None:
            return None
        return element

    @property
    def auto_reload(self) -> MetaAutoReload | None:
        """Get the MetaAutoReload <meta:auto-reload> element or None."""
        return self.get_auto_reload()

    def set_auto_reload(self, delay: timedelta, href: str = "") -> None:
        """Set the MetaAutoReload <meta:auto-reload> element."""
        autoreload = MetaAutoReload(delay=delay, href=href)
        current = self.auto_reload
        if isinstance(current, MetaAutoReload):
            current.delete()
        self.get_meta_body().append(autoreload)

    def get_hyperlink_behaviour(self) -> MetaHyperlinkBehaviour | None:
        """Get the MetaHyperlinkBehaviour <meta:hyperlink-behaviour> element or None."""
        element = self.get_element("//meta:hyperlink-behaviour")
        if element is None:
            return None
        return element

    @property
    def hyperlink_behaviour(self) -> MetaAutoReload | None:
        """Get the MetaHyperlinkBehaviour <meta:hyperlink-behaviour> element or None."""
        return self.get_hyperlink_behaviour()

    def set_hyperlink_behaviour(
        self,
        target_frame_name: str = "_blank",
        show: str = "replace",
    ) -> None:
        """Set the MetaHyperlinkBehaviour <meta:hyperlink-behaviour> element."""
        behaviour = MetaHyperlinkBehaviour(
            target_frame_name=target_frame_name, show=show
        )
        current = self.hyperlink_behaviour
        if isinstance(current, MetaHyperlinkBehaviour):
            current.delete()
        self.get_meta_body().append(behaviour)

    def get_initial_creator(self) -> str | None:
        """Get the first creator of the document.

        (Also available as "self.initial_creator" property.)

        Return: str (or None if inexistant)

        Example::

            >>> document.meta.get_initial_creator()
            Unknown
        """
        element = self.get_element("//meta:initial-creator")
        if element is None:
            return None
        return element.text

    def set_initial_creator(self, creator: str) -> None:
        """Set the first creator of the document.

        (Also available as "self.initial_creator" property.)

        Arguments:

            creator -- str

        Example::

            >>> document.meta.set_initial_creator("Plato")
        """
        element = self.get_element("//meta:initial-creator")
        if element is None:
            element = Element.from_tag("meta:initial-creator")
            self.get_meta_body().append(element)
        element.text = creator

    @property
    def initial_creator(self) -> str | None:
        """Get or set the initial creator of a document
        <meta:initial-creator>.

        Return: str (or None if inexistant)
        """
        return self.get_initial_creator()

    @initial_creator.setter
    def initial_creator(self, creator: str) -> None:
        return self.set_initial_creator(creator)

    @property
    def printed_by(self) -> str | None:
        """Get or set the name of the last person who printed a document.
        <meta:printed-by>

        Return: str (or None if inexistant)
        """
        element = self.get_element("//meta:printed-by")
        if element is None:
            return None
        return element.text

    @printed_by.setter
    def printed_by(self, printed_by: str) -> None:
        element = self.get_element("//meta:printed-by")
        if element is None:
            element = Element.from_tag("meta:printed-by")
            self.get_meta_body().append(element)
        element.text = printed_by

    def get_keywords(self) -> str | None:
        """Get the keywords of the document. Return the field as-is, without
        any assumption on the keyword separator.

        (Also available as "self.keyword" and "self.keywords" property.)

        Return: str (or None if inexistant)
        """
        element = self.get_element("//meta:keyword")
        if element is None:
            return None
        return element.text

    def set_keywords(self, keywords: str) -> None:
        """Set the keywords of the document. Although the name is plural, a
        str string is required, so join your list first.

        (Also available as "self.keyword" and "self.keywords" property.)

        Arguments:

            keywords -- str
        """
        element = self.get_element("//meta:keyword")
        if element is None:
            element = Element.from_tag("meta:keyword")
            self.get_meta_body().append(element)
        element.text = keywords

    @property
    def keyword(self) -> str | None:
        """Get or set some keyword(s) keyword pertaining to a document
        <dc:keyword>.

        Return: str (or None if inexistant)
        """
        return self.get_keywords()

    @keyword.setter
    def keyword(self, keyword: str) -> None:
        return self.set_keywords(keyword)

    keywords = keyword

    def get_editing_duration(self) -> timedelta | None:
        """Get the time the document was edited, as reported by the
        generator.

        (Also available as "self.editing_duration" property.)

        Return: timedelta (or None if inexistant)
        """
        element = self.get_element("//meta:editing-duration")
        if element is None:
            return None
        duration = element.text
        return Duration.decode(duration)

    def set_editing_duration(self, duration: timedelta) -> None:
        """Set the time the document was edited.

        (Also available as "self.editing_duration" property.)

        Arguments:

            duration -- timedelta
        """
        if not isinstance(duration, timedelta):
            raise TypeError("duration must be a timedelta")
        element = self.get_element("//meta:editing-duration")
        if element is None:
            element = Element.from_tag("meta:editing-duration")
            self.get_meta_body().append(element)
        element.text = Duration.encode(duration)

    @property
    def editing_duration(self) -> timedelta | None:
        """Get or set the total time spent editing a document
        <meta:editing-duration>.

        Return: timedelta (or None if inexistant)
        """
        return self.get_editing_duration()

    @editing_duration.setter
    def editing_duration(self, duration: timedelta) -> None:
        return self.set_editing_duration(duration)

    def get_editing_cycles(self) -> int | None:
        """Get the number of times the document was edited, as reported by
        the generator.

        (Also available as "self.editing_cycles" property.)

        Return: int (or None if inexistant)
        """
        element = self.get_element("//meta:editing-cycles")
        if element is None:
            return None
        cycles = element.text
        return int(cycles)

    def set_editing_cycles(self, cycles: int) -> None:
        """Set the number of times the document was edited.

        (Also available as "self.editing_cycles" property.)

        Arguments:

            cycles -- int
        """
        if not isinstance(cycles, int):
            raise TypeError("cycles must be an int")
        if cycles < 1:
            raise ValueError("cycles must be a positive int")
        element = self.get_element("//meta:editing-cycles")
        if element is None:
            element = Element.from_tag("meta:editing-cycles")
            self.get_meta_body().append(element)
        element.text = str(cycles)

    @property
    def editing_cycles(self) -> int | None:
        """Get or set the number of times a document has been edited
        <meta:editing-cycles>.

        When a document is created, this value is set to 1. Each time
        a document is saved, the editing-cycles number is incremented by 1.

        Return: int (or None if inexistant)
        """
        return self.get_editing_cycles()

    @editing_cycles.setter
    def editing_cycles(self, cycles: int) -> None:
        return self.set_editing_cycles(cycles)

    @property
    def generator(self) -> str | None:
        """Get or set the signature of the software that generated this
        document.

        Return: str (or None if inexistant)

        Example::

            >>> document.meta.generator
            KOffice/2.0.0
            >>> document.meta.generator = "Odfdo experiment"
        """
        element = self.get_element("//meta:generator")
        if element is None:
            return None
        return element.text

    @generator.setter
    def generator(self, generator: str) -> None:
        element = self.get_element("//meta:generator")
        if element is None:
            element = Element.from_tag("meta:generator")
            self.get_meta_body().append(element)
        element.text = generator
        self._generator_modified = True

    def get_generator(self) -> str | None:
        """Get the signature of the software that generated this document.

        (Also available as "self.generator" property.)

        Return: str (or None if inexistant)

        Example::

            >>> document.meta.get_generator()
            KOffice/2.0.0
        """
        return self.generator

    def set_generator(self, generator: str) -> None:
        """Set the signature of the software that generated this document.

        (Also available as "self.generator" property.)

        Arguments:

            generator -- str

        Example::

            >>> document.meta.set_generator("Odfdo experiment")
        """
        self.generator = generator

    def set_generator_default(self) -> None:
        """Set the signature of the software that generated this document
        to ourself.

        Example::

            >>> document.meta.set_generator_default()
        """
        if not self._generator_modified:
            self.generator = GENERATOR

    def get_statistic(self) -> dict[str, int] | None:
        """Get the statistics about a document.

        (Also available as "self.statistic" property.)

        Return: dict (or None if inexistant)

        Example::

            >>> document.get_statistic():
            {'meta:table-count': 1,
             'meta:image-count': 2,
             'meta:object-count': 3,
             'meta:page-count': 4,
             'meta:paragraph-count': 5,
             'meta:word-count': 6,
             'meta:character-count': 7,
             'meta:non-whitespace-character-count': 3}
        """
        element = self.get_element("//meta:document-statistic")
        if element is None:
            return None
        statistic = {}
        for key, value in element.attributes.items():
            statistic[to_str(key)] = int(value)
        return statistic

    def set_statistic(self, statistic: dict[str, int]) -> None:
        """Set the statistics about a document.

        (Also available as "self.statistic" property.)

        Arguments:

            statistic -- dict

        Example::

            >>> statistic = {'meta:table-count': 1,
                             'meta:image-count': 2,
                             'meta:object-count': 3,
                             'meta:page-count': 4,
                             'meta:paragraph-count': 5,
                             'meta:word-count': 6,
                             'meta:character-count': 7,
                             'meta:non-whitespace-character-count': 3}
            >>> document.meta.set_statistic(statistic)
        """
        if not isinstance(statistic, dict):
            raise TypeError("Statistic must be a dict")
        element = self.get_element("//meta:document-statistic")
        for key, value in statistic.items():
            try:
                ivalue = int(value)
            except ValueError as e:
                raise TypeError("Statistic value must be a int") from e
            element.set_attribute(to_str(key), str(ivalue))

    @property
    def statistic(self) -> dict[str, int] | None:
        """Get or set the statistics about a document
        <meta:document-statistic>.

        Return: dict (or None if inexistant)

        Example::

            >>> document.get_statistic():
            {'meta:table-count': 1,
             'meta:image-count': 2,
             'meta:object-count': 3,
             'meta:page-count': 4,
             'meta:paragraph-count': 5,
             'meta:word-count': 6,
             'meta:character-count': 7,
             'meta:non-whitespace-character-count':3}
        """
        return self.get_statistic()

    @statistic.setter
    def statistic(self, statistic: dict[str, int]) -> None:
        return self.set_statistic(statistic)

    def get_user_defined_metadata(self) -> dict[str, Any]:
        """Get all additional user-defined metadata for a document.

        (Also available as "self.user_defined_metadata" property.)

        Return a dict of str/value mapping.

        Value types can be: Decimal, date, time, boolean or str.
        """
        result: dict[str, Any] = {}
        for item in self.get_elements("//meta:user-defined"):
            if not isinstance(item, Element):
                continue
            # Read the values
            name = item.get_attribute_string("meta:name")
            if name is None:
                continue
            value = self._get_meta_value(item)
            result[name] = value
        return result

    def _user_defined_metadata_list(self) -> list[dict[str, Any]]:
        user_defined: list[dict[str, Any]] = []
        for item in self.get_elements("//meta:user-defined"):
            if not isinstance(item, Element):
                continue
            # Read the values
            name = item.get_attribute_string("meta:name")
            if not name:
                continue
            value, value_type, _text = self._get_meta_value_full(item)
            user_defined.append(
                {"meta:name": name, "meta:value-type": value_type, "value": value}
            )
        return sorted(user_defined, key=itemgetter("meta:name"))

    def clear_user_defined_metadata(self) -> None:
        """Remove all user-defined metadata."""
        while True:
            element = self.get_element("//meta:user-defined")
            if isinstance(element, Element):
                element.delete()
                continue
            break

    @property
    def user_defined_metadata(self) -> dict[str, Any]:
        """Get or set all additional user-defined metadata for a document.

        Return a dict of str/value mapping.

        Value types can be: Decimal, date, time, boolean or str.
        """
        return self.get_user_defined_metadata()

    @user_defined_metadata.setter
    def user_defined_metadata(self, metadata: dict[str, Any]) -> None:
        self.clear_user_defined_metadata()
        for key, val in metadata.items():
            self.set_user_defined_metadata(name=key, value=val)

    def get_user_defined_metadata_of_name(self, keyname: str) -> dict[str, Any] | None:
        """Return the content of the user defined metadata of that name.
        Return None if no name matchs or a dic of fields.

        Arguments:

            name -- string, name (meta:name content)
        """
        result = {}
        found = False
        for item in self.get_elements("//meta:user-defined"):
            if not isinstance(item, Element):
                continue
            # Read the values
            name = item.get_attribute("meta:name")
            if name == keyname:
                found = True
                break
        if not found:
            return None
        result["name"] = name
        value, value_type, text = self._get_meta_value(item, full=True)  # type: ignore
        result["value"] = value
        result["value_type"] = value_type
        result["text"] = text
        return result

    def set_user_defined_metadata(self, name: str, value: Any) -> None:
        if isinstance(value, bool):
            value_type = "boolean"
            value = "true" if value else "false"
        elif isinstance(value, (int, float, Decimal)):
            value_type = "float"
            value = str(value)
        elif isinstance(value, dtdate):
            value_type = "date"
            value = str(Date.encode(value))
        elif isinstance(value, datetime):
            value_type = "date"
            value = str(DateTime.encode(value))
        elif isinstance(value, str):
            value_type = "string"
        elif isinstance(value, timedelta):
            value_type = "time"
            value = str(Duration.encode(value))
        else:
            raise TypeError(f'unexpected type "{type(value)}" for value')
        # Already the same element ?
        for metadata in self.get_elements("//meta:user-defined"):
            if not isinstance(metadata, Element):
                continue
            if metadata.get_attribute("meta:name") == name:
                break
        else:
            metadata = Element.from_tag("meta:user-defined")
            metadata.set_attribute("meta:name", name)
            self.get_meta_body().append(metadata)
        metadata.set_attribute("meta:value-type", value_type)
        metadata.text = value

    def _get_meta_value(
        self, element: Element, full: bool = False
    ) -> Any | tuple[Any, str, str]:
        """get_value() deicated to the meta data part, for one meta element."""
        if full:
            return self._get_meta_value_full(element)
        else:
            return self._get_meta_value_full(element)[0]

    @staticmethod
    def _get_meta_value_full(element: Element) -> tuple[Any, str, str]:
        """get_value deicated to the meta data part, for one meta element."""
        # name = element.get_attribute('meta:name')
        value_type = element.get_attribute_string("meta:value-type")
        if value_type is None:
            value_type = "string"
        text = element.text
        # Interpretation
        if value_type == "boolean":
            return (Boolean.decode(text), value_type, text)
        if value_type in ("float", "percentage", "currency"):
            return (Decimal(text), value_type, text)
        if value_type == "date":
            if "T" in text:
                return (DateTime.decode(text), value_type, text)
            else:
                return (Date.decode(text), value_type, text)
        if value_type == "string":
            return (text, value_type, text)
        if value_type == "time":
            return (Duration.decode(text), value_type, text)
        raise TypeError(f"Unknown value type: '{value_type!r}'")

    def as_dict(self, full: bool = False) -> dict[str, Any]:
        """Return the metadata of the document as a Python dict.

        if 'full' is True, export also the keys with no value assigned.

        Arguments:

            full -- boolean
        """

        def _stats() -> dict[str, int]:
            doc_stats = self.statistic
            if doc_stats is None:
                msg = "Document statitics not found"
                raise LookupError(msg)
            return {
                key: doc_stats.get(key, 0)
                for key in (
                    "meta:table-count",
                    "meta:image-count",
                    "meta:object-count",
                    "meta:page-count",
                    "meta:paragraph-count",
                    "meta:word-count",
                    "meta:character-count",
                    "meta:non-whitespace-character-count",
                )
            }

        def _meta_template() -> dict[str, Any] | None:
            template = self.template
            if template is None:
                return None
            return template.as_dict()

        def _meta_reload() -> dict[str, Any] | None:
            reload = self.auto_reload
            if reload is None:
                return None
            return reload.as_dict()

        def _meta_behaviour() -> dict[str, Any] | None:
            behaviour = self.hyperlink_behaviour
            if behaviour is None:
                return None
            return behaviour.as_dict()

        meta_data: dict[str, Any] = {
            "meta:creation-date": self.creation_date,
            "dc:date": self.date,
            "meta:editing-duration": self.editing_duration,
            "meta:editing-cycles": self.editing_cycles,
            "meta:document-statistic": _stats(),
            "meta:generator": self.generator,
            "dc:title": self.title,
            "dc:description": self.description,
            "dc:creator": self.creator,
            "meta:keyword": self.keyword,
            "dc:subject": self.subject,
            "dc:language": self.language,
            "meta:initial-creator": self.initial_creator,
            "meta:print-date": self.print_date,
            "meta:printed-by": self.printed_by,
            "meta:template": _meta_template(),
            "meta:auto-reload": _meta_reload(),
            "meta:hyperlink-behaviour": _meta_behaviour(),
            "meta:user-defined": self._user_defined_metadata_list(),
        }

        if not full:
            meta_data = {key: val for key, val in meta_data.items() if val}
        return meta_data

    def _as_json_dict(self, full: bool = False) -> dict[str, Any]:
        def _convert(data: dict[str, Any]) -> None:
            for key, val in data.items():
                if isinstance(val, datetime):
                    data[key] = DateTime.encode(val)
                elif isinstance(val, timedelta):
                    data[key] = Duration.encode(val)
                elif isinstance(val, Decimal):
                    data[key] = json.loads(str(Decimal(val)))

        meta_data: dict[str, Any] = self.as_dict(full=full)
        user_defined = meta_data.get("meta:user-defined", [])
        for item in user_defined:
            _convert(item)
        meta_data["meta:user-defined"] = user_defined
        _convert(meta_data)
        return meta_data

    def as_json(self, full: bool = False) -> str:
        """Return the metadata of the document as a JSON string.

        if 'full' is True, export also the keys with no value assigned.

        Arguments:

            full -- boolean
        """
        return json.dumps(
            self._as_json_dict(full=full),
            ensure_ascii=False,
            sort_keys=False,
            indent=4,
        )

    def as_text(self) -> str:
        """Return meta informations as text, with some formatting for printing."""
        data = self._as_json_dict(full=False)
        result: list[str] = []

        def _append_info(name: str, key: str) -> None:
            value = data.get(key)
            if value:
                result.append(f"{name}: {value}")

        _append_info("Title", "dc:title")
        _append_info("Subject", "dc:subject")
        _append_info("Description", "dc:description")
        _append_info("Language", "dc:language")
        _append_info("Modification date", "dc:date")
        _append_info("Creation date", "meta:creation-date")
        _append_info("Creator", "dc:creator")
        _append_info("Initial creator", "meta:initial-creator")
        _append_info("Keyword", "meta:keyword")
        _append_info("Editing duration", "meta:editing-duration")
        _append_info("Editing cycles", "meta:editing-cycles")
        _append_info("Generator", "meta:generator")

        result.append("Statistic:")
        statistic = data.get("meta:document-statistic", {})
        if statistic:
            for name, value in statistic.items():
                result.append(f"  - {name[5:].replace('-', ' ').capitalize()}: {value}")

        result.append("User defined metadata:")
        user_metadata = data.get("meta:user-defined", [])
        for item in user_metadata:
            result.append(f"  - {item['meta:name']}: {item['value']}")

        return "\n".join(result)

    @staticmethod
    def _complete_stats(
        current_stats: dict[str, int],
        imported_stats: dict[str, int] | None,
    ) -> dict[str, int]:
        if imported_stats is None:
            imported_stats = {}
            current_stats = {}
        new: dict[str, int] = {}
        for key in (
            "meta:table-count",
            "meta:image-count",
            "meta:object-count",
            "meta:page-count",
            "meta:paragraph-count",
            "meta:word-count",
            "meta:character-count",
            "meta:non-whitespace-character-count",
        ):
            new[key] = imported_stats.get(key, current_stats.get(key, 0))
        return new

    def from_dict(self, data: dict[str, Any]) -> None:
        """Set the metadata of the document from a Python dict.

        The loaded metadata are merged with the existing metadata.
        If the new value of a key is None:
            - meta:creation-date: use current time,
            - dc:date: use creation date,
            - meta:editing-duration: set to zero,
            - meta:editing-cycles: set to 1,
            - meta:generator: use odfdo generator string.
            Other keys (not mandatory keys): remove key/value pair from
            metadata.

        Arguments:

            data -- dict of metadata.
        """

        def _value_delete(key: str) -> Any:
            value = data.get(key, current.get(key))
            if value is None:
                child = self.get_element(f"//{key}")
                if child is not None:
                    self.delete_element(child)
            return value

        current = self.as_dict()
        new_stats = self._complete_stats(
            current["meta:document-statistic"],
            data.get("meta:document-statistic", {}),
        )
        # mandatory
        self.statistic = new_stats

        key = "meta:creation-date"
        creation_date = data.get(key, current.get(key))
        if creation_date is None:
            creation_date = datetime.now().replace(microsecond=0)
        self.creation_date = creation_date

        key = "dc:date"
        dc_date = data.get(key, current.get(key))
        if dc_date is None:
            dc_date = creation_date
        if dc_date < creation_date:
            dc_date = creation_date
        max_editing = dc_date - creation_date
        self.date = dc_date

        key = "meta:editing-duration"
        editing_duration = data.get(key, current.get(key))
        if editing_duration is None:
            editing_duration = timedelta(0)
        if editing_duration > max_editing:
            editing_duration = max_editing
        self.editing_duration = editing_duration

        key = "meta:editing-cycles"
        editing_cycles = data.get(key, current.get(key))
        if editing_cycles is None:
            editing_cycles = 1
        self.editing_cycles = max(editing_cycles, 1)

        key = "meta:generator"
        generator = data.get(key, current.get(key))
        if not generator:
            generator = GENERATOR
        self.generator = generator

        # not mandatory values
        # - if not in imported: keep original value
        # - if imported is None: erase original value

        key = "dc:title"
        value = _value_delete(key)
        if value is not None:
            self.title = value

        key = "dc:description"
        value = _value_delete(key)
        if value is not None:
            self.description = value

        key = "dc:creator"
        value = _value_delete(key)
        if value is not None:
            self.creator = value

        key = "meta:keyword"
        value = _value_delete(key)
        if value is not None:
            self.keyword = value

        key = "dc:subject"
        value = _value_delete(key)
        if value is not None:
            self.subject = value

        key = "dc:language"
        value = _value_delete(key)
        if value is not None:
            self.language = value

        key = "meta:initial-creator"
        value = _value_delete(key)
        if value is not None:
            self.initial_creator = value

        key = "meta:print-date"
        value = _value_delete(key)
        if value is not None:
            self.print_date = value

        key = "meta:printed-by"
        value = _value_delete(key)
        if value is not None:
            self.printed_by = value

        key = "meta:template"
        value = _value_delete(key)
        if value is not None:
            value = value.as_dict()
            self.set_template(
                date=value["meta:date"],
                href=value["xlink:href"],
                title=value["xlink:title"],
            )

        key = "meta:auto-reload"
        value = _value_delete(key)
        if value is not None:
            value = value.as_dict()
            self.set_auto_reload(
                delay=value["meta:delay"],
                href=value["xlink:href"],
            )

        key = "meta:hyperlink-behaviour"
        value = _value_delete(key)
        if value is not None:
            value = value.as_dict()
            self.set_hyperlink_behaviour(
                target_frame_name=value["office:target-frame-name"],
                show=value["xlink:show"],
            )

        key = "meta:user-defined"
        if key in data:
            value = data[key]
            if value is None:
                self.clear_user_defined_metadata()
            else:
                current = self._user_defined_metadata_list()
                current_dict = {d["meta:name"]: d for d in current}
                current_value = {d["meta:name"]: d for d in value}
                current_dict.update(current_value)
                new_ud = {
                    v["meta:name"]: v["value"]
                    for v in current_dict.values()
                    if v["value"] is not None
                }
                self.user_defined_metadata = new_ud

auto_reload property

auto_reload: MetaAutoReload | None

Get the MetaAutoReload element or None.

creation_date property writable

creation_date: datetime | None

Get or set the date and time when a document was created .

If provided datetime is None, use current time.

Return: datetime (or None if inexistant)

description property writable

description: str | None

Get or set the description of a document .

Return: str (or None if inexistant)

editing_cycles property writable

editing_cycles: int | None

Get or set the number of times a document has been edited .

When a document is created, this value is set to 1. Each time a document is saved, the editing-cycles number is incremented by 1.

Return: int (or None if inexistant)

editing_duration property writable

editing_duration: timedelta | None

Get or set the total time spent editing a document .

Return: timedelta (or None if inexistant)

generator property writable

generator: str | None

Get or set the signature of the software that generated this document.

Return: str (or None if inexistant)

Example::

>>> document.meta.generator
KOffice/2.0.0
>>> document.meta.generator = "Odfdo experiment"

get_comments class-attribute instance-attribute

get_comments = get_description
hyperlink_behaviour: MetaAutoReload | None

Get the MetaHyperlinkBehaviour element or None.

initial_creator property writable

initial_creator: str | None

Get or set the initial creator of a document .

Return: str (or None if inexistant)

keyword property writable

keyword: str | None

Get or set some keyword(s) keyword pertaining to a document .

Return: str (or None if inexistant)

keywords class-attribute instance-attribute

keywords = keyword

language property writable

language: str | None

Get or set the default language of the document .

Return: str (or None if inexistant)

print_date property writable

print_date: datetime | None

Get or set the date and time when a document when a document was last printed

If provided datetime is None, use current time.

Return: datetime (or None if inexistant)

printed_by property writable

printed_by: str | None

Get or set the name of the last person who printed a document.

Return: str (or None if inexistant)

set_comments class-attribute instance-attribute

set_comments = set_description

statistic property writable

statistic: dict[str, int] | None

Get or set the statistics about a document .

Return: dict (or None if inexistant)

Example::

>>> document.get_statistic():
{'meta:table-count': 1,
 'meta:image-count': 2,
 'meta:object-count': 3,
 'meta:page-count': 4,
 'meta:paragraph-count': 5,
 'meta:word-count': 6,
 'meta:character-count': 7,
 'meta:non-whitespace-character-count':3}

subject property writable

subject: str | None

Get or set the subject of a document .

Return: str (or None if inexistant)

template property

template: MetaTemplate | None

Get the MetaTemplate element or None.

title property writable

title: str | None

Get or set the title of the document .

Return: str (or None if inexistant)

user_defined_metadata property writable

user_defined_metadata: dict[str, Any]

Get or set all additional user-defined metadata for a document.

Return a dict of str/value mapping.

Value types can be: Decimal, date, time, boolean or str.

__init__

__init__(*args: Any, **kwargs: Any) -> None
Source code in odfdo/meta.py
52
53
54
def __init__(self, *args: Any, **kwargs: Any) -> None:
    super().__init__(*args, **kwargs)
    self._generator_modified: bool = False

as_dict

as_dict(full: bool = False) -> dict[str, Any]

Return the metadata of the document as a Python dict.

if ‘full’ is True, export also the keys with no value assigned.

Arguments:

full -- boolean
Source code in odfdo/meta.py
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
def as_dict(self, full: bool = False) -> dict[str, Any]:
    """Return the metadata of the document as a Python dict.

    if 'full' is True, export also the keys with no value assigned.

    Arguments:

        full -- boolean
    """

    def _stats() -> dict[str, int]:
        doc_stats = self.statistic
        if doc_stats is None:
            msg = "Document statitics not found"
            raise LookupError(msg)
        return {
            key: doc_stats.get(key, 0)
            for key in (
                "meta:table-count",
                "meta:image-count",
                "meta:object-count",
                "meta:page-count",
                "meta:paragraph-count",
                "meta:word-count",
                "meta:character-count",
                "meta:non-whitespace-character-count",
            )
        }

    def _meta_template() -> dict[str, Any] | None:
        template = self.template
        if template is None:
            return None
        return template.as_dict()

    def _meta_reload() -> dict[str, Any] | None:
        reload = self.auto_reload
        if reload is None:
            return None
        return reload.as_dict()

    def _meta_behaviour() -> dict[str, Any] | None:
        behaviour = self.hyperlink_behaviour
        if behaviour is None:
            return None
        return behaviour.as_dict()

    meta_data: dict[str, Any] = {
        "meta:creation-date": self.creation_date,
        "dc:date": self.date,
        "meta:editing-duration": self.editing_duration,
        "meta:editing-cycles": self.editing_cycles,
        "meta:document-statistic": _stats(),
        "meta:generator": self.generator,
        "dc:title": self.title,
        "dc:description": self.description,
        "dc:creator": self.creator,
        "meta:keyword": self.keyword,
        "dc:subject": self.subject,
        "dc:language": self.language,
        "meta:initial-creator": self.initial_creator,
        "meta:print-date": self.print_date,
        "meta:printed-by": self.printed_by,
        "meta:template": _meta_template(),
        "meta:auto-reload": _meta_reload(),
        "meta:hyperlink-behaviour": _meta_behaviour(),
        "meta:user-defined": self._user_defined_metadata_list(),
    }

    if not full:
        meta_data = {key: val for key, val in meta_data.items() if val}
    return meta_data

as_json

as_json(full: bool = False) -> str

Return the metadata of the document as a JSON string.

if ‘full’ is True, export also the keys with no value assigned.

Arguments:

full -- boolean
Source code in odfdo/meta.py
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
def as_json(self, full: bool = False) -> str:
    """Return the metadata of the document as a JSON string.

    if 'full' is True, export also the keys with no value assigned.

    Arguments:

        full -- boolean
    """
    return json.dumps(
        self._as_json_dict(full=full),
        ensure_ascii=False,
        sort_keys=False,
        indent=4,
    )

as_text

as_text() -> str

Return meta informations as text, with some formatting for printing.

Source code in odfdo/meta.py
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
def as_text(self) -> str:
    """Return meta informations as text, with some formatting for printing."""
    data = self._as_json_dict(full=False)
    result: list[str] = []

    def _append_info(name: str, key: str) -> None:
        value = data.get(key)
        if value:
            result.append(f"{name}: {value}")

    _append_info("Title", "dc:title")
    _append_info("Subject", "dc:subject")
    _append_info("Description", "dc:description")
    _append_info("Language", "dc:language")
    _append_info("Modification date", "dc:date")
    _append_info("Creation date", "meta:creation-date")
    _append_info("Creator", "dc:creator")
    _append_info("Initial creator", "meta:initial-creator")
    _append_info("Keyword", "meta:keyword")
    _append_info("Editing duration", "meta:editing-duration")
    _append_info("Editing cycles", "meta:editing-cycles")
    _append_info("Generator", "meta:generator")

    result.append("Statistic:")
    statistic = data.get("meta:document-statistic", {})
    if statistic:
        for name, value in statistic.items():
            result.append(f"  - {name[5:].replace('-', ' ').capitalize()}: {value}")

    result.append("User defined metadata:")
    user_metadata = data.get("meta:user-defined", [])
    for item in user_metadata:
        result.append(f"  - {item['meta:name']}: {item['value']}")

    return "\n".join(result)

clear_user_defined_metadata

clear_user_defined_metadata() -> None

Remove all user-defined metadata.

Source code in odfdo/meta.py
794
795
796
797
798
799
800
801
def clear_user_defined_metadata(self) -> None:
    """Remove all user-defined metadata."""
    while True:
        element = self.get_element("//meta:user-defined")
        if isinstance(element, Element):
            element.delete()
            continue
        break

from_dict

from_dict(data: dict[str, Any]) -> None

Set the metadata of the document from a Python dict.

The loaded metadata are merged with the existing metadata. If the new value of a key is None: - meta:creation-date: use current time, - dc:date: use creation date, - meta:editing-duration: set to zero, - meta:editing-cycles: set to 1, - meta:generator: use odfdo generator string. Other keys (not mandatory keys): remove key/value pair from metadata.

Arguments:

data -- dict of metadata.
Source code in odfdo/meta.py
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
def from_dict(self, data: dict[str, Any]) -> None:
    """Set the metadata of the document from a Python dict.

    The loaded metadata are merged with the existing metadata.
    If the new value of a key is None:
        - meta:creation-date: use current time,
        - dc:date: use creation date,
        - meta:editing-duration: set to zero,
        - meta:editing-cycles: set to 1,
        - meta:generator: use odfdo generator string.
        Other keys (not mandatory keys): remove key/value pair from
        metadata.

    Arguments:

        data -- dict of metadata.
    """

    def _value_delete(key: str) -> Any:
        value = data.get(key, current.get(key))
        if value is None:
            child = self.get_element(f"//{key}")
            if child is not None:
                self.delete_element(child)
        return value

    current = self.as_dict()
    new_stats = self._complete_stats(
        current["meta:document-statistic"],
        data.get("meta:document-statistic", {}),
    )
    # mandatory
    self.statistic = new_stats

    key = "meta:creation-date"
    creation_date = data.get(key, current.get(key))
    if creation_date is None:
        creation_date = datetime.now().replace(microsecond=0)
    self.creation_date = creation_date

    key = "dc:date"
    dc_date = data.get(key, current.get(key))
    if dc_date is None:
        dc_date = creation_date
    if dc_date < creation_date:
        dc_date = creation_date
    max_editing = dc_date - creation_date
    self.date = dc_date

    key = "meta:editing-duration"
    editing_duration = data.get(key, current.get(key))
    if editing_duration is None:
        editing_duration = timedelta(0)
    if editing_duration > max_editing:
        editing_duration = max_editing
    self.editing_duration = editing_duration

    key = "meta:editing-cycles"
    editing_cycles = data.get(key, current.get(key))
    if editing_cycles is None:
        editing_cycles = 1
    self.editing_cycles = max(editing_cycles, 1)

    key = "meta:generator"
    generator = data.get(key, current.get(key))
    if not generator:
        generator = GENERATOR
    self.generator = generator

    # not mandatory values
    # - if not in imported: keep original value
    # - if imported is None: erase original value

    key = "dc:title"
    value = _value_delete(key)
    if value is not None:
        self.title = value

    key = "dc:description"
    value = _value_delete(key)
    if value is not None:
        self.description = value

    key = "dc:creator"
    value = _value_delete(key)
    if value is not None:
        self.creator = value

    key = "meta:keyword"
    value = _value_delete(key)
    if value is not None:
        self.keyword = value

    key = "dc:subject"
    value = _value_delete(key)
    if value is not None:
        self.subject = value

    key = "dc:language"
    value = _value_delete(key)
    if value is not None:
        self.language = value

    key = "meta:initial-creator"
    value = _value_delete(key)
    if value is not None:
        self.initial_creator = value

    key = "meta:print-date"
    value = _value_delete(key)
    if value is not None:
        self.print_date = value

    key = "meta:printed-by"
    value = _value_delete(key)
    if value is not None:
        self.printed_by = value

    key = "meta:template"
    value = _value_delete(key)
    if value is not None:
        value = value.as_dict()
        self.set_template(
            date=value["meta:date"],
            href=value["xlink:href"],
            title=value["xlink:title"],
        )

    key = "meta:auto-reload"
    value = _value_delete(key)
    if value is not None:
        value = value.as_dict()
        self.set_auto_reload(
            delay=value["meta:delay"],
            href=value["xlink:href"],
        )

    key = "meta:hyperlink-behaviour"
    value = _value_delete(key)
    if value is not None:
        value = value.as_dict()
        self.set_hyperlink_behaviour(
            target_frame_name=value["office:target-frame-name"],
            show=value["xlink:show"],
        )

    key = "meta:user-defined"
    if key in data:
        value = data[key]
        if value is None:
            self.clear_user_defined_metadata()
        else:
            current = self._user_defined_metadata_list()
            current_dict = {d["meta:name"]: d for d in current}
            current_value = {d["meta:name"]: d for d in value}
            current_dict.update(current_value)
            new_ud = {
                v["meta:name"]: v["value"]
                for v in current_dict.values()
                if v["value"] is not None
            }
            self.user_defined_metadata = new_ud

get_auto_reload

get_auto_reload() -> MetaAutoReload | None

Get the MetaAutoReload element or None.

Source code in odfdo/meta.py
356
357
358
359
360
361
def get_auto_reload(self) -> MetaAutoReload | None:
    """Get the MetaAutoReload <meta:auto-reload> element or None."""
    element = self.get_element("//meta:auto-reload")
    if element is None:
        return None
    return element

get_creation_date

get_creation_date() -> datetime | None

Get the creation date of the document.

(Also available as “self.creation_date” property.)

Return: datetime (or None if inexistant)

Source code in odfdo/meta.py
259
260
261
262
263
264
265
266
267
268
269
270
def get_creation_date(self) -> datetime | None:
    """Get the creation date of the document.

    (Also available as "self.creation_date" property.)

    Return: datetime (or None if inexistant)
    """
    element = self.get_element("//meta:creation-date")
    if element is None:
        return None
    creation_date = element.text
    return DateTime.decode(creation_date)

get_description

get_description() -> str | None

Get the description of the document. Also known as comments.

(Also available as “self.description” property.)

Return: str (or None if inexistant)

Source code in odfdo/meta.py
102
103
104
105
106
107
108
109
110
111
112
def get_description(self) -> str | None:
    """Get the description of the document. Also known as comments.

    (Also available as "self.description" property.)

    Return: str (or None if inexistant)
    """
    element = self.get_element("//dc:description")
    if element is None:
        return None
    return element.text

get_editing_cycles

get_editing_cycles() -> int | None

Get the number of times the document was edited, as reported by the generator.

(Also available as “self.editing_cycles” property.)

Return: int (or None if inexistant)

Source code in odfdo/meta.py
559
560
561
562
563
564
565
566
567
568
569
570
571
def get_editing_cycles(self) -> int | None:
    """Get the number of times the document was edited, as reported by
    the generator.

    (Also available as "self.editing_cycles" property.)

    Return: int (or None if inexistant)
    """
    element = self.get_element("//meta:editing-cycles")
    if element is None:
        return None
    cycles = element.text
    return int(cycles)

get_editing_duration

get_editing_duration() -> timedelta | None

Get the time the document was edited, as reported by the generator.

(Also available as “self.editing_duration” property.)

Return: timedelta (or None if inexistant)

Source code in odfdo/meta.py
515
516
517
518
519
520
521
522
523
524
525
526
527
def get_editing_duration(self) -> timedelta | None:
    """Get the time the document was edited, as reported by the
    generator.

    (Also available as "self.editing_duration" property.)

    Return: timedelta (or None if inexistant)
    """
    element = self.get_element("//meta:editing-duration")
    if element is None:
        return None
    duration = element.text
    return Duration.decode(duration)

get_generator

get_generator() -> str | None

Get the signature of the software that generated this document.

(Also available as “self.generator” property.)

Return: str (or None if inexistant)

Example::

>>> document.meta.get_generator()
KOffice/2.0.0
Source code in odfdo/meta.py
635
636
637
638
639
640
641
642
643
644
645
646
647
def get_generator(self) -> str | None:
    """Get the signature of the software that generated this document.

    (Also available as "self.generator" property.)

    Return: str (or None if inexistant)

    Example::

        >>> document.meta.get_generator()
        KOffice/2.0.0
    """
    return self.generator
get_hyperlink_behaviour() -> MetaHyperlinkBehaviour | None

Get the MetaHyperlinkBehaviour element or None.

Source code in odfdo/meta.py
376
377
378
379
380
381
def get_hyperlink_behaviour(self) -> MetaHyperlinkBehaviour | None:
    """Get the MetaHyperlinkBehaviour <meta:hyperlink-behaviour> element or None."""
    element = self.get_element("//meta:hyperlink-behaviour")
    if element is None:
        return None
    return element

get_initial_creator

get_initial_creator() -> str | None

Get the first creator of the document.

(Also available as “self.initial_creator” property.)

Return: str (or None if inexistant)

Example::

>>> document.meta.get_initial_creator()
Unknown
Source code in odfdo/meta.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def get_initial_creator(self) -> str | None:
    """Get the first creator of the document.

    (Also available as "self.initial_creator" property.)

    Return: str (or None if inexistant)

    Example::

        >>> document.meta.get_initial_creator()
        Unknown
    """
    element = self.get_element("//meta:initial-creator")
    if element is None:
        return None
    return element.text

get_keywords

get_keywords() -> str | None

Get the keywords of the document. Return the field as-is, without any assumption on the keyword separator.

(Also available as “self.keyword” and “self.keywords” property.)

Return: str (or None if inexistant)

Source code in odfdo/meta.py
471
472
473
474
475
476
477
478
479
480
481
482
def get_keywords(self) -> str | None:
    """Get the keywords of the document. Return the field as-is, without
    any assumption on the keyword separator.

    (Also available as "self.keyword" and "self.keywords" property.)

    Return: str (or None if inexistant)
    """
    element = self.get_element("//meta:keyword")
    if element is None:
        return None
    return element.text

get_language

get_language() -> str | None

Get the default language of the document.

(Also available as “self.language” property.)

Return: str (or None if inexistant)

Example::

>>> document.meta.get_language()
fr-FR
Source code in odfdo/meta.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def get_language(self) -> str | None:
    """Get the default language of the document.

    (Also available as "self.language" property.)

    Return: str (or None if inexistant)

    Example::

        >>> document.meta.get_language()
        fr-FR
    """
    element = self.get_element("//dc:language")
    if element is None:
        return None
    return element.text

get_meta_body

get_meta_body() -> Element
Source code in odfdo/meta.py
56
57
def get_meta_body(self) -> Element:
    return self.get_element("//office:meta")

get_statistic

get_statistic() -> dict[str, int] | None

Get the statistics about a document.

(Also available as “self.statistic” property.)

Return: dict (or None if inexistant)

Example::

>>> document.get_statistic():
{'meta:table-count': 1,
 'meta:image-count': 2,
 'meta:object-count': 3,
 'meta:page-count': 4,
 'meta:paragraph-count': 5,
 'meta:word-count': 6,
 'meta:character-count': 7,
 'meta:non-whitespace-character-count': 3}
Source code in odfdo/meta.py
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
def get_statistic(self) -> dict[str, int] | None:
    """Get the statistics about a document.

    (Also available as "self.statistic" property.)

    Return: dict (or None if inexistant)

    Example::

        >>> document.get_statistic():
        {'meta:table-count': 1,
         'meta:image-count': 2,
         'meta:object-count': 3,
         'meta:page-count': 4,
         'meta:paragraph-count': 5,
         'meta:word-count': 6,
         'meta:character-count': 7,
         'meta:non-whitespace-character-count': 3}
    """
    element = self.get_element("//meta:document-statistic")
    if element is None:
        return None
    statistic = {}
    for key, value in element.attributes.items():
        statistic[to_str(key)] = int(value)
    return statistic

get_subject

get_subject() -> str | None

Get the subject of the document.

(Also available as “self.subject” property.)

Return: str (or None if inexistant)

Source code in odfdo/meta.py
146
147
148
149
150
151
152
153
154
155
156
def get_subject(self) -> str | None:
    """Get the subject of the document.

    (Also available as "self.subject" property.)

    Return: str (or None if inexistant)
    """
    element = self.get_element("//dc:subject")
    if element is None:
        return None
    return element.text

get_template

get_template() -> MetaTemplate | None

Get the MetaTemplate element or None.

Source code in odfdo/meta.py
331
332
333
334
335
336
def get_template(self) -> MetaTemplate | None:
    """Get the MetaTemplate <meta:template> element or None."""
    element = self.get_element("//meta:template")
    if element is None:
        return None
    return element

get_title

get_title() -> str | None

Get the title of the document.

This is not the first heading but the title metadata.

(Also available as “self.title” property.)

Return: str (or None if inexistant)

Source code in odfdo/meta.py
59
60
61
62
63
64
65
66
67
68
69
70
71
def get_title(self) -> str | None:
    """Get the title of the document.

    This is not the first heading but the title metadata.

    (Also available as "self.title" property.)

    Return: str (or None if inexistant)
    """
    element = self.get_element("//dc:title")
    if element is None:
        return None
    return element.text

get_user_defined_metadata

get_user_defined_metadata() -> dict[str, Any]

Get all additional user-defined metadata for a document.

(Also available as “self.user_defined_metadata” property.)

Return a dict of str/value mapping.

Value types can be: Decimal, date, time, boolean or str.

Source code in odfdo/meta.py
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
def get_user_defined_metadata(self) -> dict[str, Any]:
    """Get all additional user-defined metadata for a document.

    (Also available as "self.user_defined_metadata" property.)

    Return a dict of str/value mapping.

    Value types can be: Decimal, date, time, boolean or str.
    """
    result: dict[str, Any] = {}
    for item in self.get_elements("//meta:user-defined"):
        if not isinstance(item, Element):
            continue
        # Read the values
        name = item.get_attribute_string("meta:name")
        if name is None:
            continue
        value = self._get_meta_value(item)
        result[name] = value
    return result

get_user_defined_metadata_of_name

get_user_defined_metadata_of_name(
    keyname: str,
) -> dict[str, Any] | None

Return the content of the user defined metadata of that name. Return None if no name matchs or a dic of fields.

Arguments:

name -- string, name (meta:name content)
Source code in odfdo/meta.py
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
def get_user_defined_metadata_of_name(self, keyname: str) -> dict[str, Any] | None:
    """Return the content of the user defined metadata of that name.
    Return None if no name matchs or a dic of fields.

    Arguments:

        name -- string, name (meta:name content)
    """
    result = {}
    found = False
    for item in self.get_elements("//meta:user-defined"):
        if not isinstance(item, Element):
            continue
        # Read the values
        name = item.get_attribute("meta:name")
        if name == keyname:
            found = True
            break
    if not found:
        return None
    result["name"] = name
    value, value_type, text = self._get_meta_value(item, full=True)  # type: ignore
    result["value"] = value
    result["value_type"] = value_type
    result["text"] = text
    return result

set_auto_reload

set_auto_reload(delay: timedelta, href: str = '') -> None

Set the MetaAutoReload element.

Source code in odfdo/meta.py
368
369
370
371
372
373
374
def set_auto_reload(self, delay: timedelta, href: str = "") -> None:
    """Set the MetaAutoReload <meta:auto-reload> element."""
    autoreload = MetaAutoReload(delay=delay, href=href)
    current = self.auto_reload
    if isinstance(current, MetaAutoReload):
        current.delete()
    self.get_meta_body().append(autoreload)

set_creation_date

set_creation_date(date: datetime | None = None) -> None

Set the creation date of the document.

If provided datetime is None, use current time.

(Also available as “self.creation_date” property.)

Arguments:

date -- datetime
Source code in odfdo/meta.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def set_creation_date(self, date: datetime | None = None) -> None:
    """Set the creation date of the document.

    If provided datetime is None, use current time.

    (Also available as "self.creation_date" property.)

    Arguments:

        date -- datetime
    """
    element = self.get_element("//meta:creation-date")
    if element is None:
        element = Element.from_tag("meta:creation-date")
        self.get_meta_body().append(element)
    if date is None:
        date = datetime.now()
    element.text = DateTime.encode(date)

set_description

set_description(description: str) -> None

Set the description of the document. Also known as comments.

(Also available as “self.description” property.)

Arguments:

description -- str
Source code in odfdo/meta.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def set_description(self, description: str) -> None:
    """Set the description of the document. Also known as comments.

    (Also available as "self.description" property.)

    Arguments:

        description -- str
    """
    element = self.get_element("//dc:description")
    if element is None:
        element = Element.from_tag("dc:description")
        self.get_meta_body().append(element)
    element.text = description

set_editing_cycles

set_editing_cycles(cycles: int) -> None

Set the number of times the document was edited.

(Also available as “self.editing_cycles” property.)

Arguments:

cycles -- int
Source code in odfdo/meta.py
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
def set_editing_cycles(self, cycles: int) -> None:
    """Set the number of times the document was edited.

    (Also available as "self.editing_cycles" property.)

    Arguments:

        cycles -- int
    """
    if not isinstance(cycles, int):
        raise TypeError("cycles must be an int")
    if cycles < 1:
        raise ValueError("cycles must be a positive int")
    element = self.get_element("//meta:editing-cycles")
    if element is None:
        element = Element.from_tag("meta:editing-cycles")
        self.get_meta_body().append(element)
    element.text = str(cycles)

set_editing_duration

set_editing_duration(duration: timedelta) -> None

Set the time the document was edited.

(Also available as “self.editing_duration” property.)

Arguments:

duration -- timedelta
Source code in odfdo/meta.py
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
def set_editing_duration(self, duration: timedelta) -> None:
    """Set the time the document was edited.

    (Also available as "self.editing_duration" property.)

    Arguments:

        duration -- timedelta
    """
    if not isinstance(duration, timedelta):
        raise TypeError("duration must be a timedelta")
    element = self.get_element("//meta:editing-duration")
    if element is None:
        element = Element.from_tag("meta:editing-duration")
        self.get_meta_body().append(element)
    element.text = Duration.encode(duration)

set_generator

set_generator(generator: str) -> None

Set the signature of the software that generated this document.

(Also available as “self.generator” property.)

Arguments:

generator -- str

Example::

>>> document.meta.set_generator("Odfdo experiment")
Source code in odfdo/meta.py
649
650
651
652
653
654
655
656
657
658
659
660
661
662
def set_generator(self, generator: str) -> None:
    """Set the signature of the software that generated this document.

    (Also available as "self.generator" property.)

    Arguments:

        generator -- str

    Example::

        >>> document.meta.set_generator("Odfdo experiment")
    """
    self.generator = generator

set_generator_default

set_generator_default() -> None

Set the signature of the software that generated this document to ourself.

Example::

>>> document.meta.set_generator_default()
Source code in odfdo/meta.py
664
665
666
667
668
669
670
671
672
673
def set_generator_default(self) -> None:
    """Set the signature of the software that generated this document
    to ourself.

    Example::

        >>> document.meta.set_generator_default()
    """
    if not self._generator_modified:
        self.generator = GENERATOR
set_hyperlink_behaviour(
    target_frame_name: str = "_blank", show: str = "replace"
) -> None

Set the MetaHyperlinkBehaviour element.

Source code in odfdo/meta.py
388
389
390
391
392
393
394
395
396
397
398
399
400
def set_hyperlink_behaviour(
    self,
    target_frame_name: str = "_blank",
    show: str = "replace",
) -> None:
    """Set the MetaHyperlinkBehaviour <meta:hyperlink-behaviour> element."""
    behaviour = MetaHyperlinkBehaviour(
        target_frame_name=target_frame_name, show=show
    )
    current = self.hyperlink_behaviour
    if isinstance(current, MetaHyperlinkBehaviour):
        current.delete()
    self.get_meta_body().append(behaviour)

set_initial_creator

set_initial_creator(creator: str) -> None

Set the first creator of the document.

(Also available as “self.initial_creator” property.)

Arguments:

creator -- str

Example::

>>> document.meta.set_initial_creator("Plato")
Source code in odfdo/meta.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def set_initial_creator(self, creator: str) -> None:
    """Set the first creator of the document.

    (Also available as "self.initial_creator" property.)

    Arguments:

        creator -- str

    Example::

        >>> document.meta.set_initial_creator("Plato")
    """
    element = self.get_element("//meta:initial-creator")
    if element is None:
        element = Element.from_tag("meta:initial-creator")
        self.get_meta_body().append(element)
    element.text = creator

set_keywords

set_keywords(keywords: str) -> None

Set the keywords of the document. Although the name is plural, a str string is required, so join your list first.

(Also available as “self.keyword” and “self.keywords” property.)

Arguments:

keywords -- str
Source code in odfdo/meta.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def set_keywords(self, keywords: str) -> None:
    """Set the keywords of the document. Although the name is plural, a
    str string is required, so join your list first.

    (Also available as "self.keyword" and "self.keywords" property.)

    Arguments:

        keywords -- str
    """
    element = self.get_element("//meta:keyword")
    if element is None:
        element = Element.from_tag("meta:keyword")
        self.get_meta_body().append(element)
    element.text = keywords

set_language

set_language(language: str) -> None

Set the default language of the document.

(Also available as “self.language” property.)

Arguments:

language -- str

Example::

>>> document.meta.set_language('fr-FR')
Source code in odfdo/meta.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def set_language(self, language: str) -> None:
    """Set the default language of the document.

    (Also available as "self.language" property.)

    Arguments:

        language -- str

    Example::

        >>> document.meta.set_language('fr-FR')
    """
    language = str(language)
    if not self._is_RFC3066(language):
        raise TypeError(
            'Language must be "xx" lang or "xx-YY" lang-COUNTRY code (RFC3066)'
        )
    element = self.get_element("//dc:language")
    if element is None:
        element = Element.from_tag("dc:language")
        self.get_meta_body().append(element)
    element.text = language

set_statistic

set_statistic(statistic: dict[str, int]) -> None

Set the statistics about a document.

(Also available as “self.statistic” property.)

Arguments:

statistic -- dict

Example::

>>> statistic = {'meta:table-count': 1,
                 'meta:image-count': 2,
                 'meta:object-count': 3,
                 'meta:page-count': 4,
                 'meta:paragraph-count': 5,
                 'meta:word-count': 6,
                 'meta:character-count': 7,
                 'meta:non-whitespace-character-count': 3}
>>> document.meta.set_statistic(statistic)
Source code in odfdo/meta.py
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
def set_statistic(self, statistic: dict[str, int]) -> None:
    """Set the statistics about a document.

    (Also available as "self.statistic" property.)

    Arguments:

        statistic -- dict

    Example::

        >>> statistic = {'meta:table-count': 1,
                         'meta:image-count': 2,
                         'meta:object-count': 3,
                         'meta:page-count': 4,
                         'meta:paragraph-count': 5,
                         'meta:word-count': 6,
                         'meta:character-count': 7,
                         'meta:non-whitespace-character-count': 3}
        >>> document.meta.set_statistic(statistic)
    """
    if not isinstance(statistic, dict):
        raise TypeError("Statistic must be a dict")
    element = self.get_element("//meta:document-statistic")
    for key, value in statistic.items():
        try:
            ivalue = int(value)
        except ValueError as e:
            raise TypeError("Statistic value must be a int") from e
        element.set_attribute(to_str(key), str(ivalue))

set_subject

set_subject(subject: str) -> None

Set the subject of the document.

(Also available as “self.subject” property.)

Arguments:

subject -- str
Source code in odfdo/meta.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def set_subject(self, subject: str) -> None:
    """Set the subject of the document.

    (Also available as "self.subject" property.)

    Arguments:

        subject -- str
    """
    element = self.get_element("//dc:subject")
    if element is None:
        element = Element.from_tag("dc:subject")
        self.get_meta_body().append(element)
    element.text = subject

set_template

set_template(
    date: datetime | None = None,
    href: str = "",
    title: str = "",
) -> None

Set the MetaTemplate element.

Source code in odfdo/meta.py
343
344
345
346
347
348
349
350
351
352
353
354
def set_template(
    self,
    date: datetime | None = None,
    href: str = "",
    title: str = "",
) -> None:
    """Set the MetaTemplate <meta:template> element."""
    template = MetaTemplate(date=date, href=href, title=title)
    current = self.template
    if isinstance(current, MetaTemplate):
        current.delete()
    self.get_meta_body().append(template)

set_title

set_title(title: str) -> None

Set the title of the document.

This is not the first heading but the title metadata.

(Also available as “self.title” property.)

Arguments:

title -- str
Source code in odfdo/meta.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def set_title(self, title: str) -> None:
    """Set the title of the document.

    This is not the first heading but the title metadata.

    (Also available as "self.title" property.)

    Arguments:

        title -- str
    """
    element = self.get_element("//dc:title")
    if element is None:
        element = Element.from_tag("dc:title")
        self.get_meta_body().append(element)
    element.text = title

set_user_defined_metadata

set_user_defined_metadata(name: str, value: Any) -> None
Source code in odfdo/meta.py
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
def set_user_defined_metadata(self, name: str, value: Any) -> None:
    if isinstance(value, bool):
        value_type = "boolean"
        value = "true" if value else "false"
    elif isinstance(value, (int, float, Decimal)):
        value_type = "float"
        value = str(value)
    elif isinstance(value, dtdate):
        value_type = "date"
        value = str(Date.encode(value))
    elif isinstance(value, datetime):
        value_type = "date"
        value = str(DateTime.encode(value))
    elif isinstance(value, str):
        value_type = "string"
    elif isinstance(value, timedelta):
        value_type = "time"
        value = str(Duration.encode(value))
    else:
        raise TypeError(f'unexpected type "{type(value)}" for value')
    # Already the same element ?
    for metadata in self.get_elements("//meta:user-defined"):
        if not isinstance(metadata, Element):
            continue
        if metadata.get_attribute("meta:name") == name:
            break
    else:
        metadata = Element.from_tag("meta:user-defined")
        metadata.set_attribute("meta:name", name)
        self.get_meta_body().append(metadata)
    metadata.set_attribute("meta:value-type", value_type)
    metadata.text = value

MetaAutoReload

Bases: Element

Container for auto-reload properties, “meta:auto-reload”.

Methods:

Name Description
__init__

Container for auto-reload properties, “meta:auto-reload”.

as_dict

Return the MetaAutoReload attributes as a Python dict.

Attributes:

Name Type Description
actuate
delay
href
show
type
Source code in odfdo/meta_auto_reload.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class MetaAutoReload(Element):
    """Container for auto-reload properties, "meta:auto-reload"."""

    _tag = "meta:auto-reload"
    _properties: tuple[PropDef, ...] = (
        PropDef("delay", "meta:delay"),
        PropDef("actuate", "xlink:actuate"),
        PropDef("href", "xlink:href"),
        PropDef("show", "xlink:show"),
        PropDef("type", "xlink:type"),
    )

    def __init__(
        self,
        delay: timedelta | None = None,
        href: str = "",
        **kwargs: Any,
    ) -> None:
        """Container for auto-reload properties, "meta:auto-reload".

        The <meta:auto-reload> element specifies whether a document is
        reloaded or replaced by another document after a specified period
        of time has elapsed.

        Arguments:

            delay -- timedelta

            href -- str
        """
        super().__init__(**kwargs)

        self.actuate = "onLoad"
        self.show = "replace"
        self.type = "simple"
        if self._do_init:
            if not isinstance(delay, timedelta):
                raise TypeError("delay must be a timedelta")
            self.delay = Duration.encode(delay)
            self.href = href

    def __repr__(self) -> str:
        return (
            f"<{self.__class__.__name__} tag={self.tag} "
            f"href={self.href} delay={Duration.decode(self.delay)}>"
        )

    def __str__(self) -> str:
        return f"({self.href})"

    def as_dict(self) -> dict[str, Any]:
        """Return the MetaAutoReload attributes as a Python dict."""
        return {
            "meta:delay": self.delay,
            "xlink:actuate": self.actuate,
            "xlink:href": self.href,
            "xlink:show": self.show,
            "xlink:type": self.type,
        }

actuate instance-attribute

actuate = 'onLoad'

delay instance-attribute

delay = encode(delay)

href instance-attribute

href = href

show instance-attribute

show = 'replace'

type instance-attribute

type = 'simple'

__init__

__init__(
    delay: timedelta | None = None,
    href: str = "",
    **kwargs: Any,
) -> None

Container for auto-reload properties, “meta:auto-reload”.

The element specifies whether a document is reloaded or replaced by another document after a specified period of time has elapsed.

Arguments:

delay -- timedelta

href -- str
Source code in odfdo/meta_auto_reload.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(
    self,
    delay: timedelta | None = None,
    href: str = "",
    **kwargs: Any,
) -> None:
    """Container for auto-reload properties, "meta:auto-reload".

    The <meta:auto-reload> element specifies whether a document is
    reloaded or replaced by another document after a specified period
    of time has elapsed.

    Arguments:

        delay -- timedelta

        href -- str
    """
    super().__init__(**kwargs)

    self.actuate = "onLoad"
    self.show = "replace"
    self.type = "simple"
    if self._do_init:
        if not isinstance(delay, timedelta):
            raise TypeError("delay must be a timedelta")
        self.delay = Duration.encode(delay)
        self.href = href

as_dict

as_dict() -> dict[str, Any]

Return the MetaAutoReload attributes as a Python dict.

Source code in odfdo/meta_auto_reload.py
81
82
83
84
85
86
87
88
89
def as_dict(self) -> dict[str, Any]:
    """Return the MetaAutoReload attributes as a Python dict."""
    return {
        "meta:delay": self.delay,
        "xlink:actuate": self.actuate,
        "xlink:href": self.href,
        "xlink:show": self.show,
        "xlink:type": self.type,
    }

MetaHyperlinkBehaviour

Bases: Element

Container for hyperlink-behaviour properties, “meta:hyperlink-behaviour”.

Methods:

Name Description
__init__

Container for hyperlink-behaviour properties, “meta:hyperlink-behaviour”.

as_dict

Return the MetaHyperlinkBehaviour attributes as a Python dict.

Attributes:

Name Type Description
show
target_frame_name
Source code in odfdo/meta_hyperlink_behaviour.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class MetaHyperlinkBehaviour(Element):
    """Container for hyperlink-behaviour properties, "meta:hyperlink-behaviour"."""

    _tag = "meta:hyperlink-behaviour"
    _properties: tuple[PropDef, ...] = (
        PropDef("target_frame_name", "office:target-frame-name"),
        PropDef("show", "xlink:show"),
    )

    def __init__(
        self,
        target_frame_name: str = "_blank",
        show: str = "replace",
        **kwargs: Any,
    ) -> None:
        """
        Container for hyperlink-behaviour properties, "meta:hyperlink-behaviour".

        The "meta:hyperlink-behaviour" element specifies the default behavior
        for hyperlinks in a document.

        Arguments:

            target_frame_name -- str

            show -- str
        """
        super().__init__(**kwargs)

        if self._do_init:
            self.target_frame_name = target_frame_name
            self.show = show

    def __repr__(self) -> str:
        return (
            f"<{self.__class__.__name__} tag={self.tag} "
            f"target={self.target_frame_name} show={self.show}>"
        )

    def __str__(self) -> str:
        return f"({self.target_frame_name})"

    def as_dict(self) -> dict[str, Any]:
        """Return the MetaHyperlinkBehaviour attributes as a Python dict."""
        return {
            "office:target-frame-name": self.target_frame_name,
            "xlink:show": self.show,
        }

show instance-attribute

show = show

target_frame_name instance-attribute

target_frame_name = target_frame_name

__init__

__init__(
    target_frame_name: str = "_blank",
    show: str = "replace",
    **kwargs: Any,
) -> None

Container for hyperlink-behaviour properties, “meta:hyperlink-behaviour”.

The “meta:hyperlink-behaviour” element specifies the default behavior for hyperlinks in a document.

Arguments:

target_frame_name -- str

show -- str
Source code in odfdo/meta_hyperlink_behaviour.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def __init__(
    self,
    target_frame_name: str = "_blank",
    show: str = "replace",
    **kwargs: Any,
) -> None:
    """
    Container for hyperlink-behaviour properties, "meta:hyperlink-behaviour".

    The "meta:hyperlink-behaviour" element specifies the default behavior
    for hyperlinks in a document.

    Arguments:

        target_frame_name -- str

        show -- str
    """
    super().__init__(**kwargs)

    if self._do_init:
        self.target_frame_name = target_frame_name
        self.show = show

as_dict

as_dict() -> dict[str, Any]

Return the MetaHyperlinkBehaviour attributes as a Python dict.

Source code in odfdo/meta_hyperlink_behaviour.py
71
72
73
74
75
76
def as_dict(self) -> dict[str, Any]:
    """Return the MetaHyperlinkBehaviour attributes as a Python dict."""
    return {
        "office:target-frame-name": self.target_frame_name,
        "xlink:show": self.show,
    }

MetaTemplate

Bases: Element

Container for the meta template properties, “meta:template”.

Methods:

Name Description
__init__

Container for the meta template properties, “meta:template”.

as_dict

Return the MetaTemplate attributes as a Python dict.

Attributes:

Name Type Description
actuate
date
href
title
type
Source code in odfdo/meta_template.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class MetaTemplate(Element):
    """Container for the meta template properties, "meta:template"."""

    _tag = "meta:template"
    _properties: tuple[PropDef, ...] = (
        PropDef("date", "meta:date"),
        PropDef("actuate", "xlink:actuate"),
        PropDef("href", "xlink:href"),
        PropDef("title", "xlink:title"),
        PropDef("type", "xlink:type"),
    )

    def __init__(
        self,
        date: datetime | None = None,
        href: str = "",
        title: str = "",
        **kwargs: Any,
    ) -> None:
        """Container for the meta template properties, "meta:template".

        The <meta:template> element specifies a IRI for the document template
        that was used to create a document. The IRI is specified as an
        Xlink.

        Arguments:

            date -- datetime or None

            href -- str

            title -- str
        """
        super().__init__(**kwargs)

        self.actuate = "onRequest"
        self.type = "simple"
        if self._do_init:
            if date is None:
                date = datetime.now()
            self.date = DateTime.encode(date)
            self.href = href
            self.title = title

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} tag={self.tag} href={self.href}>"

    def __str__(self) -> str:
        if self.title:
            return f"[{self.title}]({self.href})"
        return f"({self.href})"

    def as_dict(self) -> dict[str, Any]:
        """Return the MetaTemplate attributes as a Python dict."""
        return {
            "meta:date": self.date,
            "xlink:actuate": self.actuate,
            "xlink:href": self.href,
            "xlink:title": self.title,
            "xlink:type": self.type,
        }

actuate instance-attribute

actuate = 'onRequest'

date instance-attribute

date = encode(date)

href instance-attribute

href = href

title instance-attribute

title = title

type instance-attribute

type = 'simple'

__init__

__init__(
    date: datetime | None = None,
    href: str = "",
    title: str = "",
    **kwargs: Any,
) -> None

Container for the meta template properties, “meta:template”.

The element specifies a IRI for the document template that was used to create a document. The IRI is specified as an Xlink.

Arguments:

date -- datetime or None

href -- str

title -- str
Source code in odfdo/meta_template.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    date: datetime | None = None,
    href: str = "",
    title: str = "",
    **kwargs: Any,
) -> None:
    """Container for the meta template properties, "meta:template".

    The <meta:template> element specifies a IRI for the document template
    that was used to create a document. The IRI is specified as an
    Xlink.

    Arguments:

        date -- datetime or None

        href -- str

        title -- str
    """
    super().__init__(**kwargs)

    self.actuate = "onRequest"
    self.type = "simple"
    if self._do_init:
        if date is None:
            date = datetime.now()
        self.date = DateTime.encode(date)
        self.href = href
        self.title = title

as_dict

as_dict() -> dict[str, Any]

Return the MetaTemplate attributes as a Python dict.

Source code in odfdo/meta_template.py
83
84
85
86
87
88
89
90
91
def as_dict(self) -> dict[str, Any]:
    """Return the MetaTemplate attributes as a Python dict."""
    return {
        "meta:date": self.date,
        "xlink:actuate": self.actuate,
        "xlink:href": self.href,
        "xlink:title": self.title,
        "xlink:type": self.type,
    }

NamedRange

Bases: Element

Named range of cells in a table, “table:named-range”.

Identifies inside the spreadsheet a range of cells of a table by a name and the name of the table.

Name Ranges have the following attributes:

name -- name of the named range

table_name -- name of the table

start -- first cell of the named range, tuple (x, y)

end -- last cell of the named range, tuple (x, y)

crange -- range of the named range, tuple (x, y, z, t)

usage -- None or str, usage of the named range.

Methods:

Name Description
__init__

Named range of cells in a table, “table:named-range”.

get_value

Shortcut to retrieve the value of the first cell of the named range.

get_values

Shortcut to retrieve the values of the cells of the named range. See

set_range

Set the range of the named range. Range can be either one cell

set_table_name

Set the name of the table of the Named Range. The name is mandatory.

set_usage

Set the usage of the Named Range. Usage can be None (default) or one

set_value

Shortcut to set the value of the first cell of the named range.

set_values

Shortcut to set the values of the cells of the named range.

Attributes:

Name Type Description
crange tuple[int, int, int, int]
end tuple[int, int]
name str | None

Get / set the name of the table.

start tuple[int, int]
table_name str
usage str | None
Source code in odfdo/named_range.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class NamedRange(Element):
    """Named range of cells in a table, "table:named-range".

    Identifies inside the spreadsheet
    a range of cells of a table by a name and the name of the table.

    Name Ranges have the following attributes:

        name -- name of the named range

        table_name -- name of the table

        start -- first cell of the named range, tuple (x, y)

        end -- last cell of the named range, tuple (x, y)

        crange -- range of the named range, tuple (x, y, z, t)

        usage -- None or str, usage of the named range.
    """

    _tag = "table:named-range"

    def __init__(
        self,
        name: str | None = None,
        crange: str | tuple | list | None = None,
        table_name: str | None = None,
        usage: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Named range of cells in a table, "table:named-range".

        Create a Named Range element. 'name' must contains only letters, digits
        and '_', and must not be like a coordinate as 'A1'. 'table_name' must be
        a correct table name (no "'" or "/" in it).

        Arguments:

             name -- str, name of the named range

             crange -- str or tuple of int, cell or area coordinate

             table_name -- str, name of the table

             usage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
        """
        super().__init__(**kwargs)
        self.usage: str | None = None
        self.table_name: str = ""
        self.start: tuple[int, int] = 0, 0
        self.end: tuple[int, int] = 0, 0
        self.crange: tuple[int, int, int, int] = 0, 0, 0, 0
        self.usage = None
        if self._do_init:
            self.name = name or ""
            self.table_name = table_name_check(table_name)
            self.set_range(crange or "")
            self.set_usage(usage)
        cell_range_address = self.get_attribute_string("table:cell-range-address") or ""
        if not cell_range_address:
            return
        self.usage = self.get_attribute_string("table:range-usable-as")
        name_range = cell_range_address.replace("$", "")
        name, crange = name_range.split(".", 1)
        if name.startswith("'") and name.endswith("'"):
            name = name[1:-1]
        self.table_name = name
        crange = crange.replace(".", "")
        self._set_range(crange)

    def set_usage(self, usage: str | None = None) -> None:
        """Set the usage of the Named Range. Usage can be None (default) or one
        of :

            - 'print-range'
            - 'filter'
            - 'repeat-column'
            - 'repeat-row'

        Arguments:

            usage -- None or str
        """
        if usage is not None:
            usage = usage.strip().lower()
            if usage not in ("print-range", "filter", "repeat-column", "repeat-row"):
                usage = None
        if usage is None:
            with contextlib.suppress(KeyError):
                self.del_attribute("table:range-usable-as")
            self.usage = None
        else:
            self.set_attribute("table:range-usable-as", usage)
            self.usage = usage

    @property
    def name(self) -> str | None:
        """Get / set the name of the table."""
        return self.get_attribute_string("table:name")

    @name.setter
    def name(self, name: str) -> None:
        """Set the name of the Named Range. The name is mandatory, if a Named
        Range of the same name exists, it will be replaced. Name must contains
        only alphanumerics characters and '_', and can not be of a cell
        coordinates form like 'AB12'.

        Arguments:

            name -- str
        """
        name = name.strip()
        if not name:
            raise ValueError("Name required.")
        for x in name:
            if x in _forbidden_in_named_range():
                raise ValueError(f"Character forbidden '{x}' ")
        step = ""
        for x in name:
            if x in string.ascii_letters and step in ("", "A"):
                step = "A"
                continue
            elif step in ("A", "A1") and x in string.digits:
                step = "A1"
                continue
            else:
                step = ""
                break
        if step == "A1":
            raise ValueError("Name of the type 'ABC123' is not allowed.")
        with contextlib.suppress(Exception):
            # we are not on an inserted in a document.
            body = self.document_body
            named_range = body.get_named_range(name)  # type: ignore
            if named_range:
                named_range.delete()
        self.set_attribute("table:name", name)

    def set_table_name(self, name: str) -> None:
        """Set the name of the table of the Named Range. The name is mandatory.

        Arguments:

            name -- str
        """
        self.table_name = table_name_check(name)
        self._update_attributes()

    def _set_range(self, coord: tuple | list | str) -> None:
        digits = convert_coordinates(coord)
        if len(digits) == 4:
            x, y, z, t = digits
        else:
            x, y = digits
            z, t = digits
        self.start = x, y  # type: ignore
        self.end = z, t  # type: ignore
        self.crange = x, y, z, t  # type: ignore

    def set_range(
        self,
        crange: str | tuple[int, int] | tuple[int, int, int, int] | list[int],
    ) -> None:
        """Set the range of the named range. Range can be either one cell
        (like 'A1') or an area ('A1:B2'). It can be provided as an alpha numeric
        value like "A1:B2' or a tuple like (0, 0, 1, 1) or (0, 0).

        Arguments:

            crange -- str or tuple of int, cell or area coordinate
        """
        self._set_range(crange)
        self._update_attributes()

    def _update_attributes(self) -> None:
        self.set_attribute("table:base-cell-address", self._make_base_cell_address())
        self.set_attribute("table:cell-range-address", self._make_cell_range_address())

    def _make_base_cell_address(self) -> str:
        # assuming we got table_name and range
        if " " in self.table_name:
            name = f"'{self.table_name}'"
        else:
            name = self.table_name
        return f"${name}.${digit_to_alpha(self.start[0])}${self.start[1] + 1}"

    def _make_cell_range_address(self) -> str:
        # assuming we got table_name and range
        if " " in self.table_name:
            name = f"'{self.table_name}'"
        else:
            name = self.table_name
        if self.start == self.end:
            return self._make_base_cell_address()
        return (
            f"${name}.${digit_to_alpha(self.start[0])}${self.start[1] + 1}:"
            f".${digit_to_alpha(self.end[0])}${self.end[1] + 1}"
        )

    def get_values(
        self,
        cell_type: str | None = None,
        complete: bool = True,
        get_type: bool = False,
        flat: bool = False,
    ) -> list:
        """Shortcut to retrieve the values of the cells of the named range. See
        table.get_values() for the arguments description and return format.
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document.")
        table = body.get_table(name=self.table_name)
        if table is None:
            raise ValueError
        return table.get_values(self.crange, cell_type, complete, get_type, flat)

    def get_value(self, get_type: bool = False) -> Any:
        """Shortcut to retrieve the value of the first cell of the named range.
        See table.get_value() for the arguments description and return format.
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document.")
        table: Table | None = body.get_table(name=self.table_name)
        if table is None:
            raise ValueError
        return table.get_value(self.start, get_type)

    def set_values(
        self,
        values: list,
        style: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
    ) -> None:
        """Shortcut to set the values of the cells of the named range.
        See table.set_values() for the arguments description.
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document.")
        table = body.get_table(name=self.table_name)
        if table is None:
            raise ValueError
        table.set_values(
            values,
            coord=self.crange,
            style=style,
            cell_type=cell_type,
            currency=currency,
        )

    def set_value(
        self,
        value: Any,
        cell_type: str | None = None,
        currency: str | None = None,
        style: str | None = None,
    ) -> None:
        """Shortcut to set the value of the first cell of the named range.
        See table.set_value() for the arguments description.
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document.")
        table = body.get_table(name=self.table_name)
        if table is None:
            raise ValueError
        table.set_value(
            coord=self.start,
            value=value,
            cell_type=cell_type,
            currency=currency,
            style=style,
        )

crange instance-attribute

crange: tuple[int, int, int, int] = (0, 0, 0, 0)

end instance-attribute

end: tuple[int, int] = (0, 0)

name property writable

name: str | None

Get / set the name of the table.

start instance-attribute

start: tuple[int, int] = (0, 0)

table_name instance-attribute

table_name: str = name

usage instance-attribute

usage: str | None = get_attribute_string(
    "table:range-usable-as"
)

__init__

__init__(
    name: str | None = None,
    crange: str | tuple | list | None = None,
    table_name: str | None = None,
    usage: str | None = None,
    **kwargs: Any,
) -> None

Named range of cells in a table, “table:named-range”.

Create a Named Range element. ‘name’ must contains only letters, digits and ‘_’, and must not be like a coordinate as ‘A1’. ‘table_name’ must be a correct table name (no “’” or “/” in it).

Arguments:

 name -- str, name of the named range

 crange -- str or tuple of int, cell or area coordinate

 table_name -- str, name of the table

 usage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
Source code in odfdo/named_range.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def __init__(
    self,
    name: str | None = None,
    crange: str | tuple | list | None = None,
    table_name: str | None = None,
    usage: str | None = None,
    **kwargs: Any,
) -> None:
    """Named range of cells in a table, "table:named-range".

    Create a Named Range element. 'name' must contains only letters, digits
    and '_', and must not be like a coordinate as 'A1'. 'table_name' must be
    a correct table name (no "'" or "/" in it).

    Arguments:

         name -- str, name of the named range

         crange -- str or tuple of int, cell or area coordinate

         table_name -- str, name of the table

         usage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
    """
    super().__init__(**kwargs)
    self.usage: str | None = None
    self.table_name: str = ""
    self.start: tuple[int, int] = 0, 0
    self.end: tuple[int, int] = 0, 0
    self.crange: tuple[int, int, int, int] = 0, 0, 0, 0
    self.usage = None
    if self._do_init:
        self.name = name or ""
        self.table_name = table_name_check(table_name)
        self.set_range(crange or "")
        self.set_usage(usage)
    cell_range_address = self.get_attribute_string("table:cell-range-address") or ""
    if not cell_range_address:
        return
    self.usage = self.get_attribute_string("table:range-usable-as")
    name_range = cell_range_address.replace("$", "")
    name, crange = name_range.split(".", 1)
    if name.startswith("'") and name.endswith("'"):
        name = name[1:-1]
    self.table_name = name
    crange = crange.replace(".", "")
    self._set_range(crange)

get_value

get_value(get_type: bool = False) -> Any

Shortcut to retrieve the value of the first cell of the named range. See table.get_value() for the arguments description and return format.

Source code in odfdo/named_range.py
283
284
285
286
287
288
289
290
291
292
293
def get_value(self, get_type: bool = False) -> Any:
    """Shortcut to retrieve the value of the first cell of the named range.
    See table.get_value() for the arguments description and return format.
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document.")
    table: Table | None = body.get_table(name=self.table_name)
    if table is None:
        raise ValueError
    return table.get_value(self.start, get_type)

get_values

get_values(
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
    flat: bool = False,
) -> list

Shortcut to retrieve the values of the cells of the named range. See table.get_values() for the arguments description and return format.

Source code in odfdo/named_range.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def get_values(
    self,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
    flat: bool = False,
) -> list:
    """Shortcut to retrieve the values of the cells of the named range. See
    table.get_values() for the arguments description and return format.
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document.")
    table = body.get_table(name=self.table_name)
    if table is None:
        raise ValueError
    return table.get_values(self.crange, cell_type, complete, get_type, flat)

set_range

set_range(
    crange: str
    | tuple[int, int]
    | tuple[int, int, int, int]
    | list[int],
) -> None

Set the range of the named range. Range can be either one cell (like ‘A1’) or an area (‘A1:B2’). It can be provided as an alpha numeric value like “A1:B2’ or a tuple like (0, 0, 1, 1) or (0, 0).

Arguments:

crange -- str or tuple of int, cell or area coordinate
Source code in odfdo/named_range.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def set_range(
    self,
    crange: str | tuple[int, int] | tuple[int, int, int, int] | list[int],
) -> None:
    """Set the range of the named range. Range can be either one cell
    (like 'A1') or an area ('A1:B2'). It can be provided as an alpha numeric
    value like "A1:B2' or a tuple like (0, 0, 1, 1) or (0, 0).

    Arguments:

        crange -- str or tuple of int, cell or area coordinate
    """
    self._set_range(crange)
    self._update_attributes()

set_table_name

set_table_name(name: str) -> None

Set the name of the table of the Named Range. The name is mandatory.

Arguments:

name -- str
Source code in odfdo/named_range.py
204
205
206
207
208
209
210
211
212
def set_table_name(self, name: str) -> None:
    """Set the name of the table of the Named Range. The name is mandatory.

    Arguments:

        name -- str
    """
    self.table_name = table_name_check(name)
    self._update_attributes()

set_usage

set_usage(usage: str | None = None) -> None

Set the usage of the Named Range. Usage can be None (default) or one of :

- 'print-range'
- 'filter'
- 'repeat-column'
- 'repeat-row'

Arguments:

usage -- None or str
Source code in odfdo/named_range.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def set_usage(self, usage: str | None = None) -> None:
    """Set the usage of the Named Range. Usage can be None (default) or one
    of :

        - 'print-range'
        - 'filter'
        - 'repeat-column'
        - 'repeat-row'

    Arguments:

        usage -- None or str
    """
    if usage is not None:
        usage = usage.strip().lower()
        if usage not in ("print-range", "filter", "repeat-column", "repeat-row"):
            usage = None
    if usage is None:
        with contextlib.suppress(KeyError):
            self.del_attribute("table:range-usable-as")
        self.usage = None
    else:
        self.set_attribute("table:range-usable-as", usage)
        self.usage = usage

set_value

set_value(
    value: Any,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None

Shortcut to set the value of the first cell of the named range. See table.set_value() for the arguments description.

Source code in odfdo/named_range.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def set_value(
    self,
    value: Any,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None:
    """Shortcut to set the value of the first cell of the named range.
    See table.set_value() for the arguments description.
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document.")
    table = body.get_table(name=self.table_name)
    if table is None:
        raise ValueError
    table.set_value(
        coord=self.start,
        value=value,
        cell_type=cell_type,
        currency=currency,
        style=style,
    )

set_values

set_values(
    values: list,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None

Shortcut to set the values of the cells of the named range. See table.set_values() for the arguments description.

Source code in odfdo/named_range.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def set_values(
    self,
    values: list,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None:
    """Shortcut to set the values of the cells of the named range.
    See table.set_values() for the arguments description.
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document.")
    table = body.get_table(name=self.table_name)
    if table is None:
        raise ValueError
    table.set_values(
        values,
        coord=self.crange,
        style=style,
        cell_type=cell_type,
        currency=currency,
    )

Note

Bases: MDNote, Element

A note (footnote or endnote), “text:note”.

Either a footnote or a endnote element with the given text, optionally referencing it using the given note_id.

Methods:

Name Description
__init__

A note (footnote or endnote), “text:note”.

check_validity

Attributes:

Name Type Description
NOTE_CLASS ClassVar
citation str
note_body str
note_class
note_id
Source code in odfdo/note.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class Note(MDNote, Element):
    """A note (footnote or endnote), "text:note".

    Either a footnote or a endnote element with the given text,
    optionally referencing it using the given note_id.
    """

    _tag = "text:note"
    _properties = (
        PropDef("note_class", "text:note-class"),
        PropDef("note_id", "text:id"),
    )
    NOTE_CLASS: ClassVar = {"footnote", "endnote"}

    def __init__(
        self,
        note_class: str = "footnote",
        note_id: str | None = None,
        citation: str | None = None,
        body: str | None = None,
        **kwargs: Any,
    ) -> None:
        """A note (footnote or endnote), "text:note".

        Arguments:

            note_class -- 'footnote' or 'endnote'

            note_id -- str

            citation -- str

            body -- str or Element
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.insert(Element.from_tag("text:note-body"), position=0)
            self.insert(Element.from_tag("text:note-citation"), position=0)
            self.note_class = note_class
            if note_id is not None:
                self.note_id = note_id
            if citation is not None:
                self.citation = citation
            if body is not None:
                self.note_body = body

    @property
    def citation(self) -> str:
        note_citation = self.get_element("text:note-citation")
        if note_citation:
            return note_citation.text
        return ""

    @citation.setter
    def citation(self, text: str | None) -> None:
        note_citation = self.get_element("text:note-citation")
        if note_citation:
            note_citation.text = text

    @property
    def note_body(self) -> str:
        note_body = self.get_element("text:note-body")
        if note_body:
            return note_body.text_content
        return ""

    @note_body.setter
    def note_body(self, text_or_element: Element | str | None) -> None:
        note_body = self.get_element("text:note-body")
        if not note_body:
            return None
        if text_or_element is None:
            note_body.text_content = ""
        elif isinstance(text_or_element, str):
            note_body.text_content = text_or_element
        elif isinstance(text_or_element, Element):
            note_body.clear()
            note_body.append(text_or_element)
        else:
            raise TypeError(f'Unexpected type for body: "{type(text_or_element)}"')

    def check_validity(self) -> None:
        if not self.note_class or self.note_class not in self.NOTE_CLASS:
            raise ValueError('Note class must be "footnote" or "endnote"')
        if not self.note_id:
            raise ValueError("Note must have an id")
        if not self.citation:
            raise ValueError("Note must have a citation")
        if not self.note_body:
            pass

    def __str__(self) -> str:
        if self.citation:
            return f"{self.citation}. {self.note_body}"
        return self.note_body

NOTE_CLASS class-attribute instance-attribute

NOTE_CLASS: ClassVar = {'footnote', 'endnote'}

citation property writable

citation: str

note_body property writable

note_body: str

note_class instance-attribute

note_class = note_class

note_id instance-attribute

note_id = note_id

__init__

__init__(
    note_class: str = "footnote",
    note_id: str | None = None,
    citation: str | None = None,
    body: str | None = None,
    **kwargs: Any,
) -> None

A note (footnote or endnote), “text:note”.

Arguments:

note_class -- 'footnote' or 'endnote'

note_id -- str

citation -- str

body -- str or Element
Source code in odfdo/note.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def __init__(
    self,
    note_class: str = "footnote",
    note_id: str | None = None,
    citation: str | None = None,
    body: str | None = None,
    **kwargs: Any,
) -> None:
    """A note (footnote or endnote), "text:note".

    Arguments:

        note_class -- 'footnote' or 'endnote'

        note_id -- str

        citation -- str

        body -- str or Element
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.insert(Element.from_tag("text:note-body"), position=0)
        self.insert(Element.from_tag("text:note-citation"), position=0)
        self.note_class = note_class
        if note_id is not None:
            self.note_id = note_id
        if citation is not None:
            self.citation = citation
        if body is not None:
            self.note_body = body

check_validity

check_validity() -> None
Source code in odfdo/note.py
115
116
117
118
119
120
121
122
123
def check_validity(self) -> None:
    if not self.note_class or self.note_class not in self.NOTE_CLASS:
        raise ValueError('Note class must be "footnote" or "endnote"')
    if not self.note_id:
        raise ValueError("Note must have an id")
    if not self.citation:
        raise ValueError("Note must have a citation")
    if not self.note_body:
        pass

ParaMixin

Mixin for Paragraph methods.

Methods:

Name Description
append
append_plain_text

Append plain text to the paragraph, replacing ,

insert_annotation

Insert an annotation, at the position defined by the regex (before,

insert_annotation_end

Insert an annotation end tag for an existing annotation. If some end

insert_note
insert_reference

Create and insert a reference to a content marked by a reference

insert_variable
remove_link

Send back a copy of the element (not a clone), with the sub links

remove_links

Send back a copy of the element, without links tags.

remove_span

Send back a copy of the element, the spans (not a clone) removed.

remove_spans

Send back a copy of the element, without span styles.

set_bookmark

Insert a bookmark before or after the characters in the text which

set_link

set_link(url, regex=None, offset=None, length=0)

set_reference_mark

Insert a reference mark, at the position defined by the regex

set_reference_mark_end

Insert/move a ReferenceMarkEnd for an existing reference mark. If

set_span

set_span(style, regex=None, offset=None, length=0)

Source code in odfdo/mixin_paragraph.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
class ParaMixin:
    """Mixin for Paragraph methods."""

    def _expand_spaces(self, added_string: str) -> list[Element | str]:
        result: list[Element | str] = []

        def _merge_text(txt: str):
            nonlocal result
            if result and isinstance(result[-1], str):
                result[-1] += txt
            else:
                result.append(txt)

        for obj in self.xpath("*|text()"):
            if isinstance(obj, EText):
                _merge_text(str(obj))
                continue
            obj.tail = ""
            if obj.tag != "text:s":
                result.append(obj)
                continue
            _merge_text(obj.text)
        _merge_text(added_string)
        return result

    def _merge_spaces(self, content: list[Element | str]) -> list[Element | str]:
        result: list[Element | str] = []
        for item in content:
            if isinstance(item, str):
                result.extend(self._sub_merge_spaces(item))
            else:
                result.append(item)
        return result

    @staticmethod
    def _sub_merge_spaces(text: str) -> list[Element | str]:
        result: list[Element | str] = []
        content = [x for x in _re_spaces_split.split(text) if x]

        def _merge_text(txt: str):
            nonlocal result
            if result and isinstance(result[-1], str):
                result[-1] += txt
            else:
                result.append(txt)

        if content:
            item0 = content[0]
            if isinstance(item0, str) and _re_only_spaces.match(item0):
                spacer = Spacer(len(item0))
                result.append(spacer)
            else:
                result.append(item0)
        for item in content[1:-1]:
            if isinstance(item, str):
                if len(item) > 1 and _re_only_spaces.match(item):
                    spacer = Spacer(len(item) - 1)
                    _merge_text(" ")
                    result.append(spacer)
                else:
                    _merge_text(item)
            else:
                result.append(item)
        if len(content) > 1:
            last_item = content[-1]
            if isinstance(last_item, str):
                if _re_only_spaces.match(last_item):
                    spacer = Spacer(len(last_item))
                    result.append(spacer)
                else:
                    _merge_text(last_item)
            else:
                result.append(last_item)
        return result

    def _replace_tabs_lb(self, content: list[Element | str]) -> list[Element | str]:
        result: list[Element | str] = []
        for item in content:
            if isinstance(item, str):
                result.extend(self._sub_replace_tabs_lb(item))
            else:
                result.append(item)
        return result

    @staticmethod
    def _sub_replace_tabs_lb(text: str) -> list[Element | str]:
        if not text:
            return []
        blocs = _re_splitter.split(text)
        result: list[Element | str] = []
        for bloc in blocs:
            if not bloc:
                continue
            if bloc == "\n":
                result.append(LineBreak())
                continue
            if bloc == "\t":
                result.append(Tab())
                continue
            result.append(bloc)
        return result

    def append_plain_text(self, text: str | bytes | None = "") -> None:
        """Append plain text to the paragraph, replacing <CR>, <TAB>
        and multiple spaces by ODF corresponding tags.
        """
        if text is None:
            stext = ""
        elif isinstance(text, bytes):
            stext = text.decode("utf-8")
        else:
            stext = str(text)
        content = self._expand_spaces(stext)
        content = self._merge_spaces(content)
        content = self._replace_tabs_lb(content)
        for child in self.children:
            self.delete(child, keep_tail=False)
        self.text = None
        for element in content:
            self._Element__append(element)

    @staticmethod
    def _unformatted(text: str | bytes | None) -> str:
        if not text:
            return ""
        if isinstance(text, bytes):
            stext = text.decode("utf-8")
        else:
            stext = str(text)
        return _re_sub_splitter.sub(" ", stext)

    def append(
        self,
        str_or_element: str | bytes | Element,
        formatted: bool = True,
    ) -> None:
        if isinstance(str_or_element, Element):
            if str_or_element.tag in {"text:p", "text:h", "text:s"}:
                # minimal compliancy or spacer summation
                self.append_plain_text(str(str_or_element))
                self.append_plain_text(str_or_element.tail)
            else:
                self._Element__append(str_or_element)
        elif formatted:
            self.append_plain_text(str_or_element)
        else:
            # self._Element__append(self._unformatted(str_or_element))
            # The added text is first "unformatted", but result needs
            # to be compliant
            self.append_plain_text(self._unformatted(str_or_element))

    def insert_note(
        self,
        note_element: Note | None = None,
        after: str | Element | None = None,
        note_class: str = "footnote",
        note_id: str | None = None,
        citation: str | None = None,
        body: str | None = None,
    ) -> None:
        if note_element is None:
            note_element = Note(
                note_class=note_class, note_id=note_id, citation=citation, body=body
            )
        else:
            # XXX clone or modify the argument?
            if note_class:
                note_element.note_class = note_class
            if note_id:
                note_element.note_id = note_id
            if citation:
                note_element.citation = citation
            if body:
                note_element.note_body = body
        note_element.check_validity()
        if isinstance(after, str):
            self._insert(note_element, after=after, main_text=True)
        elif isinstance(after, Element):
            after.insert(note_element, FIRST_CHILD)
        else:
            self.insert(note_element, FIRST_CHILD)

    def insert_annotation(
        self,
        annotation_element: Annotation | None = None,
        before: str | None = None,
        after: str | Element | None = None,
        position: int | tuple = 0,
        content: str | Element | None = None,
        body: str | None = None,
        creator: str | None = None,
        date: datetime | None = None,
    ) -> Annotation:
        """Insert an annotation, at the position defined by the regex (before,
        after, content) or by positionnal argument (position). If content is
        provided, the annotation covers the full content regex. Else, the
        annotation is positionned either 'before' or 'after' provided regex.

        If content is an odf element (ie: paragraph, span, ...), the full inner
        content is covered by the annotation (of the position just after if
        content is a single empty tag).

        If content/before or after exists (regex) and return a group of matching
        positions, the position value is the index of matching place to use.

        annotation_element can contain a previously created annotation, else
        the annotation is created from the body, creator and optional date
        (current date by default).

        Arguments:

            annotation_element -- Annotation or None

            before -- str regular expression or None

            after -- str regular expression or Element or None

            content -- str regular expression or None, or Element

            position -- int or tuple of int

            body -- str or Element

            creator -- str

            date -- datetime
        """

        if annotation_element is None:
            annotation_element = Annotation(
                text_or_element=body, creator=creator, date=date, parent=self
            )
        else:
            # XXX clone or modify the argument?
            if body:
                annotation_element.note_body = body
            if creator:
                annotation_element.creator = creator
            if date:
                annotation_element.date = date
        annotation_element.check_validity()

        # special case: content is an odf element (ie: a paragraph)
        if isinstance(content, Element):
            if content.is_empty():
                content.insert(annotation_element, xmlposition=NEXT_SIBLING)
                return annotation_element
            content.insert(annotation_element, start=True)
            annotation_end = AnnotationEnd(annotation_element)
            content.append(annotation_end)
            return annotation_element

        # special case
        if isinstance(after, Element):
            after.insert(annotation_element, FIRST_CHILD)
            return annotation_element

        # With "content" => automatically insert a "start" and an "end"
        # bookmark
        if (
            before is None
            and after is None
            and content is not None
            and isinstance(position, int)
        ):
            # Start tag
            self._insert(
                annotation_element, before=content, position=position, main_text=True
            )
            # End tag
            annotation_end = AnnotationEnd(annotation_element)
            self._insert(
                annotation_end, after=content, position=position, main_text=True
            )
            return annotation_element

        # With "(int, int)" =>  automatically insert a "start" and an "end"
        # bookmark
        if (
            before is None
            and after is None
            and content is None
            and isinstance(position, tuple)
        ):
            # Start
            self._insert(annotation_element, position=position[0], main_text=True)
            # End
            annotation_end = AnnotationEnd(annotation_element)
            self._insert(annotation_end, position=position[1], main_text=True)
            return annotation_element

        # Without "content" nor "position"
        if content is not None or not isinstance(position, int):
            raise ValueError("Bad arguments")

        # Insert
        self._insert(
            annotation_element,
            before=before,
            after=after,
            position=position,
            main_text=True,
        )
        return annotation_element

    def insert_annotation_end(
        self,
        annotation_element: Annotation,
        before: str | None = None,
        after: str | None = None,
        position: int = 0,
    ) -> AnnotationEnd:
        """Insert an annotation end tag for an existing annotation. If some end
        tag already exists, replace it. Annotation end tag is set at the
        position defined by the regex (before or after).

        If content/before or after (regex) returns a group of matching
        positions, the position value is the index of matching place to use.

        Arguments:

            annotation_element -- Annotation (mandatory)

            before -- str regular expression or None

            after -- str regular expression or None

            position -- int
        """

        if annotation_element is None:
            raise ValueError
        if not isinstance(annotation_element, Annotation):
            raise TypeError("Not a <office:annotation> Annotation")

        # remove existing end tag
        name = annotation_element.name
        existing_end_tag = self.get_annotation_end(name=name)
        if existing_end_tag:
            existing_end_tag.delete()

        # create the end tag
        end_tag = AnnotationEnd(annotation_element)

        # Insert
        self._insert(
            end_tag, before=before, after=after, position=position, main_text=True
        )
        return end_tag

    def set_reference_mark(
        self,
        name: str,
        before: str | None = None,
        after: str | None = None,
        position: int = 0,
        content: str | Element | None = None,
    ) -> Element:
        """Insert a reference mark, at the position defined by the regex
        (before, after, content) or by positionnal argument (position). If
        content is provided, the annotation covers the full range content regex
        (instances of ReferenceMarkStart and ReferenceMarkEnd are
        created). Else, an instance of ReferenceMark is positionned either
        'before' or 'after' provided regex.

        If content is an ODF Element (ie: Paragraph, Span, ...), the full inner
        content is referenced (of the position just after if content is a single
        empty tag).

        If content/before or after exists (regex) and return a group of matching
        positions, the position value is the index of matching place to use.

        Name is mandatory and shall be unique in the document for the preference
        mark range.

        Arguments:

            name -- str

            before -- str regular expression or None

            after -- str regular expression or None,

            content -- str regular expression or None, or Element

            position -- int or tuple of int

        Return: the created ReferenceMark or ReferenceMarkStart
        """
        # special case: content is an odf element (ie: a paragraph)
        if isinstance(content, Element):
            if content.is_empty():
                reference = ReferenceMark(name)
                content.insert(reference, xmlposition=NEXT_SIBLING)
                return reference
            reference_start = ReferenceMarkStart(name)
            content.insert(reference_start, start=True)
            reference_end = ReferenceMarkEnd(name)
            content.append(reference_end)
            return reference_start

        # With "content" => automatically insert a "start" and an "end"
        # reference
        if (
            before is None
            and after is None
            and content is not None
            and isinstance(position, int)
        ):
            # Start tag
            reference_start = ReferenceMarkStart(name)
            self._insert(
                reference_start, before=content, position=position, main_text=True
            )
            # End tag
            reference_end = ReferenceMarkEnd(name)
            self._insert(
                reference_end, after=content, position=position, main_text=True
            )
            return reference_start

        # With "(int, int)" =>  automatically insert a "start" and an "end"
        if (
            before is None
            and after is None
            and content is None
            and isinstance(position, tuple)
        ):
            # Start
            reference_start = ReferenceMarkStart(name)
            self._insert(reference_start, position=position[0], main_text=True)
            # End
            reference_end = ReferenceMarkEnd(name)
            self._insert(reference_end, position=position[1], main_text=True)
            return reference_start

        # Without "content" nor "position"
        if content is not None or not isinstance(position, int):
            raise ValueError("bad arguments")

        # Insert a positional reference mark
        reference = ReferenceMark(name)
        self._insert(
            reference,
            before=before,
            after=after,
            position=position,
            main_text=True,
        )
        return reference

    def set_reference_mark_end(
        self,
        reference_mark: Element,
        before: str | None = None,
        after: str | None = None,
        position: int = 0,
    ) -> ReferenceMarkEnd:
        """Insert/move a ReferenceMarkEnd for an existing reference mark. If
        some end tag already exists, replace it. Reference tag is set at the
        position defined by the regex (before or after).

        If content/before or after (regex) returns a group of matching
        positions, the position value is the index of matching place to use.

        Arguments:

            reference_mark -- ReferenceMark or ReferenceMarkStart (mandatory)

            before -- str regular expression or None

            after -- str regular expression or None

            position -- int
        """
        if not isinstance(reference_mark, (ReferenceMark, ReferenceMarkStart)):
            raise TypeError("Not a ReferenceMark or ReferenceMarkStart")
        name = reference_mark.name
        if isinstance(reference_mark, ReferenceMark):
            # change it to a range reference:
            reference_mark.tag = ReferenceMarkStart._tag

        existing_end_tag = self.get_reference_mark_end(name=name)
        if existing_end_tag:
            existing_end_tag.delete()

        # create the end tag
        end_tag = ReferenceMarkEnd(name)

        # Insert
        self._insert(
            end_tag, before=before, after=after, position=position, main_text=True
        )
        return end_tag

    def insert_variable(self, variable_element: Element, after: str | None) -> None:
        self._insert(variable_element, after=after, main_text=True)

    @_by_regex_offset
    def set_span(
        self,
        match: str,
        tail: str,
        style: str,
        regex: str | None = None,
        offset: int | None = None,
        length: int = 0,
    ) -> Span:
        """
        set_span(style, regex=None, offset=None, length=0)
        Apply the given style to text content matching the regex OR the
        positional arguments offset and length.

        (match, tail: provided by regex decorator)

        Arguments:

            style -- str

            regex -- str regular expression

            offset -- int

            length -- int
        """
        # span = Span(match, style=style)
        # span.tail = tail
        span: Span = Element.from_tag("text:span")
        span.text = ""
        span.append_plain_text(match)
        span.tail = tail
        span.style = style

        return span

    def remove_spans(self, keep_heading: bool = True) -> Element | list:
        """Send back a copy of the element, without span styles.
        If keep_heading is True (default), the first level heading style is left
        unchanged.
        """
        strip = ("text:span",)
        if keep_heading:
            protect = ("text:h",)
        else:
            protect = None
        return self.strip_tags(strip=strip, protect=protect)

    def remove_span(self, spans: Element | list[Element]) -> Element | list:
        """Send back a copy of the element, the spans (not a clone) removed.

        Arguments:

            spans -- Element or list of Element
        """
        return self.strip_elements(spans)

    @_by_regex_offset
    def set_link(
        self,
        match: str,
        tail: str,
        url: str,
        regex: str | None = None,
        offset: int | None = None,
        length: int = 0,
    ) -> Element:
        """
        set_link(url, regex=None, offset=None, length=0)
        Make a link to the provided url from text content matching the regex
        OR the positional arguments offset and length.

        (match, tail: provided by regex decorator)

        Arguments:

            url -- str

            regex -- str regular expression

            offset -- int

            length -- int
        """
        link = Link(url, text=match)
        link.tail = tail
        return link

    def remove_links(self) -> Element | list:
        """Send back a copy of the element, without links tags."""
        strip = (Link._tag,)
        return self.strip_tags(strip=strip)

    def remove_link(self, links: Link | list[Link]) -> Element | list:
        """Send back a copy of the element (not a clone), with the sub links
           removed.

        Arguments:

            links -- Link or list of Link
        """
        return self.strip_elements(links)

    def insert_reference(
        self,
        name: str,
        ref_format: str = "",
        before: str | None = None,
        after: str | Element | None = None,
        position: int = 0,
        display: str | None = None,
    ) -> None:
        """Create and insert a reference to a content marked by a reference
        mark. The Reference element ("text:reference-ref") represents a
        field that references a "text:reference-mark-start" or
        "text:reference-mark" element. Its "text:reference-format" attribute
        specifies what is displayed from the referenced element. Default is
        'page'. Actual content is not automatically updated except for the 'text'
        format.

        name is mandatory and should represent an existing reference mark of the
        document.

        ref_format is the argument for format reference (default is 'page').

        The reference is inserted the position defined by the regex (before /
        after), or by positionnal argument (position). If 'display' is provided,
        it will be used as the text value for the reference.

        If after is an ODF Element, the reference is inserted as first child of
        this element.

        Arguments:

            name -- str

            ref_format -- one of : 'chapter', 'direction', 'page', 'text',
                                    'caption', 'category-and-value', 'value',
                                    'number', 'number-all-superior',
                                    'number-no-superior'

            before -- str regular expression or None

            after -- str regular expression or odf element or None

            position -- int

            display -- str or None
        """
        reference = Reference(name, ref_format)
        if display is None and ref_format == "text":
            # get reference content
            body = self.document_body
            if not body:
                body = self.root
            mark = body.get_reference_mark(name=name)
            if mark:
                display = mark.referenced_text  # type: ignore
        if not display:
            display = " "
        reference.text = display
        if isinstance(after, Element):
            after.insert(reference, FIRST_CHILD)
        else:
            self._insert(
                reference, before=before, after=after, position=position, main_text=True
            )

    def set_bookmark(
        self,
        name: str,
        before: str | None = None,
        after: str | None = None,
        position: int | tuple = 0,
        role: str | None = None,
        content: str | None = None,
    ) -> Element | tuple[Element, Element]:
        """Insert a bookmark before or after the characters in the text which
        match the regex before/after. When the regex matches more of one part
        of the text, position can be set to choose which part must be used.
        If before and after are None, we use only position that is the number
        of characters.

        So, by default, this function inserts a bookmark before the first
        character of the content. Role can be None, "start" or "end", we
        insert respectively a position bookmark a bookmark-start or a
        bookmark-end.

        If content is not None these 2 calls are equivalent:

          paragraph.set_bookmark("bookmark", content="xyz")

        and:

          paragraph.set_bookmark("bookmark", before="xyz", role="start")
          paragraph.set_bookmark("bookmark", after="xyz", role="end")


        If position is a 2-tuple, these 2 calls are equivalent:

          paragraph.set_bookmark("bookmark", position=(10, 20))

        and:

          paragraph.set_bookmark("bookmark", position=10, role="start")
          paragraph.set_bookmark("bookmark", position=20, role="end")


        Arguments:

            name -- str

            before -- str regex

            after -- str regex

            position -- int or (int, int)

            role -- None, "start" or "end"

            content -- str regex
        """
        # With "content" => automatically insert a "start" and an "end"
        # bookmark
        if (
            before is None
            and after is None
            and role is None
            and content is not None
            and isinstance(position, int)
        ):
            # Start
            start = BookmarkStart(name)
            self._insert(start, before=content, position=position, main_text=True)
            # End
            end = BookmarkEnd(name)
            self._insert(end, after=content, position=position, main_text=True)
            return start, end

        # With "(int, int)" =>  automatically insert a "start" and an "end"
        # bookmark
        if (
            before is None
            and after is None
            and role is None
            and content is None
            and isinstance(position, tuple)
        ):
            # Start
            start = BookmarkStart(name)
            self._insert(start, position=position[0], main_text=True)
            # End
            end = BookmarkEnd(name)
            self._insert(end, position=position[1], main_text=True)
            return start, end

        # Without "content" nor "position"
        if content is not None or not isinstance(position, int):
            raise ValueError("bad arguments")

        # Role
        if role is None:
            bookmark: Element = Bookmark(name)
        elif role == "start":
            bookmark = BookmarkStart(name)
        elif role == "end":
            bookmark = BookmarkEnd(name)
        else:
            raise ValueError("bad arguments")

        # Insert
        self._insert(
            bookmark, before=before, after=after, position=position, main_text=True
        )

        return bookmark

append

append(
    str_or_element: str | bytes | Element,
    formatted: bool = True,
) -> None
Source code in odfdo/mixin_paragraph.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def append(
    self,
    str_or_element: str | bytes | Element,
    formatted: bool = True,
) -> None:
    if isinstance(str_or_element, Element):
        if str_or_element.tag in {"text:p", "text:h", "text:s"}:
            # minimal compliancy or spacer summation
            self.append_plain_text(str(str_or_element))
            self.append_plain_text(str_or_element.tail)
        else:
            self._Element__append(str_or_element)
    elif formatted:
        self.append_plain_text(str_or_element)
    else:
        # self._Element__append(self._unformatted(str_or_element))
        # The added text is first "unformatted", but result needs
        # to be compliant
        self.append_plain_text(self._unformatted(str_or_element))

append_plain_text

append_plain_text(text: str | bytes | None = '') -> None

Append plain text to the paragraph, replacing , and multiple spaces by ODF corresponding tags.

Source code in odfdo/mixin_paragraph.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def append_plain_text(self, text: str | bytes | None = "") -> None:
    """Append plain text to the paragraph, replacing <CR>, <TAB>
    and multiple spaces by ODF corresponding tags.
    """
    if text is None:
        stext = ""
    elif isinstance(text, bytes):
        stext = text.decode("utf-8")
    else:
        stext = str(text)
    content = self._expand_spaces(stext)
    content = self._merge_spaces(content)
    content = self._replace_tabs_lb(content)
    for child in self.children:
        self.delete(child, keep_tail=False)
    self.text = None
    for element in content:
        self._Element__append(element)

insert_annotation

insert_annotation(
    annotation_element: Annotation | None = None,
    before: str | None = None,
    after: str | Element | None = None,
    position: int | tuple = 0,
    content: str | Element | None = None,
    body: str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
) -> Annotation

Insert an annotation, at the position defined by the regex (before, after, content) or by positionnal argument (position). If content is provided, the annotation covers the full content regex. Else, the annotation is positionned either ‘before’ or ‘after’ provided regex.

If content is an odf element (ie: paragraph, span, …), the full inner content is covered by the annotation (of the position just after if content is a single empty tag).

If content/before or after exists (regex) and return a group of matching positions, the position value is the index of matching place to use.

annotation_element can contain a previously created annotation, else the annotation is created from the body, creator and optional date (current date by default).

Arguments:

annotation_element -- Annotation or None

before -- str regular expression or None

after -- str regular expression or Element or None

content -- str regular expression or None, or Element

position -- int or tuple of int

body -- str or Element

creator -- str

date -- datetime
Source code in odfdo/mixin_paragraph.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def insert_annotation(
    self,
    annotation_element: Annotation | None = None,
    before: str | None = None,
    after: str | Element | None = None,
    position: int | tuple = 0,
    content: str | Element | None = None,
    body: str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
) -> Annotation:
    """Insert an annotation, at the position defined by the regex (before,
    after, content) or by positionnal argument (position). If content is
    provided, the annotation covers the full content regex. Else, the
    annotation is positionned either 'before' or 'after' provided regex.

    If content is an odf element (ie: paragraph, span, ...), the full inner
    content is covered by the annotation (of the position just after if
    content is a single empty tag).

    If content/before or after exists (regex) and return a group of matching
    positions, the position value is the index of matching place to use.

    annotation_element can contain a previously created annotation, else
    the annotation is created from the body, creator and optional date
    (current date by default).

    Arguments:

        annotation_element -- Annotation or None

        before -- str regular expression or None

        after -- str regular expression or Element or None

        content -- str regular expression or None, or Element

        position -- int or tuple of int

        body -- str or Element

        creator -- str

        date -- datetime
    """

    if annotation_element is None:
        annotation_element = Annotation(
            text_or_element=body, creator=creator, date=date, parent=self
        )
    else:
        # XXX clone or modify the argument?
        if body:
            annotation_element.note_body = body
        if creator:
            annotation_element.creator = creator
        if date:
            annotation_element.date = date
    annotation_element.check_validity()

    # special case: content is an odf element (ie: a paragraph)
    if isinstance(content, Element):
        if content.is_empty():
            content.insert(annotation_element, xmlposition=NEXT_SIBLING)
            return annotation_element
        content.insert(annotation_element, start=True)
        annotation_end = AnnotationEnd(annotation_element)
        content.append(annotation_end)
        return annotation_element

    # special case
    if isinstance(after, Element):
        after.insert(annotation_element, FIRST_CHILD)
        return annotation_element

    # With "content" => automatically insert a "start" and an "end"
    # bookmark
    if (
        before is None
        and after is None
        and content is not None
        and isinstance(position, int)
    ):
        # Start tag
        self._insert(
            annotation_element, before=content, position=position, main_text=True
        )
        # End tag
        annotation_end = AnnotationEnd(annotation_element)
        self._insert(
            annotation_end, after=content, position=position, main_text=True
        )
        return annotation_element

    # With "(int, int)" =>  automatically insert a "start" and an "end"
    # bookmark
    if (
        before is None
        and after is None
        and content is None
        and isinstance(position, tuple)
    ):
        # Start
        self._insert(annotation_element, position=position[0], main_text=True)
        # End
        annotation_end = AnnotationEnd(annotation_element)
        self._insert(annotation_end, position=position[1], main_text=True)
        return annotation_element

    # Without "content" nor "position"
    if content is not None or not isinstance(position, int):
        raise ValueError("Bad arguments")

    # Insert
    self._insert(
        annotation_element,
        before=before,
        after=after,
        position=position,
        main_text=True,
    )
    return annotation_element

insert_annotation_end

insert_annotation_end(
    annotation_element: Annotation,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
) -> AnnotationEnd

Insert an annotation end tag for an existing annotation. If some end tag already exists, replace it. Annotation end tag is set at the position defined by the regex (before or after).

If content/before or after (regex) returns a group of matching positions, the position value is the index of matching place to use.

Arguments:

annotation_element -- Annotation (mandatory)

before -- str regular expression or None

after -- str regular expression or None

position -- int
Source code in odfdo/mixin_paragraph.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
def insert_annotation_end(
    self,
    annotation_element: Annotation,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
) -> AnnotationEnd:
    """Insert an annotation end tag for an existing annotation. If some end
    tag already exists, replace it. Annotation end tag is set at the
    position defined by the regex (before or after).

    If content/before or after (regex) returns a group of matching
    positions, the position value is the index of matching place to use.

    Arguments:

        annotation_element -- Annotation (mandatory)

        before -- str regular expression or None

        after -- str regular expression or None

        position -- int
    """

    if annotation_element is None:
        raise ValueError
    if not isinstance(annotation_element, Annotation):
        raise TypeError("Not a <office:annotation> Annotation")

    # remove existing end tag
    name = annotation_element.name
    existing_end_tag = self.get_annotation_end(name=name)
    if existing_end_tag:
        existing_end_tag.delete()

    # create the end tag
    end_tag = AnnotationEnd(annotation_element)

    # Insert
    self._insert(
        end_tag, before=before, after=after, position=position, main_text=True
    )
    return end_tag

insert_note

insert_note(
    note_element: Note | None = None,
    after: str | Element | None = None,
    note_class: str = "footnote",
    note_id: str | None = None,
    citation: str | None = None,
    body: str | None = None,
) -> None
Source code in odfdo/mixin_paragraph.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def insert_note(
    self,
    note_element: Note | None = None,
    after: str | Element | None = None,
    note_class: str = "footnote",
    note_id: str | None = None,
    citation: str | None = None,
    body: str | None = None,
) -> None:
    if note_element is None:
        note_element = Note(
            note_class=note_class, note_id=note_id, citation=citation, body=body
        )
    else:
        # XXX clone or modify the argument?
        if note_class:
            note_element.note_class = note_class
        if note_id:
            note_element.note_id = note_id
        if citation:
            note_element.citation = citation
        if body:
            note_element.note_body = body
    note_element.check_validity()
    if isinstance(after, str):
        self._insert(note_element, after=after, main_text=True)
    elif isinstance(after, Element):
        after.insert(note_element, FIRST_CHILD)
    else:
        self.insert(note_element, FIRST_CHILD)

insert_reference

insert_reference(
    name: str,
    ref_format: str = "",
    before: str | None = None,
    after: str | Element | None = None,
    position: int = 0,
    display: str | None = None,
) -> None

Create and insert a reference to a content marked by a reference mark. The Reference element (“text:reference-ref”) represents a field that references a “text:reference-mark-start” or “text:reference-mark” element. Its “text:reference-format” attribute specifies what is displayed from the referenced element. Default is ‘page’. Actual content is not automatically updated except for the ‘text’ format.

name is mandatory and should represent an existing reference mark of the document.

ref_format is the argument for format reference (default is ‘page’).

The reference is inserted the position defined by the regex (before / after), or by positionnal argument (position). If ‘display’ is provided, it will be used as the text value for the reference.

If after is an ODF Element, the reference is inserted as first child of this element.

Arguments:

name -- str

ref_format -- one of : 'chapter', 'direction', 'page', 'text',
                        'caption', 'category-and-value', 'value',
                        'number', 'number-all-superior',
                        'number-no-superior'

before -- str regular expression or None

after -- str regular expression or odf element or None

position -- int

display -- str or None
Source code in odfdo/mixin_paragraph.py
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
def insert_reference(
    self,
    name: str,
    ref_format: str = "",
    before: str | None = None,
    after: str | Element | None = None,
    position: int = 0,
    display: str | None = None,
) -> None:
    """Create and insert a reference to a content marked by a reference
    mark. The Reference element ("text:reference-ref") represents a
    field that references a "text:reference-mark-start" or
    "text:reference-mark" element. Its "text:reference-format" attribute
    specifies what is displayed from the referenced element. Default is
    'page'. Actual content is not automatically updated except for the 'text'
    format.

    name is mandatory and should represent an existing reference mark of the
    document.

    ref_format is the argument for format reference (default is 'page').

    The reference is inserted the position defined by the regex (before /
    after), or by positionnal argument (position). If 'display' is provided,
    it will be used as the text value for the reference.

    If after is an ODF Element, the reference is inserted as first child of
    this element.

    Arguments:

        name -- str

        ref_format -- one of : 'chapter', 'direction', 'page', 'text',
                                'caption', 'category-and-value', 'value',
                                'number', 'number-all-superior',
                                'number-no-superior'

        before -- str regular expression or None

        after -- str regular expression or odf element or None

        position -- int

        display -- str or None
    """
    reference = Reference(name, ref_format)
    if display is None and ref_format == "text":
        # get reference content
        body = self.document_body
        if not body:
            body = self.root
        mark = body.get_reference_mark(name=name)
        if mark:
            display = mark.referenced_text  # type: ignore
    if not display:
        display = " "
    reference.text = display
    if isinstance(after, Element):
        after.insert(reference, FIRST_CHILD)
    else:
        self._insert(
            reference, before=before, after=after, position=position, main_text=True
        )

insert_variable

insert_variable(
    variable_element: Element, after: str | None
) -> None
Source code in odfdo/mixin_paragraph.py
675
676
def insert_variable(self, variable_element: Element, after: str | None) -> None:
    self._insert(variable_element, after=after, main_text=True)
remove_link(links: Link | list[Link]) -> Element | list

Send back a copy of the element (not a clone), with the sub links removed.

Arguments:

links -- Link or list of Link
Source code in odfdo/mixin_paragraph.py
772
773
774
775
776
777
778
779
780
def remove_link(self, links: Link | list[Link]) -> Element | list:
    """Send back a copy of the element (not a clone), with the sub links
       removed.

    Arguments:

        links -- Link or list of Link
    """
    return self.strip_elements(links)
remove_links() -> Element | list

Send back a copy of the element, without links tags.

Source code in odfdo/mixin_paragraph.py
767
768
769
770
def remove_links(self) -> Element | list:
    """Send back a copy of the element, without links tags."""
    strip = (Link._tag,)
    return self.strip_tags(strip=strip)

remove_span

remove_span(
    spans: Element | list[Element],
) -> Element | list

Send back a copy of the element, the spans (not a clone) removed.

Arguments:

spans -- Element or list of Element
Source code in odfdo/mixin_paragraph.py
727
728
729
730
731
732
733
734
def remove_span(self, spans: Element | list[Element]) -> Element | list:
    """Send back a copy of the element, the spans (not a clone) removed.

    Arguments:

        spans -- Element or list of Element
    """
    return self.strip_elements(spans)

remove_spans

remove_spans(keep_heading: bool = True) -> Element | list

Send back a copy of the element, without span styles. If keep_heading is True (default), the first level heading style is left unchanged.

Source code in odfdo/mixin_paragraph.py
715
716
717
718
719
720
721
722
723
724
725
def remove_spans(self, keep_heading: bool = True) -> Element | list:
    """Send back a copy of the element, without span styles.
    If keep_heading is True (default), the first level heading style is left
    unchanged.
    """
    strip = ("text:span",)
    if keep_heading:
        protect = ("text:h",)
    else:
        protect = None
    return self.strip_tags(strip=strip, protect=protect)

set_bookmark

set_bookmark(
    name: str,
    before: str | None = None,
    after: str | None = None,
    position: int | tuple = 0,
    role: str | None = None,
    content: str | None = None,
) -> Element | tuple[Element, Element]

Insert a bookmark before or after the characters in the text which match the regex before/after. When the regex matches more of one part of the text, position can be set to choose which part must be used. If before and after are None, we use only position that is the number of characters.

So, by default, this function inserts a bookmark before the first character of the content. Role can be None, “start” or “end”, we insert respectively a position bookmark a bookmark-start or a bookmark-end.

If content is not None these 2 calls are equivalent:

paragraph.set_bookmark(“bookmark”, content=”xyz”)

and:

paragraph.set_bookmark(“bookmark”, before=”xyz”, role=”start”) paragraph.set_bookmark(“bookmark”, after=”xyz”, role=”end”)

If position is a 2-tuple, these 2 calls are equivalent:

paragraph.set_bookmark(“bookmark”, position=(10, 20))

and:

paragraph.set_bookmark(“bookmark”, position=10, role=”start”) paragraph.set_bookmark(“bookmark”, position=20, role=”end”)

Arguments:

name -- str

before -- str regex

after -- str regex

position -- int or (int, int)

role -- None, "start" or "end"

content -- str regex
Source code in odfdo/mixin_paragraph.py
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
def set_bookmark(
    self,
    name: str,
    before: str | None = None,
    after: str | None = None,
    position: int | tuple = 0,
    role: str | None = None,
    content: str | None = None,
) -> Element | tuple[Element, Element]:
    """Insert a bookmark before or after the characters in the text which
    match the regex before/after. When the regex matches more of one part
    of the text, position can be set to choose which part must be used.
    If before and after are None, we use only position that is the number
    of characters.

    So, by default, this function inserts a bookmark before the first
    character of the content. Role can be None, "start" or "end", we
    insert respectively a position bookmark a bookmark-start or a
    bookmark-end.

    If content is not None these 2 calls are equivalent:

      paragraph.set_bookmark("bookmark", content="xyz")

    and:

      paragraph.set_bookmark("bookmark", before="xyz", role="start")
      paragraph.set_bookmark("bookmark", after="xyz", role="end")


    If position is a 2-tuple, these 2 calls are equivalent:

      paragraph.set_bookmark("bookmark", position=(10, 20))

    and:

      paragraph.set_bookmark("bookmark", position=10, role="start")
      paragraph.set_bookmark("bookmark", position=20, role="end")


    Arguments:

        name -- str

        before -- str regex

        after -- str regex

        position -- int or (int, int)

        role -- None, "start" or "end"

        content -- str regex
    """
    # With "content" => automatically insert a "start" and an "end"
    # bookmark
    if (
        before is None
        and after is None
        and role is None
        and content is not None
        and isinstance(position, int)
    ):
        # Start
        start = BookmarkStart(name)
        self._insert(start, before=content, position=position, main_text=True)
        # End
        end = BookmarkEnd(name)
        self._insert(end, after=content, position=position, main_text=True)
        return start, end

    # With "(int, int)" =>  automatically insert a "start" and an "end"
    # bookmark
    if (
        before is None
        and after is None
        and role is None
        and content is None
        and isinstance(position, tuple)
    ):
        # Start
        start = BookmarkStart(name)
        self._insert(start, position=position[0], main_text=True)
        # End
        end = BookmarkEnd(name)
        self._insert(end, position=position[1], main_text=True)
        return start, end

    # Without "content" nor "position"
    if content is not None or not isinstance(position, int):
        raise ValueError("bad arguments")

    # Role
    if role is None:
        bookmark: Element = Bookmark(name)
    elif role == "start":
        bookmark = BookmarkStart(name)
    elif role == "end":
        bookmark = BookmarkEnd(name)
    else:
        raise ValueError("bad arguments")

    # Insert
    self._insert(
        bookmark, before=before, after=after, position=position, main_text=True
    )

    return bookmark
set_link(
    match: str,
    tail: str,
    url: str,
    regex: str | None = None,
    offset: int | None = None,
    length: int = 0,
) -> Element

set_link(url, regex=None, offset=None, length=0) Make a link to the provided url from text content matching the regex OR the positional arguments offset and length.

(match, tail: provided by regex decorator)

Arguments:

url -- str

regex -- str regular expression

offset -- int

length -- int
Source code in odfdo/mixin_paragraph.py
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
@_by_regex_offset
def set_link(
    self,
    match: str,
    tail: str,
    url: str,
    regex: str | None = None,
    offset: int | None = None,
    length: int = 0,
) -> Element:
    """
    set_link(url, regex=None, offset=None, length=0)
    Make a link to the provided url from text content matching the regex
    OR the positional arguments offset and length.

    (match, tail: provided by regex decorator)

    Arguments:

        url -- str

        regex -- str regular expression

        offset -- int

        length -- int
    """
    link = Link(url, text=match)
    link.tail = tail
    return link

set_reference_mark

set_reference_mark(
    name: str,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
    content: str | Element | None = None,
) -> Element

Insert a reference mark, at the position defined by the regex (before, after, content) or by positionnal argument (position). If content is provided, the annotation covers the full range content regex (instances of ReferenceMarkStart and ReferenceMarkEnd are created). Else, an instance of ReferenceMark is positionned either ‘before’ or ‘after’ provided regex.

If content is an ODF Element (ie: Paragraph, Span, …), the full inner content is referenced (of the position just after if content is a single empty tag).

If content/before or after exists (regex) and return a group of matching positions, the position value is the index of matching place to use.

Name is mandatory and shall be unique in the document for the preference mark range.

Arguments:

name -- str

before -- str regular expression or None

after -- str regular expression or None,

content -- str regular expression or None, or Element

position -- int or tuple of int

Return: the created ReferenceMark or ReferenceMarkStart

Source code in odfdo/mixin_paragraph.py
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
def set_reference_mark(
    self,
    name: str,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
    content: str | Element | None = None,
) -> Element:
    """Insert a reference mark, at the position defined by the regex
    (before, after, content) or by positionnal argument (position). If
    content is provided, the annotation covers the full range content regex
    (instances of ReferenceMarkStart and ReferenceMarkEnd are
    created). Else, an instance of ReferenceMark is positionned either
    'before' or 'after' provided regex.

    If content is an ODF Element (ie: Paragraph, Span, ...), the full inner
    content is referenced (of the position just after if content is a single
    empty tag).

    If content/before or after exists (regex) and return a group of matching
    positions, the position value is the index of matching place to use.

    Name is mandatory and shall be unique in the document for the preference
    mark range.

    Arguments:

        name -- str

        before -- str regular expression or None

        after -- str regular expression or None,

        content -- str regular expression or None, or Element

        position -- int or tuple of int

    Return: the created ReferenceMark or ReferenceMarkStart
    """
    # special case: content is an odf element (ie: a paragraph)
    if isinstance(content, Element):
        if content.is_empty():
            reference = ReferenceMark(name)
            content.insert(reference, xmlposition=NEXT_SIBLING)
            return reference
        reference_start = ReferenceMarkStart(name)
        content.insert(reference_start, start=True)
        reference_end = ReferenceMarkEnd(name)
        content.append(reference_end)
        return reference_start

    # With "content" => automatically insert a "start" and an "end"
    # reference
    if (
        before is None
        and after is None
        and content is not None
        and isinstance(position, int)
    ):
        # Start tag
        reference_start = ReferenceMarkStart(name)
        self._insert(
            reference_start, before=content, position=position, main_text=True
        )
        # End tag
        reference_end = ReferenceMarkEnd(name)
        self._insert(
            reference_end, after=content, position=position, main_text=True
        )
        return reference_start

    # With "(int, int)" =>  automatically insert a "start" and an "end"
    if (
        before is None
        and after is None
        and content is None
        and isinstance(position, tuple)
    ):
        # Start
        reference_start = ReferenceMarkStart(name)
        self._insert(reference_start, position=position[0], main_text=True)
        # End
        reference_end = ReferenceMarkEnd(name)
        self._insert(reference_end, position=position[1], main_text=True)
        return reference_start

    # Without "content" nor "position"
    if content is not None or not isinstance(position, int):
        raise ValueError("bad arguments")

    # Insert a positional reference mark
    reference = ReferenceMark(name)
    self._insert(
        reference,
        before=before,
        after=after,
        position=position,
        main_text=True,
    )
    return reference

set_reference_mark_end

set_reference_mark_end(
    reference_mark: Element,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
) -> ReferenceMarkEnd

Insert/move a ReferenceMarkEnd for an existing reference mark. If some end tag already exists, replace it. Reference tag is set at the position defined by the regex (before or after).

If content/before or after (regex) returns a group of matching positions, the position value is the index of matching place to use.

Arguments:

reference_mark -- ReferenceMark or ReferenceMarkStart (mandatory)

before -- str regular expression or None

after -- str regular expression or None

position -- int
Source code in odfdo/mixin_paragraph.py
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
def set_reference_mark_end(
    self,
    reference_mark: Element,
    before: str | None = None,
    after: str | None = None,
    position: int = 0,
) -> ReferenceMarkEnd:
    """Insert/move a ReferenceMarkEnd for an existing reference mark. If
    some end tag already exists, replace it. Reference tag is set at the
    position defined by the regex (before or after).

    If content/before or after (regex) returns a group of matching
    positions, the position value is the index of matching place to use.

    Arguments:

        reference_mark -- ReferenceMark or ReferenceMarkStart (mandatory)

        before -- str regular expression or None

        after -- str regular expression or None

        position -- int
    """
    if not isinstance(reference_mark, (ReferenceMark, ReferenceMarkStart)):
        raise TypeError("Not a ReferenceMark or ReferenceMarkStart")
    name = reference_mark.name
    if isinstance(reference_mark, ReferenceMark):
        # change it to a range reference:
        reference_mark.tag = ReferenceMarkStart._tag

    existing_end_tag = self.get_reference_mark_end(name=name)
    if existing_end_tag:
        existing_end_tag.delete()

    # create the end tag
    end_tag = ReferenceMarkEnd(name)

    # Insert
    self._insert(
        end_tag, before=before, after=after, position=position, main_text=True
    )
    return end_tag

set_span

set_span(
    match: str,
    tail: str,
    style: str,
    regex: str | None = None,
    offset: int | None = None,
    length: int = 0,
) -> Span

set_span(style, regex=None, offset=None, length=0) Apply the given style to text content matching the regex OR the positional arguments offset and length.

(match, tail: provided by regex decorator)

Arguments:

style -- str

regex -- str regular expression

offset -- int

length -- int
Source code in odfdo/mixin_paragraph.py
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
@_by_regex_offset
def set_span(
    self,
    match: str,
    tail: str,
    style: str,
    regex: str | None = None,
    offset: int | None = None,
    length: int = 0,
) -> Span:
    """
    set_span(style, regex=None, offset=None, length=0)
    Apply the given style to text content matching the regex OR the
    positional arguments offset and length.

    (match, tail: provided by regex decorator)

    Arguments:

        style -- str

        regex -- str regular expression

        offset -- int

        length -- int
    """
    # span = Span(match, style=style)
    # span.tail = tail
    span: Span = Element.from_tag("text:span")
    span.text = ""
    span.append_plain_text(match)
    span.tail = tail
    span.style = style

    return span

Paragraph

Bases: MDParagraph, ParaFormattedTextMixin, ParaMixin, Element

An ODF paragraph, “text:p”.

The “text:p” element represents a paragraph, which is the basic unit of text in an OpenDocument file.

Methods:

Name Description
__init__

Create a paragraph element “text:p” of the given style containing the

Attributes:

Name Type Description
style
text
Source code in odfdo/paragraph.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class Paragraph(MDParagraph, ParaFormattedTextMixin, ParaMixin, Element):
    """An ODF paragraph, "text:p".

    The "text:p" element represents a paragraph, which is
    the basic unit of text in an OpenDocument file.
    """

    _tag = "text:p"
    _properties: tuple[PropDef, ...] = (PropDef("style", "text:style-name"),)

    def __init__(
        self,
        text_or_element: str | bytes | Element | None = None,
        style: str | None = None,
        formatted: bool = True,
        **kwargs: Any,
    ):
        """
        Create a paragraph element "text:p" of the given style containing the
        pptional given text.

        If "formatted" is True (the default), the given text is appended with <CR>,
        <TAB> and multiple spaces replaced by ODF corresponding tags.

        Arguments:

            text -- str, bytes or Element

            style -- str

            formatted -- bool
        """
        super().__init__(**kwargs)
        if self._do_init:
            if isinstance(text_or_element, Element):
                self.append(text_or_element)
            else:
                self.text = ""
                if formatted:
                    self.append_plain_text(text_or_element)
                else:
                    self.append_plain_text(self._unformatted(text_or_element))
            if style is not None:
                self.style = style

    def __str__(self) -> str:
        # '\n' at the end slightly breaks compatibility, but is clearly better
        return self.inner_text + "\n"

style instance-attribute

style = style

text instance-attribute

text = ''

__init__

__init__(
    text_or_element: str | bytes | Element | None = None,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
)

Create a paragraph element “text:p” of the given style containing the pptional given text.

If “formatted” is True (the default), the given text is appended with , and multiple spaces replaced by ODF corresponding tags.

Arguments:

text -- str, bytes or Element

style -- str

formatted -- bool
Source code in odfdo/paragraph.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(
    self,
    text_or_element: str | bytes | Element | None = None,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
):
    """
    Create a paragraph element "text:p" of the given style containing the
    pptional given text.

    If "formatted" is True (the default), the given text is appended with <CR>,
    <TAB> and multiple spaces replaced by ODF corresponding tags.

    Arguments:

        text -- str, bytes or Element

        style -- str

        formatted -- bool
    """
    super().__init__(**kwargs)
    if self._do_init:
        if isinstance(text_or_element, Element):
            self.append(text_or_element)
        else:
            self.text = ""
            if formatted:
                self.append_plain_text(text_or_element)
            else:
                self.append_plain_text(self._unformatted(text_or_element))
        if style is not None:
            self.style = style

Presentation

Bases: Body

Root of the Presentation document content, “office:presentation”.

Source code in odfdo/body.py
123
124
125
126
127
class Presentation(Body):
    """Root of the Presentation document content, "office:presentation"."""

    _tag: str = "office:presentation"
    _properties: tuple[PropDef, ...] = ()

RectangleShape

Bases: ShapeBase

A rectangle shape, “draw:rect”.

Methods:

Name Description
__init__

Create a rectangle shape “draw:rect”.

Source code in odfdo/shapes.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class RectangleShape(ShapeBase):
    """A rectangle shape, "draw:rect"."""

    _tag = "draw:rect"
    _properties: tuple[PropDef, ...] = ()

    def __init__(
        self,
        style: str | None = None,
        text_style: str | None = None,
        draw_id: str | None = None,
        layer: str | None = None,
        position: tuple | None = None,
        size: tuple | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a rectangle shape "draw:rect".

        Arguments:

            style -- str

            text_style -- str

            draw_id -- str

            layer -- str

            position -- (str, str)

            size -- (str, str)
        """
        kwargs.update(
            {
                "style": style,
                "text_style": text_style,
                "draw_id": draw_id,
                "layer": layer,
                "size": size,
                "position": position,
            }
        )
        super().__init__(**kwargs)

__init__

__init__(
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    position: tuple | None = None,
    size: tuple | None = None,
    **kwargs: Any,
) -> None

Create a rectangle shape “draw:rect”.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

position -- (str, str)

size -- (str, str)
Source code in odfdo/shapes.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def __init__(
    self,
    style: str | None = None,
    text_style: str | None = None,
    draw_id: str | None = None,
    layer: str | None = None,
    position: tuple | None = None,
    size: tuple | None = None,
    **kwargs: Any,
) -> None:
    """Create a rectangle shape "draw:rect".

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        position -- (str, str)

        size -- (str, str)
    """
    kwargs.update(
        {
            "style": style,
            "text_style": text_style,
            "draw_id": draw_id,
            "layer": layer,
            "size": size,
            "position": position,
        }
    )
    super().__init__(**kwargs)

Reference

Bases: Element

A reference to a content marked by a reference mark, “text:reference-ref”.”

The odf_reference element (“text:reference-ref”) represents a field that references a “text:reference-mark-start” or “text:reference-mark” element. Its text:reference-format attribute specifies what is displayed from the referenced element. Default is ‘page’ Actual content is not updated except for the ‘text’ format by the update() method.

Creation of references can be tricky, consider using this method: odfdo.paragraph.insert_reference()

Values for text:reference-format : The defined values for the text:reference-format attribute supported by all reference fields are: - ‘chapter’: displays the number of the chapter in which the referenced item appears. - ‘direction’: displays whether the referenced item is above or below the reference field. - ‘page’: displays the number of the page on which the referenced item appears. - ‘text’: displays the text of the referenced item. Additional defined values for the text:reference-format attribute supported by references to sequence fields are: - ‘caption’: displays the caption in which the sequence is used. - ‘category-and-value’: displays the name and value of the sequence. - ‘value’: displays the value of the sequence.

References to bookmarks and other references support additional values,
which display the list label of the referenced item. If the referenced
item is contained in a list or a numbered paragraph, the list label is
the formatted number of the paragraph which contains the referenced
item. If the referenced item is not contained in a list or numbered
paragraph, the list label is empty, and the referenced field therefore
displays nothing. If the referenced bookmark or reference contains more
than one paragraph, the list label of the paragraph at which the
bookmark or reference starts is taken.

Additional defined values for the text:reference-format attribute
supported by all references to bookmark's or other reference fields
are:
  - 'number': displays the list label of the referenced item. [...]
  - 'number-all-superior': displays the list label of the referenced
    item and adds the contents of all list labels of superior levels
    in front of it. [...]
  - 'number-no-superior': displays the contents of the list label of
    the referenced item.

Methods:

Name Description
__init__

Create a reference to a content marked by a reference mark

update

Update the content of the reference text field. Currently only

Attributes:

Name Type Description
FORMAT_ALLOWED
name
ref_format str | None
Source code in odfdo/reference.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class Reference(Element):
    """A reference to a content marked by a reference mark, "text:reference-ref"."

    The odf_reference element ("text:reference-ref") represents a field that
    references a "text:reference-mark-start" or "text:reference-mark" element.
    Its text:reference-format attribute specifies what is displayed from the
    referenced element. Default is 'page'
    Actual content is not updated except for the 'text' format by the
    update() method.


    Creation of references can be tricky, consider using this method:
        odfdo.paragraph.insert_reference()

    Values for text:reference-format :
        The defined values for the text:reference-format attribute supported by
        all reference fields are:
          - 'chapter': displays the number of the chapter in which the
            referenced item appears.
          - 'direction': displays whether the referenced item is above or
            below the reference field.
          - 'page': displays the number of the page on which the referenced
            item appears.
          - 'text': displays the text of the referenced item.
        Additional defined values for the text:reference-format attribute
        supported by references to sequence fields are:
          - 'caption': displays the caption in which the sequence is used.
          - 'category-and-value': displays the name and value of the sequence.
          - 'value': displays the value of the sequence.

        References to bookmarks and other references support additional values,
        which display the list label of the referenced item. If the referenced
        item is contained in a list or a numbered paragraph, the list label is
        the formatted number of the paragraph which contains the referenced
        item. If the referenced item is not contained in a list or numbered
        paragraph, the list label is empty, and the referenced field therefore
        displays nothing. If the referenced bookmark or reference contains more
        than one paragraph, the list label of the paragraph at which the
        bookmark or reference starts is taken.

        Additional defined values for the text:reference-format attribute
        supported by all references to bookmark's or other reference fields
        are:
          - 'number': displays the list label of the referenced item. [...]
          - 'number-all-superior': displays the list label of the referenced
            item and adds the contents of all list labels of superior levels
            in front of it. [...]
          - 'number-no-superior': displays the contents of the list label of
            the referenced item.
    """

    _tag = "text:reference-ref"
    _properties = (PropDef("name", "text:ref-name"),)
    FORMAT_ALLOWED = (
        "chapter",
        "direction",
        "page",
        "text",
        "caption",
        "category-and-value",
        "value",
        "number",
        "number-all-superior",
        "number-no-superior",
    )

    def __init__(self, name: str = "", ref_format: str = "", **kwargs: Any) -> None:
        """Create a reference to a content marked by a reference mark
        "text:reference-ref".

        An actual reference mark with the provided name should exist.

        Consider using: odfdo.paragraph.insert_reference()

        The text:ref-name attribute identifies a "text:reference-mark" or
        "text:referencemark-start" element by the value of that element's
        text:name attribute.
        If ref_format is 'text', the current text content of the reference_mark
        is retrieved.

        Arguments:

            name -- str : name of the reference mark

            ref_format -- str : format of the field. Default is 'page', allowed
                            values are 'chapter', 'direction', 'page', 'text',
                            'caption', 'category-and-value', 'value', 'number',
                            'number-all-superior', 'number-no-superior'.
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name
            self.ref_format = ref_format

    @property
    def ref_format(self) -> str | None:
        reference = self.get_attribute("text:reference-format")
        if isinstance(reference, str):
            return reference
        return None

    @ref_format.setter
    def ref_format(self, ref_format: str) -> None:
        """Set the text:reference-format attribute.

        Arguments:

            ref_format -- str
        """
        if not ref_format or ref_format not in self.FORMAT_ALLOWED:
            ref_format = "page"
        self.set_attribute("text:reference-format", ref_format)

    def update(self) -> None:
        """Update the content of the reference text field. Currently only
        'text' format is implemented. Other values, for example the 'page' text
        field, may need to be refreshed through a visual ODF parser.
        """
        ref_format = self.ref_format
        if ref_format != "text":
            # only 'text' is implemented
            return None
        body = self.document_body
        if not body:
            body = self.root  # pragma: nocover
        name = self.name
        reference = body.get_reference_mark(name=name)
        if not reference:
            return None
        # we know it is a ReferenceMarkStart:
        self.text = reference.referenced_text()  # type: ignore

FORMAT_ALLOWED class-attribute instance-attribute

FORMAT_ALLOWED = (
    "chapter",
    "direction",
    "page",
    "text",
    "caption",
    "category-and-value",
    "value",
    "number",
    "number-all-superior",
    "number-no-superior",
)

name instance-attribute

name = name

ref_format property writable

ref_format: str | None

__init__

__init__(
    name: str = "", ref_format: str = "", **kwargs: Any
) -> None

Create a reference to a content marked by a reference mark “text:reference-ref”.

An actual reference mark with the provided name should exist.

Consider using: odfdo.paragraph.insert_reference()

The text:ref-name attribute identifies a “text:reference-mark” or “text:referencemark-start” element by the value of that element’s text:name attribute. If ref_format is ‘text’, the current text content of the reference_mark is retrieved.

Arguments:

name -- str : name of the reference mark

ref_format -- str : format of the field. Default is 'page', allowed
                values are 'chapter', 'direction', 'page', 'text',
                'caption', 'category-and-value', 'value', 'number',
                'number-all-superior', 'number-no-superior'.
Source code in odfdo/reference.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def __init__(self, name: str = "", ref_format: str = "", **kwargs: Any) -> None:
    """Create a reference to a content marked by a reference mark
    "text:reference-ref".

    An actual reference mark with the provided name should exist.

    Consider using: odfdo.paragraph.insert_reference()

    The text:ref-name attribute identifies a "text:reference-mark" or
    "text:referencemark-start" element by the value of that element's
    text:name attribute.
    If ref_format is 'text', the current text content of the reference_mark
    is retrieved.

    Arguments:

        name -- str : name of the reference mark

        ref_format -- str : format of the field. Default is 'page', allowed
                        values are 'chapter', 'direction', 'page', 'text',
                        'caption', 'category-and-value', 'value', 'number',
                        'number-all-superior', 'number-no-superior'.
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name
        self.ref_format = ref_format

update

update() -> None

Update the content of the reference text field. Currently only ‘text’ format is implemented. Other values, for example the ‘page’ text field, may need to be refreshed through a visual ODF parser.

Source code in odfdo/reference.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def update(self) -> None:
    """Update the content of the reference text field. Currently only
    'text' format is implemented. Other values, for example the 'page' text
    field, may need to be refreshed through a visual ODF parser.
    """
    ref_format = self.ref_format
    if ref_format != "text":
        # only 'text' is implemented
        return None
    body = self.document_body
    if not body:
        body = self.root  # pragma: nocover
    name = self.name
    reference = body.get_reference_mark(name=name)
    if not reference:
        return None
    # we know it is a ReferenceMarkStart:
    self.text = reference.referenced_text()  # type: ignore

ReferenceMark

Bases: Element

A point reference, “text:reference-mark”.

A point reference marks a position in text and is represented by a single “text:reference-mark” element.

Methods:

Name Description
__init__

A point reference “text:reference-mark”.

Attributes:

Name Type Description
name
Source code in odfdo/reference.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
class ReferenceMark(Element):
    """A point reference, "text:reference-mark".

    A point reference marks a position in text and is represented by a single
    "text:reference-mark" element.
    """

    _tag = "text:reference-mark"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        """A point reference "text:reference-mark".

        A point reference marks a position in text and is
        represented by a single "text:reference-mark" element.
        Consider using the wrapper: odfdo.paragraph.set_reference_mark()

        Arguments:

            name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None

A point reference “text:reference-mark”.

A point reference marks a position in text and is represented by a single “text:reference-mark” element. Consider using the wrapper: odfdo.paragraph.set_reference_mark()

Arguments:

name -- str
Source code in odfdo/reference.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """A point reference "text:reference-mark".

    A point reference marks a position in text and is
    represented by a single "text:reference-mark" element.
    Consider using the wrapper: odfdo.paragraph.set_reference_mark()

    Arguments:

        name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

ReferenceMarkEnd

Bases: Element

End of a range reference, “text:reference-mark-end”.

Methods:

Name Description
__init__

The “text:reference-mark-end” element represent the end of a range

referenced_text

Return the text between reference-mark-start and reference-mark-end.

Attributes:

Name Type Description
name
Source code in odfdo/reference.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class ReferenceMarkEnd(Element):
    """End of a range reference, "text:reference-mark-end"."""

    _tag = "text:reference-mark-end"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        """The "text:reference-mark-end" element represent the end of a range
        reference.
        Consider using the wrappers: odfdo.paragraph.set_reference_mark() and
        odfdo.paragraph.set_reference_mark_end()

        Arguments:

            name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

    def referenced_text(self) -> str:
        """Return the text between reference-mark-start and reference-mark-end."""
        name = self.name
        request = (
            f"//text()"
            f"[preceding::text:reference-mark-start[@text:name='{name}'] "
            f"and following::text:reference-mark-end[@text:name='{name}']]"
        )
        result = " ".join(str(x) for x in self.xpath(request))
        return result

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None

The “text:reference-mark-end” element represent the end of a range reference. Consider using the wrappers: odfdo.paragraph.set_reference_mark() and odfdo.paragraph.set_reference_mark_end()

Arguments:

name -- str
Source code in odfdo/reference.py
202
203
204
205
206
207
208
209
210
211
212
213
214
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """The "text:reference-mark-end" element represent the end of a range
    reference.
    Consider using the wrappers: odfdo.paragraph.set_reference_mark() and
    odfdo.paragraph.set_reference_mark_end()

    Arguments:

        name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

referenced_text

referenced_text() -> str

Return the text between reference-mark-start and reference-mark-end.

Source code in odfdo/reference.py
216
217
218
219
220
221
222
223
224
225
def referenced_text(self) -> str:
    """Return the text between reference-mark-start and reference-mark-end."""
    name = self.name
    request = (
        f"//text()"
        f"[preceding::text:reference-mark-start[@text:name='{name}'] "
        f"and following::text:reference-mark-end[@text:name='{name}']]"
    )
    result = " ".join(str(x) for x in self.xpath(request))
    return result

ReferenceMarkStart

Bases: Element

Start of a range reference, “text:reference-mark-start”.

Methods:

Name Description
__init__

The “text:reference-mark-start” element represent the start of a range

delete

Delete the given element from the XML tree. If no element is given,

get_referenced

Return the document content between the start and end tags of the

referenced_text

Return the text between reference-mark-start and reference-mark-end.

Attributes:

Name Type Description
name
Source code in odfdo/reference.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
class ReferenceMarkStart(Element):
    """Start of a range reference, "text:reference-mark-start"."""

    _tag = "text:reference-mark-start"
    _properties = (PropDef("name", "text:name"),)

    def __init__(self, name: str = "", **kwargs: Any) -> None:
        """The "text:reference-mark-start" element represent the start of a range
        reference.
        Consider using the wrapper: odfdo.paragraph.set_reference_mark()

        Arguments:

            name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            self.name = name

    def referenced_text(self) -> str:
        """Return the text between reference-mark-start and reference-mark-end."""
        name = self.name
        request = (
            f"//text()"
            f"[preceding::text:reference-mark-start[@text:name='{name}'] "
            f"and following::text:reference-mark-end[@text:name='{name}']]"
        )
        result = " ".join(str(x) for x in self.xpath(request))
        return result

    def get_referenced(
        self,
        no_header: bool = False,
        clean: bool = True,
        as_xml: bool = False,
        as_list: bool = False,
    ) -> Element | list | str | None:
        """Return the document content between the start and end tags of the
        reference. The content returned by this method can spread over several
        headers and paragraphs.
        By default, the content is returned as an "office:text" odf element.


        Arguments:

            no_header -- boolean (default to False), translate existing headers
                         tags "text:h" into paragraphs "text:p".

            clean -- boolean (default to True), suppress unwanted tags. Striped
                     tags are : 'text:change', 'text:change-start',
                     'text:change-end', 'text:reference-mark',
                     'text:reference-mark-start', 'text:reference-mark-end'.

            as_xml -- boolean (default to False), format the returned content as
                      a XML string (serialization).

            as_list -- boolean (default to False), do not embed the returned
                       content in a "office:text'" element, instead simply
                       return a raw list of odf elements.
        """
        if self.parent is None:
            raise ValueError(
                "Reference need some upper document part"
            )  # pragma: nocover
        body = self.document_body
        if not body:
            body = self.parent
        end = body.get_reference_mark_end(name=self.name)
        if end is None:
            raise ValueError("No reference-end found")
        content_list = body.get_between(
            self, end, as_text=False, no_header=no_header, clean=clean
        )
        if as_list:
            return content_list
        referenced = Element.from_tag("office:text")
        for chunk in content_list:
            referenced.append(chunk)
        if as_xml:
            return referenced.serialize()
        else:
            return referenced

    def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
        """Delete the given element from the XML tree. If no element is given,
        "self" is deleted. The XML library may allow to continue to use an
        element now "orphan" as long as you have a reference to it.

        For odf_reference_mark_start : delete the reference-end tag if exists.

        Arguments:

            child -- Element

            keep_tail -- boolean (default to True), True for most usages.
        """
        if child is not None:  # act like normal delete
            return super().delete(child, keep_tail)  # pragma: nocover
        name = self.name
        if self.parent is None:
            raise ValueError("Can't delete the root element")  # pragma: nocover
        body = self.document_body
        if not body:
            body = self.parent  # pragma: nocover
        ref_end = body.get_reference_mark_end(name=name)
        if ref_end:  # pragma: nocover
            ref_end.delete()
        # act like normal delete
        return super().delete()

name instance-attribute

name = name

__init__

__init__(name: str = '', **kwargs: Any) -> None

The “text:reference-mark-start” element represent the start of a range reference. Consider using the wrapper: odfdo.paragraph.set_reference_mark()

Arguments:

name -- str
Source code in odfdo/reference.py
237
238
239
240
241
242
243
244
245
246
247
248
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """The "text:reference-mark-start" element represent the start of a range
    reference.
    Consider using the wrapper: odfdo.paragraph.set_reference_mark()

    Arguments:

        name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.name = name

delete

delete(
    child: Element | None = None, keep_tail: bool = True
) -> None

Delete the given element from the XML tree. If no element is given, “self” is deleted. The XML library may allow to continue to use an element now “orphan” as long as you have a reference to it.

For odf_reference_mark_start : delete the reference-end tag if exists.

Arguments:

child -- Element

keep_tail -- boolean (default to True), True for most usages.
Source code in odfdo/reference.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def delete(self, child: Element | None = None, keep_tail: bool = True) -> None:
    """Delete the given element from the XML tree. If no element is given,
    "self" is deleted. The XML library may allow to continue to use an
    element now "orphan" as long as you have a reference to it.

    For odf_reference_mark_start : delete the reference-end tag if exists.

    Arguments:

        child -- Element

        keep_tail -- boolean (default to True), True for most usages.
    """
    if child is not None:  # act like normal delete
        return super().delete(child, keep_tail)  # pragma: nocover
    name = self.name
    if self.parent is None:
        raise ValueError("Can't delete the root element")  # pragma: nocover
    body = self.document_body
    if not body:
        body = self.parent  # pragma: nocover
    ref_end = body.get_reference_mark_end(name=name)
    if ref_end:  # pragma: nocover
        ref_end.delete()
    # act like normal delete
    return super().delete()

get_referenced

get_referenced(
    no_header: bool = False,
    clean: bool = True,
    as_xml: bool = False,
    as_list: bool = False,
) -> Element | list | str | None

Return the document content between the start and end tags of the reference. The content returned by this method can spread over several headers and paragraphs. By default, the content is returned as an “office:text” odf element.

Arguments:

no_header -- boolean (default to False), translate existing headers
             tags "text:h" into paragraphs "text:p".

clean -- boolean (default to True), suppress unwanted tags. Striped
         tags are : 'text:change', 'text:change-start',
         'text:change-end', 'text:reference-mark',
         'text:reference-mark-start', 'text:reference-mark-end'.

as_xml -- boolean (default to False), format the returned content as
          a XML string (serialization).

as_list -- boolean (default to False), do not embed the returned
           content in a "office:text'" element, instead simply
           return a raw list of odf elements.
Source code in odfdo/reference.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def get_referenced(
    self,
    no_header: bool = False,
    clean: bool = True,
    as_xml: bool = False,
    as_list: bool = False,
) -> Element | list | str | None:
    """Return the document content between the start and end tags of the
    reference. The content returned by this method can spread over several
    headers and paragraphs.
    By default, the content is returned as an "office:text" odf element.


    Arguments:

        no_header -- boolean (default to False), translate existing headers
                     tags "text:h" into paragraphs "text:p".

        clean -- boolean (default to True), suppress unwanted tags. Striped
                 tags are : 'text:change', 'text:change-start',
                 'text:change-end', 'text:reference-mark',
                 'text:reference-mark-start', 'text:reference-mark-end'.

        as_xml -- boolean (default to False), format the returned content as
                  a XML string (serialization).

        as_list -- boolean (default to False), do not embed the returned
                   content in a "office:text'" element, instead simply
                   return a raw list of odf elements.
    """
    if self.parent is None:
        raise ValueError(
            "Reference need some upper document part"
        )  # pragma: nocover
    body = self.document_body
    if not body:
        body = self.parent
    end = body.get_reference_mark_end(name=self.name)
    if end is None:
        raise ValueError("No reference-end found")
    content_list = body.get_between(
        self, end, as_text=False, no_header=no_header, clean=clean
    )
    if as_list:
        return content_list
    referenced = Element.from_tag("office:text")
    for chunk in content_list:
        referenced.append(chunk)
    if as_xml:
        return referenced.serialize()
    else:
        return referenced

referenced_text

referenced_text() -> str

Return the text between reference-mark-start and reference-mark-end.

Source code in odfdo/reference.py
250
251
252
253
254
255
256
257
258
259
def referenced_text(self) -> str:
    """Return the text between reference-mark-start and reference-mark-end."""
    name = self.name
    request = (
        f"//text()"
        f"[preceding::text:reference-mark-start[@text:name='{name}'] "
        f"and following::text:reference-mark-end[@text:name='{name}']]"
    )
    result = " ".join(str(x) for x in self.xpath(request))
    return result

Row

Bases: Element

A row of a table, “table:table-row”.

Methods:

Name Description
__init__

Create a Row, “table:table-row”, optionally filled with

append_cell

Append the given cell at the end of the row. Repeated cells are

clear

Remove text, children and attributes from the Row.

delete_cell

Delete the cell at the given position “x” starting from 0.

extend_cells
force_width

Change the repeated property of the last cell of the row

get_cell

Get the cell at position “x” starting from 0. Alphabetical

get_cells

Get the list of cells matching the criteria.

get_elements
get_sub_elements

Shortcut to get the Elements inside cells in this row.

get_value

Shortcut to get the value of the cell at position “x”.

get_values

Shortcut to get the cell values in this row.

insert_cell

Insert the given cell at position “x” starting from 0. If no cell

is_empty

Return whether every cell in the row has no value or the value

last_cell

Return the las cell of the row.

minimized_width

Return the length of the row if the last repeated sequence is

rstrip

Remove in-place empty cells at the right of the row. An empty

set_cell

Push the cell back in the row at position “x” starting from 0.

set_cells

Set the cells in the row, from the ‘start’ column.

set_value

Shortcut to set the value of the cell at position “x”.

set_values

Shortcut to set the value of cells in the row, from the ‘start’

traverse

Yield as many cell elements as expected cells in the row, i.e.

Attributes:

Name Type Description
append
cells list[Cell]

Get the list of all cells.

clone Row
repeated int | None

Get / set the number of times the row is repeated.

style str | None

Get /set the style of the row itself.

width int

Get the number of expected cells in the row, i.e. addition

y
Source code in odfdo/row.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
class Row(Element):
    """A row of a table, "table:table-row"."""

    _tag = "table:table-row"
    _append = Element.append

    def __init__(
        self,
        width: int | None = None,
        repeated: int | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a Row, "table:table-row", optionally filled with
        "width" number of cells.

        Rows contain cells, their number determine the number of columns.

        You don't generally have to create rows by hand, use the Table API.

        Arguments:

            width -- int

            repeated -- int

            style -- str
        """
        super().__init__(**kwargs)
        self._table_cache = TableCache()
        self._row_cache = RowCache()
        self.y = None
        self._compute_row_cache()
        if self._do_init:
            if width is not None:
                for _i in range(width):
                    self.append(Cell())
            if repeated:
                self.repeated = repeated
            if style is not None:
                self.style = style
            self._compute_row_cache()

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} y={self.y}>"

    def get_elements(self, xpath_query: XPath | str) -> list[Element]:
        element = self._Element__element
        if isinstance(xpath_query, str):
            new_xpath_query = xpath_compile(xpath_query)
            result = new_xpath_query(element)
        else:
            result = xpath_query(element)
        if not isinstance(result, list):  # pragma: no cover
            raise TypeError("Bad XPath result")
        cache = (self._table_cache, self._row_cache)
        return [
            Element.from_tag_for_clone(e, cache)
            for e in result
            if isinstance(e, _Element)
        ]

    def _copy_cache(self, cache: tuple | None) -> None:
        """Copy cache when cloning."""
        self._table_cache = cache[0]
        if cache[1]:  # pragma: no cover
            self._row_cache = cache[1]

    def clear(self) -> None:
        """Remove text, children and attributes from the Row."""
        self._Element__element.clear()
        self._table_cache = TableCache()
        self._row_cache = RowCache()

    def _get_cells(self) -> list[Cell]:
        return self.get_elements(_xpath_cell)  # type: ignore

    def _translate_row_coordinates(
        self,
        coord: tuple | list | str,
    ) -> tuple[int | None, int | None]:
        xyzt = convert_coordinates(coord)
        if len(xyzt) == 2:
            x, z = xyzt
        else:
            x, _, z, __ = xyzt
        if x and x < 0:
            x = increment(x, self.width)
        if z and z < 0:
            z = increment(z, self.width)
        return (x, z)

    def _compute_row_cache(self) -> None:
        idx_repeated_seq = self.elements_repeated_sequence(
            _xpath_cell, "table:number-columns-repeated"
        )
        self._row_cache.make_cell_map(idx_repeated_seq)

    # Public API

    @property
    def clone(self) -> Row:
        clone = Element.clone.fget(self)  # type: ignore
        clone.y = self.y
        clone._table_cache = TableCache.copy(self._table_cache)
        clone._row_cache = RowCache.copy(self._row_cache)
        return clone

    def _set_repeated(self, repeated: int | None) -> None:
        """Method Internal only. Set the numnber of times the row is
        repeated, or None to delete it. Without changing cache.

        Arguments:

            repeated -- int
        """
        if repeated is None or repeated < 2:
            with contextlib.suppress(KeyError):
                self.del_attribute("table:number-rows-repeated")
            return
        self.set_attribute("table:number-rows-repeated", str(repeated))

    @property
    def repeated(self) -> int | None:
        """Get / set the number of times the row is repeated.

        Always None when using the table API.

        Return: int or None
        """
        repeated = self.get_attribute("table:number-rows-repeated")
        if repeated is None:
            return None
        return int(repeated)

    @repeated.setter
    def repeated(self, repeated: int | None) -> None:
        self._set_repeated(repeated)
        # update cache
        current: Element = self
        while True:
            # look for Table, parent may be group of rows
            upper = current.parent
            if not upper:
                # lonely row
                return
            # parent may be group of rows, not table
            if isinstance(upper, Element) and upper._tag == "table:table":
                upper._table_cache = self._table_cache  # type: ignore
                upper._compute_table_cache()  # type: ignore
                return
            current = upper

    @property
    def style(self) -> str | None:
        """Get /set the style of the row itself.

        Return: str
        """
        return self.get_attribute("table:style-name")  # type: ignore

    @style.setter
    def style(self, style: str | Element) -> None:
        self.set_style_attribute("table:style-name", style)

    @property
    def width(self) -> int:
        """Get the number of expected cells in the row, i.e. addition
        repetitions.

        Return: int
        """
        return self._row_cache.width()

    def _translate_x_from_any(self, x: str | int) -> int:
        return translate_from_any(x, self.width, 0)

    def _yield_odf_cells(self):
        for cell in self._get_cells():
            if cell.repeated is None:
                yield cell
            else:
                for _ in range(cell.repeated):
                    cell_copy = cell.clone
                    cell_copy.repeated = None
                    yield cell_copy

    def traverse(
        self,
        start: int | None = None,
        end: int | None = None,
    ) -> Iterator[Cell]:
        """Yield as many cell elements as expected cells in the row, i.e.
        expand repetitions by returning the same cell as many times as
        necessary.

            Arguments:

                start -- int

                end -- int

        Copies are returned, use set_cell() to push them back.
        """
        if start is None:
            start = 0
        start = max(0, start)
        if end is None:
            end = 2**32
        if end < start:
            return
        x = -1
        for cell in self._yield_odf_cells():
            x += 1
            if x < start:
                continue
            if x > end:
                return
            cell.x = x
            cell.y = self.y
            yield cell

    def get_cells(
        self,
        coord: str | tuple | None = None,
        style: str | None = None,
        content: str | None = None,
        cell_type: str | None = None,
    ) -> list[Cell]:
        """Get the list of cells matching the criteria.

        Filter by cell_type, with cell_type 'all' will retrieve cells of any
        type, aka non empty cells.

        Filter by coordinates will retrieve the amount of cells defined by
        'coord', minus the other filters.

        Arguments:

            coord -- str or tuple of int : coordinates

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            content -- str regex

            style -- str

        Return: list of Cell
        """
        # fixme : not clones ?
        if coord:
            x, z = self._translate_row_coordinates(coord)
        else:
            x = None
            z = None
        if cell_type:
            cell_type = cell_type.lower().strip()
        cells: list[Cell] = []
        for cell in self.traverse(start=x, end=z):
            # Filter the cells by cell_type
            if cell_type:
                ctype = cell.type
                if not ctype or not (ctype == cell_type or cell_type == "all"):
                    continue
            # Filter the cells with the regex
            if content and not cell.match(content):
                continue
            # Filter the cells with the style
            if style and style != cell.style:
                continue
            cells.append(cell)
        return cells

    @property
    def cells(self) -> list[Cell]:
        """Get the list of all cells.

        Return: list of Cell
        """
        # fixme : not clones ?
        return list(self.traverse())

    def _get_cell2(self, x: int, clone: bool = True) -> Cell | None:
        if x >= self.width:
            return Cell()
        if clone:
            return self._get_cell2_base(x).clone  # type: ignore
        else:
            return self._get_cell2_base(x)

    def _get_cell2_base(self, x: int) -> Cell | None:
        idx = self._row_cache.cell_idx(x)
        if idx is None:
            return None
        cell = self._row_cache.cached_cell(idx)
        if cell is None:
            cell = self._get_element_idx2(_XP_CELL_IDX, idx)
            self._row_cache.store_cell(cell, idx)
        return cell

    def get_cell(self, x: int, clone: bool = True) -> Cell | None:
        """Get the cell at position "x" starting from 0. Alphabetical
        positions like "D" are accepted.

        A  copy is returned, use set_cell() to push it back.

        Arguments:

            x -- int or str

        Return: Cell | None
        """
        x = self._translate_x_from_any(x)
        cell = self._get_cell2(x, clone=clone)
        if not cell:
            return None  # pragma: no cover
        cell.y = self.y
        cell.x = x
        return cell

    def get_value(
        self,
        x: int | str,
        get_type: bool = False,
    ) -> Any | tuple[Any, str]:
        """Shortcut to get the value of the cell at position "x".
        If get_type is True, returns the tuples (value, ODF type).

        If the cell is empty, returns None or (None, None)

        See get_cell() and Cell.get_value().
        """
        if get_type:
            x = self._translate_x_from_any(x)
            cell = self._get_cell2_base(x)
            if cell is None:
                return (None, None)
            return cell.get_value(get_type=get_type)
        x = self._translate_x_from_any(x)
        cell = self._get_cell2_base(x)
        if cell is None:
            return None
        return cell.get_value()

    def set_cell(
        self,
        x: int | str,
        cell: Cell | None = None,
        clone: bool = True,
    ) -> Cell:
        """Push the cell back in the row at position "x" starting from 0.
        Alphabetical positions like "D" are accepted.

        Arguments:

            x -- int or str

        returns the cell with x and y updated
        """
        cell_back: Cell
        if cell is None:
            cell = Cell()
            repeated = 1
            clone = False
        else:
            repeated = cell.repeated or 1
        x = self._translate_x_from_any(x)
        # Outside the defined row
        diff = x - self.width
        if diff == 0:
            cell_back = self.append_cell(cell, _repeated=repeated, clone=clone)
        elif diff > 0:
            self.append_cell(Cell(repeated=diff), _repeated=diff, clone=False)
            cell_back = self.append_cell(cell, _repeated=repeated, clone=clone)
        else:
            # Inside the defined row
            cell_back = self._row_cache.set_cell_in_cache(x, cell, self, clone=clone)
            cell_back.x = x
            cell_back.y = self.y
        return cell_back

    def set_value(
        self,
        x: int | str,
        value: Any,
        style: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
    ) -> None:
        """Shortcut to set the value of the cell at position "x".

        Arguments:

            x -- int or str

            value -- Python type

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

            currency -- three-letter str

            style -- str

        See get_cell() and Cell.get_value().
        """
        self.set_cell(
            x,
            Cell(value, style=style, cell_type=cell_type, currency=currency),
            clone=False,
        )

    def insert_cell(
        self,
        x: int | str,
        cell: Cell | None = None,
        clone: bool = True,
    ) -> Cell:
        """Insert the given cell at position "x" starting from 0. If no cell
        is given, an empty one is created.

        Alphabetical positions like "D" are accepted.

        Do not use when working on a table, use Table.insert_cell().

        Arguments:

            x -- int or str

            cell -- Cell

        returns the cell with x and y updated
        """
        cell_back: Cell
        if cell is None:
            cell = Cell()
        x = self._translate_x_from_any(x)
        # Outside the defined row
        diff = x - self.width
        if diff < 0:
            cell_back = self._row_cache.insert_cell_in_cache(x, cell, self)
            cell_back.x = x
            cell_back.y = self.y
        elif diff == 0:
            cell_back = self.append_cell(cell, clone=clone)
        else:
            self.append_cell(Cell(repeated=diff), _repeated=diff, clone=False)
            cell_back = self.append_cell(cell, clone=clone)
        return cell_back

    def extend_cells(self, cells: Iterable[Cell] | None = None) -> None:
        if cells is None:
            cells = []
        self.extend(cells)
        self._compute_row_cache()

    def append_cell(
        self,
        cell: Cell | None = None,
        clone: bool = True,
        _repeated: int | None = None,
    ) -> Cell:
        """Append the given cell at the end of the row. Repeated cells are
        accepted. If no cell is given, an empty one is created.

        Do not use when working on a table, use Table.append_cell().

        Arguments:

            cell -- Cell

            _repeated -- (optional), repeated value of the row

        returns the cell with x and y updated
        """
        if cell is None:
            cell = Cell()
            clone = False
        if clone:
            cell = cell.clone
        self._append(cell)
        if _repeated is None:
            _repeated = cell.repeated or 1
        self._row_cache.insert_cell_map_once(_repeated)
        cell.x = self.width - 1
        cell.y = self.y
        return cell

    # fix for unit test and typos
    append = append_cell  # type: ignore

    def delete_cell(self, x: int | str) -> None:
        """Delete the cell at the given position "x" starting from 0.
        Alphabetical positions like "D" are accepted.

        Cells on the right will be shifted to the left. In a table, other
        rows remain unaffected.

        Arguments:

            x -- int or str
        """
        x = self._translate_x_from_any(x)
        if x >= self.width:
            return
        self._row_cache.delete_cell_in_cache(x, self)

    def get_values(
        self,
        coord: str | tuple | None = None,
        cell_type: str | None = None,
        complete: bool = False,
        get_type: bool = False,
    ) -> list[Any | tuple[Any, Any]]:
        """Shortcut to get the cell values in this row.

        Filter by cell_type, with cell_type 'all' will retrieve cells of any
        type, aka non empty cells.
        If cell_type is used and complete is True, missing values are
        replaced by None.
        If cell_type is None, complete is always True : with no cell type
        queried, get_values() returns None for each empty cell, the length
        of the list is equal to the length of the row (depending on
        coordinates use).

        If get_type is True, returns a tuple (value, ODF type of value), or
        (None, None) for empty cells if complete is True.

        Filter by coordinates will retrieve the amount of cells defined by
        coordinates with None for empty cells, except when using cell_type.


        Arguments:

            coord -- str or tuple of int : coordinates in row

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            complete -- boolean

            get_type -- boolean

        Return: list of Python types, or list of tuples.
        """
        if coord:
            x, z = self._translate_row_coordinates(coord)
        else:
            x = None
            z = None
        if cell_type:
            cell_type = cell_type.lower().strip()
            values: list[Any | tuple[Any, Any]] = []
            for cell in self.traverse(start=x, end=z):
                # Filter the cells by cell_type
                ctype = cell.type
                if not ctype or not (ctype == cell_type or cell_type == "all"):
                    if complete:
                        if get_type:
                            values.append((None, None))
                        else:
                            values.append(None)
                    continue
                values.append(cell.get_value(get_type=get_type))
            return values
        else:
            return [
                cell.get_value(get_type=get_type)
                for cell in self.traverse(start=x, end=z)
            ]

    def get_sub_elements(
        self,
    ) -> list[Any]:
        """Shortcut to get the Elements inside cells in this row.

        Missing values are replaced by None. Cell type should be always
        'string' when using this method, the length of the list is equal
        to the length of the row.

        Return: list of Elements.
        """
        return [cell.children for cell in self.traverse()]

    def set_cells(
        self,
        cells: list[Cell] | tuple[Cell] | None = None,
        start: int | str = 0,
        clone: bool = True,
    ) -> None:
        """Set the cells in the row, from the 'start' column.
        This method does not clear the row, use row.clear() before to start
        with an empty row.

        Arguments:

            cells -- list of cells

            start -- int or str
        """
        if cells is None:
            cells = []
        if start is None:
            start = 0
        else:
            start = self._translate_x_from_any(start)
        if start == 0 and clone is False and (len(cells) >= self.width):
            self.clear()
            self.extend_cells(cells)
        else:
            x = start
            for cell in cells:
                self.set_cell(x, cell, clone=clone)
                if cell:
                    x += cell.repeated or 1
                else:
                    x += 1

    def set_values(
        self,
        values: list[Any],
        start: int | str = 0,
        style: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
    ) -> None:
        """Shortcut to set the value of cells in the row, from the 'start'
        column vith values.
        This method does not clear the row, use row.clear() before to start
        with an empty row.

        Arguments:

            values -- list of Python types

            start -- int or str

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency' or 'percentage'

            currency -- three-letter str

            style -- cell style
        """
        # fixme : if values n, n+ are same, use repeat
        if start is None:
            start = 0
        else:
            start = self._translate_x_from_any(start)
        if start == 0 and (len(values) >= self.width):
            self.clear()
            cells = [
                Cell(value, style=style, cell_type=cell_type, currency=currency)
                for value in values
            ]
            self.extend_cells(cells)
        else:
            x = start
            for value in values:
                self.set_cell(
                    x,
                    Cell(value, style=style, cell_type=cell_type, currency=currency),
                    clone=False,
                )
                x += 1

    def rstrip(self, aggressive: bool = False) -> None:
        """Remove *in-place* empty cells at the right of the row. An empty
        cell has no value but can have style. If "aggressive" is True, style
        is ignored.

        Arguments:

            aggressive -- bool
        """
        for cell in reversed(self._get_cells()):
            if not cell.is_empty(aggressive=aggressive):
                break
            self.delete(cell)
        self._compute_row_cache()
        self._row_cache.clear_cell_indexes()

    def _current_length(self) -> int:
        """Return the current estimated length of the row.

        Return: int
        """
        idx_repeated_seq = self.elements_repeated_sequence(
            _xpath_cell, "table:number-columns-repeated"
        )
        repeated = [item[1] for item in idx_repeated_seq]
        if repeated:
            return sum(repeated)
        return 1

    def minimized_width(self) -> int:
        """Return the length of the row if the last repeated sequence is
        reduced to one.

        Return: int
        """
        idx_repeated_seq = self.elements_repeated_sequence(
            _xpath_cell, "table:number-columns-repeated"
        )
        repeated = [item[1] for item in idx_repeated_seq]
        if repeated:
            cell = self.last_cell()
            if cell is not None and cell.is_empty(aggressive=True):
                repeated[-1] = 1
            min_width = sum(repeated)
        else:
            min_width = 1
        self._compute_row_cache()
        self._row_cache.clear_cell_indexes()
        return min_width

    def last_cell(self) -> Cell | None:
        """Return the las cell of the row.

        Return Cell | None
        """
        try:
            return self._get_cells()[-1]
        except IndexError:
            return None

    def force_width(self, width: int) -> None:
        """Change the repeated property of the last cell of the row
        to comply with the required max width.

        Arguments:

            width -- int
        """
        cell = self.last_cell()
        if cell is None or not cell.is_empty(aggressive=True):
            return
        repeated = cell.repeated
        if repeated is None:
            return
        # empty repeated cell
        delta = self._current_length() - width
        if delta > 0:
            cell._set_repeated(repeated - delta)
            self._compute_row_cache()

    def is_empty(self, aggressive: bool = False) -> bool:
        """Return whether every cell in the row has no value or the value
        evaluates to False (empty string), and no style.

        If aggressive is True, empty cells with style are considered empty.

        Arguments:

            aggressive -- bool

        Return: bool
        """
        return all(cell.is_empty(aggressive=aggressive) for cell in self._get_cells())

append class-attribute instance-attribute

append = append_cell

cells property

cells: list[Cell]

Get the list of all cells.

Return: list of Cell

clone property

clone: Row

repeated property writable

repeated: int | None

Get / set the number of times the row is repeated.

Always None when using the table API.

Return: int or None

style property writable

style: str | None

Get /set the style of the row itself.

Return: str

width property

width: int

Get the number of expected cells in the row, i.e. addition repetitions.

Return: int

y instance-attribute

y = None

__init__

__init__(
    width: int | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Create a Row, “table:table-row”, optionally filled with “width” number of cells.

Rows contain cells, their number determine the number of columns.

You don’t generally have to create rows by hand, use the Table API.

Arguments:

width -- int

repeated -- int

style -- str
Source code in odfdo/row.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def __init__(
    self,
    width: int | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a Row, "table:table-row", optionally filled with
    "width" number of cells.

    Rows contain cells, their number determine the number of columns.

    You don't generally have to create rows by hand, use the Table API.

    Arguments:

        width -- int

        repeated -- int

        style -- str
    """
    super().__init__(**kwargs)
    self._table_cache = TableCache()
    self._row_cache = RowCache()
    self.y = None
    self._compute_row_cache()
    if self._do_init:
        if width is not None:
            for _i in range(width):
                self.append(Cell())
        if repeated:
            self.repeated = repeated
        if style is not None:
            self.style = style
        self._compute_row_cache()

append_cell

append_cell(
    cell: Cell | None = None,
    clone: bool = True,
    _repeated: int | None = None,
) -> Cell

Append the given cell at the end of the row. Repeated cells are accepted. If no cell is given, an empty one is created.

Do not use when working on a table, use Table.append_cell().

Arguments:

cell -- Cell

_repeated -- (optional), repeated value of the row

returns the cell with x and y updated

Source code in odfdo/row.py
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def append_cell(
    self,
    cell: Cell | None = None,
    clone: bool = True,
    _repeated: int | None = None,
) -> Cell:
    """Append the given cell at the end of the row. Repeated cells are
    accepted. If no cell is given, an empty one is created.

    Do not use when working on a table, use Table.append_cell().

    Arguments:

        cell -- Cell

        _repeated -- (optional), repeated value of the row

    returns the cell with x and y updated
    """
    if cell is None:
        cell = Cell()
        clone = False
    if clone:
        cell = cell.clone
    self._append(cell)
    if _repeated is None:
        _repeated = cell.repeated or 1
    self._row_cache.insert_cell_map_once(_repeated)
    cell.x = self.width - 1
    cell.y = self.y
    return cell

clear

clear() -> None

Remove text, children and attributes from the Row.

Source code in odfdo/row.py
110
111
112
113
114
def clear(self) -> None:
    """Remove text, children and attributes from the Row."""
    self._Element__element.clear()
    self._table_cache = TableCache()
    self._row_cache = RowCache()

delete_cell

delete_cell(x: int | str) -> None

Delete the cell at the given position “x” starting from 0. Alphabetical positions like “D” are accepted.

Cells on the right will be shifted to the left. In a table, other rows remain unaffected.

Arguments:

x -- int or str
Source code in odfdo/row.py
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
def delete_cell(self, x: int | str) -> None:
    """Delete the cell at the given position "x" starting from 0.
    Alphabetical positions like "D" are accepted.

    Cells on the right will be shifted to the left. In a table, other
    rows remain unaffected.

    Arguments:

        x -- int or str
    """
    x = self._translate_x_from_any(x)
    if x >= self.width:
        return
    self._row_cache.delete_cell_in_cache(x, self)

extend_cells

extend_cells(cells: Iterable[Cell] | None = None) -> None
Source code in odfdo/row.py
493
494
495
496
497
def extend_cells(self, cells: Iterable[Cell] | None = None) -> None:
    if cells is None:
        cells = []
    self.extend(cells)
    self._compute_row_cache()

force_width

force_width(width: int) -> None

Change the repeated property of the last cell of the row to comply with the required max width.

Arguments:

width -- int
Source code in odfdo/row.py
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
def force_width(self, width: int) -> None:
    """Change the repeated property of the last cell of the row
    to comply with the required max width.

    Arguments:

        width -- int
    """
    cell = self.last_cell()
    if cell is None or not cell.is_empty(aggressive=True):
        return
    repeated = cell.repeated
    if repeated is None:
        return
    # empty repeated cell
    delta = self._current_length() - width
    if delta > 0:
        cell._set_repeated(repeated - delta)
        self._compute_row_cache()

get_cell

get_cell(x: int, clone: bool = True) -> Cell | None

Get the cell at position “x” starting from 0. Alphabetical positions like “D” are accepted.

A copy is returned, use set_cell() to push it back.

Arguments:

x -- int or str

Return: Cell | None

Source code in odfdo/row.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def get_cell(self, x: int, clone: bool = True) -> Cell | None:
    """Get the cell at position "x" starting from 0. Alphabetical
    positions like "D" are accepted.

    A  copy is returned, use set_cell() to push it back.

    Arguments:

        x -- int or str

    Return: Cell | None
    """
    x = self._translate_x_from_any(x)
    cell = self._get_cell2(x, clone=clone)
    if not cell:
        return None  # pragma: no cover
    cell.y = self.y
    cell.x = x
    return cell

get_cells

get_cells(
    coord: str | tuple | None = None,
    style: str | None = None,
    content: str | None = None,
    cell_type: str | None = None,
) -> list[Cell]

Get the list of cells matching the criteria.

Filter by cell_type, with cell_type ‘all’ will retrieve cells of any type, aka non empty cells.

Filter by coordinates will retrieve the amount of cells defined by ‘coord’, minus the other filters.

Arguments:

coord -- str or tuple of int : coordinates

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

content -- str regex

style -- str

Return: list of Cell

Source code in odfdo/row.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
def get_cells(
    self,
    coord: str | tuple | None = None,
    style: str | None = None,
    content: str | None = None,
    cell_type: str | None = None,
) -> list[Cell]:
    """Get the list of cells matching the criteria.

    Filter by cell_type, with cell_type 'all' will retrieve cells of any
    type, aka non empty cells.

    Filter by coordinates will retrieve the amount of cells defined by
    'coord', minus the other filters.

    Arguments:

        coord -- str or tuple of int : coordinates

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        content -- str regex

        style -- str

    Return: list of Cell
    """
    # fixme : not clones ?
    if coord:
        x, z = self._translate_row_coordinates(coord)
    else:
        x = None
        z = None
    if cell_type:
        cell_type = cell_type.lower().strip()
    cells: list[Cell] = []
    for cell in self.traverse(start=x, end=z):
        # Filter the cells by cell_type
        if cell_type:
            ctype = cell.type
            if not ctype or not (ctype == cell_type or cell_type == "all"):
                continue
        # Filter the cells with the regex
        if content and not cell.match(content):
            continue
        # Filter the cells with the style
        if style and style != cell.style:
            continue
        cells.append(cell)
    return cells

get_elements

get_elements(xpath_query: XPath | str) -> list[Element]
Source code in odfdo/row.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def get_elements(self, xpath_query: XPath | str) -> list[Element]:
    element = self._Element__element
    if isinstance(xpath_query, str):
        new_xpath_query = xpath_compile(xpath_query)
        result = new_xpath_query(element)
    else:
        result = xpath_query(element)
    if not isinstance(result, list):  # pragma: no cover
        raise TypeError("Bad XPath result")
    cache = (self._table_cache, self._row_cache)
    return [
        Element.from_tag_for_clone(e, cache)
        for e in result
        if isinstance(e, _Element)
    ]

get_sub_elements

get_sub_elements() -> list[Any]

Shortcut to get the Elements inside cells in this row.

Missing values are replaced by None. Cell type should be always ‘string’ when using this method, the length of the list is equal to the length of the row.

Return: list of Elements.

Source code in odfdo/row.py
614
615
616
617
618
619
620
621
622
623
624
625
def get_sub_elements(
    self,
) -> list[Any]:
    """Shortcut to get the Elements inside cells in this row.

    Missing values are replaced by None. Cell type should be always
    'string' when using this method, the length of the list is equal
    to the length of the row.

    Return: list of Elements.
    """
    return [cell.children for cell in self.traverse()]

get_value

get_value(
    x: int | str, get_type: bool = False
) -> Any | tuple[Any, str]

Shortcut to get the value of the cell at position “x”. If get_type is True, returns the tuples (value, ODF type).

If the cell is empty, returns None or (None, None)

See get_cell() and Cell.get_value().

Source code in odfdo/row.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def get_value(
    self,
    x: int | str,
    get_type: bool = False,
) -> Any | tuple[Any, str]:
    """Shortcut to get the value of the cell at position "x".
    If get_type is True, returns the tuples (value, ODF type).

    If the cell is empty, returns None or (None, None)

    See get_cell() and Cell.get_value().
    """
    if get_type:
        x = self._translate_x_from_any(x)
        cell = self._get_cell2_base(x)
        if cell is None:
            return (None, None)
        return cell.get_value(get_type=get_type)
    x = self._translate_x_from_any(x)
    cell = self._get_cell2_base(x)
    if cell is None:
        return None
    return cell.get_value()

get_values

get_values(
    coord: str | tuple | None = None,
    cell_type: str | None = None,
    complete: bool = False,
    get_type: bool = False,
) -> list[Any | tuple[Any, Any]]

Shortcut to get the cell values in this row.

Filter by cell_type, with cell_type ‘all’ will retrieve cells of any type, aka non empty cells. If cell_type is used and complete is True, missing values are replaced by None. If cell_type is None, complete is always True : with no cell type queried, get_values() returns None for each empty cell, the length of the list is equal to the length of the row (depending on coordinates use).

If get_type is True, returns a tuple (value, ODF type of value), or (None, None) for empty cells if complete is True.

Filter by coordinates will retrieve the amount of cells defined by coordinates with None for empty cells, except when using cell_type.

Arguments:

coord -- str or tuple of int : coordinates in row

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

complete -- boolean

get_type -- boolean

Return: list of Python types, or list of tuples.

Source code in odfdo/row.py
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
def get_values(
    self,
    coord: str | tuple | None = None,
    cell_type: str | None = None,
    complete: bool = False,
    get_type: bool = False,
) -> list[Any | tuple[Any, Any]]:
    """Shortcut to get the cell values in this row.

    Filter by cell_type, with cell_type 'all' will retrieve cells of any
    type, aka non empty cells.
    If cell_type is used and complete is True, missing values are
    replaced by None.
    If cell_type is None, complete is always True : with no cell type
    queried, get_values() returns None for each empty cell, the length
    of the list is equal to the length of the row (depending on
    coordinates use).

    If get_type is True, returns a tuple (value, ODF type of value), or
    (None, None) for empty cells if complete is True.

    Filter by coordinates will retrieve the amount of cells defined by
    coordinates with None for empty cells, except when using cell_type.


    Arguments:

        coord -- str or tuple of int : coordinates in row

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        complete -- boolean

        get_type -- boolean

    Return: list of Python types, or list of tuples.
    """
    if coord:
        x, z = self._translate_row_coordinates(coord)
    else:
        x = None
        z = None
    if cell_type:
        cell_type = cell_type.lower().strip()
        values: list[Any | tuple[Any, Any]] = []
        for cell in self.traverse(start=x, end=z):
            # Filter the cells by cell_type
            ctype = cell.type
            if not ctype or not (ctype == cell_type or cell_type == "all"):
                if complete:
                    if get_type:
                        values.append((None, None))
                    else:
                        values.append(None)
                continue
            values.append(cell.get_value(get_type=get_type))
        return values
    else:
        return [
            cell.get_value(get_type=get_type)
            for cell in self.traverse(start=x, end=z)
        ]

insert_cell

insert_cell(
    x: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell

Insert the given cell at position “x” starting from 0. If no cell is given, an empty one is created.

Alphabetical positions like “D” are accepted.

Do not use when working on a table, use Table.insert_cell().

Arguments:

x -- int or str

cell -- Cell

returns the cell with x and y updated

Source code in odfdo/row.py
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def insert_cell(
    self,
    x: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell:
    """Insert the given cell at position "x" starting from 0. If no cell
    is given, an empty one is created.

    Alphabetical positions like "D" are accepted.

    Do not use when working on a table, use Table.insert_cell().

    Arguments:

        x -- int or str

        cell -- Cell

    returns the cell with x and y updated
    """
    cell_back: Cell
    if cell is None:
        cell = Cell()
    x = self._translate_x_from_any(x)
    # Outside the defined row
    diff = x - self.width
    if diff < 0:
        cell_back = self._row_cache.insert_cell_in_cache(x, cell, self)
        cell_back.x = x
        cell_back.y = self.y
    elif diff == 0:
        cell_back = self.append_cell(cell, clone=clone)
    else:
        self.append_cell(Cell(repeated=diff), _repeated=diff, clone=False)
        cell_back = self.append_cell(cell, clone=clone)
    return cell_back

is_empty

is_empty(aggressive: bool = False) -> bool

Return whether every cell in the row has no value or the value evaluates to False (empty string), and no style.

If aggressive is True, empty cells with style are considered empty.

Arguments:

aggressive -- bool

Return: bool

Source code in odfdo/row.py
789
790
791
792
793
794
795
796
797
798
799
800
801
def is_empty(self, aggressive: bool = False) -> bool:
    """Return whether every cell in the row has no value or the value
    evaluates to False (empty string), and no style.

    If aggressive is True, empty cells with style are considered empty.

    Arguments:

        aggressive -- bool

    Return: bool
    """
    return all(cell.is_empty(aggressive=aggressive) for cell in self._get_cells())

last_cell

last_cell() -> Cell | None

Return the las cell of the row.

Return Cell | None

Source code in odfdo/row.py
759
760
761
762
763
764
765
766
767
def last_cell(self) -> Cell | None:
    """Return the las cell of the row.

    Return Cell | None
    """
    try:
        return self._get_cells()[-1]
    except IndexError:
        return None

minimized_width

minimized_width() -> int

Return the length of the row if the last repeated sequence is reduced to one.

Return: int

Source code in odfdo/row.py
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
def minimized_width(self) -> int:
    """Return the length of the row if the last repeated sequence is
    reduced to one.

    Return: int
    """
    idx_repeated_seq = self.elements_repeated_sequence(
        _xpath_cell, "table:number-columns-repeated"
    )
    repeated = [item[1] for item in idx_repeated_seq]
    if repeated:
        cell = self.last_cell()
        if cell is not None and cell.is_empty(aggressive=True):
            repeated[-1] = 1
        min_width = sum(repeated)
    else:
        min_width = 1
    self._compute_row_cache()
    self._row_cache.clear_cell_indexes()
    return min_width

rstrip

rstrip(aggressive: bool = False) -> None

Remove in-place empty cells at the right of the row. An empty cell has no value but can have style. If “aggressive” is True, style is ignored.

Arguments:

aggressive -- bool
Source code in odfdo/row.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
def rstrip(self, aggressive: bool = False) -> None:
    """Remove *in-place* empty cells at the right of the row. An empty
    cell has no value but can have style. If "aggressive" is True, style
    is ignored.

    Arguments:

        aggressive -- bool
    """
    for cell in reversed(self._get_cells()):
        if not cell.is_empty(aggressive=aggressive):
            break
        self.delete(cell)
    self._compute_row_cache()
    self._row_cache.clear_cell_indexes()

set_cell

set_cell(
    x: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell

Push the cell back in the row at position “x” starting from 0. Alphabetical positions like “D” are accepted.

Arguments:

x -- int or str

returns the cell with x and y updated

Source code in odfdo/row.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def set_cell(
    self,
    x: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell:
    """Push the cell back in the row at position "x" starting from 0.
    Alphabetical positions like "D" are accepted.

    Arguments:

        x -- int or str

    returns the cell with x and y updated
    """
    cell_back: Cell
    if cell is None:
        cell = Cell()
        repeated = 1
        clone = False
    else:
        repeated = cell.repeated or 1
    x = self._translate_x_from_any(x)
    # Outside the defined row
    diff = x - self.width
    if diff == 0:
        cell_back = self.append_cell(cell, _repeated=repeated, clone=clone)
    elif diff > 0:
        self.append_cell(Cell(repeated=diff), _repeated=diff, clone=False)
        cell_back = self.append_cell(cell, _repeated=repeated, clone=clone)
    else:
        # Inside the defined row
        cell_back = self._row_cache.set_cell_in_cache(x, cell, self, clone=clone)
        cell_back.x = x
        cell_back.y = self.y
    return cell_back

set_cells

set_cells(
    cells: list[Cell] | tuple[Cell] | None = None,
    start: int | str = 0,
    clone: bool = True,
) -> None

Set the cells in the row, from the ‘start’ column. This method does not clear the row, use row.clear() before to start with an empty row.

Arguments:

cells -- list of cells

start -- int or str
Source code in odfdo/row.py
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
def set_cells(
    self,
    cells: list[Cell] | tuple[Cell] | None = None,
    start: int | str = 0,
    clone: bool = True,
) -> None:
    """Set the cells in the row, from the 'start' column.
    This method does not clear the row, use row.clear() before to start
    with an empty row.

    Arguments:

        cells -- list of cells

        start -- int or str
    """
    if cells is None:
        cells = []
    if start is None:
        start = 0
    else:
        start = self._translate_x_from_any(start)
    if start == 0 and clone is False and (len(cells) >= self.width):
        self.clear()
        self.extend_cells(cells)
    else:
        x = start
        for cell in cells:
            self.set_cell(x, cell, clone=clone)
            if cell:
                x += cell.repeated or 1
            else:
                x += 1

set_value

set_value(
    x: int | str,
    value: Any,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None

Shortcut to set the value of the cell at position “x”.

Arguments:

x -- int or str

value -- Python type

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
         'string' or 'time'

currency -- three-letter str

style -- str

See get_cell() and Cell.get_value().

Source code in odfdo/row.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
def set_value(
    self,
    x: int | str,
    value: Any,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None:
    """Shortcut to set the value of the cell at position "x".

    Arguments:

        x -- int or str

        value -- Python type

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                 'string' or 'time'

        currency -- three-letter str

        style -- str

    See get_cell() and Cell.get_value().
    """
    self.set_cell(
        x,
        Cell(value, style=style, cell_type=cell_type, currency=currency),
        clone=False,
    )

set_values

set_values(
    values: list[Any],
    start: int | str = 0,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None

Shortcut to set the value of cells in the row, from the ‘start’ column vith values. This method does not clear the row, use row.clear() before to start with an empty row.

Arguments:

values -- list of Python types

start -- int or str

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency' or 'percentage'

currency -- three-letter str

style -- cell style
Source code in odfdo/row.py
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
def set_values(
    self,
    values: list[Any],
    start: int | str = 0,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None:
    """Shortcut to set the value of cells in the row, from the 'start'
    column vith values.
    This method does not clear the row, use row.clear() before to start
    with an empty row.

    Arguments:

        values -- list of Python types

        start -- int or str

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency' or 'percentage'

        currency -- three-letter str

        style -- cell style
    """
    # fixme : if values n, n+ are same, use repeat
    if start is None:
        start = 0
    else:
        start = self._translate_x_from_any(start)
    if start == 0 and (len(values) >= self.width):
        self.clear()
        cells = [
            Cell(value, style=style, cell_type=cell_type, currency=currency)
            for value in values
        ]
        self.extend_cells(cells)
    else:
        x = start
        for value in values:
            self.set_cell(
                x,
                Cell(value, style=style, cell_type=cell_type, currency=currency),
                clone=False,
            )
            x += 1

traverse

traverse(
    start: int | None = None, end: int | None = None
) -> Iterator[Cell]

Yield as many cell elements as expected cells in the row, i.e. expand repetitions by returning the same cell as many times as necessary.

Arguments:

    start -- int

    end -- int

Copies are returned, use set_cell() to push them back.

Source code in odfdo/row.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def traverse(
    self,
    start: int | None = None,
    end: int | None = None,
) -> Iterator[Cell]:
    """Yield as many cell elements as expected cells in the row, i.e.
    expand repetitions by returning the same cell as many times as
    necessary.

        Arguments:

            start -- int

            end -- int

    Copies are returned, use set_cell() to push them back.
    """
    if start is None:
        start = 0
    start = max(0, start)
    if end is None:
        end = 2**32
    if end < start:
        return
    x = -1
    for cell in self._yield_odf_cells():
        x += 1
        if x < start:
            continue
        if x > end:
            return
        cell.x = x
        cell.y = self.y
        yield cell

RowGroup

Bases: Element

A group of rows with common properties, “table:table-row-group”.

Partial implementation.

The “table:table-row-group” element groups adjacent table rows. Every row group can contain header rows, and nested row groups. A row group can be visible or hidden.

Methods:

Name Description
__init__

Create a group of rows, “table:table-row-group”, optionnaly

Source code in odfdo/row_group.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class RowGroup(Element):
    """A group of rows  with common properties, "table:table-row-group".

    Partial implementation.

    The "table:table-row-group" element groups adjacent table rows.
    Every row group can contain header rows, and nested row groups.
    A row group can be visible or hidden."""

    # TODO
    _tag = "table:table-row-group"

    def __init__(
        self,
        height: int | None = None,
        width: int | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a group of rows, "table:table-row-group", optionnaly
        filled with "height" number of rows, of "width" cells each.

        Row group bear style information applied to a series of rows.

        Arguments:

            height -- int

            width -- int
        """
        super().__init__(**kwargs)
        if self._do_init and height is not None:
            for _i in range(height):
                row = Row(width=width)
                self.append(row)

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}>"

__init__

__init__(
    height: int | None = None,
    width: int | None = None,
    **kwargs: Any,
) -> None

Create a group of rows, “table:table-row-group”, optionnaly filled with “height” number of rows, of “width” cells each.

Row group bear style information applied to a series of rows.

Arguments:

height -- int

width -- int
Source code in odfdo/row_group.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    height: int | None = None,
    width: int | None = None,
    **kwargs: Any,
) -> None:
    """Create a group of rows, "table:table-row-group", optionnaly
    filled with "height" number of rows, of "width" cells each.

    Row group bear style information applied to a series of rows.

    Arguments:

        height -- int

        width -- int
    """
    super().__init__(**kwargs)
    if self._do_init and height is not None:
        for _i in range(height):
            row = Row(width=width)
            self.append(row)

Section

Bases: Element

Section of the text document, “text:section”.

Methods:

Name Description
__init__

Section of the text document, “text:section”.

get_formatted_text

Attributes:

Name Type Description
name
style
Source code in odfdo/section.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Section(Element):
    """Section of the text document, "text:section"."""

    _tag = "text:section"
    _properties = (
        PropDef("style", "text:style-name"),
        PropDef("name", "text:name"),
    )

    def __init__(
        self,
        style: str | None = None,
        name: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Section of the text document, "text:section".

        Arguments:

            style -- str

            name -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            if style:
                self.style = style
            if name:
                self.name = name

    def get_formatted_text(self, context: dict | None = None) -> str:
        result = [element.get_formatted_text(context) for element in self.children]
        result.append("\n")
        return "".join(result)

name instance-attribute

name = name

style instance-attribute

style = style

__init__

__init__(
    style: str | None = None,
    name: str | None = None,
    **kwargs: Any,
) -> None

Section of the text document, “text:section”.

Arguments:

style -- str

name -- str
Source code in odfdo/section.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    style: str | None = None,
    name: str | None = None,
    **kwargs: Any,
) -> None:
    """Section of the text document, "text:section".

    Arguments:

        style -- str

        name -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        if style:
            self.style = style
        if name:
            self.name = name

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/section.py
61
62
63
64
def get_formatted_text(self, context: dict | None = None) -> str:
    result = [element.get_formatted_text(context) for element in self.children]
    result.append("\n")
    return "".join(result)

Spacer

Bases: MDSpacer, Element

Representation of several spaces, “text:s”.

This element shall be used to represent the second and all following “ “ (U+0020, SPACE) characters in a sequence of “ “ (U+0020, SPACE) characters. Note: It is not an error if the character preceding the element is not a white space character, but it is good practice to use this element only for the second and all following SPACE characters in a sequence.

Methods:

Name Description
__init__

Representation of several spaces, “text:s”.

Attributes:

Name Type Description
length int
number
text str

Get / set the text content of the element.

Source code in odfdo/spacer.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class Spacer(MDSpacer, Element):
    """Representation of several spaces, "text:s".

    This element shall be used to represent the second and all following “ “
    (U+0020, SPACE) characters in a sequence of “ “ (U+0020, SPACE) characters.
    Note: It is not an error if the character preceding the element is not a
    white space character, but it is good practice to use this element only for
    the second and all following SPACE characters in a sequence.
    """

    _tag = "text:s"
    _properties: tuple[PropDef, ...] = (PropDef("number", "text:c"),)

    def __init__(self, number: int | None = 1, **kwargs: Any):
        """Representation of several spaces, "text:s".

        Arguments:

            number -- int
        """
        super().__init__(**kwargs)
        if self._do_init:
            if number and number >= 2:
                self.number = str(number)
            else:
                self.number = None

    def __str__(self) -> str:
        return self.text

    @property
    def text(self) -> str:
        """Get / set the text content of the element."""
        return " " * self.length

    @text.setter
    def text(self, text: str | None) -> None:
        if text is None:
            text = ""
        self.length = len(text)

    @property
    def length(self) -> int:
        name = _get_lxml_tag("text:c")
        value = self._Element__element.get(name)
        if value is None:
            return 1  # minimum 1 space
        return int(value)

    @length.setter
    def length(self, value: int | None) -> None:
        name = _get_lxml_tag("text:c")
        if value is None or value < 2:
            with contextlib.suppress(KeyError):
                del self._Element__element.attrib[name]
            return
        self._Element__element.set(name, str(value))

length property writable

length: int

number instance-attribute

number = str(number)

text property writable

text: str

Get / set the text content of the element.

__init__

__init__(number: int | None = 1, **kwargs: Any)

Representation of several spaces, “text:s”.

Arguments:

number -- int
Source code in odfdo/spacer.py
48
49
50
51
52
53
54
55
56
57
58
59
60
def __init__(self, number: int | None = 1, **kwargs: Any):
    """Representation of several spaces, "text:s".

    Arguments:

        number -- int
    """
    super().__init__(**kwargs)
    if self._do_init:
        if number and number >= 2:
            self.number = str(number)
        else:
            self.number = None

Span

Bases: MDSpan, MDParagraph, ParaFormattedTextMixin, ParaMixin, Element

A span tag (syled text in paragraph), “text:span”.

Methods:

Name Description
__init__

Create a span element “text:span” of the given style containing the optional

Attributes:

Name Type Description
style
text
Source code in odfdo/span.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Span(MDSpan, MDParagraph, ParaFormattedTextMixin, ParaMixin, Element):
    """A span tag (syled text in paragraph), "text:span"."""

    _tag = "text:span"
    _properties = (
        PropDef("style", "text:style-name"),
        PropDef("class_names", "text:class-names"),
    )

    def __init__(
        self,
        text: str | None = None,
        style: str | None = None,
        formatted: bool = True,
        **kwargs: Any,
    ) -> None:
        """Create a span element "text:span" of the given style containing the optional
        given text.

        If "formatted" is True (the default), the given text is appended with <CR>,
        <TAB> and multiple spaces replaced by ODF corresponding tags.

        Arguments:

            text -- str

            style -- str

            formatted -- bool
        """
        super().__init__(**kwargs)
        if self._do_init:
            if text:
                if formatted:
                    self.text = ""
                    self.append_plain_text(text)
                else:
                    self.text = self._unformatted(text)
            if style:
                self.style = style

    def __str__(self) -> str:
        return self.inner_text

style instance-attribute

style = style

text instance-attribute

text = ''

__init__

__init__(
    text: str | None = None,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
) -> None

Create a span element “text:span” of the given style containing the optional given text.

If “formatted” is True (the default), the given text is appended with , and multiple spaces replaced by ODF corresponding tags.

Arguments:

text -- str

style -- str

formatted -- bool
Source code in odfdo/span.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def __init__(
    self,
    text: str | None = None,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
) -> None:
    """Create a span element "text:span" of the given style containing the optional
    given text.

    If "formatted" is True (the default), the given text is appended with <CR>,
    <TAB> and multiple spaces replaced by ODF corresponding tags.

    Arguments:

        text -- str

        style -- str

        formatted -- bool
    """
    super().__init__(**kwargs)
    if self._do_init:
        if text:
            if formatted:
                self.text = ""
                self.append_plain_text(text)
            else:
                self.text = self._unformatted(text)
        if style:
            self.style = style

Spreadsheet

Bases: Body

Root of the Spreadsheet document content, “office:spreadsheet”.

Source code in odfdo/body.py
130
131
132
133
134
class Spreadsheet(Body):
    """Root of the Spreadsheet document content, "office:spreadsheet"."""

    _tag: str = "office:spreadsheet"
    _properties: tuple[PropDef, ...] = ()

Style

Bases: Element

Style class for many ODF tags, “style:style”, “number:date-style”,…

Partial list: “style:style”, “number:date-style”, “number:number-style”, “number:percentage-style”, “number:time-style”, “style:font-face”, “style:master-page”, “style:page-layout”, “style:presentation-page-layout”, “text:list-style”, “text:outline-style”, “style:tab-stops” …

Methods:

Name Description
__init__

Create a style of the given family.

del_properties

Delete the given properties, either by list argument or

get_footer_style
get_header_style
get_level_style
get_list_style_properties

Get text properties of style as a dict, with some enhanced values.

get_page_footer

Get the element that contains the footer contents.

get_page_header

Get the element that contains the header contents.

get_properties

Get the mapping of all properties of this style. By default the

get_text_properties

Get text properties of style as a dict, with some enhanced values.

set_background

Set the background color of a text style, or the background color

set_font
set_footer_style
set_header_style
set_level_style

Arguments:

set_page_footer

Create or replace the footer by the given content. It can already

set_page_header

Create or replace the header by the given content. It can already

set_properties

Set the properties of the “area” type of this style. Properties

Attributes:

Name Type Description
display_name
family str | None
master_page
name
next_style
page_layout
parent_style
Source code in odfdo/style.py
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
class Style(Element):
    """Style class for many ODF tags, "style:style", "number:date-style",...

    Partial list:
    "style:style",
    "number:date-style",
    "number:number-style",
    "number:percentage-style",
    "number:time-style",
    "style:font-face",
    "style:master-page",
    "style:page-layout",
    "style:presentation-page-layout",
    "text:list-style",
    "text:outline-style",
    "style:tab-stops"
    ...
    """

    _properties: tuple[PropDef, ...] = (
        PropDef("page_layout", "style:page-layout-name", "master-page"),
        PropDef("next_style", "style:next-style-name", "master-page"),
        PropDef("name", "style:name"),
        PropDef("parent_style", "style:parent-style-name"),
        PropDef("display_name", "style:display-name"),
        PropDef("svg_font_family", "svg:font-family"),
        PropDef("font_family_generic", "style:font-family-generic"),
        PropDef("font_pitch", "style:font-pitch"),
        PropDef("text_style", "text:style-name"),
        PropDef("master_page", "style:master-page-name", "paragraph"),
        PropDef("master_page", "style:master-page-name", "paragraph"),
        PropDef("master_page", "style:master-page-name", "paragraph"),
        # style:tab-stop
        PropDef("style_type", "style:type"),
        PropDef("leader_style", "style:leader-style"),
        PropDef("leader_text", "style:leader-text"),
        PropDef("style_position", "style:position"),
        PropDef("leader_text", "style:position"),
        PropDef("list_style_name", "style:list-style-name"),
        PropDef("style_num_format", "style:num-format"),
    )

    def __init__(
        self,
        family: str | None = None,
        name: str | None = None,
        display_name: str | None = None,
        parent_style: str | None = None,
        # Where properties apply
        area: str | None = None,
        # For family 'text':
        color: str | tuple | None = None,
        background_color: str | tuple | None = None,
        italic: bool = False,
        bold: bool = False,
        # For family 'paragraph'
        master_page: str | None = None,
        # For family 'master-page'
        page_layout: str | None = None,
        next_style: str | None = None,
        # For family 'table-cell'
        data_style: str | None = None,  # unused
        border: str | None = None,
        border_top: str | None = None,
        border_right: str | None = None,
        border_bottom: str | None = None,
        border_left: str | None = None,
        padding: str | None = None,
        padding_top: str | None = None,
        padding_bottom: str | None = None,
        padding_left: str | None = None,
        padding_right: str | None = None,
        shadow: str | None = None,
        # For family 'table-row'
        height: str | None = None,
        use_optimal_height: bool = False,
        # For family 'table-column'
        break_before: str | None = None,
        break_after: str | None = None,
        # for family 'table'
        align: str | None = None,
        # For family 'table-column' or 'table'
        width: str | None = None,
        # For family 'graphic'
        min_height: str | None = None,
        # For family 'font-face'
        font_name: str | None = None,
        font_family: str | None = None,
        font_family_generic: str | None = None,
        font_pitch: str = "variable",
        # Every other property
        **kwargs: Any,
    ) -> None:
        """Create a style of the given family.

        The name is not mandatory at this
        point but will become required when inserting in a document as a common
        style.

        The display name is the name the user sees in an office application.

        The parent_style is the name of the style this style will inherit from.

        To set properties, pass them as keyword arguments. The area properties
        apply to is optional and defaults to the family.

        Arguments:

            family -- 'paragraph', 'text', 'section', 'table', 'table-column',
                      'table-row', 'table-cell', 'table-page', 'chart',
                      'drawing-page', 'graphic', 'presentation',
                      'control', 'ruby', 'list', 'number', 'page-layout'
                      'font-face', or 'master-page'

            name -- str

            display_name -- str

            parent_style -- str

            area -- str

        'text' Properties:

            italic -- bool

            bold -- bool

        'paragraph' Properties:

            master_page -- str

        'master-page' Properties:

            page_layout -- str

            next_style -- str

        'table-cell' Properties:

            border, border_top, border_right, border_bottom, border_left -- str,
            e.g. "0.002cm solid #000000" or 'none'

            padding, padding_top, padding_right, padding_bottom, padding_left -- str,
            e.g. "0.002cm" or 'none'

            shadow -- str, e.g. "#808080 0.176cm 0.176cm"

        'table-row' Properties:

            height -- str, e.g. '5cm'

            use_optimal_height -- bool

        'table-column' Properties:

            width -- str, e.g. '5cm'

            break_before -- 'page', 'column' or 'auto'

            break_after -- 'page', 'column' or 'auto'

        'table' Properties:

            width -- str, e.g. '5cm'

            align -- 'left', 'center', 'margins' or 'right'
        """
        self._family: str | None = None
        tag_or_elem = kwargs.get("tag_or_elem")
        if tag_or_elem is None:
            family = to_str(family)
            if family not in FAMILY_MAPPING:
                raise ValueError(f"Unknown family value: '{family}'")
            kwargs["tag"] = FAMILY_MAPPING[family]
        super().__init__(**kwargs)
        if self._do_init and family not in SUBCLASSED_STYLES:
            kwargs.pop("tag", None)
            kwargs.pop("tag_or_elem", None)
            self.family = family  # relevant test made by property
            # Common attributes
            if name:
                self.name = name
            if display_name:
                self.display_name = display_name
            if parent_style:
                self.parent_style = parent_style
            # Paragraph
            if family == "paragraph":
                if master_page:
                    self.master_page = master_page
            # Master Page
            elif family == "master-page":
                if page_layout:
                    self.page_layout = page_layout
                if next_style:
                    self.next_style = next_style
            # Font face
            elif family == "font-face":
                if not font_name:
                    raise ValueError("A font_name is required for 'font-face' style")
                self.set_font(
                    font_name,
                    family=font_family,
                    family_generic=font_family_generic,
                    pitch=font_pitch,
                )
            # Properties
            if area is None:
                area = family
            area = to_str(area)
            # Text
            if area == "text":
                if color:
                    kwargs["fo:color"] = color
                if background_color:
                    kwargs["fo:background-color"] = background_color
                if italic:
                    kwargs["fo:font-style"] = "italic"
                    kwargs["style:font-style-asian"] = "italic"
                    kwargs["style:font-style-complex"] = "italic"
                if bold:
                    kwargs["fo:font-weight"] = "bold"
                    kwargs["style:font-weight-asian"] = "bold"
                    kwargs["style:font-weight-complex"] = "bold"
            # Table cell
            elif area == "table-cell":
                if border:
                    kwargs["fo:border"] = border
                elif border_top or border_right or border_bottom or border_left:
                    kwargs["fo:border-top"] = border_top or "none"
                    kwargs["fo:border-right"] = border_right or "none"
                    kwargs["fo:border-bottom"] = border_bottom or "none"
                    kwargs["fo:border-left"] = border_left or "none"
                else:  # no border_top, ... neither border are defined
                    pass  # left untouched
                if padding:
                    kwargs["fo:padding"] = padding
                elif padding_top or padding_right or padding_bottom or padding_left:
                    kwargs["fo:padding-top"] = padding_top or "none"
                    kwargs["fo:padding-right"] = padding_right or "none"
                    kwargs["fo:padding-bottom"] = padding_bottom or "none"
                    kwargs["fo:padding-left"] = padding_left or "none"
                else:  # no border_top, ... neither border are defined
                    pass  # left untouched
                if shadow:
                    kwargs["style:shadow"] = shadow
                if background_color:
                    kwargs["fo:background-color"] = background_color
            # Table row
            elif area == "table-row":
                if height:
                    kwargs["style:row-height"] = height
                if use_optimal_height:
                    kwargs["style:use-optimal-row-height"] = Boolean.encode(
                        use_optimal_height
                    )
                if background_color:
                    kwargs["fo:background-color"] = background_color
            # Table column
            elif area == "table-column":
                if width:
                    kwargs["style:column-width"] = width
                if break_before:
                    kwargs["fo:break-before"] = break_before
                if break_after:
                    kwargs["fo:break-after"] = break_after
            # Table
            elif area == "table":
                if width:
                    kwargs["style:width"] = width
                if align:
                    if align not in {"center", "left", "margins", "right"}:
                        raise ValueError(f"Invalid align value: {align!r}")
                    kwargs["table:align"] = align
            # Graphic
            elif area == "graphic":
                if min_height:
                    kwargs["fo:min-height"] = min_height
            # Every other properties
            if kwargs:
                self.set_properties(kwargs, area=area)

    @property
    def family(self) -> str | None:
        if self._family is None:
            self._family = FALSE_FAMILY_MAP_REVERSE.get(
                self.tag, self.get_attribute_string("style:family")
            )
        return self._family

    @family.setter
    def family(self, family: str | None) -> None:
        self._family = family
        if family in FAMILY_ODF_STD and self.tag == "style:style":
            self.set_attribute("style:family", family)

    def __repr__(self) -> str:
        return f"<Style family={self.family} name={self.name}>"

    def __str__(self) -> str:
        return repr(self)

    def get_properties(self, area: str | None = None) -> dict[str, str | dict] | None:
        """Get the mapping of all properties of this style. By default the
        properties of the same family, e.g. a paragraph style and its
        paragraph properties. Specify the area to get the text properties of
        a paragraph style for example.

        Arguments:

            area -- str

        Return: dict
        """
        if area is None:
            area = self.family
        element = self.get_element(f"style:{area}-properties")
        if element is None:
            return None
        properties: dict[str, str | dict] = element.attributes  # type: ignore
        # Nested properties are nested dictionaries
        for child in element.children:
            properties[child.tag] = child.attributes
        return properties

    @staticmethod
    def _update_boolean_styles(props: dict[str, str | bool]) -> None:
        strike = props.get("style:text-line-through-style", "")
        if strike == "none":
            strike = ""
        underline = props.get("style:text-underline-style", "")
        if underline == "none":
            underline = ""
        props.update(
            {
                "color": props.get("fo:color") or "",
                "background_color": props.get("fo:background-color") or "",
                "italic": props.get("fo:font-style", "") == "italic",
                "bold": props.get("fo:font-weight", "") == "bold",
                "fixed": props.get("style:font-pitch", "") == "fixed",
                "underline": bool(underline),
                "strike": bool(strike),
            }
        )

    def get_list_style_properties(self) -> dict[str, str | bool]:
        """Get text properties of style as a dict, with some enhanced values.

        Enhanced values returned:
         - "color": str
         - "background_color": str
         - "italic": bool
         - "bold": bool
         - "fixed": bool
         - "underline": bool
         - "strike": bool

        Return: dict[str, str | bool]
        """
        return self.get_text_properties()

    def get_text_properties(self) -> dict[str, str | bool]:
        """Get text properties of style as a dict, with some enhanced values.

        Enhanced values returned:
         - "color": str
         - "background_color": str
         - "italic": bool
         - "bold": bool
         - "fixed": bool
         - "underline": bool
         - "strike": bool

        Return: dict[str, str | bool]
        """
        props: dict[str, str | bool] = self.get_properties(area="text") or {}
        self._update_boolean_styles(props)
        return props

    def set_properties(
        self,
        properties: dict[str, str | dict] | None = None,
        style: Style | None = None,
        area: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Set the properties of the "area" type of this style. Properties
        are given either as a dict or as named arguments (or both). The area
        is identical to the style family by default. If the properties
        element is missing, it is created.

        Instead of properties, you can pass a style with properties of the
        same area. These will be copied.

        Arguments:

            properties -- dict

            style -- Style

            area -- 'paragraph', 'text'...
        """
        if properties is None:
            properties = {}
        if area is None:
            if isinstance(self.family, bool):
                area = None
            else:
                area = self.family
        element = self.get_element(f"style:{area}-properties")
        if element is None:
            element = Element.from_tag(f"style:{area}-properties")
            self.append(element)
        if properties or kwargs:
            properties = _expand_properties_dict(_merge_dicts(properties, kwargs))
        elif style is not None:
            properties = style.get_properties(area=area)
            if properties is None:
                return
        if properties is None:
            return
        for key, value in properties.items():
            if value is None:
                element.del_attribute(key)
            elif isinstance(value, (str, bool, tuple)):
                element.set_attribute(key, value)

    def del_properties(
        self,
        properties: list[str] | None = None,
        area: str | None = None,
    ) -> None:
        """Delete the given properties, either by list argument or
        positional argument (or both). Remove only from the given area,
        identical to the style family by default.

        Arguments:

            properties -- list

            area -- str
        """
        if properties is None:
            properties = []
        if area is None:
            area = self.family
        element = self.get_element(f"style:{area}-properties")
        if element is None:
            raise ValueError(
                f"properties element is inexistent for: style:{area}-properties"
            )
        for key in _expand_properties_list(properties):
            element.del_attribute(key)

    def set_background(
        self,
        color: str | None = None,
        url: str | None = None,
        position: str | None = "center",
        repeat: str | None = None,
        opacity: str | None = None,
        filter: str | None = None,  # noqa: A002
    ) -> None:
        """Set the background color of a text style, or the background color
        or image of a paragraph style or page layout.

        With no argument, remove any existing background.

        The position is one or two of 'center', 'left', 'right', 'top' or
        'bottom'.

        The repeat is 'no-repeat', 'repeat' or 'stretch'.

        The opacity is a percentage integer (not a string with the '%s' sign)

        The filter is an application-specific filter name defined elsewhere.

        Though this method is defined on the base style class, it will raise
        an error if the style type is not compatible.

        Arguments:

            color -- '#rrggbb'

            url -- str

            position -- str

            repeat -- str

            opacity -- int

            filter -- str
        """
        family = self.family
        if family not in {
            "text",
            "paragraph",
            "page-layout",
            "section",
            "table",
            "table-row",
            "table-cell",
            "graphic",
        }:
            raise TypeError("No background support for this family")
        if url is not None and family == "text":
            raise TypeError("No background image for text styles")
        properties = self.get_element(f"style:{family}-properties")
        bg_image: BackgroundImage | None = None
        if properties is not None:
            bg_image = properties.get_element("style:background-image")  # type:ignore
        # Erasing
        if color is None and url is None:
            if properties is None:
                return
            properties.del_attribute("fo:background-color")
            if bg_image is not None:
                properties.delete(bg_image)
            return
        # Add the properties if necessary
        if properties is None:
            properties = Element.from_tag(f"style:{family}-properties")
            self.append(properties)
        # Add the color...
        if color:
            properties.set_attribute("fo:background-color", color)
            if bg_image is not None:
                properties.delete(bg_image)
        # ... or the background
        elif url:
            properties.set_attribute("fo:background-color", "transparent")
            if bg_image is None:
                bg_image = Element.from_tag("style:background-image")  # type:ignore
                properties.append(bg_image)  # type:ignore
            bg_image.url = url  # type:ignore
            if position:
                bg_image.position = position  # type:ignore
            if repeat:
                bg_image.repeat = repeat  # type:ignore
            if opacity:
                bg_image.opacity = opacity  # type:ignore
            if filter:
                bg_image.filter = filter  # type:ignore

    # list-style only:

    def get_level_style(self, level: int) -> Style | None:
        if self.family != "list":
            return None
        level_styles = (
            "(text:list-level-style-number"
            "|text:list-level-style-bullet"
            "|text:list-level-style-image)"
        )
        return self._filtered_element(level_styles, 0, level=level)  # type: ignore

    def set_level_style(
        self,
        level: int,
        num_format: str | None = None,
        bullet_char: str | None = None,
        url: str | None = None,
        display_levels: int | None = None,
        prefix: str | None = None,
        suffix: str | None = None,
        start_value: int | None = None,
        style: str | None = None,
        clone: Style | None = None,
    ) -> Style | None:
        """
        Arguments:

            level -- int

            num_format (for number) -- int

            bullet_char (for bullet) -- str

            url (for image) -- str

            display_levels -- int

            prefix -- str

            suffix -- str

            start_value -- int

            style -- str

            clone -- List Style

        Return:
            level_style created
        """
        if self.family != "list":
            return None
        # Expected name
        if num_format is not None:
            level_style_name = "text:list-level-style-number"
        elif bullet_char is not None:
            level_style_name = "text:list-level-style-bullet"
        elif url is not None:
            level_style_name = "text:list-level-style-image"
        elif clone is not None:
            level_style_name = clone.tag
        else:
            raise ValueError("unknown level style type")
        was_created = False
        # Cloning or reusing an existing element
        level_style: Style | None = None
        if clone is not None:
            level_style = clone.clone  # type: ignore
            was_created = True
        else:
            level_style = self.get_level_style(level)
            if level_style is None:
                level_style = Element.from_tag(level_style_name)  # type: ignore
                was_created = True
        if level_style is None:
            return None
        # Transmute if the type changed
        if level_style.tag != level_style_name:
            print("Warn: different style", level_style_name, level_style.tag)
            level_style.tag = level_style_name
        # Set the level
        level_style.set_attribute("text:level", str(level))
        # Set the main attribute
        if num_format is not None:
            level_style.set_attribute("fo:num-format", num_format)
        elif bullet_char is not None:
            level_style.set_attribute("text:bullet-char", bullet_char)
        elif url is not None:
            level_style.set_attribute("xlink:href", url)
        # Set attributes
        if prefix:
            level_style.set_attribute("style:num-prefix", prefix)
        if suffix:
            level_style.set_attribute("style:num-suffix", suffix)
        if display_levels:
            level_style.set_attribute("text:display-levels", str(display_levels))
        if start_value:
            level_style.set_attribute("text:start-value", str(start_value))
        if style:
            level_style.text_style = style  # type: ignore
        # Commit the creation
        if was_created:
            self.append(level_style)
        return level_style

    # page-layout only:

    def get_header_style(self) -> Element | None:
        if self.family != "page-layout":
            return None
        return self.get_element("style:header-style")

    def set_header_style(self, new_style: Style) -> None:
        if self.family != "page-layout":
            return
        header_style = self.get_header_style()
        if header_style is not None:
            self.delete(header_style)
        self.append(new_style)

    def get_footer_style(self) -> Style | None:
        if self.family != "page-layout":
            return None
        return self.get_element("style:footer-style")  # type: ignore

    def set_footer_style(self, new_style: Style) -> None:
        if self.family != "page-layout":
            return
        footer_style = self.get_footer_style()
        if footer_style is not None:
            self.delete(footer_style)
        self.append(new_style)

    # master-page only:

    def _set_header_or_footer(
        self,
        text_or_element: str | Element | list[Element | str],
        name: str = "header",
        style: str = "Header",
    ) -> None:
        if name == "header":
            header_or_footer = self.get_page_header()
        else:
            header_or_footer = self.get_page_footer()
        if header_or_footer is None:
            header_or_footer = Element.from_tag("style:" + name)
            self.append(header_or_footer)
        else:
            header_or_footer.clear()
        if (
            isinstance(text_or_element, Element)
            and text_or_element.tag == f"style:{name}"
        ):
            # Already a header or footer?
            self.delete(header_or_footer)
            self.append(text_or_element)
            return
        if isinstance(text_or_element, (Element, str)):
            elem_list: list[Element | str] = [text_or_element]
        else:
            elem_list = text_or_element
        for item in elem_list:
            if isinstance(item, str):
                paragraph = Element.from_tag("text:p")
                paragraph.append_plain_text(item)  # type: ignore
                paragraph.style = style  # type: ignore
                header_or_footer.append(paragraph)
            elif isinstance(item, Element):
                header_or_footer.append(item)

    def get_page_header(self) -> Element | None:
        """Get the element that contains the header contents.

        If None, no header was set.
        """
        if self.family != "master-page":
            return None
        return self.get_element("style:header")

    def set_page_header(
        self,
        text_or_element: str | Element | list[Element | str],
    ) -> None:
        """Create or replace the header by the given content. It can already
        be a complete header.

        If you only want to update the existing header, get it and use the
        API.

        Arguments:

            text_or_element -- str or Element or a list of them
        """
        if self.family != "master-page":
            return None
        self._set_header_or_footer(text_or_element)

    def get_page_footer(self) -> Element | None:
        """Get the element that contains the footer contents.

        If None, no footer was set.
        """
        if self.family != "master-page":
            return None
        return self.get_element("style:footer")

    def set_page_footer(
        self,
        text_or_element: str | Element | list[Element | str],
    ) -> None:
        """Create or replace the footer by the given content. It can already
        be a complete footer.

        If you only want to update the existing footer, get it and use the
        API.

        Arguments:

            text_or_element -- str or Element or a list of them
        """
        if self.family != "master-page":
            return None
        self._set_header_or_footer(text_or_element, name="footer", style="Footer")

    # font-face only:

    def set_font(
        self,
        name: str,
        family: str | None = None,
        family_generic: str | None = None,
        pitch: str = "variable",
    ) -> None:
        if self.family != "font-face":
            return
        self.name = name
        if family is None:
            family = name
        self.svg_font_family = f'"{family}"'
        if family_generic is not None:
            self.font_family_generic = family_generic
        self.font_pitch = pitch

display_name instance-attribute

display_name = display_name

family property writable

family: str | None

master_page instance-attribute

master_page = master_page

name instance-attribute

name = name

next_style instance-attribute

next_style = next_style

page_layout instance-attribute

page_layout = page_layout

parent_style instance-attribute

parent_style = parent_style

__init__

__init__(
    family: str | None = None,
    name: str | None = None,
    display_name: str | None = None,
    parent_style: str | None = None,
    area: str | None = None,
    color: str | tuple | None = None,
    background_color: str | tuple | None = None,
    italic: bool = False,
    bold: bool = False,
    master_page: str | None = None,
    page_layout: str | None = None,
    next_style: str | None = None,
    data_style: str | None = None,
    border: str | None = None,
    border_top: str | None = None,
    border_right: str | None = None,
    border_bottom: str | None = None,
    border_left: str | None = None,
    padding: str | None = None,
    padding_top: str | None = None,
    padding_bottom: str | None = None,
    padding_left: str | None = None,
    padding_right: str | None = None,
    shadow: str | None = None,
    height: str | None = None,
    use_optimal_height: bool = False,
    break_before: str | None = None,
    break_after: str | None = None,
    align: str | None = None,
    width: str | None = None,
    min_height: str | None = None,
    font_name: str | None = None,
    font_family: str | None = None,
    font_family_generic: str | None = None,
    font_pitch: str = "variable",
    **kwargs: Any,
) -> None

Create a style of the given family.

The name is not mandatory at this point but will become required when inserting in a document as a common style.

The display name is the name the user sees in an office application.

The parent_style is the name of the style this style will inherit from.

To set properties, pass them as keyword arguments. The area properties apply to is optional and defaults to the family.

Arguments:

family -- 'paragraph', 'text', 'section', 'table', 'table-column',
          'table-row', 'table-cell', 'table-page', 'chart',
          'drawing-page', 'graphic', 'presentation',
          'control', 'ruby', 'list', 'number', 'page-layout'
          'font-face', or 'master-page'

name -- str

display_name -- str

parent_style -- str

area -- str

‘text’ Properties:

italic -- bool

bold -- bool

‘paragraph’ Properties:

master_page -- str

‘master-page’ Properties:

page_layout -- str

next_style -- str

‘table-cell’ Properties:

border, border_top, border_right, border_bottom, border_left -- str,
e.g. "0.002cm solid #000000" or 'none'

padding, padding_top, padding_right, padding_bottom, padding_left -- str,
e.g. "0.002cm" or 'none'

shadow -- str, e.g. "#808080 0.176cm 0.176cm"

‘table-row’ Properties:

height -- str, e.g. '5cm'

use_optimal_height -- bool

‘table-column’ Properties:

width -- str, e.g. '5cm'

break_before -- 'page', 'column' or 'auto'

break_after -- 'page', 'column' or 'auto'

‘table’ Properties:

width -- str, e.g. '5cm'

align -- 'left', 'center', 'margins' or 'right'
Source code in odfdo/style.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
def __init__(
    self,
    family: str | None = None,
    name: str | None = None,
    display_name: str | None = None,
    parent_style: str | None = None,
    # Where properties apply
    area: str | None = None,
    # For family 'text':
    color: str | tuple | None = None,
    background_color: str | tuple | None = None,
    italic: bool = False,
    bold: bool = False,
    # For family 'paragraph'
    master_page: str | None = None,
    # For family 'master-page'
    page_layout: str | None = None,
    next_style: str | None = None,
    # For family 'table-cell'
    data_style: str | None = None,  # unused
    border: str | None = None,
    border_top: str | None = None,
    border_right: str | None = None,
    border_bottom: str | None = None,
    border_left: str | None = None,
    padding: str | None = None,
    padding_top: str | None = None,
    padding_bottom: str | None = None,
    padding_left: str | None = None,
    padding_right: str | None = None,
    shadow: str | None = None,
    # For family 'table-row'
    height: str | None = None,
    use_optimal_height: bool = False,
    # For family 'table-column'
    break_before: str | None = None,
    break_after: str | None = None,
    # for family 'table'
    align: str | None = None,
    # For family 'table-column' or 'table'
    width: str | None = None,
    # For family 'graphic'
    min_height: str | None = None,
    # For family 'font-face'
    font_name: str | None = None,
    font_family: str | None = None,
    font_family_generic: str | None = None,
    font_pitch: str = "variable",
    # Every other property
    **kwargs: Any,
) -> None:
    """Create a style of the given family.

    The name is not mandatory at this
    point but will become required when inserting in a document as a common
    style.

    The display name is the name the user sees in an office application.

    The parent_style is the name of the style this style will inherit from.

    To set properties, pass them as keyword arguments. The area properties
    apply to is optional and defaults to the family.

    Arguments:

        family -- 'paragraph', 'text', 'section', 'table', 'table-column',
                  'table-row', 'table-cell', 'table-page', 'chart',
                  'drawing-page', 'graphic', 'presentation',
                  'control', 'ruby', 'list', 'number', 'page-layout'
                  'font-face', or 'master-page'

        name -- str

        display_name -- str

        parent_style -- str

        area -- str

    'text' Properties:

        italic -- bool

        bold -- bool

    'paragraph' Properties:

        master_page -- str

    'master-page' Properties:

        page_layout -- str

        next_style -- str

    'table-cell' Properties:

        border, border_top, border_right, border_bottom, border_left -- str,
        e.g. "0.002cm solid #000000" or 'none'

        padding, padding_top, padding_right, padding_bottom, padding_left -- str,
        e.g. "0.002cm" or 'none'

        shadow -- str, e.g. "#808080 0.176cm 0.176cm"

    'table-row' Properties:

        height -- str, e.g. '5cm'

        use_optimal_height -- bool

    'table-column' Properties:

        width -- str, e.g. '5cm'

        break_before -- 'page', 'column' or 'auto'

        break_after -- 'page', 'column' or 'auto'

    'table' Properties:

        width -- str, e.g. '5cm'

        align -- 'left', 'center', 'margins' or 'right'
    """
    self._family: str | None = None
    tag_or_elem = kwargs.get("tag_or_elem")
    if tag_or_elem is None:
        family = to_str(family)
        if family not in FAMILY_MAPPING:
            raise ValueError(f"Unknown family value: '{family}'")
        kwargs["tag"] = FAMILY_MAPPING[family]
    super().__init__(**kwargs)
    if self._do_init and family not in SUBCLASSED_STYLES:
        kwargs.pop("tag", None)
        kwargs.pop("tag_or_elem", None)
        self.family = family  # relevant test made by property
        # Common attributes
        if name:
            self.name = name
        if display_name:
            self.display_name = display_name
        if parent_style:
            self.parent_style = parent_style
        # Paragraph
        if family == "paragraph":
            if master_page:
                self.master_page = master_page
        # Master Page
        elif family == "master-page":
            if page_layout:
                self.page_layout = page_layout
            if next_style:
                self.next_style = next_style
        # Font face
        elif family == "font-face":
            if not font_name:
                raise ValueError("A font_name is required for 'font-face' style")
            self.set_font(
                font_name,
                family=font_family,
                family_generic=font_family_generic,
                pitch=font_pitch,
            )
        # Properties
        if area is None:
            area = family
        area = to_str(area)
        # Text
        if area == "text":
            if color:
                kwargs["fo:color"] = color
            if background_color:
                kwargs["fo:background-color"] = background_color
            if italic:
                kwargs["fo:font-style"] = "italic"
                kwargs["style:font-style-asian"] = "italic"
                kwargs["style:font-style-complex"] = "italic"
            if bold:
                kwargs["fo:font-weight"] = "bold"
                kwargs["style:font-weight-asian"] = "bold"
                kwargs["style:font-weight-complex"] = "bold"
        # Table cell
        elif area == "table-cell":
            if border:
                kwargs["fo:border"] = border
            elif border_top or border_right or border_bottom or border_left:
                kwargs["fo:border-top"] = border_top or "none"
                kwargs["fo:border-right"] = border_right or "none"
                kwargs["fo:border-bottom"] = border_bottom or "none"
                kwargs["fo:border-left"] = border_left or "none"
            else:  # no border_top, ... neither border are defined
                pass  # left untouched
            if padding:
                kwargs["fo:padding"] = padding
            elif padding_top or padding_right or padding_bottom or padding_left:
                kwargs["fo:padding-top"] = padding_top or "none"
                kwargs["fo:padding-right"] = padding_right or "none"
                kwargs["fo:padding-bottom"] = padding_bottom or "none"
                kwargs["fo:padding-left"] = padding_left or "none"
            else:  # no border_top, ... neither border are defined
                pass  # left untouched
            if shadow:
                kwargs["style:shadow"] = shadow
            if background_color:
                kwargs["fo:background-color"] = background_color
        # Table row
        elif area == "table-row":
            if height:
                kwargs["style:row-height"] = height
            if use_optimal_height:
                kwargs["style:use-optimal-row-height"] = Boolean.encode(
                    use_optimal_height
                )
            if background_color:
                kwargs["fo:background-color"] = background_color
        # Table column
        elif area == "table-column":
            if width:
                kwargs["style:column-width"] = width
            if break_before:
                kwargs["fo:break-before"] = break_before
            if break_after:
                kwargs["fo:break-after"] = break_after
        # Table
        elif area == "table":
            if width:
                kwargs["style:width"] = width
            if align:
                if align not in {"center", "left", "margins", "right"}:
                    raise ValueError(f"Invalid align value: {align!r}")
                kwargs["table:align"] = align
        # Graphic
        elif area == "graphic":
            if min_height:
                kwargs["fo:min-height"] = min_height
        # Every other properties
        if kwargs:
            self.set_properties(kwargs, area=area)

del_properties

del_properties(
    properties: list[str] | None = None,
    area: str | None = None,
) -> None

Delete the given properties, either by list argument or positional argument (or both). Remove only from the given area, identical to the style family by default.

Arguments:

properties -- list

area -- str
Source code in odfdo/style.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
def del_properties(
    self,
    properties: list[str] | None = None,
    area: str | None = None,
) -> None:
    """Delete the given properties, either by list argument or
    positional argument (or both). Remove only from the given area,
    identical to the style family by default.

    Arguments:

        properties -- list

        area -- str
    """
    if properties is None:
        properties = []
    if area is None:
        area = self.family
    element = self.get_element(f"style:{area}-properties")
    if element is None:
        raise ValueError(
            f"properties element is inexistent for: style:{area}-properties"
        )
    for key in _expand_properties_list(properties):
        element.del_attribute(key)
get_footer_style() -> Style | None
Source code in odfdo/style.py
955
956
957
958
def get_footer_style(self) -> Style | None:
    if self.family != "page-layout":
        return None
    return self.get_element("style:footer-style")  # type: ignore

get_header_style

get_header_style() -> Element | None
Source code in odfdo/style.py
942
943
944
945
def get_header_style(self) -> Element | None:
    if self.family != "page-layout":
        return None
    return self.get_element("style:header-style")

get_level_style

get_level_style(level: int) -> Style | None
Source code in odfdo/style.py
836
837
838
839
840
841
842
843
844
def get_level_style(self, level: int) -> Style | None:
    if self.family != "list":
        return None
    level_styles = (
        "(text:list-level-style-number"
        "|text:list-level-style-bullet"
        "|text:list-level-style-image)"
    )
    return self._filtered_element(level_styles, 0, level=level)  # type: ignore

get_list_style_properties

get_list_style_properties() -> dict[str, str | bool]

Get text properties of style as a dict, with some enhanced values.

Enhanced values returned
  • “color”: str
  • “background_color”: str
  • “italic”: bool
  • “bold”: bool
  • “fixed”: bool
  • “underline”: bool
  • “strike”: bool

Return: dict[str, str | bool]

Source code in odfdo/style.py
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def get_list_style_properties(self) -> dict[str, str | bool]:
    """Get text properties of style as a dict, with some enhanced values.

    Enhanced values returned:
     - "color": str
     - "background_color": str
     - "italic": bool
     - "bold": bool
     - "fixed": bool
     - "underline": bool
     - "strike": bool

    Return: dict[str, str | bool]
    """
    return self.get_text_properties()
get_page_footer() -> Element | None

Get the element that contains the footer contents.

If None, no footer was set.

Source code in odfdo/style.py
1033
1034
1035
1036
1037
1038
1039
1040
def get_page_footer(self) -> Element | None:
    """Get the element that contains the footer contents.

    If None, no footer was set.
    """
    if self.family != "master-page":
        return None
    return self.get_element("style:footer")

get_page_header

get_page_header() -> Element | None

Get the element that contains the header contents.

If None, no header was set.

Source code in odfdo/style.py
1006
1007
1008
1009
1010
1011
1012
1013
def get_page_header(self) -> Element | None:
    """Get the element that contains the header contents.

    If None, no header was set.
    """
    if self.family != "master-page":
        return None
    return self.get_element("style:header")

get_properties

get_properties(
    area: str | None = None,
) -> dict[str, str | dict] | None

Get the mapping of all properties of this style. By default the properties of the same family, e.g. a paragraph style and its paragraph properties. Specify the area to get the text properties of a paragraph style for example.

Arguments:

area -- str

Return: dict

Source code in odfdo/style.py
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
def get_properties(self, area: str | None = None) -> dict[str, str | dict] | None:
    """Get the mapping of all properties of this style. By default the
    properties of the same family, e.g. a paragraph style and its
    paragraph properties. Specify the area to get the text properties of
    a paragraph style for example.

    Arguments:

        area -- str

    Return: dict
    """
    if area is None:
        area = self.family
    element = self.get_element(f"style:{area}-properties")
    if element is None:
        return None
    properties: dict[str, str | dict] = element.attributes  # type: ignore
    # Nested properties are nested dictionaries
    for child in element.children:
        properties[child.tag] = child.attributes
    return properties

get_text_properties

get_text_properties() -> dict[str, str | bool]

Get text properties of style as a dict, with some enhanced values.

Enhanced values returned
  • “color”: str
  • “background_color”: str
  • “italic”: bool
  • “bold”: bool
  • “fixed”: bool
  • “underline”: bool
  • “strike”: bool

Return: dict[str, str | bool]

Source code in odfdo/style.py
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
def get_text_properties(self) -> dict[str, str | bool]:
    """Get text properties of style as a dict, with some enhanced values.

    Enhanced values returned:
     - "color": str
     - "background_color": str
     - "italic": bool
     - "bold": bool
     - "fixed": bool
     - "underline": bool
     - "strike": bool

    Return: dict[str, str | bool]
    """
    props: dict[str, str | bool] = self.get_properties(area="text") or {}
    self._update_boolean_styles(props)
    return props

set_background

set_background(
    color: str | None = None,
    url: str | None = None,
    position: str | None = "center",
    repeat: str | None = None,
    opacity: str | None = None,
    filter: str | None = None,
) -> None

Set the background color of a text style, or the background color or image of a paragraph style or page layout.

With no argument, remove any existing background.

The position is one or two of ‘center’, ‘left’, ‘right’, ‘top’ or ‘bottom’.

The repeat is ‘no-repeat’, ‘repeat’ or ‘stretch’.

The opacity is a percentage integer (not a string with the ‘%s’ sign)

The filter is an application-specific filter name defined elsewhere.

Though this method is defined on the base style class, it will raise an error if the style type is not compatible.

Arguments:

color -- '#rrggbb'

url -- str

position -- str

repeat -- str

opacity -- int

filter -- str
Source code in odfdo/style.py
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
def set_background(
    self,
    color: str | None = None,
    url: str | None = None,
    position: str | None = "center",
    repeat: str | None = None,
    opacity: str | None = None,
    filter: str | None = None,  # noqa: A002
) -> None:
    """Set the background color of a text style, or the background color
    or image of a paragraph style or page layout.

    With no argument, remove any existing background.

    The position is one or two of 'center', 'left', 'right', 'top' or
    'bottom'.

    The repeat is 'no-repeat', 'repeat' or 'stretch'.

    The opacity is a percentage integer (not a string with the '%s' sign)

    The filter is an application-specific filter name defined elsewhere.

    Though this method is defined on the base style class, it will raise
    an error if the style type is not compatible.

    Arguments:

        color -- '#rrggbb'

        url -- str

        position -- str

        repeat -- str

        opacity -- int

        filter -- str
    """
    family = self.family
    if family not in {
        "text",
        "paragraph",
        "page-layout",
        "section",
        "table",
        "table-row",
        "table-cell",
        "graphic",
    }:
        raise TypeError("No background support for this family")
    if url is not None and family == "text":
        raise TypeError("No background image for text styles")
    properties = self.get_element(f"style:{family}-properties")
    bg_image: BackgroundImage | None = None
    if properties is not None:
        bg_image = properties.get_element("style:background-image")  # type:ignore
    # Erasing
    if color is None and url is None:
        if properties is None:
            return
        properties.del_attribute("fo:background-color")
        if bg_image is not None:
            properties.delete(bg_image)
        return
    # Add the properties if necessary
    if properties is None:
        properties = Element.from_tag(f"style:{family}-properties")
        self.append(properties)
    # Add the color...
    if color:
        properties.set_attribute("fo:background-color", color)
        if bg_image is not None:
            properties.delete(bg_image)
    # ... or the background
    elif url:
        properties.set_attribute("fo:background-color", "transparent")
        if bg_image is None:
            bg_image = Element.from_tag("style:background-image")  # type:ignore
            properties.append(bg_image)  # type:ignore
        bg_image.url = url  # type:ignore
        if position:
            bg_image.position = position  # type:ignore
        if repeat:
            bg_image.repeat = repeat  # type:ignore
        if opacity:
            bg_image.opacity = opacity  # type:ignore
        if filter:
            bg_image.filter = filter  # type:ignore

set_font

set_font(
    name: str,
    family: str | None = None,
    family_generic: str | None = None,
    pitch: str = "variable",
) -> None
Source code in odfdo/style.py
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
def set_font(
    self,
    name: str,
    family: str | None = None,
    family_generic: str | None = None,
    pitch: str = "variable",
) -> None:
    if self.family != "font-face":
        return
    self.name = name
    if family is None:
        family = name
    self.svg_font_family = f'"{family}"'
    if family_generic is not None:
        self.font_family_generic = family_generic
    self.font_pitch = pitch
set_footer_style(new_style: Style) -> None
Source code in odfdo/style.py
960
961
962
963
964
965
966
def set_footer_style(self, new_style: Style) -> None:
    if self.family != "page-layout":
        return
    footer_style = self.get_footer_style()
    if footer_style is not None:
        self.delete(footer_style)
    self.append(new_style)

set_header_style

set_header_style(new_style: Style) -> None
Source code in odfdo/style.py
947
948
949
950
951
952
953
def set_header_style(self, new_style: Style) -> None:
    if self.family != "page-layout":
        return
    header_style = self.get_header_style()
    if header_style is not None:
        self.delete(header_style)
    self.append(new_style)

set_level_style

set_level_style(
    level: int,
    num_format: str | None = None,
    bullet_char: str | None = None,
    url: str | None = None,
    display_levels: int | None = None,
    prefix: str | None = None,
    suffix: str | None = None,
    start_value: int | None = None,
    style: str | None = None,
    clone: Style | None = None,
) -> Style | None

Arguments:

level -- int

num_format (for number) -- int

bullet_char (for bullet) -- str

url (for image) -- str

display_levels -- int

prefix -- str

suffix -- str

start_value -- int

style -- str

clone -- List Style
Return

level_style created

Source code in odfdo/style.py
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
def set_level_style(
    self,
    level: int,
    num_format: str | None = None,
    bullet_char: str | None = None,
    url: str | None = None,
    display_levels: int | None = None,
    prefix: str | None = None,
    suffix: str | None = None,
    start_value: int | None = None,
    style: str | None = None,
    clone: Style | None = None,
) -> Style | None:
    """
    Arguments:

        level -- int

        num_format (for number) -- int

        bullet_char (for bullet) -- str

        url (for image) -- str

        display_levels -- int

        prefix -- str

        suffix -- str

        start_value -- int

        style -- str

        clone -- List Style

    Return:
        level_style created
    """
    if self.family != "list":
        return None
    # Expected name
    if num_format is not None:
        level_style_name = "text:list-level-style-number"
    elif bullet_char is not None:
        level_style_name = "text:list-level-style-bullet"
    elif url is not None:
        level_style_name = "text:list-level-style-image"
    elif clone is not None:
        level_style_name = clone.tag
    else:
        raise ValueError("unknown level style type")
    was_created = False
    # Cloning or reusing an existing element
    level_style: Style | None = None
    if clone is not None:
        level_style = clone.clone  # type: ignore
        was_created = True
    else:
        level_style = self.get_level_style(level)
        if level_style is None:
            level_style = Element.from_tag(level_style_name)  # type: ignore
            was_created = True
    if level_style is None:
        return None
    # Transmute if the type changed
    if level_style.tag != level_style_name:
        print("Warn: different style", level_style_name, level_style.tag)
        level_style.tag = level_style_name
    # Set the level
    level_style.set_attribute("text:level", str(level))
    # Set the main attribute
    if num_format is not None:
        level_style.set_attribute("fo:num-format", num_format)
    elif bullet_char is not None:
        level_style.set_attribute("text:bullet-char", bullet_char)
    elif url is not None:
        level_style.set_attribute("xlink:href", url)
    # Set attributes
    if prefix:
        level_style.set_attribute("style:num-prefix", prefix)
    if suffix:
        level_style.set_attribute("style:num-suffix", suffix)
    if display_levels:
        level_style.set_attribute("text:display-levels", str(display_levels))
    if start_value:
        level_style.set_attribute("text:start-value", str(start_value))
    if style:
        level_style.text_style = style  # type: ignore
    # Commit the creation
    if was_created:
        self.append(level_style)
    return level_style
set_page_footer(
    text_or_element: str | Element | list[Element | str],
) -> None

Create or replace the footer by the given content. It can already be a complete footer.

If you only want to update the existing footer, get it and use the API.

Arguments:

text_or_element -- str or Element or a list of them
Source code in odfdo/style.py
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
def set_page_footer(
    self,
    text_or_element: str | Element | list[Element | str],
) -> None:
    """Create or replace the footer by the given content. It can already
    be a complete footer.

    If you only want to update the existing footer, get it and use the
    API.

    Arguments:

        text_or_element -- str or Element or a list of them
    """
    if self.family != "master-page":
        return None
    self._set_header_or_footer(text_or_element, name="footer", style="Footer")

set_page_header

set_page_header(
    text_or_element: str | Element | list[Element | str],
) -> None

Create or replace the header by the given content. It can already be a complete header.

If you only want to update the existing header, get it and use the API.

Arguments:

text_or_element -- str or Element or a list of them
Source code in odfdo/style.py
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
def set_page_header(
    self,
    text_or_element: str | Element | list[Element | str],
) -> None:
    """Create or replace the header by the given content. It can already
    be a complete header.

    If you only want to update the existing header, get it and use the
    API.

    Arguments:

        text_or_element -- str or Element or a list of them
    """
    if self.family != "master-page":
        return None
    self._set_header_or_footer(text_or_element)

set_properties

set_properties(
    properties: dict[str, str | dict] | None = None,
    style: Style | None = None,
    area: str | None = None,
    **kwargs: Any,
) -> None

Set the properties of the “area” type of this style. Properties are given either as a dict or as named arguments (or both). The area is identical to the style family by default. If the properties element is missing, it is created.

Instead of properties, you can pass a style with properties of the same area. These will be copied.

Arguments:

properties -- dict

style -- Style

area -- 'paragraph', 'text'...
Source code in odfdo/style.py
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def set_properties(
    self,
    properties: dict[str, str | dict] | None = None,
    style: Style | None = None,
    area: str | None = None,
    **kwargs: Any,
) -> None:
    """Set the properties of the "area" type of this style. Properties
    are given either as a dict or as named arguments (or both). The area
    is identical to the style family by default. If the properties
    element is missing, it is created.

    Instead of properties, you can pass a style with properties of the
    same area. These will be copied.

    Arguments:

        properties -- dict

        style -- Style

        area -- 'paragraph', 'text'...
    """
    if properties is None:
        properties = {}
    if area is None:
        if isinstance(self.family, bool):
            area = None
        else:
            area = self.family
    element = self.get_element(f"style:{area}-properties")
    if element is None:
        element = Element.from_tag(f"style:{area}-properties")
        self.append(element)
    if properties or kwargs:
        properties = _expand_properties_dict(_merge_dicts(properties, kwargs))
    elif style is not None:
        properties = style.get_properties(area=area)
        if properties is None:
            return
    if properties is None:
        return
    for key, value in properties.items():
        if value is None:
            element.del_attribute(key)
        elif isinstance(value, (str, bool, tuple)):
            element.set_attribute(key, value)

Styles

Bases: XmlPart

Representation of the “styles.xml” part.

Methods:

Name Description
get_master_page
get_master_pages
get_style

Return the style uniquely identified by the name/family pair. If

get_styles

Return the list of styles in the Content part, optionally limited

Source code in odfdo/styles.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class Styles(XmlPart):
    """Representation of the "styles.xml" part."""

    def _get_style_contexts(
        self, family: str, automatic: bool = False
    ) -> list[Element]:
        if automatic:
            return [self.get_element("//office:automatic-styles")]
        if not family:
            # All possibilities
            return [
                self.get_element("//office:automatic-styles"),
                self.get_element("//office:styles"),
                self.get_element("//office:master-styles"),
                self.get_element("//office:font-face-decls"),
            ]
        queries = CONTEXT_MAPPING.get(family) or (
            "//office:styles",
            "//office:automatic-styles",
        )
        # if queries is None:
        #     raise ValueError(f"unknown family: {family}")
        return [self.get_element(query) for query in queries]

    def get_styles(self, family: str = "", automatic: bool = False) -> list[Element]:
        """Return the list of styles in the Content part, optionally limited
        to the given family, optionaly limited to automatic styles.

        Arguments:

            family -- str

            automatic -- bool

        Return: list of Style
        """
        result = []
        for context in self._get_style_contexts(family, automatic=automatic):
            if context is None:
                continue
            # print('-ctx----', automatic)
            # print(context.tag)
            # print(context.__class__)
            # print(context.serialize())
            result.extend(context.get_styles(family=family))
        return result

    def get_style(
        self,
        family: str,
        name_or_element: str | Style | None = None,
        display_name: str | None = None,
    ) -> Style | None:
        """Return the style uniquely identified by the name/family pair. If
        the argument is already a style object, it will return it.

        If the name is None, the default style is fetched.

        If the name is not the internal name but the name you gave in the
        desktop application, use display_name instead.

        Arguments:

            family -- 'paragraph', 'text',  'graphic', 'table', 'list',
                      'number', 'page-layout', 'master-page'

            name_or_element -- str, odf_style or None

            display_name -- str or None

        Return: odf_style or None if not found
        """
        for context in self._get_style_contexts(family):
            if context is None:
                continue  # pragma: nocover
            style = context.get_style(
                family,
                name_or_element=name_or_element,
                display_name=display_name,
            )
            if style is not None:
                return style  # type: ignore
        return None

    def get_master_pages(self) -> list[Element]:
        query = make_xpath_query("descendant::style:master-page")
        return self.get_elements(query)  # type:ignore

    def get_master_page(self, position: int = 0) -> Element | None:
        results = self.get_master_pages()
        try:
            return results[position]
        except IndexError:
            return None

get_master_page

get_master_page(position: int = 0) -> Element | None
Source code in odfdo/styles.py
145
146
147
148
149
150
def get_master_page(self, position: int = 0) -> Element | None:
    results = self.get_master_pages()
    try:
        return results[position]
    except IndexError:
        return None

get_master_pages

get_master_pages() -> list[Element]
Source code in odfdo/styles.py
141
142
143
def get_master_pages(self) -> list[Element]:
    query = make_xpath_query("descendant::style:master-page")
    return self.get_elements(query)  # type:ignore

get_style

get_style(
    family: str,
    name_or_element: str | Style | None = None,
    display_name: str | None = None,
) -> Style | None

Return the style uniquely identified by the name/family pair. If the argument is already a style object, it will return it.

If the name is None, the default style is fetched.

If the name is not the internal name but the name you gave in the desktop application, use display_name instead.

Arguments:

family -- 'paragraph', 'text',  'graphic', 'table', 'list',
          'number', 'page-layout', 'master-page'

name_or_element -- str, odf_style or None

display_name -- str or None

Return: odf_style or None if not found

Source code in odfdo/styles.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def get_style(
    self,
    family: str,
    name_or_element: str | Style | None = None,
    display_name: str | None = None,
) -> Style | None:
    """Return the style uniquely identified by the name/family pair. If
    the argument is already a style object, it will return it.

    If the name is None, the default style is fetched.

    If the name is not the internal name but the name you gave in the
    desktop application, use display_name instead.

    Arguments:

        family -- 'paragraph', 'text',  'graphic', 'table', 'list',
                  'number', 'page-layout', 'master-page'

        name_or_element -- str, odf_style or None

        display_name -- str or None

    Return: odf_style or None if not found
    """
    for context in self._get_style_contexts(family):
        if context is None:
            continue  # pragma: nocover
        style = context.get_style(
            family,
            name_or_element=name_or_element,
            display_name=display_name,
        )
        if style is not None:
            return style  # type: ignore
    return None

get_styles

get_styles(
    family: str = "", automatic: bool = False
) -> list[Element]

Return the list of styles in the Content part, optionally limited to the given family, optionaly limited to automatic styles.

Arguments:

family -- str

automatic -- bool

Return: list of Style

Source code in odfdo/styles.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def get_styles(self, family: str = "", automatic: bool = False) -> list[Element]:
    """Return the list of styles in the Content part, optionally limited
    to the given family, optionaly limited to automatic styles.

    Arguments:

        family -- str

        automatic -- bool

    Return: list of Style
    """
    result = []
    for context in self._get_style_contexts(family, automatic=automatic):
        if context is None:
            continue
        # print('-ctx----', automatic)
        # print(context.tag)
        # print(context.__class__)
        # print(context.serialize())
        result.extend(context.get_styles(family=family))
    return result

TOC

Bases: MDToc, Element

A table of content, “text:table-of-content”.

The “text:table-of-content” element represents a table of contents for a document. The items that can be listed in a table of contents are:

  • Headings (as defined by the outline structure of the document), up to a selected level.

  • Table of contents index marks.

  • Paragraphs formatted with specified paragraph styles.

Methods:

Name Description
__init__

Create a table of content “text:table-of-content”.

create_toc_source
fill

Fill the TOC with the titles found in the document. A TOC is not

get_formatted_text
get_title
set_toc_title

Attributes:

Name Type Description
body Element | None
name
outline_level int | None
protected
style
Source code in odfdo/toc.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
class TOC(MDToc, Element):
    """A table of content, "text:table-of-content".

    The "text:table-of-content" element represents a table of contents for a
    document. The items that can be listed in a table of contents are:

      - Headings (as defined by the outline structure of the document), up to
        a selected level.

      - Table of contents index marks.

      - Paragraphs formatted with specified paragraph styles.
    """

    _tag = "text:table-of-content"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "text:style-name"),
        PropDef("xml_id", "xml:id"),
        PropDef("protected", "text:protected"),
        PropDef("protection_key", "text:protection-key"),
        PropDef(
            "protection_key_digest_algorithm", "text:protection-key-digest-algorithm"
        ),
    )

    def __init__(
        self,
        title: str = "Table of Contents",
        name: str | None = None,
        protected: bool = True,
        outline_level: int = 0,
        style: str | None = None,
        title_style: str = "Contents_20_Heading",
        entry_style: str = "Contents_20_%d",
        **kwargs: Any,
    ) -> None:
        """Create a table of content "text:table-of-content".

        Default parameters are what most people use: protected from manual
        modifications and not limited in title levels.

        The name is mandatory and derived automatically from the title if not
        given. Provide one in case of a conflict with other TOCs in the same
        document.

        The "text:table-of-content" element has the following attributes:
        text:name, text:protected, text:protection-key,
        text:protection-key-digest-algorithm, text:style-name and xml:id.

        Arguments:

            title -- str

            name -- str

            protected -- bool

            outline_level -- int

            style -- str

            title_style -- str

            entry_style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            if style:
                self.style = style
            if protected:
                self.protected = protected
            if name is None:
                self.name = f"{title}1"
            # Create the source template
            toc_source = self.create_toc_source(
                title, outline_level, title_style, entry_style
            )
            self.append(toc_source)
            # Create the index body automatically with the index title
            if title:
                # This style is in the template document
                self.set_toc_title(title, text_style=title_style)

    @staticmethod
    def create_toc_source(
        title: str,
        outline_level: int,
        title_style: str,
        entry_style: str,
    ) -> Element:
        toc_source = Element.from_tag("text:table-of-content-source")
        toc_source.set_attribute("text:outline-level", str(outline_level))
        if title:
            title_template = IndexTitleTemplate()
            if title_style:
                # This style is in the template document
                title_template.style = title_style
            title_template.text = title
            toc_source.append(title_template)
        for level in range(1, 11):
            template = TocEntryTemplate(outline_level=level)
            if entry_style:
                template.style = entry_style % level
            toc_source.append(template)
        return toc_source

    def __str__(self) -> str:
        return self.get_formatted_text()

    def get_formatted_text(self, context: dict | None = None) -> str:
        index_body = self.get_element("text:index-body")

        if index_body is None:
            return ""
        if context is None:
            context = {}
        if context.get("rst_mode"):
            return "\n.. contents::\n\n"

        result = []
        for element in index_body.children:
            if element.tag == "text:index-title":
                for child_element in element.children:
                    result.append(child_element.get_formatted_text(context).strip())
            else:
                result.append(element.get_formatted_text(context).strip())
        return "\n".join(x for x in result if x)

    @property
    def outline_level(self) -> int | None:
        source = self.get_element("text:table-of-content-source")
        if source is None:
            return None
        return source.get_attribute_integer("text:outline-level")

    @outline_level.setter
    def outline_level(self, level: int) -> None:
        source = self.get_element("text:table-of-content-source")
        if source is None:
            source = Element.from_tag("text:table-of-content-source")
            self.insert(source, FIRST_CHILD)
        source.set_attribute("text:outline-level", str(level))

    @property
    def body(self) -> Element | None:
        return self.get_element("text:index-body")

    @body.setter
    def body(self, body: Element | None = None) -> Element | None:
        old_body = self.body
        if old_body is not None:
            self.delete(old_body)
        if body is None:
            body = Element.from_tag("text:index-body")
        self.append(body)
        return body

    def get_title(self) -> str:
        index_body = self.body
        if index_body is None:
            return ""
        index_title = index_body.get_element(IndexTitle._tag)
        if index_title is None:
            return ""
        return index_title.text_content

    def set_toc_title(
        self,
        title: str,
        style: str | None = None,
        text_style: str | None = None,
    ) -> None:
        index_body = self.body
        if index_body is None:
            self.body = None
            index_body = self.body
        index_title = index_body.get_element(IndexTitle._tag)  # type: ignore
        if index_title is None:
            name = f"{self.name}_Head"
            index_title = IndexTitle(
                name=name, style=style, title_text=title, text_style=text_style
            )
            index_body.append(index_title)  # type: ignore
        else:
            if style:
                index_title.style = style  # type: ignore
            paragraph = index_title.get_paragraph()
            if paragraph is None:
                paragraph = Paragraph()
                index_title.append(paragraph)
            if text_style:
                paragraph.style = text_style  # type: ignore
            paragraph.text = title

    @staticmethod
    def _header_numbering(level_indexes: dict[int, int], level: int) -> str:
        """Return the header hierarchical number (like "1.2.3.")."""
        numbers: list[int] = []
        # before header level
        for idx in range(1, level):
            numbers.append(level_indexes.setdefault(idx, 1))
        # header level
        index = level_indexes.get(level, 0) + 1
        level_indexes[level] = index
        numbers.append(index)
        # after header level
        idx = level + 1
        while idx in level_indexes:
            del level_indexes[idx]
            idx += 1
        return ".".join(str(x) for x in numbers) + "."

    def fill(
        self,
        document: Document | None = None,
        use_default_styles: bool = True,
    ) -> None:
        """Fill the TOC with the titles found in the document. A TOC is not
        contextual so it will catch all titles before and after its insertion.
        If the TOC is not attached to a document, attach it beforehand or
        provide one as argument.

        For having a pretty TOC, let use_default_styles by default.

        Arguments:

            document -- Document

            use_default_styles -- bool
        """
        # Find the body
        if document is not None:
            body: Element | None = document.body
        else:
            body = self.document_body
        if body is None:
            raise ValueError("The TOC must be related to a document somehow")

        # Save the title
        index_body = self.body
        if index_body is None:
            title = ""
        else:
            title = index_body.get_element("text:index-title")  # type: ignore

        # Clean the old index-body
        self.body = None
        index_body = self.body

        # Restore the title
        if title and str(title):
            index_body.insert(title, position=0)  # type: ignore

        # Insert default TOC style
        if use_default_styles:
            automatic_styles = body.get_element("//office:automatic-styles")
            if isinstance(automatic_styles, Element):  # pragma: nocover
                for level in range(1, 11):
                    if (
                        automatic_styles.get_style(
                            "paragraph", _toc_entry_style_name(level)
                        )
                        is None
                    ):
                        level_style = default_toc_level_style(level)
                        automatic_styles.append(level_style)

        # Auto-fill the index
        outline_level = self.outline_level or 10
        level_indexes: dict[int, int] = {}
        for header in body.headers:
            level = header.get_attribute_integer("text:outline-level") or 0
            if level > outline_level:
                continue
            number_str = self._header_numbering(level_indexes, level)
            # Make the title with "1.2.3. Title" format
            paragraph = Paragraph(f"{number_str} {header}")
            if use_default_styles:
                paragraph.style = _toc_entry_style_name(level)
            index_body.append(paragraph)  # type: ignore

body property writable

body: Element | None

name instance-attribute

name = f'{title}1'

outline_level property writable

outline_level: int | None

protected instance-attribute

protected = protected

style instance-attribute

style = style

__init__

__init__(
    title: str = "Table of Contents",
    name: str | None = None,
    protected: bool = True,
    outline_level: int = 0,
    style: str | None = None,
    title_style: str = "Contents_20_Heading",
    entry_style: str = "Contents_20_%d",
    **kwargs: Any,
) -> None

Create a table of content “text:table-of-content”.

Default parameters are what most people use: protected from manual modifications and not limited in title levels.

The name is mandatory and derived automatically from the title if not given. Provide one in case of a conflict with other TOCs in the same document.

The “text:table-of-content” element has the following attributes: text:name, text:protected, text:protection-key, text:protection-key-digest-algorithm, text:style-name and xml:id.

Arguments:

title -- str

name -- str

protected -- bool

outline_level -- int

style -- str

title_style -- str

entry_style -- str
Source code in odfdo/toc.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def __init__(
    self,
    title: str = "Table of Contents",
    name: str | None = None,
    protected: bool = True,
    outline_level: int = 0,
    style: str | None = None,
    title_style: str = "Contents_20_Heading",
    entry_style: str = "Contents_20_%d",
    **kwargs: Any,
) -> None:
    """Create a table of content "text:table-of-content".

    Default parameters are what most people use: protected from manual
    modifications and not limited in title levels.

    The name is mandatory and derived automatically from the title if not
    given. Provide one in case of a conflict with other TOCs in the same
    document.

    The "text:table-of-content" element has the following attributes:
    text:name, text:protected, text:protection-key,
    text:protection-key-digest-algorithm, text:style-name and xml:id.

    Arguments:

        title -- str

        name -- str

        protected -- bool

        outline_level -- int

        style -- str

        title_style -- str

        entry_style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        if style:
            self.style = style
        if protected:
            self.protected = protected
        if name is None:
            self.name = f"{title}1"
        # Create the source template
        toc_source = self.create_toc_source(
            title, outline_level, title_style, entry_style
        )
        self.append(toc_source)
        # Create the index body automatically with the index title
        if title:
            # This style is in the template document
            self.set_toc_title(title, text_style=title_style)

create_toc_source staticmethod

create_toc_source(
    title: str,
    outline_level: int,
    title_style: str,
    entry_style: str,
) -> Element
Source code in odfdo/toc.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
@staticmethod
def create_toc_source(
    title: str,
    outline_level: int,
    title_style: str,
    entry_style: str,
) -> Element:
    toc_source = Element.from_tag("text:table-of-content-source")
    toc_source.set_attribute("text:outline-level", str(outline_level))
    if title:
        title_template = IndexTitleTemplate()
        if title_style:
            # This style is in the template document
            title_template.style = title_style
        title_template.text = title
        toc_source.append(title_template)
    for level in range(1, 11):
        template = TocEntryTemplate(outline_level=level)
        if entry_style:
            template.style = entry_style % level
        toc_source.append(template)
    return toc_source

fill

fill(
    document: Document | None = None,
    use_default_styles: bool = True,
) -> None

Fill the TOC with the titles found in the document. A TOC is not contextual so it will catch all titles before and after its insertion. If the TOC is not attached to a document, attach it beforehand or provide one as argument.

For having a pretty TOC, let use_default_styles by default.

Arguments:

document -- Document

use_default_styles -- bool
Source code in odfdo/toc.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def fill(
    self,
    document: Document | None = None,
    use_default_styles: bool = True,
) -> None:
    """Fill the TOC with the titles found in the document. A TOC is not
    contextual so it will catch all titles before and after its insertion.
    If the TOC is not attached to a document, attach it beforehand or
    provide one as argument.

    For having a pretty TOC, let use_default_styles by default.

    Arguments:

        document -- Document

        use_default_styles -- bool
    """
    # Find the body
    if document is not None:
        body: Element | None = document.body
    else:
        body = self.document_body
    if body is None:
        raise ValueError("The TOC must be related to a document somehow")

    # Save the title
    index_body = self.body
    if index_body is None:
        title = ""
    else:
        title = index_body.get_element("text:index-title")  # type: ignore

    # Clean the old index-body
    self.body = None
    index_body = self.body

    # Restore the title
    if title and str(title):
        index_body.insert(title, position=0)  # type: ignore

    # Insert default TOC style
    if use_default_styles:
        automatic_styles = body.get_element("//office:automatic-styles")
        if isinstance(automatic_styles, Element):  # pragma: nocover
            for level in range(1, 11):
                if (
                    automatic_styles.get_style(
                        "paragraph", _toc_entry_style_name(level)
                    )
                    is None
                ):
                    level_style = default_toc_level_style(level)
                    automatic_styles.append(level_style)

    # Auto-fill the index
    outline_level = self.outline_level or 10
    level_indexes: dict[int, int] = {}
    for header in body.headers:
        level = header.get_attribute_integer("text:outline-level") or 0
        if level > outline_level:
            continue
        number_str = self._header_numbering(level_indexes, level)
        # Make the title with "1.2.3. Title" format
        paragraph = Paragraph(f"{number_str} {header}")
        if use_default_styles:
            paragraph.style = _toc_entry_style_name(level)
        index_body.append(paragraph)  # type: ignore

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/toc.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def get_formatted_text(self, context: dict | None = None) -> str:
    index_body = self.get_element("text:index-body")

    if index_body is None:
        return ""
    if context is None:
        context = {}
    if context.get("rst_mode"):
        return "\n.. contents::\n\n"

    result = []
    for element in index_body.children:
        if element.tag == "text:index-title":
            for child_element in element.children:
                result.append(child_element.get_formatted_text(context).strip())
        else:
            result.append(element.get_formatted_text(context).strip())
    return "\n".join(x for x in result if x)

get_title

get_title() -> str
Source code in odfdo/toc.py
336
337
338
339
340
341
342
343
def get_title(self) -> str:
    index_body = self.body
    if index_body is None:
        return ""
    index_title = index_body.get_element(IndexTitle._tag)
    if index_title is None:
        return ""
    return index_title.text_content

set_toc_title

set_toc_title(
    title: str,
    style: str | None = None,
    text_style: str | None = None,
) -> None
Source code in odfdo/toc.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def set_toc_title(
    self,
    title: str,
    style: str | None = None,
    text_style: str | None = None,
) -> None:
    index_body = self.body
    if index_body is None:
        self.body = None
        index_body = self.body
    index_title = index_body.get_element(IndexTitle._tag)  # type: ignore
    if index_title is None:
        name = f"{self.name}_Head"
        index_title = IndexTitle(
            name=name, style=style, title_text=title, text_style=text_style
        )
        index_body.append(index_title)  # type: ignore
    else:
        if style:
            index_title.style = style  # type: ignore
        paragraph = index_title.get_paragraph()
        if paragraph is None:
            paragraph = Paragraph()
            index_title.append(paragraph)
        if text_style:
            paragraph.style = text_style  # type: ignore
        paragraph.text = title

Tab

Bases: MDTab, Element

Representation of a tabulation, “text:tab”.

This element represents the [UNICODE] tab character (HORIZONTAL TABULATION, U+0009).

The position attribute contains the number of the tab-stop to which a tab character refers. The position 0 marks the start margin of a paragraph. Note: The position attribute is only a hint to help non-layout oriented consumers to determine the tab/tab-stop association. Layout oriented consumers should determine the tab positions based on the style information

Methods:

Name Description
__init__

Representation of a tabulation, “text:tab”.

Attributes:

Name Type Description
position
text str
Source code in odfdo/tab.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Tab(MDTab, Element):
    """Representation of a tabulation, "text:tab".

    This element represents the [UNICODE] tab character (HORIZONTAL
    TABULATION, U+0009).

    The position attribute contains the number of the tab-stop to which
    a tab character refers. The position 0 marks the start margin of a
    paragraph. Note: The position attribute is only a hint to help non-layout
    oriented consumers to determine the tab/tab-stop association. Layout
    oriented consumers should determine the tab positions based on the style
    information
    """

    _tag = "text:tab"
    _properties: tuple[PropDef, ...] = (PropDef("position", "text:tab-ref"),)

    def __init__(self, position: int | None = None, **kwargs: Any) -> None:
        """Representation of a tabulation, "text:tab".

        Arguments:

            position -- int
        """
        super().__init__(**kwargs)
        if self._do_init and position is not None and position >= 0:
            self.position = str(position)

    def __str__(self) -> str:
        return "\t"

    @property
    def text(self) -> str:
        return "\t"

position instance-attribute

position = str(position)

text property

text: str

__init__

__init__(
    position: int | None = None, **kwargs: Any
) -> None

Representation of a tabulation, “text:tab”.

Arguments:

position -- int
Source code in odfdo/tab.py
51
52
53
54
55
56
57
58
59
60
def __init__(self, position: int | None = None, **kwargs: Any) -> None:
    """Representation of a tabulation, "text:tab".

    Arguments:

        position -- int
    """
    super().__init__(**kwargs)
    if self._do_init and position is not None and position >= 0:
        self.position = str(position)

TabStopStyle

Bases: Element

Base style for a TOC entryBase style for a TOC entry, “style:tab-stop”.

Methods:

Name Description
__init__

Create style for a TOC entryBase style for a TOC entry “style:tab-stop”.

Attributes:

Name Type Description
leader_color
leader_style
leader_text
leader_text_style
leader_type
leader_width
style_char
style_position
style_type
Source code in odfdo/toc.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class TabStopStyle(Element):
    """Base style for a TOC entryBase style for a TOC entry, "style:tab-stop"."""

    _tag = "style:tab-stop"
    _properties = (
        PropDef("style_char", "style:char"),
        PropDef("leader_color", "style:leader-color"),
        PropDef("leader_style", "style:leader-style"),
        PropDef("leader_text", "style:leader-text"),
        PropDef("leader_text_style", "style:leader-text-style"),
        PropDef("leader_type", "style:leader-type"),
        PropDef("leader_width", "style:leader-width"),
        PropDef("style_position", "style:position"),
        PropDef("style_type", "style:type"),
    )

    def __init__(
        self,
        style_char: str | None = None,
        leader_color: str | None = None,
        leader_style: str | None = None,
        leader_text: str | None = None,
        leader_text_style: str | None = None,
        leader_type: str | None = None,
        leader_width: str | None = None,
        style_position: str | None = None,
        style_type: str | None = None,
        **kwargs: Any,
    ):
        """Create style for a TOC entryBase style for a TOC entry "style:tab-stop"."""
        super().__init__(**kwargs)
        if self._do_init:
            if style_char:
                self.style_char = style_char
            if leader_color:
                self.leader_color = leader_color
            if leader_style:
                self.leader_style = leader_style
            if leader_text:
                self.leader_text = leader_text
            if leader_text_style:
                self.leader_text_style = leader_text_style
            if leader_type:
                self.leader_type = leader_type
            if leader_width:
                self.leader_width = leader_width
            if style_position:
                self.style_position = style_position
            if style_type:
                self.style_type = style_type
        else:
            pass  # pragma: nocover

leader_color instance-attribute

leader_color = leader_color

leader_style instance-attribute

leader_style = leader_style

leader_text instance-attribute

leader_text = leader_text

leader_text_style instance-attribute

leader_text_style = leader_text_style

leader_type instance-attribute

leader_type = leader_type

leader_width instance-attribute

leader_width = leader_width

style_char instance-attribute

style_char = style_char

style_position instance-attribute

style_position = style_position

style_type instance-attribute

style_type = style_type

__init__

__init__(
    style_char: str | None = None,
    leader_color: str | None = None,
    leader_style: str | None = None,
    leader_text: str | None = None,
    leader_text_style: str | None = None,
    leader_type: str | None = None,
    leader_width: str | None = None,
    style_position: str | None = None,
    style_type: str | None = None,
    **kwargs: Any,
)

Create style for a TOC entryBase style for a TOC entry “style:tab-stop”.

Source code in odfdo/toc.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __init__(
    self,
    style_char: str | None = None,
    leader_color: str | None = None,
    leader_style: str | None = None,
    leader_text: str | None = None,
    leader_text_style: str | None = None,
    leader_type: str | None = None,
    leader_width: str | None = None,
    style_position: str | None = None,
    style_type: str | None = None,
    **kwargs: Any,
):
    """Create style for a TOC entryBase style for a TOC entry "style:tab-stop"."""
    super().__init__(**kwargs)
    if self._do_init:
        if style_char:
            self.style_char = style_char
        if leader_color:
            self.leader_color = leader_color
        if leader_style:
            self.leader_style = leader_style
        if leader_text:
            self.leader_text = leader_text
        if leader_text_style:
            self.leader_text_style = leader_text_style
        if leader_type:
            self.leader_type = leader_type
        if leader_width:
            self.leader_width = leader_width
        if style_position:
            self.style_position = style_position
        if style_type:
            self.style_type = style_type
    else:
        pass  # pragma: nocover

Table

Bases: MDTable, Element

A table, in a spreadsheet or other document, “table:table”.

Methods:

Name Description
__init__

Create a table element “table:table”, optionally prefilled with

append

Dispatch .append() call to append_row() or append_column().

append_cell

Append the given cell at the “y” coordinate. Repeated cells are

append_column

Append the column at the end of the table. If no column is given,

append_row

Append the row at the end of the table. If no row is given, an

clear

Remove text, children and attributes from the Row.

del_span

Delete a Cell Span. ‘area’ is the cell coordiante of the upper left

delete_cell

Delete the cell at the given coordinates, so that next cells are

delete_column

Delete the column at the given position. ODF columns don’t contain

delete_named_range

Delete the Named Range of specified name from the spreadsheet.

delete_row

Delete the row at the given “y” position.

extend_rows

Append a list of rows at the end of the table.

from_csv

Import the CSV text content into a Table. If the path_or_file

get_cell

Get the cell at the given coordinates.

get_cells

Get the cells matching the criteria. If ‘coord’ is None,

get_column

Get the column at the given “x” position.

get_column_cells

Get the list of cells at the given position.

get_column_values

Shortcut to get the list of Python values for the cells at the

get_columns

Get the list of columns matching the criteria.

get_elements
get_formatted_text
get_named_range

Returns the Name Ranges of the specified name. If

get_named_ranges

Returns the list of available Name Ranges of the spreadsheet. If

get_row

Get the row at the given “y” position.

get_row_sub_elements

Shortcut to get the list of Elements values for the cells of the row

get_row_values

Shortcut to get the list of Python values for the cells of the row

get_rows

Get the list of rows matching the criteria.

get_value

Shortcut to get the Python value of the cell at the given

get_values

Get a matrix of values of the table.

insert_cell

Insert the given cell at the given coordinates. If no cell is

insert_column

Insert the column before the given “x” position. If no column is

insert_row

Insert the row before the given “y” position. If no row is given,

is_column_empty

Return wether every cell in the column at “x” position has no value

is_empty

Return whether every cell in the table has no value or the value

is_row_empty

Return wether every cell in the row at the given “y” position has

iter_values

Iterate through lines of Python values of the table.

optimize_width

Remove in-place empty rows below and empty cells at the right of

rstrip

Remove in-place empty rows below and empty cells at the right of

set_cell

Replace a cell of the table at the given coordinates.

set_cell_image

Do all the magic to display an image in the cell at the given

set_cells

Set the cells in the table, from the ‘coord’ position.

set_column

Replace the column at the given “x” position.

set_column_cells

Shortcut to set the list of cells at the given position.

set_column_values

Shortcut to set the list of Python values of cells at the given

set_named_range

Create a Named Range element and insert it in the document.

set_row

Replace the row at the given position with the new one. Repetions of

set_row_cells

Shortcut to set all the cells of the row at the given

set_row_values

Shortcut to set the values of all cells of the row at the given

set_span

Create a Cell Span : span the first cell of the area on several

set_value

Set the Python value of the cell at the given coordinates.

set_values

Set the value of cells in the table, from the ‘coord’ position

to_csv

Export the table as CSV.

transpose

Swap in-place rows and columns of the table.

traverse

Yield as many row elements as expected rows in the table, i.e.

traverse_columns

Yield as many column elements as expected columns in the table,

Attributes:

Name Type Description
cells list

Get all cells of the table.

columns list[Column]

Get the list of all columns matching the criteria.

height int

Get the current height of the table.

name str | None

Get / set the name of the table.

print_ranges list[str]
printable bool
protected bool
protection_key str | None
row_groups list[RowGroup]

Get the list of all RowGroup.

rows list[Row]

Get the list of all rows.

set_protection_key
size tuple[int, int]

Shortcut to get the current width and height of the table.

style str | None

Get / set the style of the table

width int

Get the current width of the table, measured on columns.

Source code in odfdo/table.py
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
class Table(MDTable, Element):
    """A table, in a spreadsheet or other document, "table:table"."""

    _tag = "table:table"
    _append = Element.append

    def __init__(
        self,
        name: str | None = None,
        width: int | str | None = None,
        height: int | str | None = None,
        protected: bool = False,
        protection_key: str | None = None,
        printable: bool = True,
        print_ranges: list[str] | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a table element "table:table", optionally prefilled with
        "height" rows of "width" cells each.

        The "name" parameter is required and cannot contain []*?:/ or \\
        characters, ' (apostrophe) cannot be the first or last character.

        If the table is to be protected, a protection key must be provided,
        i.e. a hash value of the password.

        If the table must not be printed, set "printable" to False. The table
        will not be printed when it is not displayed, whatever the value of
        this argument.

        Ranges of cells to print can be provided as a list of cell ranges,
        e.g. ['E6:K12', 'P6:R12'] or directly as a raw string, e.g.
        "E6:K12 P6:R12".

        You can access and modify the XML tree manually, but you probably want
        to use the API to access and alter cells. It will save you from
        handling repetitions and the same number of cells for each row.

        If you use both the table API and the XML API, you are on your own for
        ensuiring model integrity.

        Arguments:

            name -- str

            width -- int | str

            height -- int | str

            protected -- bool

            protection_key -- str

            printable -- bool

            print_ranges -- list

            style -- str
        """
        super().__init__(**kwargs)
        self._table_cache = TableCache()
        if self._do_init:
            self.name = name
            if protected:
                self.protected = protected
                self.set_protection_key = protection_key
            if not printable:
                self.printable = printable
            if print_ranges:
                self.print_ranges = print_ranges
            if style:
                self.style = style
            # Prefill the table
            if width is not None or height is not None:
                width = int(width or 1)
                height = int(height or 1)
                # Column groups for style information
                columns = Column(repeated=width)
                self._append(columns)
                for _i in range(height):
                    row = Row(width)
                    self._append(row)
        self._compute_table_cache()

    def __str__(self) -> str:
        def write_content(csv_writer: object) -> None:
            for values in self.iter_values():
                line = []
                for value in values:
                    if value is None:
                        value = ""
                    if isinstance(value, str):
                        value = value.strip()
                    line.append(value)
                csv_writer.writerow(line)  # type: ignore

        out = StringIO(newline=os.linesep)
        csv_writer = csv.writer(
            out,
            delimiter=" ",
            doublequote=False,
            escapechar="\\",
            lineterminator=os.linesep,
            quotechar='"',
            quoting=csv.QUOTE_NONNUMERIC,
        )
        write_content(csv_writer)
        return out.getvalue()

    def get_elements(self, xpath_query: XPath | str) -> list[Element]:
        element = self._Element__element  # type: ignore
        if isinstance(xpath_query, str):
            new_xpath_query = xpath_compile(xpath_query)
            result = new_xpath_query(element)
        else:
            result = xpath_query(element)
        if not isinstance(result, list):
            raise TypeError("Bad XPath result")
        cache = (self._table_cache, None)
        return [
            Element.from_tag_for_clone(e, cache)
            for e in result
            if isinstance(e, _Element)
        ]

    def _copy_cache(self, cache: tuple | None) -> None:
        """Copy cache when cloning."""
        self._table_cache = cache[0]

    def clear(self) -> None:
        """Remove text, children and attributes from the Row."""
        self._Element__element.clear()  # type: ignore
        self._table_cache = TableCache()

    def _translate_y_from_any(self, y: str | int) -> int:
        # "3" (couting from 1) -> 2 (couting from 0)
        return translate_from_any(y, self.height, 1)

    def _translate_table_coordinates_list(
        self,
        coord: tuple | list,
    ) -> tuple[int | None, ...]:
        height = self.height
        width = self.width
        # assuming we got int values
        if len(coord) == 1:
            # It is a row
            y = coord[0]
            if y and y < 0:
                y = increment(y, height)
            return (None, y, None, y)
        if len(coord) == 2:
            # It is a row range, not a cell, because context is table
            y = coord[0]
            if y and y < 0:
                y = increment(y, height)
            t = coord[1]
            if t and t < 0:
                t = increment(t, height)
            return (None, y, None, t)
        # should be 4 int
        x, y, z, t = coord
        if x and x < 0:
            x = increment(x, width)
        if y and y < 0:
            y = increment(y, height)
        if z and z < 0:
            z = increment(z, width)
        if t and t < 0:
            t = increment(t, height)
        return (x, y, z, t)

    def _translate_table_coordinates_str(
        self,
        coord_str: str,
    ) -> tuple[int | None, ...]:
        height = self.height
        width = self.width
        coord = convert_coordinates(coord_str)
        if len(coord) == 2:
            x, y = coord
            if x and x < 0:
                x = increment(x, width)
            if y and y < 0:
                y = increment(y, height)
            # extent to an area :
            return (x, y, x, y)
        x, y, z, t = coord
        if x and x < 0:
            x = increment(x, width)
        if y and y < 0:
            y = increment(y, height)
        if z and z < 0:
            z = increment(z, width)
        if t and t < 0:
            t = increment(t, height)
        return (x, y, z, t)

    def _translate_table_coordinates(
        self,
        coord: tuple | list | str,
    ) -> tuple[int | None, ...]:
        if isinstance(coord, str):
            return self._translate_table_coordinates_str(coord)
        return self._translate_table_coordinates_list(coord)

    def _translate_column_coordinates_str(
        self,
        coord_str: str,
    ) -> tuple[int | None, ...]:
        width = self.width
        height = self.height
        coord = convert_coordinates(coord_str)
        if len(coord) == 2:
            x, y = coord
            if x and x < 0:
                x = increment(x, width)
            if y and y < 0:
                y = increment(y, height)
            # extent to an area :
            return (x, y, x, y)
        x, y, z, t = coord
        if x and x < 0:
            x = increment(x, width)
        if y and y < 0:
            y = increment(y, height)
        if z and z < 0:
            z = increment(z, width)
        if t and t < 0:
            t = increment(t, height)
        return (x, y, z, t)

    def _translate_column_coordinates_list(
        self,
        coord: tuple | list,
    ) -> tuple[int | None, ...]:
        width = self.width
        height = self.height
        # assuming we got int values
        if len(coord) == 1:
            # It is a column
            x = coord[0]
            if x and x < 0:
                x = increment(x, width)
            return (x, None, x, None)
        if len(coord) == 2:
            # It is a column range, not a cell, because context is table
            x = coord[0]
            if x and x < 0:
                x = increment(x, width)
            z = coord[1]
            if z and z < 0:
                z = increment(z, width)
            return (x, None, z, None)
        # should be 4 int
        x, y, z, t = coord
        if x and x < 0:
            x = increment(x, width)
        if y and y < 0:
            y = increment(y, height)
        if z and z < 0:
            z = increment(z, width)
        if t and t < 0:
            t = increment(t, height)
        return (x, y, z, t)

    def _translate_column_coordinates(
        self,
        coord: tuple | list | str,
    ) -> tuple[int | None, ...]:
        if isinstance(coord, str):
            return self._translate_column_coordinates_str(coord)
        return self._translate_column_coordinates_list(coord)

    def _translate_cell_coordinates(
        self,
        coord: tuple | list | str,
    ) -> tuple[int | None, int | None]:
        # we want an x,y result
        coord = convert_coordinates(coord)
        if len(coord) == 2:
            x, y = coord
        # If we got an area, take the first cell
        elif len(coord) == 4:
            x, y, _z, _t = coord
        else:
            raise ValueError(str(coord))
        if x and x < 0:
            x = increment(x, self.width)
        if y and y < 0:
            y = increment(y, self.height)
        return (x, y)

    def _compute_table_cache(self) -> None:
        idx_repeated_seq = self.elements_repeated_sequence(
            _XP_ROW, "table:number-rows-repeated"
        )
        self._table_cache.make_row_map(idx_repeated_seq)
        idx_repeated_seq = self.elements_repeated_sequence(
            _XP_COLUMN, "table:number-columns-repeated"
        )
        self._table_cache.make_col_map(idx_repeated_seq)

    def _update_width(self, row: Row) -> None:
        """Synchronize the number of columns if the row is bigger.

        Append, don't insert, not to disturb the current layout.
        """
        diff = row.width - self.width
        if diff > 0:
            self.append_column(Column(repeated=diff))

    def _get_formatted_text_normal(self, context: dict | None) -> str:
        result = []
        for row in self.traverse():
            for cell in row.traverse():
                value = cell.get_value(try_get_text=False)
                # None ?
                if value is None:
                    # Try with get_formatted_text on the elements
                    value = []
                    for element in cell.children:
                        value.append(element.get_formatted_text(context))
                    value = "".join(value)
                else:
                    value = str(value)
                result.append(value)
                result.append("\n")
            result.append("\n")
        return "".join(result)

    def _get_formatted_text_rst(self, context: dict) -> str:
        context["no_img_level"] += 1
        # Strip the table => We must clone
        table = self.clone
        table.rstrip(aggressive=True)  # type: ignore

        # Fill the rows
        rows = []
        cols_nb = 0
        cols_size: dict[int, int] = {}
        for odf_row in table.traverse():  # type: ignore
            row = []
            for i, cell in enumerate(odf_row.traverse()):
                value = cell.get_value(try_get_text=False)
                # None ?
                if value is None:
                    # Try with get_formatted_text on the elements
                    value = []
                    for element in cell.children:
                        value.append(element.get_formatted_text(context))
                    value = "".join(value)
                else:
                    value = str(value)
                value = value.strip()
                # Strip the empty columns
                if value:
                    cols_nb = max(cols_nb, i + 1)
                # Compute the size of each columns (at least 2)
                cols_size[i] = max(cols_size.get(i, 2), len(value))
                # Append
                row.append(value)
            rows.append(row)

        # Nothing ?
        if cols_nb == 0:
            return ""

        # Prevent a crash with empty columns (by example with images)
        for col, size in cols_size.items():
            if size == 0:
                cols_size[col] = 1

        # Update cols_size
        LINE_MAX = 100
        COL_MIN = 16

        free_size = LINE_MAX - (cols_nb - 1) * 3 - 4
        real_size = sum(cols_size[i] for i in range(cols_nb))
        if real_size > free_size:
            factor = float(free_size) / real_size

            for i in range(cols_nb):
                old_size = cols_size[i]

                # The cell is already small
                if old_size <= COL_MIN:
                    continue

                new_size = int(factor * old_size)

                if new_size < COL_MIN:
                    new_size = COL_MIN
                cols_size[i] = new_size

        # Convert !
        result: list[str] = [""]
        # Construct the first/last line
        line: list[str] = []
        for i in range(cols_nb):
            line.append("=" * cols_size[i])
            line.append(" ")
        line_str = "".join(line)

        # Add the lines
        result.append(line_str)
        for row in rows:
            # Wrap the row
            wrapped_row = []
            for i, value in enumerate(row[:cols_nb]):
                wrapped_value = []
                for part in value.split("\n"):
                    # Hack to handle correctly the lists or the directives
                    subsequent_indent = ""
                    part_lstripped = part.lstrip()
                    if part_lstripped.startswith("-") or part_lstripped.startswith(
                        ".."
                    ):
                        subsequent_indent = " " * (len(part) - len(part.lstrip()) + 2)
                    wrapped_part = wrap(
                        part, width=cols_size[i], subsequent_indent=subsequent_indent
                    )
                    if wrapped_part:
                        wrapped_value.extend(wrapped_part)
                    else:
                        wrapped_value.append("")
                wrapped_row.append(wrapped_value)

            # Append!
            for j in range(max([1] + [len(values) for values in wrapped_row])):
                txt_row: list[str] = []
                for i in range(cols_nb):
                    values = wrapped_row[i] if i < len(wrapped_row) else []

                    # An empty cell ?
                    if len(values) - 1 < j or not values[j]:
                        if i == 0 and j == 0:
                            txt_row.append("..")
                            txt_row.append(" " * (cols_size[i] - 1))
                        else:
                            txt_row.append(" " * (cols_size[i] + 1))
                        continue

                    # Not empty
                    value = values[j]
                    txt_row.append(value)
                    txt_row.append(" " * (cols_size[i] - len(value) + 1))
                result.append("".join(txt_row))

        result.append(line_str)
        result.append("")
        result.append("")
        result_str = "\n".join(result)

        context["no_img_level"] -= 1
        return result_str

    def _translate_x_from_any(self, x: str | int) -> int:
        return translate_from_any(x, self.width, 0)

    #
    # Public API
    #

    def append(self, something: Element | str) -> None:
        """Dispatch .append() call to append_row() or append_column()."""
        if isinstance(something, Row):
            self.append_row(something)
        elif isinstance(something, Column):
            self.append_column(something)
        else:
            # probably still an error
            self._append(something)

    @property
    def height(self) -> int:
        """Get the current height of the table.

        Return: int
        """
        return self._table_cache.height()

    @property
    def width(self) -> int:
        """Get the current width of the table, measured on columns.

        Rows may have different widths, use the Table API to ensure width
        consistency.

        Return: int
        """
        # Columns are our reference for user expected width
        return self._table_cache.width()

    @property
    def size(self) -> tuple[int, int]:
        """Shortcut to get the current width and height of the table.

        Return: (int, int)
        """
        return self.width, self.height

    @property
    def name(self) -> str | None:
        """Get / set the name of the table.

        The "name" parameter is required and cannot contain []*?:/ or \\
        characters, ' (apostrophe) cannot be the first or last character.
        """
        return self.get_attribute_string("table:name")

    @name.setter
    def name(self, name: str | None) -> None:
        name = table_name_check(name)
        # first, update named ranges
        # fixme : delete name ranges when deleting table, too.
        for named_range in self.get_named_ranges(table_name=self.name):
            named_range.set_table_name(name)
        self.set_attribute("table:name", name)

    @property
    def protected(self) -> bool:
        return bool(self.get_attribute("table:protected"))

    @protected.setter
    def protected(self, protect: bool) -> None:
        self.set_attribute("table:protected", protect)

    @property
    def protection_key(self) -> str | None:
        return self.get_attribute_string("table:protection-key")

    @protection_key.setter
    def protection_key(self, key: str) -> None:
        self.set_attribute("table:protection-key", key)

    @property
    def printable(self) -> bool:
        printable = self.get_attribute("table:print")
        # Default value
        if printable is None:
            return True
        return bool(printable)

    @printable.setter
    def printable(self, printable: bool) -> None:
        self.set_attribute("table:print", printable)

    @property
    def print_ranges(self) -> list[str]:
        ranges = self.get_attribute_string("table:print-ranges")
        if isinstance(ranges, str):
            return ranges.split()
        return []

    @print_ranges.setter
    def print_ranges(self, ranges: list[str] | None) -> None:
        if isinstance(ranges, (list, tuple)):
            self.set_attribute("table:print-ranges", " ".join(ranges))
        else:
            self.set_attribute("table:print-ranges", ranges)

    @property
    def style(self) -> str | None:
        """Get / set the style of the table

        Return: str
        """
        return self.get_attribute_string("table:style-name")

    @style.setter
    def style(self, style: str | Element) -> None:
        self.set_style_attribute("table:style-name", style)

    def get_formatted_text(self, context: dict | None = None) -> str:
        if context and context["rst_mode"]:
            return self._get_formatted_text_rst(context)
        return self._get_formatted_text_normal(context)

    def get_values(
        self,
        coord: tuple | list | str | None = None,
        cell_type: str | None = None,
        complete: bool = True,
        get_type: bool = False,
        flat: bool = False,
    ) -> list:
        """Get a matrix of values of the table.

        Filter by coordinates will parse the area defined by the coordinates.

        If 'cell_type' is used and 'complete' is True (default), missing values
        are replaced by None.
        Filter by ' cell_type = "all" ' will retrieve cells of any
        type, aka non empty cells.

        If 'cell_type' is None, complete is always True : with no cell type
        queried, get_values() returns None for each empty cell, the length
        each lists is equal to the width of the table.

        If get_type is True, returns tuples (value, ODF type of value), or
        (None, None) for empty cells with complete True.

        If flat is True, the methods return a single list of all the values.
        By default, flat is False.

        Arguments:

            coord -- str or tuple of int : coordinates of area

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            complete -- boolean

            get_type -- boolean

        Return: list of lists of Python types
        """
        if coord:
            x, y, z, t = self._translate_table_coordinates(coord)
        else:
            x = y = z = t = None
        data = []
        for row in self.traverse(start=y, end=t):
            if z is None:
                width = self.width
            else:
                width = min(z + 1, self.width)
            if x is not None:
                width -= x
            values = row.get_values(
                (x, z),
                cell_type=cell_type,
                complete=complete,
                get_type=get_type,
            )
            # complete row to match request width
            if complete:
                if get_type:
                    values.extend([(None, None)] * (width - len(values)))
                else:
                    values.extend([None] * (width - len(values)))
            if flat:
                data.extend(values)
            else:
                data.append(values)
        return data

    def iter_values(
        self,
        coord: tuple | list | str | None = None,
        cell_type: str | None = None,
        complete: bool = True,
        get_type: bool = False,
    ) -> Iterator[list]:
        """Iterate through lines of Python values of the table.

        Filter by coordinates will parse the area defined by the coordinates.

        cell_type, complete, grt_type : see get_values()



        Arguments:

            coord -- str or tuple of int : coordinates of area

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            complete -- boolean

            get_type -- boolean

        Return: iterator of lists
        """
        if coord:
            x, y, z, t = self._translate_table_coordinates(coord)
        else:
            x = y = z = t = None
        for row in self.traverse(start=y, end=t):
            if z is None:
                width = self.width
            else:
                width = min(z + 1, self.width)
            if x is not None:
                width -= x
            values = row.get_values(
                (x, z),
                cell_type=cell_type,
                complete=complete,
                get_type=get_type,
            )
            # complete row to match column width
            if complete:
                if get_type:
                    values.extend([(None, None)] * (width - len(values)))
                else:
                    values.extend([None] * (width - len(values)))
            yield values

    def set_values(
        self,
        values: list,
        coord: tuple | list | str | None = None,
        style: str | None = None,
        cell_type: str | None = None,
        currency: str | None = None,
    ) -> None:
        """Set the value of cells in the table, from the 'coord' position
        with values.

        'coord' is the coordinate of the upper left cell to be modified by
        values. If 'coord' is None, default to the position (0,0) ("A1").
        If 'coord' is an area (e.g. "A2:B5"), the upper left position of this
        area is used as coordinate.

        The table is *not* cleared before the operation, to reset the table
        before setting values, use table.clear().

        A list of lists is expected, with as many lists as rows, and as many
        items in each sublist as cells to be setted. None values in the list
        will create empty cells with no cell type (but eventually a style).

        Arguments:

            coord -- tuple or str

            values -- list of lists of python types

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                         'string' or 'time'

            currency -- three-letter str

            style -- str
        """
        if coord:
            x, y = self._translate_cell_coordinates(coord)
        else:
            x = y = 0
        if y is None:
            y = 0
        if x is None:
            x = 0
        y -= 1
        for row_values in values:
            y += 1
            if not row_values:
                continue
            row = self.get_row(y, clone=True)
            repeated = row.repeated or 1
            if repeated >= 2:
                row.repeated = None
            row.set_values(
                row_values,
                start=x,
                cell_type=cell_type,
                currency=currency,
                style=style,
            )
            self.set_row(y, row, clone=False)
            self._update_width(row)

    def rstrip(self, aggressive: bool = False) -> None:
        """Remove *in-place* empty rows below and empty cells at the right of
        the table. Cells are empty if they contain no value or it evaluates
        to False, and no style.

        If aggressive is True, empty cells with style are removed too.

        Argument:

            aggressive -- bool
        """
        # Step 1: remove empty rows below the table
        for row in reversed(self._get_rows()):
            if row.is_empty(aggressive=aggressive):
                row.parent.delete(row)  # type: ignore
            else:
                break
        # Step 2: rstrip remaining rows
        max_width = 0
        for row in self._get_rows():
            row.rstrip(aggressive=aggressive)
            # keep count of the biggest row
            max_width = max(max_width, row.width)
        # raz cache of rows
        self._table_cache.clear_row_indexes()
        # Step 3: trim columns to match max_width
        columns = self._get_columns()
        repeated_cols = self.xpath("table:table-column/@table:number-columns-repeated")
        if not isinstance(repeated_cols, list):
            raise TypeError
        unrepeated = len(columns) - len(repeated_cols)
        column_width = sum(int(r) for r in repeated_cols) + unrepeated  # type: ignore
        diff = column_width - max_width
        if diff > 0:
            for column in reversed(columns):
                repeated = column.repeated or 1
                repeated = repeated - diff
                if repeated > 0:
                    column.repeated = repeated
                    break
                else:
                    column.parent.delete(column)
                    diff = -repeated
                    if diff == 0:
                        break
        # raz cache of columns
        self._table_cache.clear_col_indexes()
        self._compute_table_cache()

    def optimize_width(self) -> None:
        """Remove *in-place* empty rows below and empty cells at the right of
        the table. Keep repeated styles of empty cells but minimize row width.
        """
        self._optimize_width_trim_rows()
        width = self._optimize_width_length()
        self._optimize_width_rstrip_rows(width)
        self._optimize_width_adapt_columns(width)

    def _optimize_width_trim_rows(self) -> None:
        count = -1  # to keep one empty row
        for row in reversed(self._get_rows()):
            if row.is_empty(aggressive=False):
                count += 1
            else:
                break
        if count > 0:
            for row in reversed(self._get_rows()):
                row.parent.delete(row)  # type: ignore
                count -= 1
                if count <= 0:
                    break
        try:
            last_row = self._get_rows()[-1]
            last_row._set_repeated(None)
        except IndexError:
            pass
        # raz cache of rows
        self._table_cache.clear_row_indexes()

    def _optimize_width_length(self) -> int:
        try:
            return max(row.minimized_width() for row in self._get_rows())
        except ValueError:
            return 0

    def _optimize_width_rstrip_rows(self, width: int) -> None:
        for row in self._get_rows():
            row.force_width(width)

    def _optimize_width_adapt_columns(self, width: int) -> None:
        # trim columns to match minimal_width
        columns = self._get_columns()
        repeated_cols = self.xpath("table:table-column/@table:number-columns-repeated")
        if not isinstance(repeated_cols, list):
            raise TypeError
        unrepeated = len(columns) - len(repeated_cols)
        column_width = sum(int(r) for r in repeated_cols) + unrepeated  # type: ignore
        diff = column_width - width
        if diff > 0:
            for column in reversed(columns):
                repeated = column.repeated or 1
                repeated = repeated - diff
                if repeated > 0:
                    column.repeated = repeated
                    break
                else:
                    column.parent.delete(column)
                    diff = -repeated
                    if diff == 0:
                        break
        # raz cache of columns
        self._table_cache.clear_col_indexes()
        self._compute_table_cache()

    def transpose(self, coord: tuple | list | str | None = None) -> None:
        """Swap *in-place* rows and columns of the table.

        If 'coord' is not None, apply transpose only to the area defined by the
        coordinates. Beware, if area is not square, some cells mays be over
        written during the process.

        Arguments:

            coord -- str or tuple of int : coordinates of area

            start -- int or str
        """
        data = []
        if coord is None:
            for row in self.traverse():
                data.append(list(row.traverse()))
            transposed_data = zip_longest(*data)
            self.clear()
            # new_rows = []
            for row_cells in transposed_data:
                if not isiterable(row_cells):
                    row_cells = (row_cells,)
                row = Row()
                row.extend_cells(row_cells)
                self.append_row(row, clone=False)
            self._compute_table_cache()
        else:
            x, y, z, t = self._translate_table_coordinates(coord)
            if x is None:
                x = 0
            else:
                x = min(x, self.width - 1)
            if z is None:
                z = self.width - 1
            else:
                z = min(z, self.width - 1)
            if y is None:
                y = 0
            else:
                y = min(y, self.height - 1)
            if t is None:
                t = self.height - 1
            else:
                t = min(t, self.height - 1)
            for row in self.traverse(start=y, end=t):
                data.append(list(row.traverse(start=x, end=z)))
            transposed_data = zip_longest(*data)
            # clear locally
            w = z - x + 1
            h = t - y + 1
            if w != h:
                nones = [[None] * w for i in range(h)]
                self.set_values(nones, coord=(x, y, z, t))
            # put transposed
            filtered_data: list[tuple[Cell]] = []
            for row_cells in transposed_data:
                if isinstance(row_cells, (list, tuple)):
                    filtered_data.append(row_cells)
                else:
                    filtered_data.append((row_cells,))
            self.set_cells(filtered_data, (x, y, x + h - 1, y + w - 1))
            self._compute_table_cache()

    def is_empty(self, aggressive: bool = False) -> bool:
        """Return whether every cell in the table has no value or the value
        evaluates to False (empty string), and no style.

        If aggressive is True, empty cells with style are considered empty.

        Arguments:

            aggressive -- bool
        """
        return all(row.is_empty(aggressive=aggressive) for row in self._get_rows())

    #
    # Rows
    #

    @property
    def row_groups(self) -> list[RowGroup]:
        """Get the list of all RowGroup.

        Return: list of RowGroup
        """
        return self.get_elements(_XP_ROW_GROUP)  # type: ignore

    def _get_rows(self) -> list[Row]:
        return self.get_elements(_XP_ROW)  # type: ignore

    def traverse(
        self,
        start: int | None = None,
        end: int | None = None,
    ) -> Iterator[Row]:
        """Yield as many row elements as expected rows in the table, i.e.
        expand repetitions by returning the same row as many times as
        necessary.

            Arguments:

                start -- int

                end -- int

        Copies are returned, use set_row() to push them back.
        """
        if start is None:
            start = 0
        start = max(0, start)
        if end is None:
            end = 2**32
        if end < start:
            return
        y = -1
        for row in self._yield_odf_rows():
            y += 1
            if y < start:
                continue
            if y > end:
                return
            row.y = y
            yield row

    def get_rows(
        self,
        coord: tuple | list | str | None = None,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Row]:
        """Get the list of rows matching the criteria.

        Filter by coordinates will parse the area defined by the coordinates.

        Arguments:

            coord -- str or tuple of int : coordinates of rows

            content -- str regex

            style -- str

        Return: list of rows
        """
        if coord:
            _x, y, _z, t = self._translate_table_coordinates(coord)
        else:
            y = t = None
        # fixme : not clones ?
        if not content and not style:
            return list(self.traverse(start=y, end=t))
        rows = []
        for row in self.traverse(start=y, end=t):
            if content and not row.match(content):
                continue
            if style and style != row.style:
                continue
            rows.append(row)
        return rows

    @property
    def rows(self) -> list[Row]:
        """Get the list of all rows.

        Return: list of rows
        """
        # fixme : not clones ?
        return list(self.traverse())

    def _yield_odf_rows(self):
        for row in self._get_rows():
            if row.repeated is None:
                yield row
            else:
                for _ in range(row.repeated):
                    row_copy = row.clone
                    row_copy.repeated = None
                    yield row_copy

    def _get_row2(self, y: int, clone: bool = True, create: bool = True) -> Row:
        if y >= self.height:
            if create:
                return Row()
            raise ValueError("Row not found")
        row = self._get_row2_base(y)
        if row is None:
            raise ValueError("Row not found")
        if clone:
            return row.clone
        return row

    def _get_row2_base(self, y: int) -> Row | None:
        idx = self._table_cache.row_idx(y)
        if idx is None:
            return None
        row = self._table_cache.cached_row(idx)
        if row is None:
            row = self._get_element_idx2(_XP_ROW_IDX, idx)
            self._table_cache.store_row(row, idx)
        return row

    def get_row(self, y: int | str, clone: bool = True, create: bool = True) -> Row:
        """Get the row at the given "y" position.

        Position start at 0. So cell A4 is on row 3.

        A copy is returned, use set_cell() to push it back.

        Arguments:

            y -- int or str

        Return: Row
        """
        # fixme : keep repeat ? maybe an option to functions : "raw=False"
        y = self._translate_y_from_any(y)
        row = self._get_row2(y, clone=clone, create=create)
        if row is None:
            raise ValueError("Row not found")
        row.y = y
        return row

    def set_row(self, y: int | str, row: Row | None = None, clone: bool = True) -> Row:
        """Replace the row at the given position with the new one. Repetions of
        the old row will be adjusted.

        If row is None, a new empty row is created.

        Position start at 0. So cell A4 is on row 3.

        Arguments:

            y -- int or str

            row -- Row

        returns the row, with updated row.y
        """
        if row is None:
            row = Row()
            repeated = 1
            clone = False
        else:
            repeated = row.repeated or 1
        y = self._translate_y_from_any(y)
        row.y = y
        # Outside the defined table ?
        diff = y - self.height
        if diff == 0:
            row_back = self.append_row(row, _repeated=repeated, clone=clone)
        elif diff > 0:
            self.append_row(Row(repeated=diff), _repeated=diff, clone=clone)
            row_back = self.append_row(row, _repeated=repeated, clone=clone)
        else:
            # Inside the defined table
            row_back = self._table_cache.set_row_in_cache(y, row, self, clone=clone)
        # print self.serialize(True)
        # Update width if necessary
        self._update_width(row_back)
        return row_back

    def insert_row(
        self, y: str | int, row: Row | None = None, clone: bool = True
    ) -> Row:
        """Insert the row before the given "y" position. If no row is given,
        an empty one is created.

        Position start at 0. So cell A4 is on row 3.

        If row is None, a new empty row is created.

        Arguments:

            y -- int or str

            row -- Row

        returns the row, with updated row.y
        """
        if row is None:
            row = Row()
            clone = False
        y = self._translate_y_from_any(y)
        diff = y - self.height
        if diff < 0:
            row_back = self._table_cache.insert_row_in_cache(y, row, self)
        elif diff == 0:
            row_back = self.append_row(row, clone=clone)
        else:
            self.append_row(Row(repeated=diff), _repeated=diff, clone=False)
            row_back = self.append_row(row, clone=clone)
        row_back.y = y  # type: ignore
        # Update width if necessary
        self._update_width(row_back)  # type: ignore
        return row_back  # type: ignore

    def extend_rows(self, rows: list[Row] | None = None) -> None:
        """Append a list of rows at the end of the table.

        Arguments:

            rows -- list of Row
        """
        if rows is None:
            rows = []
        self.extend(rows)
        self._compute_table_cache()
        # Update width if necessary
        width = self.width
        for row in self.traverse():
            if row.width > width:
                width = row.width
        diff = width - self.width
        if diff > 0:
            self.append_column(Column(repeated=diff))

    def append_row(
        self,
        row: Row | None = None,
        clone: bool = True,
        _repeated: int | None = None,
    ) -> Row:
        """Append the row at the end of the table. If no row is given, an
        empty one is created.

        Position start at 0. So cell A4 is on row 3.

        Note the columns are automatically created when the first row is
        inserted in an empty table. So better insert a filled row.

        Arguments:

            row -- Row

            _repeated -- (optional), repeated value of the row

        returns the row, with updated row.y
        """
        if row is None:
            row = Row()
            _repeated = 1
        elif clone:
            row = row.clone
        # Appending a repeated row accepted
        # Do not insert next to the last row because it could be in a group
        self._append(row)
        if _repeated is None:
            _repeated = row.repeated or 1
        self._table_cache.insert_row_map_once(_repeated)
        row.y = self.height - 1
        # Initialize columns
        if not self._get_columns():
            repeated = row.width
            self.insert(Column(repeated=repeated), position=0)
            self._compute_table_cache()
        # Update width if necessary
        self._update_width(row)
        return row

    def delete_row(self, y: int | str) -> None:
        """Delete the row at the given "y" position.

        Position start at 0. So cell A4 is on row 3.

        Arguments:

            y -- int or str
        """
        y = self._translate_y_from_any(y)
        # Outside the defined table
        if y >= self.height:
            return
        # Inside the defined table
        self._table_cache.delete_row_in_cache(y, self)

    def get_row_values(
        self,
        y: int | str,
        cell_type: str | None = None,
        complete: bool = True,
        get_type: bool = False,
    ) -> list:
        """Shortcut to get the list of Python values for the cells of the row
        at the given "y" position.

        Position start at 0. So cell A4 is on row 3.

        Filter by cell_type, with cell_type 'all' will retrieve cells of any
        type, aka non empty cells.
        If cell_type and complete is True, replace missing values by None.

        If get_type is True, returns a tuple (value, ODF type of value)

        Arguments:

            y -- int, str

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            complete -- boolean

            get_type -- boolean

        Return: list of lists of Python types
        """
        values = self.get_row(y, clone=False).get_values(
            cell_type=cell_type, complete=complete, get_type=get_type
        )
        # complete row to match column width
        if complete:
            if get_type:
                values.extend([(None, None)] * (self.width - len(values)))
            else:
                values.extend([None] * (self.width - len(values)))
        return values

    def get_row_sub_elements(self, y: int | str) -> list[Any]:
        """Shortcut to get the list of Elements values for the cells of the row
        at the given "y" position.

        Position start at 0. So cell A4 is on row 3.

        Missing values replaced by None.

        Arguments:

            y -- int, str

        Return: list of lists of Elements
        """
        values = self.get_row(y, clone=False).get_sub_elements()
        values.extend([None] * (self.width - len(values)))
        return values

    def set_row_values(
        self,
        y: int | str,
        values: list,
        cell_type: str | None = None,
        currency: str | None = None,
        style: str | None = None,
    ) -> Row:
        """Shortcut to set the values of *all* cells of the row at the given
        "y" position.

        Position start at 0. So cell A4 is on row 3.

        Arguments:

            y -- int or str

            values -- list of Python types

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                         'string' or 'time'

            currency -- three-letter str

            style -- str

        returns the row, with updated row.y
        """
        row = Row()  # needed if clones rows
        row.set_values(values, style=style, cell_type=cell_type, currency=currency)
        return self.set_row(y, row)  # needed if clones rows

    def set_row_cells(self, y: int | str, cells: list | None = None) -> Row:
        """Shortcut to set *all* the cells of the row at the given
        "y" position.

        Position start at 0. So cell A4 is on row 3.

        Arguments:

            y -- int or str

            cells -- list of Python types

            style -- str

        returns the row, with updated row.y
        """
        if cells is None:
            cells = []
        row = Row()  # needed if clones rows
        row.extend_cells(cells)
        return self.set_row(y, row)  # needed if clones rows

    def is_row_empty(self, y: int | str, aggressive: bool = False) -> bool:
        """Return wether every cell in the row at the given "y" position has
        no value or the value evaluates to False (empty string), and no style.

        Position start at 0. So cell A4 is on row 3.

        If aggressive is True, empty cells with style are considered empty.

        Arguments:

            y -- int or str

            aggressive -- bool
        """
        return self.get_row(y, clone=False).is_empty(aggressive=aggressive)

    #
    # Cells
    #

    def get_cells(
        self,
        coord: tuple | list | str | None = None,
        cell_type: str | None = None,
        style: str | None = None,
        content: str | None = None,
        flat: bool = False,
    ) -> list:
        """Get the cells matching the criteria. If 'coord' is None,
        parse the whole table, else parse the area defined by 'coord'.

        Filter by  cell_type = "all"  will retrieve cells of any
        type, aka non empty cells.

        If flat is True (default is False), the method return a single list
        of all the values, else a list of lists of cells.

        if cell_type, style and content are None, get_cells() will return
        the exact number of cells of the area, including empty cells.

        Arguments:

            coordinates -- str or tuple of int : coordinates of area

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            content -- str regex

            style -- str

            flat -- boolean

        Return: list of list of Cell
        """
        if coord:
            x, y, z, t = self._translate_table_coordinates(coord)
        else:
            x = y = z = t = None
        if flat:
            cells: list[Cell] = []
            for row in self.traverse(start=y, end=t):
                row_cells = row.get_cells(
                    coord=(x, z),
                    cell_type=cell_type,
                    style=style,
                    content=content,
                )
                cells.extend(row_cells)
            return cells
        else:
            lcells: list[list[Cell]] = []
            for row in self.traverse(start=y, end=t):
                row_cells = row.get_cells(
                    coord=(x, z),
                    cell_type=cell_type,
                    style=style,
                    content=content,
                )
                lcells.append(row_cells)
            return lcells

    @property
    def cells(self) -> list:
        """Get all cells of the table.

        Return: list of list of Cell
        """
        lcells: list[list[Cell]] = []
        for row in self.traverse():
            lcells.append(row.cells)
        return lcells

    def get_cell(
        self,
        coord: tuple | list | str,
        clone: bool = True,
        keep_repeated: bool = True,
    ) -> Cell:
        """Get the cell at the given coordinates.

        They are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        A copy is returned, use ``set_cell`` to push it back.

        Arguments:

            coord -- (int, int) or str

        Return: Cell
        """
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        # Outside the defined table
        if y >= self.height:
            cell = Cell()
        else:
            # Inside the defined table
            row = self._get_row2_base(y)
            if row is None:
                raise ValueError
            read_cell = row.get_cell(x, clone=clone)
            if read_cell is None:
                raise ValueError
            cell = read_cell
            if not keep_repeated:
                repeated = cell.repeated or 1
                if repeated >= 2:
                    cell.repeated = None
        cell.x = x
        cell.y = y
        return cell

    def get_value(
        self,
        coord: tuple | list | str,
        get_type: bool = False,
    ) -> Any:
        """Shortcut to get the Python value of the cell at the given
        coordinates.

        If get_type is True, returns the tuples (value, ODF type)

        coord is either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4". If an Area is given, the upper
        left position is used as coord.

        Arguments:

            coord -- (int, int) or str : coordinate

        Return: Python type
        """
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        # Outside the defined table
        if y >= self.height:
            if get_type:
                return (None, None)
            return None
        else:
            # Inside the defined table
            row = self._get_row2_base(y)
            if row is None:
                raise ValueError
            cell = row._get_cell2_base(x)
            if cell is None:
                if get_type:
                    return (None, None)
                return None
            return cell.get_value(get_type=get_type)

    def set_cell(
        self,
        coord: tuple | list | str,
        cell: Cell | None = None,
        clone: bool = True,
    ) -> Cell:
        """Replace a cell of the table at the given coordinates.

        They are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        Arguments:

            coord -- (int, int) or str : coordinate

            cell -- Cell

        return the cell, with x and y updated
        """
        if cell is None:
            cell = Cell()
            clone = False
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        cell.x = x
        cell.y = y
        if y >= self.height:
            row = Row()
            cell_back = row.set_cell(x, cell, clone=clone)
            self.set_row(y, row, clone=False)
        else:
            row_read = self._get_row2_base(y)
            if row_read is None:
                raise ValueError
            row = row_read
            row.y = y
            repeated = row.repeated or 1
            if repeated > 1:
                row = row.clone
                row.repeated = None
                cell_back = row.set_cell(x, cell, clone=clone)
                self.set_row(y, row, clone=False)
            else:
                cell_back = row.set_cell(x, cell, clone=clone)
                # Update width if necessary, since we don't use set_row
                self._update_width(row)
        return cell_back

    def set_cells(
        self,
        cells: list[list[Cell]] | list[tuple[Cell]],
        coord: tuple | list | str | None = None,
        clone: bool = True,
    ) -> None:
        """Set the cells in the table, from the 'coord' position.

        'coord' is the coordinate of the upper left cell to be modified by
        values. If 'coord' is None, default to the position (0,0) ("A1").
        If 'coord' is an area (e.g. "A2:B5"), the upper left position of this
        area is used as coordinate.

        The table is *not* cleared before the operation, to reset the table
        before setting cells, use table.clear().

        A list of lists is expected, with as many lists as rows to be set, and
        as many cell in each sublist as cells to be setted in the row.

        Arguments:

            cells -- list of list of cells

            coord -- tuple or str

            values -- list of lists of python types
        """
        if coord:
            x, y = self._translate_cell_coordinates(coord)
        else:
            x = y = 0
        if y is None:
            y = 0
        if x is None:
            x = 0
        y -= 1
        for row_cells in cells:
            y += 1
            if not row_cells:
                continue
            row = self.get_row(y, clone=True)
            repeated = row.repeated or 1
            if repeated >= 2:
                row.repeated = None
            row.set_cells(row_cells, start=x, clone=clone)
            self.set_row(y, row, clone=False)
            self._update_width(row)

    def set_value(
        self,
        coord: tuple | list | str,
        value: Any,
        cell_type: str | None = None,
        currency: str | None = None,
        style: str | None = None,
    ) -> None:
        """Set the Python value of the cell at the given coordinates.

        They are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        Arguments:

            coord -- (int, int) or str

            value -- Python type

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

            currency -- three-letter str

            style -- str

        """
        self.set_cell(
            coord,
            Cell(value, cell_type=cell_type, currency=currency, style=style),
            clone=False,
        )

    def set_cell_image(
        self,
        coord: tuple | list | str,
        image_frame: Frame,
        doc_type: str | None = None,
    ) -> None:
        """Do all the magic to display an image in the cell at the given
        coordinates.

        They are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        The frame element must contain the expected image position and
        dimensions.

        DrawImage insertion depends on the document type, so the type must be
        provided or the table element must be already attached to a document.

        Arguments:

            coord -- (int, int) or str

            image_frame -- Frame including an image

            doc_type -- 'spreadsheet' or 'text'
        """
        # Test document type
        if doc_type is None:
            body = self.document_body
            if body is None:
                raise ValueError("document type not found")
            doc_type = {"office:spreadsheet": "spreadsheet", "office:text": "text"}.get(
                body.tag
            )
            if doc_type is None:
                raise ValueError("document type not supported for images")
        # We need the end address of the image
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        cell = self.get_cell((x, y))
        image_frame = image_frame.clone  # type: ignore
        # Remove any previous paragraph, frame, etc.
        for child in cell.children:
            cell.delete(child)
        # Now it all depends on the document type
        if doc_type == "spreadsheet":
            image_frame.anchor_type = "char"
            # The frame needs end coordinates
            width, height = image_frame.size
            image_frame.set_attribute("table:end-x", width)
            image_frame.set_attribute("table:end-y", height)
            # FIXME what happens when the address changes?
            address = f"{self.name}.{digit_to_alpha(x)}{y + 1}"
            image_frame.set_attribute("table:end-cell-address", address)
            # The frame is directly in the cell
            cell.append(image_frame)
        elif doc_type == "text":
            # The frame must be in a paragraph
            cell.set_value("")
            paragraph = cell.get_element("text:p")
            if paragraph is None:
                raise ValueError
            paragraph.append(image_frame)
        self.set_cell(coord, cell)

    def insert_cell(
        self,
        coord: tuple | list | str,
        cell: Cell | None = None,
        clone: bool = True,
    ) -> Cell:
        """Insert the given cell at the given coordinates. If no cell is
        given, an empty one is created.

        Coordinates are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        Cells on the right are shifted. Other rows remain untouched.

        Arguments:

            coord -- (int, int) or str

            cell -- Cell

        returns the cell with x and y updated
        """
        if cell is None:
            cell = Cell()
            clone = False
        if clone:
            cell = cell.clone
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        row = self._get_row2(y, clone=True)
        row.y = y
        row.repeated = None
        cell_back = row.insert_cell(x, cell, clone=False)
        self.set_row(y, row, clone=False)
        # Update width if necessary
        self._update_width(row)
        return cell_back

    def append_cell(
        self,
        y: int | str,
        cell: Cell | None = None,
        clone: bool = True,
    ) -> Cell:
        """Append the given cell at the "y" coordinate. Repeated cells are
        accepted. If no cell is given, an empty one is created.

        Position start at 0. So cell A4 is on row 3.

        Other rows remain untouched.

        Arguments:

            y -- int or str

            cell -- Cell

        returns the cell with x and y updated
        """
        if cell is None:
            cell = Cell()
            clone = False
        if clone:
            cell = cell.clone
        y = self._translate_y_from_any(y)
        row = self._get_row2(y)
        row.y = y
        cell_back = row.append_cell(cell, clone=False)
        self.set_row(y, row)
        # Update width if necessary
        self._update_width(row)
        return cell_back

    def delete_cell(self, coord: tuple | list | str) -> None:
        """Delete the cell at the given coordinates, so that next cells are
        shifted to the left.

        Coordinates are either a 2-uplet of (x, y) starting from 0, or a
        human-readable position like "C4".

        Use set_value() for erasing value.

        Arguments:

            coord -- (int, int) or str
        """
        x, y = self._translate_cell_coordinates(coord)
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        # Outside the defined table
        if y >= self.height:
            return
        # Inside the defined table
        row = self._get_row2_base(y)
        if row is None:
            raise ValueError
        row.delete_cell(x)
        # self.set_row(y, row)

    # Columns

    def _get_columns(self) -> list[Column]:
        return self.get_elements(_XP_COLUMN)  # type: ignore

    def traverse_columns(
        self,
        start: int | None = None,
        end: int | None = None,
    ) -> Iterator[Column]:
        """Yield as many column elements as expected columns in the table,
        i.e. expand repetitions by returning the same column as many times as
        necessary.

            Arguments:

                start -- int

                end -- int

        Copies are returned, use set_column() to push them back.
        """
        if start is None:
            start = 0
        start = max(0, start)
        if end is None:
            end = 2**32
        if end < start:
            return
        x = -1
        for column in self._yield_odf_columns():
            x += 1
            if x < start:
                continue
            if x > end:
                return
            column.x = x
            yield column

    def _yield_odf_columns(self):
        for column in self._get_columns():
            if column.repeated is None:
                yield column
            else:
                for _ in range(column.repeated):
                    column_copy = column.clone
                    column_copy.repeated = None
                    yield column_copy

    def get_columns(
        self,
        coord: tuple | list | str | None = None,
        style: str | None = None,
    ) -> list[Column]:
        """Get the list of columns matching the criteria.

        Copies are returned, use set_column() to push them back.

        Arguments:

            coord -- str or tuple of int : coordinates of columns

            style -- str

        Return: list of columns
        """
        if coord:
            x, _y, _z, t = self._translate_column_coordinates(coord)
        else:
            x = t = None
        if not style:
            return list(self.traverse_columns(start=x, end=t))
        columns = []
        for column in self.traverse_columns(start=x, end=t):
            if style != column.style:
                continue
            columns.append(column)
        return columns

    def _get_column2(self, x: int) -> Column | None:
        # Outside the defined table
        if x >= self.width:
            return Column()
        idx = self._table_cache.col_idx(x)
        if idx is None:
            return None
        column = self._table_cache.cached_col(idx)
        if column is None:
            column = self._get_element_idx2(_XP_COLUMN_IDX, idx)
            if column is None:
                return None
        return column.clone  # type: ignore

    @property
    def columns(self) -> list[Column]:
        """Get the list of all columns matching the criteria.

        Copies are returned, use set_column() to push them back.

        Return: list of columns
        """
        return list(self.traverse_columns())

    def get_column(self, x: int | str) -> Column:
        """Get the column at the given "x" position.

        ODF columns don't contain cells, only style information.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        A copy is returned, use set_column() to push it back.

        Arguments:

            x -- int or str

        Return: Column
        """
        x = self._translate_x_from_any(x)
        column = self._get_column2(x)
        if column is None:
            raise ValueError
        column.x = x
        return column

    def set_column(
        self,
        x: int | str,
        column: Column | None = None,
    ) -> Column:
        """Replace the column at the given "x" position.

        ODF columns don't contain cells, only style information.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Arguments:

            x -- int or str

            column -- Column
        """
        x = self._translate_x_from_any(x)
        if column is None:
            column = Column()
            repeated = 1
        else:
            repeated = column.repeated or 1
        column.x = x
        # Outside the defined table ?
        diff = x - self.width
        if diff == 0:
            column_back = self.append_column(column, _repeated=repeated)
        elif diff > 0:
            self.append_column(Column(repeated=diff), _repeated=diff)
            column_back = self.append_column(column, _repeated=repeated)
        else:
            # Inside the defined table
            column_back = self._table_cache.set_col_in_cache(x, column, self)
        return column_back

    def insert_column(
        self,
        x: int | str,
        column: Column | None = None,
    ) -> Column:
        """Insert the column before the given "x" position. If no column is
        given, an empty one is created.

        ODF columns don't contain cells, only style information.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Arguments:

            x -- int or str

            column -- Column
        """
        if column is None:
            column = Column()
        x = self._translate_x_from_any(x)
        diff = x - self.width
        if diff < 0:
            column_back = self._table_cache.insert_col_in_cache(x, column, self)
        elif diff == 0:
            column_back = self.append_column(column.clone)
        else:
            self.append_column(Column(repeated=diff), _repeated=diff)
            column_back = self.append_column(column.clone)
        column_back.x = x  # type: ignore
        # Repetitions are accepted
        repeated = column.repeated or 1
        # Update width on every row
        for row in self._get_rows():
            if row.width > x:
                row.insert_cell(x, Cell(repeated=repeated))
            # Shorter rows don't need insert
            # Longer rows shouldn't exist!
        return column_back  # type: ignore

    def append_column(
        self,
        column: Column | None = None,
        _repeated: int | None = None,
    ) -> Column:
        """Append the column at the end of the table. If no column is given,
        an empty one is created.

        ODF columns don't contain cells, only style information.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Arguments:

            column -- Column

            _repeated -- (optional), repeated value of the column
        """
        if column is None:
            column = Column()
            _repeated = 1
        else:
            column = column.clone
        odf_idx = self._table_cache.col_map_length() - 1
        if odf_idx < 0:
            position = 0
        else:
            last_column = self._get_element_idx2(_XP_COLUMN_IDX, odf_idx)
            if last_column is None:
                raise ValueError
            position = self.index(last_column) + 1
        column.x = self.width
        self.insert(column, position=position)
        # Repetitions are accepted
        if _repeated is None:
            _repeated = column.repeated or 1
        self._table_cache.insert_col_map_once(_repeated)
        # No need to update row widths
        return column

    def delete_column(self, x: int | str) -> None:
        """Delete the column at the given position. ODF columns don't contain
        cells, only style information.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Arguments:

            x -- int or str
        """
        x = self._translate_x_from_any(x)
        # Outside the defined table
        if x >= self.width:
            return
        # Inside the defined table
        self._table_cache.delete_col_in_cache(x, self)
        # Update width
        width = self.width
        for row in self._get_rows():
            if row.width >= width:
                row.delete_cell(x)

    def get_column_cells(
        self,
        x: int | str,
        style: str | None = None,
        content: str | None = None,
        cell_type: str | None = None,
        complete: bool = False,
    ) -> list[Cell | None]:
        """Get the list of cells at the given position.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Filter by cell_type, with cell_type 'all' will retrieve cells of any
        type, aka non empty cells.

        If complete is True, replace missing values by None.

        Arguments:

            x -- int or str

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            content -- str regex

            style -- str

            complete -- boolean

        Return: list of Cell
        """
        x = self._translate_x_from_any(x)
        if cell_type:
            cell_type = cell_type.lower().strip()
        cells: list[Cell | None] = []
        if not style and not content and not cell_type:
            for row in self.traverse():
                cells.append(row.get_cell(x, clone=True))
            return cells
        for row in self.traverse():
            cell = row.get_cell(x, clone=True)
            if cell is None:
                raise ValueError
            # Filter the cells by cell_type
            if cell_type:
                ctype = cell.type
                if not ctype or not (ctype == cell_type or cell_type == "all"):
                    if complete:
                        cells.append(None)
                    continue
            # Filter the cells with the regex
            if content and not cell.match(content):
                if complete:
                    cells.append(None)
                continue
            # Filter the cells with the style
            if style and style != cell.style:
                if complete:
                    cells.append(None)
                continue
            cells.append(cell)
        return cells

    def get_column_values(
        self,
        x: int | str,
        cell_type: str | None = None,
        complete: bool = True,
        get_type: bool = False,
    ) -> list[Any]:
        """Shortcut to get the list of Python values for the cells at the
        given position.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        Filter by cell_type, with cell_type 'all' will retrieve cells of any
        type, aka non empty cells.
        If cell_type and complete is True, replace missing values by None.

        If get_type is True, returns a tuple (value, ODF type of value)

        Arguments:

            x -- int or str

            cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                         'currency', 'percentage' or 'all'

            complete -- boolean

            get_type -- boolean

        Return: list of Python types
        """
        cells = self.get_column_cells(
            x, style=None, content=None, cell_type=cell_type, complete=complete
        )
        values: list[Any] = []
        for cell in cells:
            if cell is None:
                if complete:
                    if get_type:
                        values.append((None, None))
                    else:
                        values.append(None)
                continue
            if cell_type:
                ctype = cell.type
                if not ctype or not (ctype == cell_type or cell_type == "all"):
                    if complete:
                        if get_type:
                            values.append((None, None))
                        else:
                            values.append(None)
                    continue
            values.append(cell.get_value(get_type=get_type))
        return values

    def set_column_cells(self, x: int | str, cells: list[Cell]) -> None:
        """Shortcut to set the list of cells at the given position.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        The list must have the same length than the table height.

        Arguments:

            x -- int or str

            cells -- list of Cell
        """
        height = self.height
        if len(cells) != height:
            raise ValueError(f"col mismatch: {height} cells expected")
        cells_iterator = iter(cells)
        for y, row in enumerate(self.traverse()):
            row.set_cell(x, next(cells_iterator))
            self.set_row(y, row)

    def set_column_values(
        self,
        x: int | str,
        values: list,
        cell_type: str | None = None,
        currency: str | None = None,
        style: str | None = None,
    ) -> None:
        """Shortcut to set the list of Python values of cells at the given
        position.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        The list must have the same length than the table height.

        Arguments:

            x -- int or str

            values -- list of Python types

            cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                         'string' or 'time'

            currency -- three-letter str

            style -- str
        """
        cells = [
            Cell(value, cell_type=cell_type, currency=currency, style=style)
            for value in values
        ]
        self.set_column_cells(x, cells)

    def is_column_empty(self, x: int | str, aggressive: bool = False) -> bool:
        """Return wether every cell in the column at "x" position has no value
        or the value evaluates to False (empty string), and no style.

        Position start at 0. So cell C4 is on column 2. Alphabetical position
        like "C" is accepted.

        If aggressive is True, empty cells with style are considered empty.

        Return: bool
        """
        for cell in self.get_column_cells(x):
            if cell is None:
                continue
            if not cell.is_empty(aggressive=aggressive):
                return False
        return True

    # Named Range

    def get_named_ranges(  # type: ignore
        self,
        table_name: str | list[str] | None = None,
    ) -> list[NamedRange]:
        """Returns the list of available Name Ranges of the spreadsheet. If
        table_name is provided, limits the search to these tables.
        Beware : named ranges are stored at the body level, thus do not call
        this method on a cloned table.

        Arguments:

            table_names -- str or list of str, names of tables

        Return : list of table_range
        """
        body = self.document_body
        if not body:
            return []
        all_named_ranges = body.get_named_ranges()
        if not table_name:
            return all_named_ranges  # type:ignore
        filter_ = []
        if isinstance(table_name, str):
            filter_.append(table_name)
        elif isiterable(table_name):
            filter_.extend(table_name)
        else:
            raise ValueError(
                f"table_name must be string or Iterable, not {type(table_name)}"
            )
        return [
            nr
            for nr in all_named_ranges
            if nr.table_name in filter_  # type:ignore
        ]

    def get_named_range(self, name: str) -> NamedRange:
        """Returns the Name Ranges of the specified name. If
        table_name is provided, limits the search to these tables.
        Beware : named ranges are stored at the body level, thus do not call
        this method on a cloned table.

        Arguments:

            name -- str, name of the named range object

        Return : NamedRange
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document")
        return body.get_named_range(name)  # type: ignore

    def set_named_range(
        self,
        name: str,
        crange: str | tuple | list,
        table_name: str | None = None,
        usage: str | None = None,
    ) -> None:
        """Create a Named Range element and insert it in the document.
        Beware : named ranges are stored at the body level, thus do not call
        this method on a cloned table.

        Arguments:

            name -- str, name of the named range

            crange -- str or tuple of int, cell or area coordinate

            table_name -- str, name of the table

            uage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
        """
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document")
        if not name:
            raise ValueError("Name required.")
        if table_name is None:
            table_name = self.name
        named_range = NamedRange(name, crange, table_name, usage)
        body.append_named_range(named_range)

    def delete_named_range(self, name: str) -> None:
        """Delete the Named Range of specified name from the spreadsheet.
        Beware : named ranges are stored at the body level, thus do not call
        this method on a cloned table.

        Arguments:

            name -- str
        """
        name = name.strip()
        if not name:
            raise ValueError("Name required.")
        body = self.document_body
        if not body:
            raise ValueError("Table is not inside a document.")
        body.delete_named_range(name)

    #
    # Cell span
    #

    def set_span(
        self,
        area: str | tuple | list,
        merge: bool = False,
    ) -> bool:
        """Create a Cell Span : span the first cell of the area on several
        columns and/or rows.
        If merge is True, replace text of the cell by the concatenation of
        existing text in covered cells.
        Beware : if merge is True, old text is changed, if merge is False
        (the default), old text in coverd cells is still present but not
        displayed by most GUI.

        If the area defines only one cell, the set span will do nothing.
        It is not allowed to apply set span to an area whose one cell already
        belongs to previous cell span.

        Area can be either one cell (like 'A1') or an area ('A1:B2'). It can
        be provided as an alpha numeric value like "A1:B2' or a tuple like
        (0, 0, 1, 1) or (0, 0).

        Arguments:

            area -- str or tuple of int, cell or area coordinate

            merge -- boolean
        """
        # get area
        digits = convert_coordinates(area)
        if len(digits) == 4:
            x, y, z, t = digits
        else:
            x, y = digits
            z, t = digits
        start = x, y
        end = z, t
        if start == end:
            # one cell : do nothing
            return False
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        if z is None:
            raise ValueError
        if t is None:
            raise ValueError
        # check for previous span
        good = True
        # Check boundaries and empty cells : need to crate non existent cells
        # so don't use get_cells directly, but get_cell
        cells = []
        for yy in range(y, t + 1):
            row_cells = []
            for xx in range(x, z + 1):
                row_cells.append(
                    self.get_cell((xx, yy), clone=True, keep_repeated=False)
                )
            cells.append(row_cells)
        for row in cells:
            for cell in row:
                if cell.is_spanned():
                    good = False
                    break
            if not good:
                break
        if not good:
            return False
        # Check boundaries
        # if z >= self.width or t >= self.height:
        #    self.set_cell(coord = end)
        #    print area, z, t
        #    cells = self.get_cells((x, y, z, t))
        #    print cells
        # do it:
        if merge:
            val_list = []
            for row in cells:
                for cell in row:
                    if cell.is_empty(aggressive=True):
                        continue
                    val = cell.get_value()
                    if val is not None:
                        if isinstance(val, str):
                            val.strip()
                        if val != "":
                            val_list.append(val)
                        cell.clear()
            if val_list:
                if len(val_list) == 1:
                    cells[0][0].set_value(val_list[0])
                else:
                    value = " ".join([str(v) for v in val_list if v])
                    cells[0][0].set_value(value)
        cols = z - x + 1
        cells[0][0].set_attribute("table:number-columns-spanned", str(cols))
        rows = t - y + 1
        cells[0][0].set_attribute("table:number-rows-spanned", str(rows))
        for cell in cells[0][1:]:
            cell.tag = "table:covered-table-cell"
        for row in cells[1:]:
            for cell in row:
                cell.tag = "table:covered-table-cell"
        # replace cells in table
        self.set_cells(cells, coord=start, clone=False)
        return True

    def del_span(self, area: str | tuple | list) -> bool:
        """Delete a Cell Span. 'area' is the cell coordiante of the upper left
        cell of the spanned area.

        Area can be either one cell (like 'A1') or an area ('A1:B2'). It can
        be provided as an alpha numeric value like "A1:B2' or a tuple like
        (0, 0, 1, 1) or (0, 0). If an area is provided, the upper left cell
        is used.

        Arguments:

            area -- str or tuple of int, cell or area coordinate
        """
        # get area
        digits = convert_coordinates(area)
        if len(digits) == 4:
            x, y, _z, _t = digits
        else:
            x, y = digits
        if x is None:
            raise ValueError
        if y is None:
            raise ValueError
        start = x, y
        # check for previous span
        cell0 = self.get_cell(start)
        nb_cols = cell0.get_attribute_integer("table:number-columns-spanned")
        if nb_cols is None:
            return False
        nb_rows = cell0.get_attribute_integer("table:number-rows-spanned")
        if nb_rows is None:
            return False
        z = x + nb_cols - 1
        t = y + nb_rows - 1
        cells = self.get_cells((x, y, z, t))
        cells[0][0].del_attribute("table:number-columns-spanned")
        cells[0][0].del_attribute("table:number-rows-spanned")
        for cell in cells[0][1:]:
            cell.tag = "table:table-cell"
        for row in cells[1:]:
            for cell in row:
                cell.tag = "table:table-cell"
        # replace cells in table
        self.set_cells(cells, coord=start, clone=False)
        return True

    # Utilities

    def to_csv(
        self,
        path_or_file: str | Path | None = None,
        dialect: str = "excel",
        **fmtparams: Any,
    ) -> str | None:
        """Export the table as CSV.

        Save the CSV content to the path_or_file path, or return the content
        text if path_or_file is None.

        Arguments:

            path_or_file -- str or Path or None

            dialect -- str, python csv.dialect, can be 'excel', 'unix'...

            **fmtparams -- names parameters passed to csv.writer method
        """

        def write_content(csv_writer: object) -> None:
            for values in self.iter_values():
                line = []
                for value in values:
                    if value is None:
                        value = ""
                    line.append(value)
                csv_writer.writerow(line)  # type: ignore

        content = StringIO(newline="")
        csv_writer = csv.writer(content, dialect=dialect, **fmtparams)
        write_content(csv_writer)
        if path_or_file:
            # windows fix: write file as binary
            Path(path_or_file).write_bytes(content.getvalue().encode())
            return None
        return content.getvalue()

    @classmethod
    def from_csv(
        cls,
        content: str,
        name: str,
        style: str | None = None,
        **fmtparams: Any,
    ) -> Table:
        """Import the CSV text content into a Table. If the path_or_file
        parameter is a Path or a string, it is opened as a path. Else a
        opened file-like is expected.

        CSV format can be autodetected to a certain limit. Use **fmtparams to
        define cvs.reader parameters.

        Arguments:

          contenr -- str, CSV content

          name -- str, name of the Table

          style -- str, style of the Table

          **fmtparams -- names parameters passed to csv.reader method
        """
        data = content.splitlines(True)
        # Sniff the dialect
        sample = "".join(data[:100])
        dialect = csv.Sniffer().sniff(sample)
        # Make the rows
        reader = csv.reader(data, dialect, **fmtparams)
        table = cls(name, style=style)
        encoding = fmtparams.get("encoding", "utf-8")
        for line in reader:
            row = Row()
            # rstrip line
            while line and not line[-1].strip():
                line.pop()
            for value in line:
                cell = Cell(_get_python_value(value, encoding))
                row.append_cell(cell, clone=False)
            table.append_row(row, clone=False)
        return table

cells property

cells: list

Get all cells of the table.

Return: list of list of Cell

columns property

columns: list[Column]

Get the list of all columns matching the criteria.

Copies are returned, use set_column() to push them back.

Return: list of columns

height property

height: int

Get the current height of the table.

Return: int

name property writable

name: str | None

Get / set the name of the table.

The “name” parameter is required and cannot contain []*?:/ or \ characters, ’ (apostrophe) cannot be the first or last character.

print_ranges property writable

print_ranges: list[str]

printable property writable

printable: bool

protected property writable

protected: bool

protection_key property writable

protection_key: str | None

row_groups property

row_groups: list[RowGroup]

Get the list of all RowGroup.

Return: list of RowGroup

rows property

rows: list[Row]

Get the list of all rows.

Return: list of rows

set_protection_key instance-attribute

set_protection_key = protection_key

size property

size: tuple[int, int]

Shortcut to get the current width and height of the table.

Return: (int, int)

style property writable

style: str | None

Get / set the style of the table

Return: str

width property

width: int

Get the current width of the table, measured on columns.

Rows may have different widths, use the Table API to ensure width consistency.

Return: int

__init__

__init__(
    name: str | None = None,
    width: int | str | None = None,
    height: int | str | None = None,
    protected: bool = False,
    protection_key: str | None = None,
    printable: bool = True,
    print_ranges: list[str] | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Create a table element “table:table”, optionally prefilled with “height” rows of “width” cells each.

The “name” parameter is required and cannot contain []*?:/ or \ characters, ’ (apostrophe) cannot be the first or last character.

If the table is to be protected, a protection key must be provided, i.e. a hash value of the password.

If the table must not be printed, set “printable” to False. The table will not be printed when it is not displayed, whatever the value of this argument.

Ranges of cells to print can be provided as a list of cell ranges, e.g. [‘E6:K12’, ‘P6:R12’] or directly as a raw string, e.g. “E6:K12 P6:R12”.

You can access and modify the XML tree manually, but you probably want to use the API to access and alter cells. It will save you from handling repetitions and the same number of cells for each row.

If you use both the table API and the XML API, you are on your own for ensuiring model integrity.

Arguments:

name -- str

width -- int | str

height -- int | str

protected -- bool

protection_key -- str

printable -- bool

print_ranges -- list

style -- str
Source code in odfdo/table.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def __init__(
    self,
    name: str | None = None,
    width: int | str | None = None,
    height: int | str | None = None,
    protected: bool = False,
    protection_key: str | None = None,
    printable: bool = True,
    print_ranges: list[str] | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a table element "table:table", optionally prefilled with
    "height" rows of "width" cells each.

    The "name" parameter is required and cannot contain []*?:/ or \\
    characters, ' (apostrophe) cannot be the first or last character.

    If the table is to be protected, a protection key must be provided,
    i.e. a hash value of the password.

    If the table must not be printed, set "printable" to False. The table
    will not be printed when it is not displayed, whatever the value of
    this argument.

    Ranges of cells to print can be provided as a list of cell ranges,
    e.g. ['E6:K12', 'P6:R12'] or directly as a raw string, e.g.
    "E6:K12 P6:R12".

    You can access and modify the XML tree manually, but you probably want
    to use the API to access and alter cells. It will save you from
    handling repetitions and the same number of cells for each row.

    If you use both the table API and the XML API, you are on your own for
    ensuiring model integrity.

    Arguments:

        name -- str

        width -- int | str

        height -- int | str

        protected -- bool

        protection_key -- str

        printable -- bool

        print_ranges -- list

        style -- str
    """
    super().__init__(**kwargs)
    self._table_cache = TableCache()
    if self._do_init:
        self.name = name
        if protected:
            self.protected = protected
            self.set_protection_key = protection_key
        if not printable:
            self.printable = printable
        if print_ranges:
            self.print_ranges = print_ranges
        if style:
            self.style = style
        # Prefill the table
        if width is not None or height is not None:
            width = int(width or 1)
            height = int(height or 1)
            # Column groups for style information
            columns = Column(repeated=width)
            self._append(columns)
            for _i in range(height):
                row = Row(width)
                self._append(row)
    self._compute_table_cache()

append

append(something: Element | str) -> None

Dispatch .append() call to append_row() or append_column().

Source code in odfdo/table.py
579
580
581
582
583
584
585
586
587
def append(self, something: Element | str) -> None:
    """Dispatch .append() call to append_row() or append_column()."""
    if isinstance(something, Row):
        self.append_row(something)
    elif isinstance(something, Column):
        self.append_column(something)
    else:
        # probably still an error
        self._append(something)

append_cell

append_cell(
    y: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell

Append the given cell at the “y” coordinate. Repeated cells are accepted. If no cell is given, an empty one is created.

Position start at 0. So cell A4 is on row 3.

Other rows remain untouched.

Arguments:

y -- int or str

cell -- Cell

returns the cell with x and y updated

Source code in odfdo/table.py
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
def append_cell(
    self,
    y: int | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell:
    """Append the given cell at the "y" coordinate. Repeated cells are
    accepted. If no cell is given, an empty one is created.

    Position start at 0. So cell A4 is on row 3.

    Other rows remain untouched.

    Arguments:

        y -- int or str

        cell -- Cell

    returns the cell with x and y updated
    """
    if cell is None:
        cell = Cell()
        clone = False
    if clone:
        cell = cell.clone
    y = self._translate_y_from_any(y)
    row = self._get_row2(y)
    row.y = y
    cell_back = row.append_cell(cell, clone=False)
    self.set_row(y, row)
    # Update width if necessary
    self._update_width(row)
    return cell_back

append_column

append_column(
    column: Column | None = None,
    _repeated: int | None = None,
) -> Column

Append the column at the end of the table. If no column is given, an empty one is created.

ODF columns don’t contain cells, only style information.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Arguments:

column -- Column

_repeated -- (optional), repeated value of the column
Source code in odfdo/table.py
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
def append_column(
    self,
    column: Column | None = None,
    _repeated: int | None = None,
) -> Column:
    """Append the column at the end of the table. If no column is given,
    an empty one is created.

    ODF columns don't contain cells, only style information.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Arguments:

        column -- Column

        _repeated -- (optional), repeated value of the column
    """
    if column is None:
        column = Column()
        _repeated = 1
    else:
        column = column.clone
    odf_idx = self._table_cache.col_map_length() - 1
    if odf_idx < 0:
        position = 0
    else:
        last_column = self._get_element_idx2(_XP_COLUMN_IDX, odf_idx)
        if last_column is None:
            raise ValueError
        position = self.index(last_column) + 1
    column.x = self.width
    self.insert(column, position=position)
    # Repetitions are accepted
    if _repeated is None:
        _repeated = column.repeated or 1
    self._table_cache.insert_col_map_once(_repeated)
    # No need to update row widths
    return column

append_row

append_row(
    row: Row | None = None,
    clone: bool = True,
    _repeated: int | None = None,
) -> Row

Append the row at the end of the table. If no row is given, an empty one is created.

Position start at 0. So cell A4 is on row 3.

Note the columns are automatically created when the first row is inserted in an empty table. So better insert a filled row.

Arguments:

row -- Row

_repeated -- (optional), repeated value of the row

returns the row, with updated row.y

Source code in odfdo/table.py
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
def append_row(
    self,
    row: Row | None = None,
    clone: bool = True,
    _repeated: int | None = None,
) -> Row:
    """Append the row at the end of the table. If no row is given, an
    empty one is created.

    Position start at 0. So cell A4 is on row 3.

    Note the columns are automatically created when the first row is
    inserted in an empty table. So better insert a filled row.

    Arguments:

        row -- Row

        _repeated -- (optional), repeated value of the row

    returns the row, with updated row.y
    """
    if row is None:
        row = Row()
        _repeated = 1
    elif clone:
        row = row.clone
    # Appending a repeated row accepted
    # Do not insert next to the last row because it could be in a group
    self._append(row)
    if _repeated is None:
        _repeated = row.repeated or 1
    self._table_cache.insert_row_map_once(_repeated)
    row.y = self.height - 1
    # Initialize columns
    if not self._get_columns():
        repeated = row.width
        self.insert(Column(repeated=repeated), position=0)
        self._compute_table_cache()
    # Update width if necessary
    self._update_width(row)
    return row

clear

clear() -> None

Remove text, children and attributes from the Row.

Source code in odfdo/table.py
244
245
246
247
def clear(self) -> None:
    """Remove text, children and attributes from the Row."""
    self._Element__element.clear()  # type: ignore
    self._table_cache = TableCache()

del_span

del_span(area: str | tuple | list) -> bool

Delete a Cell Span. ‘area’ is the cell coordiante of the upper left cell of the spanned area.

Area can be either one cell (like ‘A1’) or an area (‘A1:B2’). It can be provided as an alpha numeric value like “A1:B2’ or a tuple like (0, 0, 1, 1) or (0, 0). If an area is provided, the upper left cell is used.

Arguments:

area -- str or tuple of int, cell or area coordinate
Source code in odfdo/table.py
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
def del_span(self, area: str | tuple | list) -> bool:
    """Delete a Cell Span. 'area' is the cell coordiante of the upper left
    cell of the spanned area.

    Area can be either one cell (like 'A1') or an area ('A1:B2'). It can
    be provided as an alpha numeric value like "A1:B2' or a tuple like
    (0, 0, 1, 1) or (0, 0). If an area is provided, the upper left cell
    is used.

    Arguments:

        area -- str or tuple of int, cell or area coordinate
    """
    # get area
    digits = convert_coordinates(area)
    if len(digits) == 4:
        x, y, _z, _t = digits
    else:
        x, y = digits
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    start = x, y
    # check for previous span
    cell0 = self.get_cell(start)
    nb_cols = cell0.get_attribute_integer("table:number-columns-spanned")
    if nb_cols is None:
        return False
    nb_rows = cell0.get_attribute_integer("table:number-rows-spanned")
    if nb_rows is None:
        return False
    z = x + nb_cols - 1
    t = y + nb_rows - 1
    cells = self.get_cells((x, y, z, t))
    cells[0][0].del_attribute("table:number-columns-spanned")
    cells[0][0].del_attribute("table:number-rows-spanned")
    for cell in cells[0][1:]:
        cell.tag = "table:table-cell"
    for row in cells[1:]:
        for cell in row:
            cell.tag = "table:table-cell"
    # replace cells in table
    self.set_cells(cells, coord=start, clone=False)
    return True

delete_cell

delete_cell(coord: tuple | list | str) -> None

Delete the cell at the given coordinates, so that next cells are shifted to the left.

Coordinates are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

Use set_value() for erasing value.

Arguments:

coord -- (int, int) or str
Source code in odfdo/table.py
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
def delete_cell(self, coord: tuple | list | str) -> None:
    """Delete the cell at the given coordinates, so that next cells are
    shifted to the left.

    Coordinates are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    Use set_value() for erasing value.

    Arguments:

        coord -- (int, int) or str
    """
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    # Outside the defined table
    if y >= self.height:
        return
    # Inside the defined table
    row = self._get_row2_base(y)
    if row is None:
        raise ValueError
    row.delete_cell(x)

delete_column

delete_column(x: int | str) -> None

Delete the column at the given position. ODF columns don’t contain cells, only style information.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Arguments:

x -- int or str
Source code in odfdo/table.py
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
def delete_column(self, x: int | str) -> None:
    """Delete the column at the given position. ODF columns don't contain
    cells, only style information.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Arguments:

        x -- int or str
    """
    x = self._translate_x_from_any(x)
    # Outside the defined table
    if x >= self.width:
        return
    # Inside the defined table
    self._table_cache.delete_col_in_cache(x, self)
    # Update width
    width = self.width
    for row in self._get_rows():
        if row.width >= width:
            row.delete_cell(x)

delete_named_range

delete_named_range(name: str) -> None

Delete the Named Range of specified name from the spreadsheet. Beware : named ranges are stored at the body level, thus do not call this method on a cloned table.

Arguments:

name -- str
Source code in odfdo/table.py
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
def delete_named_range(self, name: str) -> None:
    """Delete the Named Range of specified name from the spreadsheet.
    Beware : named ranges are stored at the body level, thus do not call
    this method on a cloned table.

    Arguments:

        name -- str
    """
    name = name.strip()
    if not name:
        raise ValueError("Name required.")
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document.")
    body.delete_named_range(name)

delete_row

delete_row(y: int | str) -> None

Delete the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

Arguments:

y -- int or str
Source code in odfdo/table.py
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
def delete_row(self, y: int | str) -> None:
    """Delete the row at the given "y" position.

    Position start at 0. So cell A4 is on row 3.

    Arguments:

        y -- int or str
    """
    y = self._translate_y_from_any(y)
    # Outside the defined table
    if y >= self.height:
        return
    # Inside the defined table
    self._table_cache.delete_row_in_cache(y, self)

extend_rows

extend_rows(rows: list[Row] | None = None) -> None

Append a list of rows at the end of the table.

Arguments:

rows -- list of Row
Source code in odfdo/table.py
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
def extend_rows(self, rows: list[Row] | None = None) -> None:
    """Append a list of rows at the end of the table.

    Arguments:

        rows -- list of Row
    """
    if rows is None:
        rows = []
    self.extend(rows)
    self._compute_table_cache()
    # Update width if necessary
    width = self.width
    for row in self.traverse():
        if row.width > width:
            width = row.width
    diff = width - self.width
    if diff > 0:
        self.append_column(Column(repeated=diff))

from_csv classmethod

from_csv(
    content: str,
    name: str,
    style: str | None = None,
    **fmtparams: Any,
) -> Table

Import the CSV text content into a Table. If the path_or_file parameter is a Path or a string, it is opened as a path. Else a opened file-like is expected.

CSV format can be autodetected to a certain limit. Use **fmtparams to define cvs.reader parameters.

Arguments:

contenr – str, CSV content

name – str, name of the Table

style – str, style of the Table

**fmtparams – names parameters passed to csv.reader method

Source code in odfdo/table.py
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
@classmethod
def from_csv(
    cls,
    content: str,
    name: str,
    style: str | None = None,
    **fmtparams: Any,
) -> Table:
    """Import the CSV text content into a Table. If the path_or_file
    parameter is a Path or a string, it is opened as a path. Else a
    opened file-like is expected.

    CSV format can be autodetected to a certain limit. Use **fmtparams to
    define cvs.reader parameters.

    Arguments:

      contenr -- str, CSV content

      name -- str, name of the Table

      style -- str, style of the Table

      **fmtparams -- names parameters passed to csv.reader method
    """
    data = content.splitlines(True)
    # Sniff the dialect
    sample = "".join(data[:100])
    dialect = csv.Sniffer().sniff(sample)
    # Make the rows
    reader = csv.reader(data, dialect, **fmtparams)
    table = cls(name, style=style)
    encoding = fmtparams.get("encoding", "utf-8")
    for line in reader:
        row = Row()
        # rstrip line
        while line and not line[-1].strip():
            line.pop()
        for value in line:
            cell = Cell(_get_python_value(value, encoding))
            row.append_cell(cell, clone=False)
        table.append_row(row, clone=False)
    return table

get_cell

get_cell(
    coord: tuple | list | str,
    clone: bool = True,
    keep_repeated: bool = True,
) -> Cell

Get the cell at the given coordinates.

They are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

A copy is returned, use set_cell to push it back.

Arguments:

coord -- (int, int) or str

Return: Cell

Source code in odfdo/table.py
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
def get_cell(
    self,
    coord: tuple | list | str,
    clone: bool = True,
    keep_repeated: bool = True,
) -> Cell:
    """Get the cell at the given coordinates.

    They are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    A copy is returned, use ``set_cell`` to push it back.

    Arguments:

        coord -- (int, int) or str

    Return: Cell
    """
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    # Outside the defined table
    if y >= self.height:
        cell = Cell()
    else:
        # Inside the defined table
        row = self._get_row2_base(y)
        if row is None:
            raise ValueError
        read_cell = row.get_cell(x, clone=clone)
        if read_cell is None:
            raise ValueError
        cell = read_cell
        if not keep_repeated:
            repeated = cell.repeated or 1
            if repeated >= 2:
                cell.repeated = None
    cell.x = x
    cell.y = y
    return cell

get_cells

get_cells(
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    style: str | None = None,
    content: str | None = None,
    flat: bool = False,
) -> list

Get the cells matching the criteria. If ‘coord’ is None, parse the whole table, else parse the area defined by ‘coord’.

Filter by cell_type = “all” will retrieve cells of any type, aka non empty cells.

If flat is True (default is False), the method return a single list of all the values, else a list of lists of cells.

if cell_type, style and content are None, get_cells() will return the exact number of cells of the area, including empty cells.

Arguments:

coordinates -- str or tuple of int : coordinates of area

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

content -- str regex

style -- str

flat -- boolean

Return: list of list of Cell

Source code in odfdo/table.py
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
def get_cells(
    self,
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    style: str | None = None,
    content: str | None = None,
    flat: bool = False,
) -> list:
    """Get the cells matching the criteria. If 'coord' is None,
    parse the whole table, else parse the area defined by 'coord'.

    Filter by  cell_type = "all"  will retrieve cells of any
    type, aka non empty cells.

    If flat is True (default is False), the method return a single list
    of all the values, else a list of lists of cells.

    if cell_type, style and content are None, get_cells() will return
    the exact number of cells of the area, including empty cells.

    Arguments:

        coordinates -- str or tuple of int : coordinates of area

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        content -- str regex

        style -- str

        flat -- boolean

    Return: list of list of Cell
    """
    if coord:
        x, y, z, t = self._translate_table_coordinates(coord)
    else:
        x = y = z = t = None
    if flat:
        cells: list[Cell] = []
        for row in self.traverse(start=y, end=t):
            row_cells = row.get_cells(
                coord=(x, z),
                cell_type=cell_type,
                style=style,
                content=content,
            )
            cells.extend(row_cells)
        return cells
    else:
        lcells: list[list[Cell]] = []
        for row in self.traverse(start=y, end=t):
            row_cells = row.get_cells(
                coord=(x, z),
                cell_type=cell_type,
                style=style,
                content=content,
            )
            lcells.append(row_cells)
        return lcells

get_column

get_column(x: int | str) -> Column

Get the column at the given “x” position.

ODF columns don’t contain cells, only style information.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

A copy is returned, use set_column() to push it back.

Arguments:

x -- int or str

Return: Column

Source code in odfdo/table.py
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
def get_column(self, x: int | str) -> Column:
    """Get the column at the given "x" position.

    ODF columns don't contain cells, only style information.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    A copy is returned, use set_column() to push it back.

    Arguments:

        x -- int or str

    Return: Column
    """
    x = self._translate_x_from_any(x)
    column = self._get_column2(x)
    if column is None:
        raise ValueError
    column.x = x
    return column

get_column_cells

get_column_cells(
    x: int | str,
    style: str | None = None,
    content: str | None = None,
    cell_type: str | None = None,
    complete: bool = False,
) -> list[Cell | None]

Get the list of cells at the given position.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Filter by cell_type, with cell_type ‘all’ will retrieve cells of any type, aka non empty cells.

If complete is True, replace missing values by None.

Arguments:

x -- int or str

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

content -- str regex

style -- str

complete -- boolean

Return: list of Cell

Source code in odfdo/table.py
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
def get_column_cells(
    self,
    x: int | str,
    style: str | None = None,
    content: str | None = None,
    cell_type: str | None = None,
    complete: bool = False,
) -> list[Cell | None]:
    """Get the list of cells at the given position.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Filter by cell_type, with cell_type 'all' will retrieve cells of any
    type, aka non empty cells.

    If complete is True, replace missing values by None.

    Arguments:

        x -- int or str

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        content -- str regex

        style -- str

        complete -- boolean

    Return: list of Cell
    """
    x = self._translate_x_from_any(x)
    if cell_type:
        cell_type = cell_type.lower().strip()
    cells: list[Cell | None] = []
    if not style and not content and not cell_type:
        for row in self.traverse():
            cells.append(row.get_cell(x, clone=True))
        return cells
    for row in self.traverse():
        cell = row.get_cell(x, clone=True)
        if cell is None:
            raise ValueError
        # Filter the cells by cell_type
        if cell_type:
            ctype = cell.type
            if not ctype or not (ctype == cell_type or cell_type == "all"):
                if complete:
                    cells.append(None)
                continue
        # Filter the cells with the regex
        if content and not cell.match(content):
            if complete:
                cells.append(None)
            continue
        # Filter the cells with the style
        if style and style != cell.style:
            if complete:
                cells.append(None)
            continue
        cells.append(cell)
    return cells

get_column_values

get_column_values(
    x: int | str,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> list[Any]

Shortcut to get the list of Python values for the cells at the given position.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Filter by cell_type, with cell_type ‘all’ will retrieve cells of any type, aka non empty cells. If cell_type and complete is True, replace missing values by None.

If get_type is True, returns a tuple (value, ODF type of value)

Arguments:

x -- int or str

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

complete -- boolean

get_type -- boolean

Return: list of Python types

Source code in odfdo/table.py
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
def get_column_values(
    self,
    x: int | str,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> list[Any]:
    """Shortcut to get the list of Python values for the cells at the
    given position.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Filter by cell_type, with cell_type 'all' will retrieve cells of any
    type, aka non empty cells.
    If cell_type and complete is True, replace missing values by None.

    If get_type is True, returns a tuple (value, ODF type of value)

    Arguments:

        x -- int or str

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        complete -- boolean

        get_type -- boolean

    Return: list of Python types
    """
    cells = self.get_column_cells(
        x, style=None, content=None, cell_type=cell_type, complete=complete
    )
    values: list[Any] = []
    for cell in cells:
        if cell is None:
            if complete:
                if get_type:
                    values.append((None, None))
                else:
                    values.append(None)
            continue
        if cell_type:
            ctype = cell.type
            if not ctype or not (ctype == cell_type or cell_type == "all"):
                if complete:
                    if get_type:
                        values.append((None, None))
                    else:
                        values.append(None)
                continue
        values.append(cell.get_value(get_type=get_type))
    return values

get_columns

get_columns(
    coord: tuple | list | str | None = None,
    style: str | None = None,
) -> list[Column]

Get the list of columns matching the criteria.

Copies are returned, use set_column() to push them back.

Arguments:

coord -- str or tuple of int : coordinates of columns

style -- str

Return: list of columns

Source code in odfdo/table.py
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
def get_columns(
    self,
    coord: tuple | list | str | None = None,
    style: str | None = None,
) -> list[Column]:
    """Get the list of columns matching the criteria.

    Copies are returned, use set_column() to push them back.

    Arguments:

        coord -- str or tuple of int : coordinates of columns

        style -- str

    Return: list of columns
    """
    if coord:
        x, _y, _z, t = self._translate_column_coordinates(coord)
    else:
        x = t = None
    if not style:
        return list(self.traverse_columns(start=x, end=t))
    columns = []
    for column in self.traverse_columns(start=x, end=t):
        if style != column.style:
            continue
        columns.append(column)
    return columns

get_elements

get_elements(xpath_query: XPath | str) -> list[Element]
Source code in odfdo/table.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def get_elements(self, xpath_query: XPath | str) -> list[Element]:
    element = self._Element__element  # type: ignore
    if isinstance(xpath_query, str):
        new_xpath_query = xpath_compile(xpath_query)
        result = new_xpath_query(element)
    else:
        result = xpath_query(element)
    if not isinstance(result, list):
        raise TypeError("Bad XPath result")
    cache = (self._table_cache, None)
    return [
        Element.from_tag_for_clone(e, cache)
        for e in result
        if isinstance(e, _Element)
    ]

get_formatted_text

get_formatted_text(context: dict | None = None) -> str
Source code in odfdo/table.py
689
690
691
692
def get_formatted_text(self, context: dict | None = None) -> str:
    if context and context["rst_mode"]:
        return self._get_formatted_text_rst(context)
    return self._get_formatted_text_normal(context)

get_named_range

get_named_range(name: str) -> NamedRange

Returns the Name Ranges of the specified name. If table_name is provided, limits the search to these tables. Beware : named ranges are stored at the body level, thus do not call this method on a cloned table.

Arguments:

name -- str, name of the named range object

Return : NamedRange

Source code in odfdo/table.py
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
def get_named_range(self, name: str) -> NamedRange:
    """Returns the Name Ranges of the specified name. If
    table_name is provided, limits the search to these tables.
    Beware : named ranges are stored at the body level, thus do not call
    this method on a cloned table.

    Arguments:

        name -- str, name of the named range object

    Return : NamedRange
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document")
    return body.get_named_range(name)  # type: ignore

get_named_ranges

get_named_ranges(
    table_name: str | list[str] | None = None,
) -> list[NamedRange]

Returns the list of available Name Ranges of the spreadsheet. If table_name is provided, limits the search to these tables. Beware : named ranges are stored at the body level, thus do not call this method on a cloned table.

Arguments:

table_names -- str or list of str, names of tables

Return : list of table_range

Source code in odfdo/table.py
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
def get_named_ranges(  # type: ignore
    self,
    table_name: str | list[str] | None = None,
) -> list[NamedRange]:
    """Returns the list of available Name Ranges of the spreadsheet. If
    table_name is provided, limits the search to these tables.
    Beware : named ranges are stored at the body level, thus do not call
    this method on a cloned table.

    Arguments:

        table_names -- str or list of str, names of tables

    Return : list of table_range
    """
    body = self.document_body
    if not body:
        return []
    all_named_ranges = body.get_named_ranges()
    if not table_name:
        return all_named_ranges  # type:ignore
    filter_ = []
    if isinstance(table_name, str):
        filter_.append(table_name)
    elif isiterable(table_name):
        filter_.extend(table_name)
    else:
        raise ValueError(
            f"table_name must be string or Iterable, not {type(table_name)}"
        )
    return [
        nr
        for nr in all_named_ranges
        if nr.table_name in filter_  # type:ignore
    ]

get_row

get_row(
    y: int | str, clone: bool = True, create: bool = True
) -> Row

Get the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

A copy is returned, use set_cell() to push it back.

Arguments:

y -- int or str

Return: Row

Source code in odfdo/table.py
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
def get_row(self, y: int | str, clone: bool = True, create: bool = True) -> Row:
    """Get the row at the given "y" position.

    Position start at 0. So cell A4 is on row 3.

    A copy is returned, use set_cell() to push it back.

    Arguments:

        y -- int or str

    Return: Row
    """
    # fixme : keep repeat ? maybe an option to functions : "raw=False"
    y = self._translate_y_from_any(y)
    row = self._get_row2(y, clone=clone, create=create)
    if row is None:
        raise ValueError("Row not found")
    row.y = y
    return row

get_row_sub_elements

get_row_sub_elements(y: int | str) -> list[Any]

Shortcut to get the list of Elements values for the cells of the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

Missing values replaced by None.

Arguments:

y -- int, str

Return: list of lists of Elements

Source code in odfdo/table.py
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
def get_row_sub_elements(self, y: int | str) -> list[Any]:
    """Shortcut to get the list of Elements values for the cells of the row
    at the given "y" position.

    Position start at 0. So cell A4 is on row 3.

    Missing values replaced by None.

    Arguments:

        y -- int, str

    Return: list of lists of Elements
    """
    values = self.get_row(y, clone=False).get_sub_elements()
    values.extend([None] * (self.width - len(values)))
    return values

get_row_values

get_row_values(
    y: int | str,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> list

Shortcut to get the list of Python values for the cells of the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

Filter by cell_type, with cell_type ‘all’ will retrieve cells of any type, aka non empty cells. If cell_type and complete is True, replace missing values by None.

If get_type is True, returns a tuple (value, ODF type of value)

Arguments:

y -- int, str

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

complete -- boolean

get_type -- boolean

Return: list of lists of Python types

Source code in odfdo/table.py
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
def get_row_values(
    self,
    y: int | str,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> list:
    """Shortcut to get the list of Python values for the cells of the row
    at the given "y" position.

    Position start at 0. So cell A4 is on row 3.

    Filter by cell_type, with cell_type 'all' will retrieve cells of any
    type, aka non empty cells.
    If cell_type and complete is True, replace missing values by None.

    If get_type is True, returns a tuple (value, ODF type of value)

    Arguments:

        y -- int, str

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        complete -- boolean

        get_type -- boolean

    Return: list of lists of Python types
    """
    values = self.get_row(y, clone=False).get_values(
        cell_type=cell_type, complete=complete, get_type=get_type
    )
    # complete row to match column width
    if complete:
        if get_type:
            values.extend([(None, None)] * (self.width - len(values)))
        else:
            values.extend([None] * (self.width - len(values)))
    return values

get_rows

get_rows(
    coord: tuple | list | str | None = None,
    style: str | None = None,
    content: str | None = None,
) -> list[Row]

Get the list of rows matching the criteria.

Filter by coordinates will parse the area defined by the coordinates.

Arguments:

coord -- str or tuple of int : coordinates of rows

content -- str regex

style -- str

Return: list of rows

Source code in odfdo/table.py
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
def get_rows(
    self,
    coord: tuple | list | str | None = None,
    style: str | None = None,
    content: str | None = None,
) -> list[Row]:
    """Get the list of rows matching the criteria.

    Filter by coordinates will parse the area defined by the coordinates.

    Arguments:

        coord -- str or tuple of int : coordinates of rows

        content -- str regex

        style -- str

    Return: list of rows
    """
    if coord:
        _x, y, _z, t = self._translate_table_coordinates(coord)
    else:
        y = t = None
    # fixme : not clones ?
    if not content and not style:
        return list(self.traverse(start=y, end=t))
    rows = []
    for row in self.traverse(start=y, end=t):
        if content and not row.match(content):
            continue
        if style and style != row.style:
            continue
        rows.append(row)
    return rows

get_value

get_value(
    coord: tuple | list | str, get_type: bool = False
) -> Any

Shortcut to get the Python value of the cell at the given coordinates.

If get_type is True, returns the tuples (value, ODF type)

coord is either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”. If an Area is given, the upper left position is used as coord.

Arguments:

coord -- (int, int) or str : coordinate

Return: Python type

Source code in odfdo/table.py
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
def get_value(
    self,
    coord: tuple | list | str,
    get_type: bool = False,
) -> Any:
    """Shortcut to get the Python value of the cell at the given
    coordinates.

    If get_type is True, returns the tuples (value, ODF type)

    coord is either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4". If an Area is given, the upper
    left position is used as coord.

    Arguments:

        coord -- (int, int) or str : coordinate

    Return: Python type
    """
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    # Outside the defined table
    if y >= self.height:
        if get_type:
            return (None, None)
        return None
    else:
        # Inside the defined table
        row = self._get_row2_base(y)
        if row is None:
            raise ValueError
        cell = row._get_cell2_base(x)
        if cell is None:
            if get_type:
                return (None, None)
            return None
        return cell.get_value(get_type=get_type)

get_values

get_values(
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
    flat: bool = False,
) -> list

Get a matrix of values of the table.

Filter by coordinates will parse the area defined by the coordinates.

If ‘cell_type’ is used and ‘complete’ is True (default), missing values are replaced by None. Filter by ’ cell_type = “all” ’ will retrieve cells of any type, aka non empty cells.

If ‘cell_type’ is None, complete is always True : with no cell type queried, get_values() returns None for each empty cell, the length each lists is equal to the width of the table.

If get_type is True, returns tuples (value, ODF type of value), or (None, None) for empty cells with complete True.

If flat is True, the methods return a single list of all the values. By default, flat is False.

Arguments:

coord -- str or tuple of int : coordinates of area

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

complete -- boolean

get_type -- boolean

Return: list of lists of Python types

Source code in odfdo/table.py
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def get_values(
    self,
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
    flat: bool = False,
) -> list:
    """Get a matrix of values of the table.

    Filter by coordinates will parse the area defined by the coordinates.

    If 'cell_type' is used and 'complete' is True (default), missing values
    are replaced by None.
    Filter by ' cell_type = "all" ' will retrieve cells of any
    type, aka non empty cells.

    If 'cell_type' is None, complete is always True : with no cell type
    queried, get_values() returns None for each empty cell, the length
    each lists is equal to the width of the table.

    If get_type is True, returns tuples (value, ODF type of value), or
    (None, None) for empty cells with complete True.

    If flat is True, the methods return a single list of all the values.
    By default, flat is False.

    Arguments:

        coord -- str or tuple of int : coordinates of area

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        complete -- boolean

        get_type -- boolean

    Return: list of lists of Python types
    """
    if coord:
        x, y, z, t = self._translate_table_coordinates(coord)
    else:
        x = y = z = t = None
    data = []
    for row in self.traverse(start=y, end=t):
        if z is None:
            width = self.width
        else:
            width = min(z + 1, self.width)
        if x is not None:
            width -= x
        values = row.get_values(
            (x, z),
            cell_type=cell_type,
            complete=complete,
            get_type=get_type,
        )
        # complete row to match request width
        if complete:
            if get_type:
                values.extend([(None, None)] * (width - len(values)))
            else:
                values.extend([None] * (width - len(values)))
        if flat:
            data.extend(values)
        else:
            data.append(values)
    return data

insert_cell

insert_cell(
    coord: tuple | list | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell

Insert the given cell at the given coordinates. If no cell is given, an empty one is created.

Coordinates are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

Cells on the right are shifted. Other rows remain untouched.

Arguments:

coord -- (int, int) or str

cell -- Cell

returns the cell with x and y updated

Source code in odfdo/table.py
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
def insert_cell(
    self,
    coord: tuple | list | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell:
    """Insert the given cell at the given coordinates. If no cell is
    given, an empty one is created.

    Coordinates are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    Cells on the right are shifted. Other rows remain untouched.

    Arguments:

        coord -- (int, int) or str

        cell -- Cell

    returns the cell with x and y updated
    """
    if cell is None:
        cell = Cell()
        clone = False
    if clone:
        cell = cell.clone
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    row = self._get_row2(y, clone=True)
    row.y = y
    row.repeated = None
    cell_back = row.insert_cell(x, cell, clone=False)
    self.set_row(y, row, clone=False)
    # Update width if necessary
    self._update_width(row)
    return cell_back

insert_column

insert_column(
    x: int | str, column: Column | None = None
) -> Column

Insert the column before the given “x” position. If no column is given, an empty one is created.

ODF columns don’t contain cells, only style information.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Arguments:

x -- int or str

column -- Column
Source code in odfdo/table.py
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
def insert_column(
    self,
    x: int | str,
    column: Column | None = None,
) -> Column:
    """Insert the column before the given "x" position. If no column is
    given, an empty one is created.

    ODF columns don't contain cells, only style information.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Arguments:

        x -- int or str

        column -- Column
    """
    if column is None:
        column = Column()
    x = self._translate_x_from_any(x)
    diff = x - self.width
    if diff < 0:
        column_back = self._table_cache.insert_col_in_cache(x, column, self)
    elif diff == 0:
        column_back = self.append_column(column.clone)
    else:
        self.append_column(Column(repeated=diff), _repeated=diff)
        column_back = self.append_column(column.clone)
    column_back.x = x  # type: ignore
    # Repetitions are accepted
    repeated = column.repeated or 1
    # Update width on every row
    for row in self._get_rows():
        if row.width > x:
            row.insert_cell(x, Cell(repeated=repeated))
        # Shorter rows don't need insert
        # Longer rows shouldn't exist!
    return column_back  # type: ignore

insert_row

insert_row(
    y: str | int, row: Row | None = None, clone: bool = True
) -> Row

Insert the row before the given “y” position. If no row is given, an empty one is created.

Position start at 0. So cell A4 is on row 3.

If row is None, a new empty row is created.

Arguments:

y -- int or str

row -- Row

returns the row, with updated row.y

Source code in odfdo/table.py
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
def insert_row(
    self, y: str | int, row: Row | None = None, clone: bool = True
) -> Row:
    """Insert the row before the given "y" position. If no row is given,
    an empty one is created.

    Position start at 0. So cell A4 is on row 3.

    If row is None, a new empty row is created.

    Arguments:

        y -- int or str

        row -- Row

    returns the row, with updated row.y
    """
    if row is None:
        row = Row()
        clone = False
    y = self._translate_y_from_any(y)
    diff = y - self.height
    if diff < 0:
        row_back = self._table_cache.insert_row_in_cache(y, row, self)
    elif diff == 0:
        row_back = self.append_row(row, clone=clone)
    else:
        self.append_row(Row(repeated=diff), _repeated=diff, clone=False)
        row_back = self.append_row(row, clone=clone)
    row_back.y = y  # type: ignore
    # Update width if necessary
    self._update_width(row_back)  # type: ignore
    return row_back  # type: ignore

is_column_empty

is_column_empty(
    x: int | str, aggressive: bool = False
) -> bool

Return wether every cell in the column at “x” position has no value or the value evaluates to False (empty string), and no style.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

If aggressive is True, empty cells with style are considered empty.

Return: bool

Source code in odfdo/table.py
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
def is_column_empty(self, x: int | str, aggressive: bool = False) -> bool:
    """Return wether every cell in the column at "x" position has no value
    or the value evaluates to False (empty string), and no style.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    If aggressive is True, empty cells with style are considered empty.

    Return: bool
    """
    for cell in self.get_column_cells(x):
        if cell is None:
            continue
        if not cell.is_empty(aggressive=aggressive):
            return False
    return True

is_empty

is_empty(aggressive: bool = False) -> bool

Return whether every cell in the table has no value or the value evaluates to False (empty string), and no style.

If aggressive is True, empty cells with style are considered empty.

Arguments:

aggressive -- bool
Source code in odfdo/table.py
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
def is_empty(self, aggressive: bool = False) -> bool:
    """Return whether every cell in the table has no value or the value
    evaluates to False (empty string), and no style.

    If aggressive is True, empty cells with style are considered empty.

    Arguments:

        aggressive -- bool
    """
    return all(row.is_empty(aggressive=aggressive) for row in self._get_rows())

is_row_empty

is_row_empty(
    y: int | str, aggressive: bool = False
) -> bool

Return wether every cell in the row at the given “y” position has no value or the value evaluates to False (empty string), and no style.

Position start at 0. So cell A4 is on row 3.

If aggressive is True, empty cells with style are considered empty.

Arguments:

y -- int or str

aggressive -- bool
Source code in odfdo/table.py
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
def is_row_empty(self, y: int | str, aggressive: bool = False) -> bool:
    """Return wether every cell in the row at the given "y" position has
    no value or the value evaluates to False (empty string), and no style.

    Position start at 0. So cell A4 is on row 3.

    If aggressive is True, empty cells with style are considered empty.

    Arguments:

        y -- int or str

        aggressive -- bool
    """
    return self.get_row(y, clone=False).is_empty(aggressive=aggressive)

iter_values

iter_values(
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> Iterator[list]

Iterate through lines of Python values of the table.

Filter by coordinates will parse the area defined by the coordinates.

cell_type, complete, grt_type : see get_values()

Arguments:

coord -- str or tuple of int : coordinates of area

cell_type -- 'boolean', 'float', 'date', 'string', 'time',
             'currency', 'percentage' or 'all'

complete -- boolean

get_type -- boolean

Return: iterator of lists

Source code in odfdo/table.py
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
def iter_values(
    self,
    coord: tuple | list | str | None = None,
    cell_type: str | None = None,
    complete: bool = True,
    get_type: bool = False,
) -> Iterator[list]:
    """Iterate through lines of Python values of the table.

    Filter by coordinates will parse the area defined by the coordinates.

    cell_type, complete, grt_type : see get_values()



    Arguments:

        coord -- str or tuple of int : coordinates of area

        cell_type -- 'boolean', 'float', 'date', 'string', 'time',
                     'currency', 'percentage' or 'all'

        complete -- boolean

        get_type -- boolean

    Return: iterator of lists
    """
    if coord:
        x, y, z, t = self._translate_table_coordinates(coord)
    else:
        x = y = z = t = None
    for row in self.traverse(start=y, end=t):
        if z is None:
            width = self.width
        else:
            width = min(z + 1, self.width)
        if x is not None:
            width -= x
        values = row.get_values(
            (x, z),
            cell_type=cell_type,
            complete=complete,
            get_type=get_type,
        )
        # complete row to match column width
        if complete:
            if get_type:
                values.extend([(None, None)] * (width - len(values)))
            else:
                values.extend([None] * (width - len(values)))
        yield values

optimize_width

optimize_width() -> None

Remove in-place empty rows below and empty cells at the right of the table. Keep repeated styles of empty cells but minimize row width.

Source code in odfdo/table.py
929
930
931
932
933
934
935
936
def optimize_width(self) -> None:
    """Remove *in-place* empty rows below and empty cells at the right of
    the table. Keep repeated styles of empty cells but minimize row width.
    """
    self._optimize_width_trim_rows()
    width = self._optimize_width_length()
    self._optimize_width_rstrip_rows(width)
    self._optimize_width_adapt_columns(width)

rstrip

rstrip(aggressive: bool = False) -> None

Remove in-place empty rows below and empty cells at the right of the table. Cells are empty if they contain no value or it evaluates to False, and no style.

If aggressive is True, empty cells with style are removed too.

Argument:

aggressive -- bool
Source code in odfdo/table.py
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
def rstrip(self, aggressive: bool = False) -> None:
    """Remove *in-place* empty rows below and empty cells at the right of
    the table. Cells are empty if they contain no value or it evaluates
    to False, and no style.

    If aggressive is True, empty cells with style are removed too.

    Argument:

        aggressive -- bool
    """
    # Step 1: remove empty rows below the table
    for row in reversed(self._get_rows()):
        if row.is_empty(aggressive=aggressive):
            row.parent.delete(row)  # type: ignore
        else:
            break
    # Step 2: rstrip remaining rows
    max_width = 0
    for row in self._get_rows():
        row.rstrip(aggressive=aggressive)
        # keep count of the biggest row
        max_width = max(max_width, row.width)
    # raz cache of rows
    self._table_cache.clear_row_indexes()
    # Step 3: trim columns to match max_width
    columns = self._get_columns()
    repeated_cols = self.xpath("table:table-column/@table:number-columns-repeated")
    if not isinstance(repeated_cols, list):
        raise TypeError
    unrepeated = len(columns) - len(repeated_cols)
    column_width = sum(int(r) for r in repeated_cols) + unrepeated  # type: ignore
    diff = column_width - max_width
    if diff > 0:
        for column in reversed(columns):
            repeated = column.repeated or 1
            repeated = repeated - diff
            if repeated > 0:
                column.repeated = repeated
                break
            else:
                column.parent.delete(column)
                diff = -repeated
                if diff == 0:
                    break
    # raz cache of columns
    self._table_cache.clear_col_indexes()
    self._compute_table_cache()

set_cell

set_cell(
    coord: tuple | list | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell

Replace a cell of the table at the given coordinates.

They are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

Arguments:

coord -- (int, int) or str : coordinate

cell -- Cell

return the cell, with x and y updated

Source code in odfdo/table.py
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
def set_cell(
    self,
    coord: tuple | list | str,
    cell: Cell | None = None,
    clone: bool = True,
) -> Cell:
    """Replace a cell of the table at the given coordinates.

    They are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    Arguments:

        coord -- (int, int) or str : coordinate

        cell -- Cell

    return the cell, with x and y updated
    """
    if cell is None:
        cell = Cell()
        clone = False
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    cell.x = x
    cell.y = y
    if y >= self.height:
        row = Row()
        cell_back = row.set_cell(x, cell, clone=clone)
        self.set_row(y, row, clone=False)
    else:
        row_read = self._get_row2_base(y)
        if row_read is None:
            raise ValueError
        row = row_read
        row.y = y
        repeated = row.repeated or 1
        if repeated > 1:
            row = row.clone
            row.repeated = None
            cell_back = row.set_cell(x, cell, clone=clone)
            self.set_row(y, row, clone=False)
        else:
            cell_back = row.set_cell(x, cell, clone=clone)
            # Update width if necessary, since we don't use set_row
            self._update_width(row)
    return cell_back

set_cell_image

set_cell_image(
    coord: tuple | list | str,
    image_frame: Frame,
    doc_type: str | None = None,
) -> None

Do all the magic to display an image in the cell at the given coordinates.

They are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

The frame element must contain the expected image position and dimensions.

DrawImage insertion depends on the document type, so the type must be provided or the table element must be already attached to a document.

Arguments:

coord -- (int, int) or str

image_frame -- Frame including an image

doc_type -- 'spreadsheet' or 'text'
Source code in odfdo/table.py
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
def set_cell_image(
    self,
    coord: tuple | list | str,
    image_frame: Frame,
    doc_type: str | None = None,
) -> None:
    """Do all the magic to display an image in the cell at the given
    coordinates.

    They are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    The frame element must contain the expected image position and
    dimensions.

    DrawImage insertion depends on the document type, so the type must be
    provided or the table element must be already attached to a document.

    Arguments:

        coord -- (int, int) or str

        image_frame -- Frame including an image

        doc_type -- 'spreadsheet' or 'text'
    """
    # Test document type
    if doc_type is None:
        body = self.document_body
        if body is None:
            raise ValueError("document type not found")
        doc_type = {"office:spreadsheet": "spreadsheet", "office:text": "text"}.get(
            body.tag
        )
        if doc_type is None:
            raise ValueError("document type not supported for images")
    # We need the end address of the image
    x, y = self._translate_cell_coordinates(coord)
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    cell = self.get_cell((x, y))
    image_frame = image_frame.clone  # type: ignore
    # Remove any previous paragraph, frame, etc.
    for child in cell.children:
        cell.delete(child)
    # Now it all depends on the document type
    if doc_type == "spreadsheet":
        image_frame.anchor_type = "char"
        # The frame needs end coordinates
        width, height = image_frame.size
        image_frame.set_attribute("table:end-x", width)
        image_frame.set_attribute("table:end-y", height)
        # FIXME what happens when the address changes?
        address = f"{self.name}.{digit_to_alpha(x)}{y + 1}"
        image_frame.set_attribute("table:end-cell-address", address)
        # The frame is directly in the cell
        cell.append(image_frame)
    elif doc_type == "text":
        # The frame must be in a paragraph
        cell.set_value("")
        paragraph = cell.get_element("text:p")
        if paragraph is None:
            raise ValueError
        paragraph.append(image_frame)
    self.set_cell(coord, cell)

set_cells

set_cells(
    cells: list[list[Cell]] | list[tuple[Cell]],
    coord: tuple | list | str | None = None,
    clone: bool = True,
) -> None

Set the cells in the table, from the ‘coord’ position.

‘coord’ is the coordinate of the upper left cell to be modified by values. If ‘coord’ is None, default to the position (0,0) (“A1”). If ‘coord’ is an area (e.g. “A2:B5”), the upper left position of this area is used as coordinate.

The table is not cleared before the operation, to reset the table before setting cells, use table.clear().

A list of lists is expected, with as many lists as rows to be set, and as many cell in each sublist as cells to be setted in the row.

Arguments:

cells -- list of list of cells

coord -- tuple or str

values -- list of lists of python types
Source code in odfdo/table.py
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
def set_cells(
    self,
    cells: list[list[Cell]] | list[tuple[Cell]],
    coord: tuple | list | str | None = None,
    clone: bool = True,
) -> None:
    """Set the cells in the table, from the 'coord' position.

    'coord' is the coordinate of the upper left cell to be modified by
    values. If 'coord' is None, default to the position (0,0) ("A1").
    If 'coord' is an area (e.g. "A2:B5"), the upper left position of this
    area is used as coordinate.

    The table is *not* cleared before the operation, to reset the table
    before setting cells, use table.clear().

    A list of lists is expected, with as many lists as rows to be set, and
    as many cell in each sublist as cells to be setted in the row.

    Arguments:

        cells -- list of list of cells

        coord -- tuple or str

        values -- list of lists of python types
    """
    if coord:
        x, y = self._translate_cell_coordinates(coord)
    else:
        x = y = 0
    if y is None:
        y = 0
    if x is None:
        x = 0
    y -= 1
    for row_cells in cells:
        y += 1
        if not row_cells:
            continue
        row = self.get_row(y, clone=True)
        repeated = row.repeated or 1
        if repeated >= 2:
            row.repeated = None
        row.set_cells(row_cells, start=x, clone=clone)
        self.set_row(y, row, clone=False)
        self._update_width(row)

set_column

set_column(
    x: int | str, column: Column | None = None
) -> Column

Replace the column at the given “x” position.

ODF columns don’t contain cells, only style information.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

Arguments:

x -- int or str

column -- Column
Source code in odfdo/table.py
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
def set_column(
    self,
    x: int | str,
    column: Column | None = None,
) -> Column:
    """Replace the column at the given "x" position.

    ODF columns don't contain cells, only style information.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    Arguments:

        x -- int or str

        column -- Column
    """
    x = self._translate_x_from_any(x)
    if column is None:
        column = Column()
        repeated = 1
    else:
        repeated = column.repeated or 1
    column.x = x
    # Outside the defined table ?
    diff = x - self.width
    if diff == 0:
        column_back = self.append_column(column, _repeated=repeated)
    elif diff > 0:
        self.append_column(Column(repeated=diff), _repeated=diff)
        column_back = self.append_column(column, _repeated=repeated)
    else:
        # Inside the defined table
        column_back = self._table_cache.set_col_in_cache(x, column, self)
    return column_back

set_column_cells

set_column_cells(x: int | str, cells: list[Cell]) -> None

Shortcut to set the list of cells at the given position.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

The list must have the same length than the table height.

Arguments:

x -- int or str

cells -- list of Cell
Source code in odfdo/table.py
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
def set_column_cells(self, x: int | str, cells: list[Cell]) -> None:
    """Shortcut to set the list of cells at the given position.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    The list must have the same length than the table height.

    Arguments:

        x -- int or str

        cells -- list of Cell
    """
    height = self.height
    if len(cells) != height:
        raise ValueError(f"col mismatch: {height} cells expected")
    cells_iterator = iter(cells)
    for y, row in enumerate(self.traverse()):
        row.set_cell(x, next(cells_iterator))
        self.set_row(y, row)

set_column_values

set_column_values(
    x: int | str,
    values: list,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None

Shortcut to set the list of Python values of cells at the given position.

Position start at 0. So cell C4 is on column 2. Alphabetical position like “C” is accepted.

The list must have the same length than the table height.

Arguments:

x -- int or str

values -- list of Python types

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
             'string' or 'time'

currency -- three-letter str

style -- str
Source code in odfdo/table.py
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
def set_column_values(
    self,
    x: int | str,
    values: list,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None:
    """Shortcut to set the list of Python values of cells at the given
    position.

    Position start at 0. So cell C4 is on column 2. Alphabetical position
    like "C" is accepted.

    The list must have the same length than the table height.

    Arguments:

        x -- int or str

        values -- list of Python types

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

        currency -- three-letter str

        style -- str
    """
    cells = [
        Cell(value, cell_type=cell_type, currency=currency, style=style)
        for value in values
    ]
    self.set_column_cells(x, cells)

set_named_range

set_named_range(
    name: str,
    crange: str | tuple | list,
    table_name: str | None = None,
    usage: str | None = None,
) -> None

Create a Named Range element and insert it in the document. Beware : named ranges are stored at the body level, thus do not call this method on a cloned table.

Arguments:

name -- str, name of the named range

crange -- str or tuple of int, cell or area coordinate

table_name -- str, name of the table

uage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
Source code in odfdo/table.py
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
def set_named_range(
    self,
    name: str,
    crange: str | tuple | list,
    table_name: str | None = None,
    usage: str | None = None,
) -> None:
    """Create a Named Range element and insert it in the document.
    Beware : named ranges are stored at the body level, thus do not call
    this method on a cloned table.

    Arguments:

        name -- str, name of the named range

        crange -- str or tuple of int, cell or area coordinate

        table_name -- str, name of the table

        uage -- None or 'print-range', 'filter', 'repeat-column', 'repeat-row'
    """
    body = self.document_body
    if not body:
        raise ValueError("Table is not inside a document")
    if not name:
        raise ValueError("Name required.")
    if table_name is None:
        table_name = self.name
    named_range = NamedRange(name, crange, table_name, usage)
    body.append_named_range(named_range)

set_row

set_row(
    y: int | str, row: Row | None = None, clone: bool = True
) -> Row

Replace the row at the given position with the new one. Repetions of the old row will be adjusted.

If row is None, a new empty row is created.

Position start at 0. So cell A4 is on row 3.

Arguments:

y -- int or str

row -- Row

returns the row, with updated row.y

Source code in odfdo/table.py
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
def set_row(self, y: int | str, row: Row | None = None, clone: bool = True) -> Row:
    """Replace the row at the given position with the new one. Repetions of
    the old row will be adjusted.

    If row is None, a new empty row is created.

    Position start at 0. So cell A4 is on row 3.

    Arguments:

        y -- int or str

        row -- Row

    returns the row, with updated row.y
    """
    if row is None:
        row = Row()
        repeated = 1
        clone = False
    else:
        repeated = row.repeated or 1
    y = self._translate_y_from_any(y)
    row.y = y
    # Outside the defined table ?
    diff = y - self.height
    if diff == 0:
        row_back = self.append_row(row, _repeated=repeated, clone=clone)
    elif diff > 0:
        self.append_row(Row(repeated=diff), _repeated=diff, clone=clone)
        row_back = self.append_row(row, _repeated=repeated, clone=clone)
    else:
        # Inside the defined table
        row_back = self._table_cache.set_row_in_cache(y, row, self, clone=clone)
    # print self.serialize(True)
    # Update width if necessary
    self._update_width(row_back)
    return row_back

set_row_cells

set_row_cells(
    y: int | str, cells: list | None = None
) -> Row

Shortcut to set all the cells of the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

Arguments:

y -- int or str

cells -- list of Python types

style -- str

returns the row, with updated row.y

Source code in odfdo/table.py
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
def set_row_cells(self, y: int | str, cells: list | None = None) -> Row:
    """Shortcut to set *all* the cells of the row at the given
    "y" position.

    Position start at 0. So cell A4 is on row 3.

    Arguments:

        y -- int or str

        cells -- list of Python types

        style -- str

    returns the row, with updated row.y
    """
    if cells is None:
        cells = []
    row = Row()  # needed if clones rows
    row.extend_cells(cells)
    return self.set_row(y, row)  # needed if clones rows

set_row_values

set_row_values(
    y: int | str,
    values: list,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> Row

Shortcut to set the values of all cells of the row at the given “y” position.

Position start at 0. So cell A4 is on row 3.

Arguments:

y -- int or str

values -- list of Python types

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
             'string' or 'time'

currency -- three-letter str

style -- str

returns the row, with updated row.y

Source code in odfdo/table.py
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
def set_row_values(
    self,
    y: int | str,
    values: list,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> Row:
    """Shortcut to set the values of *all* cells of the row at the given
    "y" position.

    Position start at 0. So cell A4 is on row 3.

    Arguments:

        y -- int or str

        values -- list of Python types

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

        currency -- three-letter str

        style -- str

    returns the row, with updated row.y
    """
    row = Row()  # needed if clones rows
    row.set_values(values, style=style, cell_type=cell_type, currency=currency)
    return self.set_row(y, row)  # needed if clones rows

set_span

set_span(
    area: str | tuple | list, merge: bool = False
) -> bool

Create a Cell Span : span the first cell of the area on several columns and/or rows. If merge is True, replace text of the cell by the concatenation of existing text in covered cells. Beware : if merge is True, old text is changed, if merge is False (the default), old text in coverd cells is still present but not displayed by most GUI.

If the area defines only one cell, the set span will do nothing. It is not allowed to apply set span to an area whose one cell already belongs to previous cell span.

Area can be either one cell (like ‘A1’) or an area (‘A1:B2’). It can be provided as an alpha numeric value like “A1:B2’ or a tuple like (0, 0, 1, 1) or (0, 0).

Arguments:

area -- str or tuple of int, cell or area coordinate

merge -- boolean
Source code in odfdo/table.py
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
def set_span(
    self,
    area: str | tuple | list,
    merge: bool = False,
) -> bool:
    """Create a Cell Span : span the first cell of the area on several
    columns and/or rows.
    If merge is True, replace text of the cell by the concatenation of
    existing text in covered cells.
    Beware : if merge is True, old text is changed, if merge is False
    (the default), old text in coverd cells is still present but not
    displayed by most GUI.

    If the area defines only one cell, the set span will do nothing.
    It is not allowed to apply set span to an area whose one cell already
    belongs to previous cell span.

    Area can be either one cell (like 'A1') or an area ('A1:B2'). It can
    be provided as an alpha numeric value like "A1:B2' or a tuple like
    (0, 0, 1, 1) or (0, 0).

    Arguments:

        area -- str or tuple of int, cell or area coordinate

        merge -- boolean
    """
    # get area
    digits = convert_coordinates(area)
    if len(digits) == 4:
        x, y, z, t = digits
    else:
        x, y = digits
        z, t = digits
    start = x, y
    end = z, t
    if start == end:
        # one cell : do nothing
        return False
    if x is None:
        raise ValueError
    if y is None:
        raise ValueError
    if z is None:
        raise ValueError
    if t is None:
        raise ValueError
    # check for previous span
    good = True
    # Check boundaries and empty cells : need to crate non existent cells
    # so don't use get_cells directly, but get_cell
    cells = []
    for yy in range(y, t + 1):
        row_cells = []
        for xx in range(x, z + 1):
            row_cells.append(
                self.get_cell((xx, yy), clone=True, keep_repeated=False)
            )
        cells.append(row_cells)
    for row in cells:
        for cell in row:
            if cell.is_spanned():
                good = False
                break
        if not good:
            break
    if not good:
        return False
    # Check boundaries
    # if z >= self.width or t >= self.height:
    #    self.set_cell(coord = end)
    #    print area, z, t
    #    cells = self.get_cells((x, y, z, t))
    #    print cells
    # do it:
    if merge:
        val_list = []
        for row in cells:
            for cell in row:
                if cell.is_empty(aggressive=True):
                    continue
                val = cell.get_value()
                if val is not None:
                    if isinstance(val, str):
                        val.strip()
                    if val != "":
                        val_list.append(val)
                    cell.clear()
        if val_list:
            if len(val_list) == 1:
                cells[0][0].set_value(val_list[0])
            else:
                value = " ".join([str(v) for v in val_list if v])
                cells[0][0].set_value(value)
    cols = z - x + 1
    cells[0][0].set_attribute("table:number-columns-spanned", str(cols))
    rows = t - y + 1
    cells[0][0].set_attribute("table:number-rows-spanned", str(rows))
    for cell in cells[0][1:]:
        cell.tag = "table:covered-table-cell"
    for row in cells[1:]:
        for cell in row:
            cell.tag = "table:covered-table-cell"
    # replace cells in table
    self.set_cells(cells, coord=start, clone=False)
    return True

set_value

set_value(
    coord: tuple | list | str,
    value: Any,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None

Set the Python value of the cell at the given coordinates.

They are either a 2-uplet of (x, y) starting from 0, or a human-readable position like “C4”.

Arguments:

coord -- (int, int) or str

value -- Python type

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
         'string' or 'time'

currency -- three-letter str

style -- str
Source code in odfdo/table.py
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
def set_value(
    self,
    coord: tuple | list | str,
    value: Any,
    cell_type: str | None = None,
    currency: str | None = None,
    style: str | None = None,
) -> None:
    """Set the Python value of the cell at the given coordinates.

    They are either a 2-uplet of (x, y) starting from 0, or a
    human-readable position like "C4".

    Arguments:

        coord -- (int, int) or str

        value -- Python type

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                 'string' or 'time'

        currency -- three-letter str

        style -- str

    """
    self.set_cell(
        coord,
        Cell(value, cell_type=cell_type, currency=currency, style=style),
        clone=False,
    )

set_values

set_values(
    values: list,
    coord: tuple | list | str | None = None,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None

Set the value of cells in the table, from the ‘coord’ position with values.

‘coord’ is the coordinate of the upper left cell to be modified by values. If ‘coord’ is None, default to the position (0,0) (“A1”). If ‘coord’ is an area (e.g. “A2:B5”), the upper left position of this area is used as coordinate.

The table is not cleared before the operation, to reset the table before setting values, use table.clear().

A list of lists is expected, with as many lists as rows, and as many items in each sublist as cells to be setted. None values in the list will create empty cells with no cell type (but eventually a style).

Arguments:

coord -- tuple or str

values -- list of lists of python types

cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
             'string' or 'time'

currency -- three-letter str

style -- str
Source code in odfdo/table.py
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
def set_values(
    self,
    values: list,
    coord: tuple | list | str | None = None,
    style: str | None = None,
    cell_type: str | None = None,
    currency: str | None = None,
) -> None:
    """Set the value of cells in the table, from the 'coord' position
    with values.

    'coord' is the coordinate of the upper left cell to be modified by
    values. If 'coord' is None, default to the position (0,0) ("A1").
    If 'coord' is an area (e.g. "A2:B5"), the upper left position of this
    area is used as coordinate.

    The table is *not* cleared before the operation, to reset the table
    before setting values, use table.clear().

    A list of lists is expected, with as many lists as rows, and as many
    items in each sublist as cells to be setted. None values in the list
    will create empty cells with no cell type (but eventually a style).

    Arguments:

        coord -- tuple or str

        values -- list of lists of python types

        cell_type -- 'boolean', 'currency', 'date', 'float', 'percentage',
                     'string' or 'time'

        currency -- three-letter str

        style -- str
    """
    if coord:
        x, y = self._translate_cell_coordinates(coord)
    else:
        x = y = 0
    if y is None:
        y = 0
    if x is None:
        x = 0
    y -= 1
    for row_values in values:
        y += 1
        if not row_values:
            continue
        row = self.get_row(y, clone=True)
        repeated = row.repeated or 1
        if repeated >= 2:
            row.repeated = None
        row.set_values(
            row_values,
            start=x,
            cell_type=cell_type,
            currency=currency,
            style=style,
        )
        self.set_row(y, row, clone=False)
        self._update_width(row)

to_csv

to_csv(
    path_or_file: str | Path | None = None,
    dialect: str = "excel",
    **fmtparams: Any,
) -> str | None

Export the table as CSV.

Save the CSV content to the path_or_file path, or return the content text if path_or_file is None.

Arguments:

path_or_file -- str or Path or None

dialect -- str, python csv.dialect, can be 'excel', 'unix'...

**fmtparams -- names parameters passed to csv.writer method
Source code in odfdo/table.py
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
def to_csv(
    self,
    path_or_file: str | Path | None = None,
    dialect: str = "excel",
    **fmtparams: Any,
) -> str | None:
    """Export the table as CSV.

    Save the CSV content to the path_or_file path, or return the content
    text if path_or_file is None.

    Arguments:

        path_or_file -- str or Path or None

        dialect -- str, python csv.dialect, can be 'excel', 'unix'...

        **fmtparams -- names parameters passed to csv.writer method
    """

    def write_content(csv_writer: object) -> None:
        for values in self.iter_values():
            line = []
            for value in values:
                if value is None:
                    value = ""
                line.append(value)
            csv_writer.writerow(line)  # type: ignore

    content = StringIO(newline="")
    csv_writer = csv.writer(content, dialect=dialect, **fmtparams)
    write_content(csv_writer)
    if path_or_file:
        # windows fix: write file as binary
        Path(path_or_file).write_bytes(content.getvalue().encode())
        return None
    return content.getvalue()

transpose

transpose(coord: tuple | list | str | None = None) -> None

Swap in-place rows and columns of the table.

If ‘coord’ is not None, apply transpose only to the area defined by the coordinates. Beware, if area is not square, some cells mays be over written during the process.

Arguments:

coord -- str or tuple of int : coordinates of area

start -- int or str
Source code in odfdo/table.py
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
def transpose(self, coord: tuple | list | str | None = None) -> None:
    """Swap *in-place* rows and columns of the table.

    If 'coord' is not None, apply transpose only to the area defined by the
    coordinates. Beware, if area is not square, some cells mays be over
    written during the process.

    Arguments:

        coord -- str or tuple of int : coordinates of area

        start -- int or str
    """
    data = []
    if coord is None:
        for row in self.traverse():
            data.append(list(row.traverse()))
        transposed_data = zip_longest(*data)
        self.clear()
        # new_rows = []
        for row_cells in transposed_data:
            if not isiterable(row_cells):
                row_cells = (row_cells,)
            row = Row()
            row.extend_cells(row_cells)
            self.append_row(row, clone=False)
        self._compute_table_cache()
    else:
        x, y, z, t = self._translate_table_coordinates(coord)
        if x is None:
            x = 0
        else:
            x = min(x, self.width - 1)
        if z is None:
            z = self.width - 1
        else:
            z = min(z, self.width - 1)
        if y is None:
            y = 0
        else:
            y = min(y, self.height - 1)
        if t is None:
            t = self.height - 1
        else:
            t = min(t, self.height - 1)
        for row in self.traverse(start=y, end=t):
            data.append(list(row.traverse(start=x, end=z)))
        transposed_data = zip_longest(*data)
        # clear locally
        w = z - x + 1
        h = t - y + 1
        if w != h:
            nones = [[None] * w for i in range(h)]
            self.set_values(nones, coord=(x, y, z, t))
        # put transposed
        filtered_data: list[tuple[Cell]] = []
        for row_cells in transposed_data:
            if isinstance(row_cells, (list, tuple)):
                filtered_data.append(row_cells)
            else:
                filtered_data.append((row_cells,))
        self.set_cells(filtered_data, (x, y, x + h - 1, y + w - 1))
        self._compute_table_cache()

traverse

traverse(
    start: int | None = None, end: int | None = None
) -> Iterator[Row]

Yield as many row elements as expected rows in the table, i.e. expand repetitions by returning the same row as many times as necessary.

Arguments:

    start -- int

    end -- int

Copies are returned, use set_row() to push them back.

Source code in odfdo/table.py
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
def traverse(
    self,
    start: int | None = None,
    end: int | None = None,
) -> Iterator[Row]:
    """Yield as many row elements as expected rows in the table, i.e.
    expand repetitions by returning the same row as many times as
    necessary.

        Arguments:

            start -- int

            end -- int

    Copies are returned, use set_row() to push them back.
    """
    if start is None:
        start = 0
    start = max(0, start)
    if end is None:
        end = 2**32
    if end < start:
        return
    y = -1
    for row in self._yield_odf_rows():
        y += 1
        if y < start:
            continue
        if y > end:
            return
        row.y = y
        yield row

traverse_columns

traverse_columns(
    start: int | None = None, end: int | None = None
) -> Iterator[Column]

Yield as many column elements as expected columns in the table, i.e. expand repetitions by returning the same column as many times as necessary.

Arguments:

    start -- int

    end -- int

Copies are returned, use set_column() to push them back.

Source code in odfdo/table.py
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
def traverse_columns(
    self,
    start: int | None = None,
    end: int | None = None,
) -> Iterator[Column]:
    """Yield as many column elements as expected columns in the table,
    i.e. expand repetitions by returning the same column as many times as
    necessary.

        Arguments:

            start -- int

            end -- int

    Copies are returned, use set_column() to push them back.
    """
    if start is None:
        start = 0
    start = max(0, start)
    if end is None:
        end = 2**32
    if end < start:
        return
    x = -1
    for column in self._yield_odf_columns():
        x += 1
        if x < start:
            continue
        if x > end:
            return
        column.x = x
        yield column

Text

Bases: Body

Root of the Text document content, “office:text”.

Source code in odfdo/body.py
137
138
139
140
141
class Text(Body):
    """Root of the Text document content, "office:text"."""

    _tag: str = "office:text"
    _properties: tuple[PropDef, ...] = ()

TextChange

Bases: Element

A text change position, “text:change”.

The TextChange “text:change” element marks a position in an empty region where text has been deleted.

Methods:

Name Description
get_change_element
get_change_info
get_changed_region
get_deleted

Shortcut to get the deleted informations stored in the

get_end

Return None.

get_id
get_inserted

Return None.

get_start

Return None.

set_id
Source code in odfdo/tracked_changes.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
class TextChange(Element):
    """A text change position, "text:change".

    The TextChange "text:change" element marks a position in an empty
    region where text has been deleted.
    """

    _tag = "text:change"

    def get_id(self) -> str | None:
        return self.get_attribute_string("text:change-id")

    def set_id(self, idx: str) -> None:
        self.set_attribute("text:change-id", idx)

    def _get_tracked_changes(self) -> Element | None:
        body = self.document_body
        if not body:
            raise ValueError
        return body.get_tracked_changes()

    def get_changed_region(
        self,
        tracked_changes: Element | None = None,
    ) -> Element | None:
        if not tracked_changes:
            tracked_changes = self._get_tracked_changes()
        idx = self.get_id()
        return tracked_changes.get_changed_region(text_id=idx)  # type: ignore

    def get_change_info(
        self,
        tracked_changes: Element | None = None,
    ) -> Element | None:
        changed_region = self.get_changed_region(tracked_changes=tracked_changes)
        if not changed_region:
            return None  # pragma: nocover
        return changed_region.get_change_info()  # type: ignore

    def get_change_element(
        self,
        tracked_changes: Element | None = None,
    ) -> Element | None:
        changed_region = self.get_changed_region(tracked_changes=tracked_changes)
        if not changed_region:
            return None  # pragma: nocover
        return changed_region.get_change_element()  # type: ignore

    def get_deleted(
        self,
        tracked_changes: Element | None = None,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> Element | None:
        """Shortcut to get the deleted informations stored in the
        TextDeletion stored in the tracked changes.

        Return: Paragraph (or None)."
        """
        changed = self.get_change_element(tracked_changes=tracked_changes)
        if not changed:
            return None  # pragma: nocover
        return changed.get_deleted(  # type: ignore
            as_text=as_text,
            no_header=no_header,
        )

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Return None."""
        return None

    def get_start(self) -> TextChangeStart | None:
        """Return None."""
        return None

    def get_end(self) -> TextChangeEnd | None:
        """Return None."""
        return None

get_change_element

get_change_element(
    tracked_changes: Element | None = None,
) -> Element | None
Source code in odfdo/tracked_changes.py
536
537
538
539
540
541
542
543
def get_change_element(
    self,
    tracked_changes: Element | None = None,
) -> Element | None:
    changed_region = self.get_changed_region(tracked_changes=tracked_changes)
    if not changed_region:
        return None  # pragma: nocover
    return changed_region.get_change_element()  # type: ignore

get_change_info

get_change_info(
    tracked_changes: Element | None = None,
) -> Element | None
Source code in odfdo/tracked_changes.py
527
528
529
530
531
532
533
534
def get_change_info(
    self,
    tracked_changes: Element | None = None,
) -> Element | None:
    changed_region = self.get_changed_region(tracked_changes=tracked_changes)
    if not changed_region:
        return None  # pragma: nocover
    return changed_region.get_change_info()  # type: ignore

get_changed_region

get_changed_region(
    tracked_changes: Element | None = None,
) -> Element | None
Source code in odfdo/tracked_changes.py
518
519
520
521
522
523
524
525
def get_changed_region(
    self,
    tracked_changes: Element | None = None,
) -> Element | None:
    if not tracked_changes:
        tracked_changes = self._get_tracked_changes()
    idx = self.get_id()
    return tracked_changes.get_changed_region(text_id=idx)  # type: ignore

get_deleted

get_deleted(
    tracked_changes: Element | None = None,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> Element | None

Shortcut to get the deleted informations stored in the TextDeletion stored in the tracked changes.

Return: Paragraph (or None).”

Source code in odfdo/tracked_changes.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
def get_deleted(
    self,
    tracked_changes: Element | None = None,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> Element | None:
    """Shortcut to get the deleted informations stored in the
    TextDeletion stored in the tracked changes.

    Return: Paragraph (or None)."
    """
    changed = self.get_change_element(tracked_changes=tracked_changes)
    if not changed:
        return None  # pragma: nocover
    return changed.get_deleted(  # type: ignore
        as_text=as_text,
        no_header=no_header,
    )

get_end

get_end() -> TextChangeEnd | None

Return None.

Source code in odfdo/tracked_changes.py
578
579
580
def get_end(self) -> TextChangeEnd | None:
    """Return None."""
    return None

get_id

get_id() -> str | None
Source code in odfdo/tracked_changes.py
506
507
def get_id(self) -> str | None:
    return self.get_attribute_string("text:change-id")

get_inserted

get_inserted(
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None

Return None.

Source code in odfdo/tracked_changes.py
565
566
567
568
569
570
571
572
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Return None."""
    return None

get_start

get_start() -> TextChangeStart | None

Return None.

Source code in odfdo/tracked_changes.py
574
575
576
def get_start(self) -> TextChangeStart | None:
    """Return None."""
    return None

set_id

set_id(idx: str) -> None
Source code in odfdo/tracked_changes.py
509
510
def set_id(self, idx: str) -> None:
    self.set_attribute("text:change-id", idx)

TextChangeEnd

Bases: TextChange

End of a changed region, “text:change-end”.

The TextChangeEnd “text:change-end” element marks the end of a region with content where text has been inserted or the format has been changed.

Methods:

Name Description
get_deleted

Return None.

get_end

Return self.

get_inserted

Return the content between text:change-start and text:change-end.

get_start

Return the corresponding annotation starting tag or None.

Source code in odfdo/tracked_changes.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
class TextChangeEnd(TextChange):
    """End of a changed region, "text:change-end".

    The TextChangeEnd "text:change-end" element marks the end of a region
    with content where text has been inserted or the format has been
    changed.
    """

    _tag = "text:change-end"

    def get_start(self) -> TextChangeStart | None:
        """Return the corresponding annotation starting tag or None."""
        idx = self.get_id()
        parent = self.parent
        if parent is None:
            raise ValueError(
                "Can not find end tag: no parent available."
            )  # pragma: nocover
        body = self.document_body
        if not body:
            body = self.root  # pragma: nocover
        return body.get_text_change_start(idx=idx)  # type: ignore

    def get_end(self) -> TextChangeEnd | None:
        """Return self."""
        return self

    def get_deleted(self, *args: Any, **kwargs: Any) -> Element | None:
        """Return None."""
        return None

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Return the content between text:change-start and text:change-end.

        If no content exists (deletion tag), returns None (or '' if text flag
        is True).
        If as_text is True: returns the text content.
        If clean is True: suppress unwanted tags (deletions marks, ...)
        If no_header is True: existing text:h are changed in text:p
        By default: returns a list of Element, cleaned and with headers

        Arguments:

            as_text -- boolean

            clean -- boolean

            no_header -- boolean

        Return: list or Element or text
        """

        # idx = self.get_id()
        start = self.get_start()
        end = self.get_end()
        if end is None or start is None:
            if as_text:
                return ""
            return None
        body = self.document_body
        if not body:
            body = self.root  # pragma: nocover
        return body.get_between(
            start, end, as_text=as_text, no_header=no_header, clean=clean
        )

get_deleted

get_deleted(*args: Any, **kwargs: Any) -> Element | None

Return None.

Source code in odfdo/tracked_changes.py
610
611
612
def get_deleted(self, *args: Any, **kwargs: Any) -> Element | None:
    """Return None."""
    return None

get_end

get_end() -> TextChangeEnd | None

Return self.

Source code in odfdo/tracked_changes.py
606
607
608
def get_end(self) -> TextChangeEnd | None:
    """Return self."""
    return self

get_inserted

get_inserted(
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None

Return the content between text:change-start and text:change-end.

If no content exists (deletion tag), returns None (or ‘’ if text flag is True). If as_text is True: returns the text content. If clean is True: suppress unwanted tags (deletions marks, …) If no_header is True: existing text:h are changed in text:p By default: returns a list of Element, cleaned and with headers

Arguments:

as_text -- boolean

clean -- boolean

no_header -- boolean

Return: list or Element or text

Source code in odfdo/tracked_changes.py
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Return the content between text:change-start and text:change-end.

    If no content exists (deletion tag), returns None (or '' if text flag
    is True).
    If as_text is True: returns the text content.
    If clean is True: suppress unwanted tags (deletions marks, ...)
    If no_header is True: existing text:h are changed in text:p
    By default: returns a list of Element, cleaned and with headers

    Arguments:

        as_text -- boolean

        clean -- boolean

        no_header -- boolean

    Return: list or Element or text
    """

    # idx = self.get_id()
    start = self.get_start()
    end = self.get_end()
    if end is None or start is None:
        if as_text:
            return ""
        return None
    body = self.document_body
    if not body:
        body = self.root  # pragma: nocover
    return body.get_between(
        start, end, as_text=as_text, no_header=no_header, clean=clean
    )

get_start

get_start() -> TextChangeStart | None

Return the corresponding annotation starting tag or None.

Source code in odfdo/tracked_changes.py
593
594
595
596
597
598
599
600
601
602
603
604
def get_start(self) -> TextChangeStart | None:
    """Return the corresponding annotation starting tag or None."""
    idx = self.get_id()
    parent = self.parent
    if parent is None:
        raise ValueError(
            "Can not find end tag: no parent available."
        )  # pragma: nocover
    body = self.document_body
    if not body:
        body = self.root  # pragma: nocover
    return body.get_text_change_start(idx=idx)  # type: ignore

TextChangeStart

Bases: TextChangeEnd

Start of a changed region, “text:change-start”.

The TextChangeStart “text:change-start” element marks the start of a region with content where text has been inserted or the format has been changed.

Methods:

Name Description
delete

Delete the given element from the XML tree. If no element is given,

get_end

Return the corresponding change-end tag or None.

get_start

Return self.

Source code in odfdo/tracked_changes.py
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
class TextChangeStart(TextChangeEnd):
    """Start of a changed region, "text:change-start".

    The TextChangeStart "text:change-start" element marks the start of a
    region with content where text has been inserted or the format has
    been changed.
    """

    _tag = "text:change-start"

    def get_start(self) -> TextChangeStart:
        """Return self."""
        return self

    def get_end(self) -> TextChangeEnd:
        """Return the corresponding change-end tag or None."""
        idx = self.get_id()
        parent = self.parent
        if parent is None:
            raise ValueError(
                "Can not find end tag: no parent available."
            )  # pragma: nocover
        body = self.document_body
        if not body:
            body = self.root
        return body.get_text_change_end(idx=idx)  # type: ignore

    def delete(
        self,
        child: Element | None = None,
        keep_tail: bool = True,
    ) -> None:
        """Delete the given element from the XML tree. If no element is given,
        "self" is deleted. The XML library may allow to continue to use an
        element now "orphan" as long as you have a reference to it.

        For TextChangeStart : delete also the end tag if exists.

        Arguments:

            child -- Element

            keep_tail -- boolean (default to True), True for most usages.
        """
        if child is not None:  # act like normal delete
            return super().delete(child, keep_tail)  # pragma: nocover
        idx = self.get_id()
        if self.parent is None:
            raise ValueError("cannot delete the root element")  # pragma: nocover
        body = self.document_body
        if not body:
            body = self.parent  # pragma: nocover
        end = body.get_text_change_end(idx=idx)
        if end:  # pragma: nocover
            end.delete()
        # act like normal delete
        super().delete()

delete

delete(
    child: Element | None = None, keep_tail: bool = True
) -> None

Delete the given element from the XML tree. If no element is given, “self” is deleted. The XML library may allow to continue to use an element now “orphan” as long as you have a reference to it.

For TextChangeStart : delete also the end tag if exists.

Arguments:

child -- Element

keep_tail -- boolean (default to True), True for most usages.
Source code in odfdo/tracked_changes.py
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
def delete(
    self,
    child: Element | None = None,
    keep_tail: bool = True,
) -> None:
    """Delete the given element from the XML tree. If no element is given,
    "self" is deleted. The XML library may allow to continue to use an
    element now "orphan" as long as you have a reference to it.

    For TextChangeStart : delete also the end tag if exists.

    Arguments:

        child -- Element

        keep_tail -- boolean (default to True), True for most usages.
    """
    if child is not None:  # act like normal delete
        return super().delete(child, keep_tail)  # pragma: nocover
    idx = self.get_id()
    if self.parent is None:
        raise ValueError("cannot delete the root element")  # pragma: nocover
    body = self.document_body
    if not body:
        body = self.parent  # pragma: nocover
    end = body.get_text_change_end(idx=idx)
    if end:  # pragma: nocover
        end.delete()
    # act like normal delete
    super().delete()

get_end

get_end() -> TextChangeEnd

Return the corresponding change-end tag or None.

Source code in odfdo/tracked_changes.py
669
670
671
672
673
674
675
676
677
678
679
680
def get_end(self) -> TextChangeEnd:
    """Return the corresponding change-end tag or None."""
    idx = self.get_id()
    parent = self.parent
    if parent is None:
        raise ValueError(
            "Can not find end tag: no parent available."
        )  # pragma: nocover
    body = self.document_body
    if not body:
        body = self.root
    return body.get_text_change_end(idx=idx)  # type: ignore

get_start

get_start() -> TextChangeStart

Return self.

Source code in odfdo/tracked_changes.py
665
666
667
def get_start(self) -> TextChangeStart:
    """Return self."""
    return self

TextChangedRegion

Bases: Element

A changed region of text, “text:changed-region”.

Each TextChangedRegion “text:changed-region” element contains a single element, one of TextInsertion, TextDeletion or TextFormatChange that corresponds to a change being tracked within the scope of the “text:tracked-changes” element that contains the “text:changed-region” instance. The xml:id attribute of the TextChangedRegion is referenced from the “text:change”, “text:change-start” and “text:change-end” elements that identify where the change applies to markup in the scope of the “text:tracked-changes” element.

Warning :

For this implementation, text:change should be referenced only once in the scope, which is different from ODF 1.2 requirement:

    " A "text:changed-region" can be referenced by more than one
     change, but the corresponding referencing change mark elements
     shall be of the same change type - insertion, format change or
     deletion. "

Methods:

Name Description
get_change_element

Get the change element child. It can be either: TextInsertion,

get_change_info

Shortcut to get the ChangeInfo element of the change

get_id

Get the “text:id” attribute.

set_change_info

Shortcut to set the ChangeInfo element of the sub change element.

set_id

Set both the “text:id” and “xml:id” attributes with same value.

Source code in odfdo/tracked_changes.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
class TextChangedRegion(Element):
    """A changed region of text, "text:changed-region".

    Each TextChangedRegion "text:changed-region" element contains a single
    element, one of TextInsertion, TextDeletion or TextFormatChange that
    corresponds to a change being tracked within the scope of the
    "text:tracked-changes" element that contains the "text:changed-region"
    instance.
    The xml:id attribute of the TextChangedRegion is referenced
    from the "text:change", "text:change-start" and "text:change-end"
    elements that identify where the change applies to markup in the scope of
    the "text:tracked-changes" element.

    Warning :

    For this implementation, text:change should be referenced only
    once in the scope, which is different from ODF 1.2 requirement:

            " A "text:changed-region" can be referenced by more than one
             change, but the corresponding referencing change mark elements
             shall be of the same change type - insertion, format change or
             deletion. "
    """

    _tag = "text:changed-region"

    def get_change_info(self) -> Element | None:
        """Shortcut to get the ChangeInfo element of the change
        element child.

        Return: ChangeInfo element.
        """
        return self.get_element("descendant::office:change-info")

    def set_change_info(
        self,
        change_info: Element | None = None,
        creator: str | None = None,
        date: datetime | None = None,
        comments: Element | list[Element] | None = None,
    ) -> None:
        """Shortcut to set the ChangeInfo element of the sub change element.
        See TextInsertion.set_change_info() for details.

        Arguments:

             change_info -- ChangeInfo element (or None)

             cretor -- str (or None)

             date -- datetime (or None)

             comments -- Paragraph or list of Paragraph elements (or None)
        """
        child = self.get_change_element()
        if not child:
            raise ValueError("Empty TextChangedRegion")
        child.set_change_info(  # type: ignore
            change_info=change_info, creator=creator, date=date, comments=comments
        )

    def get_change_element(self) -> Element | None:
        """Get the change element child. It can be either: TextInsertion,
        TextDeletion, or TextFormatChange as an Element object.

        Return: Element.
        """
        request = (
            "descendant::text:insertion "
            "| descendant::text:deletion"
            "| descendant::text:format-change"
        )
        return self._filtered_element(request, 0)

    def _get_text_id(self) -> str | None:
        return self.get_attribute_string("text:id")

    def _set_text_id(self, text_id: str) -> None:
        self.set_attribute("text:id", text_id)

    def _get_xml_id(self) -> str | None:
        return self.get_attribute_string("xml:id")

    def _set_xml_id(self, xml_id: str) -> None:
        self.set_attribute("xml:id", xml_id)

    def get_id(self) -> str | None:
        """Get the "text:id" attribute.

        Return: str
        """
        return self._get_text_id()

    def set_id(self, idx: str) -> None:
        """Set both the "text:id" and "xml:id" attributes with same value."""
        self._set_text_id(idx)
        self._set_xml_id(idx)

get_change_element

get_change_element() -> Element | None

Get the change element child. It can be either: TextInsertion, TextDeletion, or TextFormatChange as an Element object.

Return: Element.

Source code in odfdo/tracked_changes.py
401
402
403
404
405
406
407
408
409
410
411
412
def get_change_element(self) -> Element | None:
    """Get the change element child. It can be either: TextInsertion,
    TextDeletion, or TextFormatChange as an Element object.

    Return: Element.
    """
    request = (
        "descendant::text:insertion "
        "| descendant::text:deletion"
        "| descendant::text:format-change"
    )
    return self._filtered_element(request, 0)

get_change_info

get_change_info() -> Element | None

Shortcut to get the ChangeInfo element of the change element child.

Return: ChangeInfo element.

Source code in odfdo/tracked_changes.py
366
367
368
369
370
371
372
def get_change_info(self) -> Element | None:
    """Shortcut to get the ChangeInfo element of the change
    element child.

    Return: ChangeInfo element.
    """
    return self.get_element("descendant::office:change-info")

get_id

get_id() -> str | None

Get the “text:id” attribute.

Return: str

Source code in odfdo/tracked_changes.py
426
427
428
429
430
431
def get_id(self) -> str | None:
    """Get the "text:id" attribute.

    Return: str
    """
    return self._get_text_id()

set_change_info

set_change_info(
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None

Shortcut to set the ChangeInfo element of the sub change element. See TextInsertion.set_change_info() for details.

Arguments:

 change_info -- ChangeInfo element (or None)

 cretor -- str (or None)

 date -- datetime (or None)

 comments -- Paragraph or list of Paragraph elements (or None)
Source code in odfdo/tracked_changes.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def set_change_info(
    self,
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None:
    """Shortcut to set the ChangeInfo element of the sub change element.
    See TextInsertion.set_change_info() for details.

    Arguments:

         change_info -- ChangeInfo element (or None)

         cretor -- str (or None)

         date -- datetime (or None)

         comments -- Paragraph or list of Paragraph elements (or None)
    """
    child = self.get_change_element()
    if not child:
        raise ValueError("Empty TextChangedRegion")
    child.set_change_info(  # type: ignore
        change_info=change_info, creator=creator, date=date, comments=comments
    )

set_id

set_id(idx: str) -> None

Set both the “text:id” and “xml:id” attributes with same value.

Source code in odfdo/tracked_changes.py
433
434
435
436
def set_id(self, idx: str) -> None:
    """Set both the "text:id" and "xml:id" attributes with same value."""
    self._set_text_id(idx)
    self._set_xml_id(idx)

TextDeletion

Bases: TextInsertion

Informations on a text deletion, “text:deletion”.

The TextDeletion “text:deletion” contains information that identifies the person responsible for a deletion and the date of that deletion. This information may also contain one or more Paragraph which contains a comment on the deletion. The TextDeletion element may also contain content that was deleted while change tracking was enabled. The position where the text was deleted is marked by a “text:change” element. Deleted text is contained in a paragraph element. To reconstruct the original text, the paragraph containing the deleted text is merged with its surrounding paragraph or heading element. To reconstruct the text before a deletion took place: - If the change mark is inside a paragraph, insert the content that was deleted, but remove all leading start tags up to and including the first “text:p” element and all trailing end tags up to and including the last “/text:p” or “/text:h” element. If the last trailing element is a “/text:h”, change the end tag “/text:p” following this insertion to a “/text:h” element. - If the change mark is inside a heading, insert the content that was deleted, but remove all leading start tags up to and including the first “text:h” element and all trailing end tags up to and including the last “/text:h” or “/text:p” element. If the last trailing element is a “/text:p”, change the end tag “/text:h” following this insertion to a “/text:p” element. - Otherwise, copy the text content of the “text:deletion” element in place of the change mark.

Methods:

Name Description
get_deleted

Get the deleted informations stored in the TextDeletion.

get_inserted

Return None.

set_deleted

Set the deleted informations stored in the TextDeletion. An

Source code in odfdo/tracked_changes.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
class TextDeletion(TextInsertion):
    """Informations on a text deletion, "text:deletion".

    The TextDeletion "text:deletion" contains information that identifies
    the person responsible for a deletion and the date of that deletion.
    This information may also contain one or more Paragraph which contains
    a comment on the deletion. The TextDeletion element may also contain
    content that was deleted while change tracking was enabled. The position
    where the text was deleted is marked by a "text:change" element. Deleted
    text is contained in a paragraph element. To reconstruct the original
    text, the paragraph containing the deleted text is merged with its
    surrounding paragraph or heading element. To reconstruct the text before
    a deletion took place:
      - If the change mark is inside a paragraph, insert the content that was
      deleted, but remove all leading start tags up to and including the
      first "text:p" element and all trailing end tags up to and including
      the last "/text:p" or "/text:h" element. If the last trailing element
      is a "/text:h", change the end tag "/text:p" following this insertion
      to a "/text:h" element.
      - If the change mark is inside a heading, insert the content that was
      deleted, but remove all leading start tags up to and including the
      first "text:h" element and all trailing end tags up to and including
      the last "/text:h" or "/text:p" element. If the last trailing element
      is a "/text:p", change the end tag "/text:h" following this insertion
      to a "/text:p" element.
      - Otherwise, copy the text content of the "text:deletion" element in
      place of the change mark.
    """

    _tag = "text:deletion"

    def get_deleted(
        self,
        as_text: bool = False,
        no_header: bool = False,
    ) -> str | list[Element] | None:
        """Get the deleted informations stored in the TextDeletion.
        If as_text is True: returns the text content.
        If no_header is True: existing Heading are changed in Paragraph

        Arguments:

            as_text -- boolean

            no_header -- boolean

        Return: Paragraph and Header list
        """
        children = self.children
        inner = [elem for elem in children if elem.tag != "office:change-info"]
        if no_header:  # crude replace t:h by t:p
            print("noheader")
            new_inner = []
            for element in inner:
                if element.tag == "text:h":
                    children = element.children
                    text = element.text
                    para = Element.from_tag("text:p")
                    para.text = text
                    for child in children:
                        para.append(child)  # pragma: nocover
                    new_inner.append(para)
                else:
                    new_inner.append(element)
            inner = new_inner
        if as_text:
            return "\n".join([elem.get_formatted_text(context=None) for elem in inner])
        return inner

    def set_deleted(self, paragraph_or_list: Element | list[Element]) -> None:
        """Set the deleted informations stored in the TextDeletion. An
        actual content that was deleted is expected, embeded in a Paragraph
        element or Header.

        Arguments:

            paragraph_or_list -- Paragraph or Header element (or list)
        """
        for element in self.get_deleted():  # type: ignore
            self.delete(element)  # type: ignore
        if isinstance(paragraph_or_list, Element):
            paragraph_or_list = [paragraph_or_list]
        for element in paragraph_or_list:
            self.append(element)

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Return None."""
        if as_text:
            return ""
        return None

get_deleted

get_deleted(
    as_text: bool = False, no_header: bool = False
) -> str | list[Element] | None

Get the deleted informations stored in the TextDeletion. If as_text is True: returns the text content. If no_header is True: existing Heading are changed in Paragraph

Arguments:

as_text -- boolean

no_header -- boolean

Return: Paragraph and Header list

Source code in odfdo/tracked_changes.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def get_deleted(
    self,
    as_text: bool = False,
    no_header: bool = False,
) -> str | list[Element] | None:
    """Get the deleted informations stored in the TextDeletion.
    If as_text is True: returns the text content.
    If no_header is True: existing Heading are changed in Paragraph

    Arguments:

        as_text -- boolean

        no_header -- boolean

    Return: Paragraph and Header list
    """
    children = self.children
    inner = [elem for elem in children if elem.tag != "office:change-info"]
    if no_header:  # crude replace t:h by t:p
        print("noheader")
        new_inner = []
        for element in inner:
            if element.tag == "text:h":
                children = element.children
                text = element.text
                para = Element.from_tag("text:p")
                para.text = text
                for child in children:
                    para.append(child)  # pragma: nocover
                new_inner.append(para)
            else:
                new_inner.append(element)
        inner = new_inner
    if as_text:
        return "\n".join([elem.get_formatted_text(context=None) for elem in inner])
    return inner

get_inserted

get_inserted(
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None

Return None.

Source code in odfdo/tracked_changes.py
313
314
315
316
317
318
319
320
321
322
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Return None."""
    if as_text:
        return ""
    return None

set_deleted

set_deleted(
    paragraph_or_list: Element | list[Element],
) -> None

Set the deleted informations stored in the TextDeletion. An actual content that was deleted is expected, embeded in a Paragraph element or Header.

Arguments:

paragraph_or_list -- Paragraph or Header element (or list)
Source code in odfdo/tracked_changes.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def set_deleted(self, paragraph_or_list: Element | list[Element]) -> None:
    """Set the deleted informations stored in the TextDeletion. An
    actual content that was deleted is expected, embeded in a Paragraph
    element or Header.

    Arguments:

        paragraph_or_list -- Paragraph or Header element (or list)
    """
    for element in self.get_deleted():  # type: ignore
        self.delete(element)  # type: ignore
    if isinstance(paragraph_or_list, Element):
        paragraph_or_list = [paragraph_or_list]
    for element in paragraph_or_list:
        self.append(element)

TextFormatChange

Bases: TextInsertion

A change in text formatting, “text:format-change”.

The TextFormatChange “text:format-change” element represents any change in formatting attributes. The region where the change took place is marked by “text:change-start”, “text:change-end” or “text:change” elements.

Note: This element does not contain formatting changes that have taken place.

Source code in odfdo/tracked_changes.py
325
326
327
328
329
330
331
332
333
334
335
336
337
class TextFormatChange(TextInsertion):
    """A change in text formatting, "text:format-change".

    The TextFormatChange "text:format-change" element represents any change
    in formatting attributes. The region where the change took place is
    marked by "text:change-start", "text:change-end" or "text:change"
    elements.

    Note: This element does not contain formatting changes that have taken
    place.
    """

    _tag = "text:format-change"

TextInsertion

Bases: Element

Informations on a text insertion, “text:insertion”.

The TextInsertion “text:insertion” element contains the information that identifies the person responsible for a change and the date of that change. This information may also contain one or more “text:p” Paragraph which contain a comment on the insertion. The TextInsertion element’s parent “text:changed-region” element has an xml:id or text:id attribute, the value of which binds that parent element to the text:change-id attribute on the “text:change-start” and “text:change-end” elements.

Methods:

Name Description
get_change_info

Get the ChangeInfo child of the element.

get_deleted

Return: None.

get_inserted

Shortcut to text:change-start.get_inserted(). Return the content

set_change_info

Set the ChangeInfo element for the change element. If change_info

Source code in odfdo/tracked_changes.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class TextInsertion(Element):
    """Informations on a text insertion, "text:insertion".

    The TextInsertion "text:insertion" element contains the information
    that identifies the person responsible for a change and the date of
    that change. This information may also contain one or more "text:p"
    Paragraph which contain a comment on the insertion. The
    TextInsertion element's parent "text:changed-region" element has an
    xml:id or text:id attribute, the value of which binds that parent
    element to the text:change-id attribute on the "text:change-start"
    and "text:change-end" elements.
    """

    _tag = "text:insertion"

    def get_deleted(
        self,
        as_text: bool = False,
        no_header: bool = False,
    ) -> str | list[Element] | None:
        """Return: None."""
        if as_text:
            return ""
        return None

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Shortcut to text:change-start.get_inserted(). Return the content
        between text:change-start and text:change-end.

        If as_text is True: returns the text content.
        If no_header is True: existing Heading are changed in Paragraph
        If no_header is True: existing text:h are changed in text:p
        By default: returns a list of Element, cleaned and with headers

        Arguments:

            as_text -- boolean

            clean -- boolean

            no_header -- boolean

        Return: list or Element or text
        """
        current = self.parent  # text:changed-region
        if not isinstance(current, TextChangedRegion):
            raise TypeError("Missing parent TextChangedRegion")
        idx = current.get_id()  # type: ignore
        body = self.document_body
        if not body:
            body = self.root  # pragma: nocover
        text_change = body.get_text_change_start(idx=idx)
        if not text_change:
            raise ValueError  # pragma: nocover
        return text_change.get_inserted(  # type: ignore
            as_text=as_text, no_header=no_header, clean=clean
        )

    def get_change_info(self) -> Element | None:
        """Get the ChangeInfo child of the element.

        Return: ChangeInfo element.
        """
        return self.get_element("descendant::office:change-info")

    def set_change_info(
        self,
        change_info: Element | None = None,
        creator: str | None = None,
        date: datetime | None = None,
        comments: Element | list[Element] | None = None,
    ) -> None:
        """Set the ChangeInfo element for the change element. If change_info
        is not provided, creator, date and comments will be used to build a
        suitable change info element. Default for creator is 'Unknown',
        default for date is current time and default for comments is no
        comment at all.
        The new change info element will replace any existant ChangeInfo.

        Arguments:

             change_info -- ChangeInfo element (or None)

             cretor -- str (or None)

             date -- datetime (or None)

             comments -- Paragraph or list of Paragraph elements (or None)
        """
        if change_info is None:
            new_change_info = ChangeInfo(creator, date)
            if comments is not None:
                if isinstance(comments, Element):
                    # single pararagraph comment
                    comments_list = [comments]
                else:
                    comments_list = comments
                # assume iterable of Paragraph
                for paragraph in comments_list:
                    if not isinstance(paragraph, Paragraph):
                        raise TypeError(f"Not a Paragraph: '{paragraph!r}'")
                    new_change_info.insert(paragraph, xmlposition=LAST_CHILD)
        else:
            if not isinstance(change_info, ChangeInfo):
                raise TypeError(f"Not a ChangeInfo: '{change_info!r}'")
            new_change_info = change_info

        old = self.get_change_info()
        if old is not None:
            self.replace_element(old, new_change_info)
        else:
            self.insert(new_change_info, xmlposition=FIRST_CHILD)

get_change_info

get_change_info() -> Element | None

Get the ChangeInfo child of the element.

Return: ChangeInfo element.

Source code in odfdo/tracked_changes.py
172
173
174
175
176
177
def get_change_info(self) -> Element | None:
    """Get the ChangeInfo child of the element.

    Return: ChangeInfo element.
    """
    return self.get_element("descendant::office:change-info")

get_deleted

get_deleted(
    as_text: bool = False, no_header: bool = False
) -> str | list[Element] | None

Return: None.

Source code in odfdo/tracked_changes.py
124
125
126
127
128
129
130
131
132
def get_deleted(
    self,
    as_text: bool = False,
    no_header: bool = False,
) -> str | list[Element] | None:
    """Return: None."""
    if as_text:
        return ""
    return None

get_inserted

get_inserted(
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None

Shortcut to text:change-start.get_inserted(). Return the content between text:change-start and text:change-end.

If as_text is True: returns the text content. If no_header is True: existing Heading are changed in Paragraph If no_header is True: existing text:h are changed in text:p By default: returns a list of Element, cleaned and with headers

Arguments:

as_text -- boolean

clean -- boolean

no_header -- boolean

Return: list or Element or text

Source code in odfdo/tracked_changes.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Shortcut to text:change-start.get_inserted(). Return the content
    between text:change-start and text:change-end.

    If as_text is True: returns the text content.
    If no_header is True: existing Heading are changed in Paragraph
    If no_header is True: existing text:h are changed in text:p
    By default: returns a list of Element, cleaned and with headers

    Arguments:

        as_text -- boolean

        clean -- boolean

        no_header -- boolean

    Return: list or Element or text
    """
    current = self.parent  # text:changed-region
    if not isinstance(current, TextChangedRegion):
        raise TypeError("Missing parent TextChangedRegion")
    idx = current.get_id()  # type: ignore
    body = self.document_body
    if not body:
        body = self.root  # pragma: nocover
    text_change = body.get_text_change_start(idx=idx)
    if not text_change:
        raise ValueError  # pragma: nocover
    return text_change.get_inserted(  # type: ignore
        as_text=as_text, no_header=no_header, clean=clean
    )

set_change_info

set_change_info(
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None

Set the ChangeInfo element for the change element. If change_info is not provided, creator, date and comments will be used to build a suitable change info element. Default for creator is ‘Unknown’, default for date is current time and default for comments is no comment at all. The new change info element will replace any existant ChangeInfo.

Arguments:

 change_info -- ChangeInfo element (or None)

 cretor -- str (or None)

 date -- datetime (or None)

 comments -- Paragraph or list of Paragraph elements (or None)
Source code in odfdo/tracked_changes.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def set_change_info(
    self,
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None:
    """Set the ChangeInfo element for the change element. If change_info
    is not provided, creator, date and comments will be used to build a
    suitable change info element. Default for creator is 'Unknown',
    default for date is current time and default for comments is no
    comment at all.
    The new change info element will replace any existant ChangeInfo.

    Arguments:

         change_info -- ChangeInfo element (or None)

         cretor -- str (or None)

         date -- datetime (or None)

         comments -- Paragraph or list of Paragraph elements (or None)
    """
    if change_info is None:
        new_change_info = ChangeInfo(creator, date)
        if comments is not None:
            if isinstance(comments, Element):
                # single pararagraph comment
                comments_list = [comments]
            else:
                comments_list = comments
            # assume iterable of Paragraph
            for paragraph in comments_list:
                if not isinstance(paragraph, Paragraph):
                    raise TypeError(f"Not a Paragraph: '{paragraph!r}'")
                new_change_info.insert(paragraph, xmlposition=LAST_CHILD)
    else:
        if not isinstance(change_info, ChangeInfo):
            raise TypeError(f"Not a ChangeInfo: '{change_info!r}'")
        new_change_info = change_info

    old = self.get_change_info()
    if old is not None:
        self.replace_element(old, new_change_info)
    else:
        self.insert(new_change_info, xmlposition=FIRST_CHILD)

TocEntryTemplate

Bases: Element

Template for entry of TOC, “text:table-of-content-entry-template”.

Methods:

Name Description
__init__

Create template for entry of TOC “text:table-of-content-entry-template”.

complete_defaults

Attributes:

Name Type Description
outline_level int | None
style
Source code in odfdo/toc.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
class TocEntryTemplate(Element):
    """Template for entry of TOC, "text:table-of-content-entry-template"."""

    _tag = "text:table-of-content-entry-template"
    _properties = (PropDef("style", "text:style-name"),)

    def __init__(
        self,
        style: str | None = None,
        outline_level: int | None = None,
        **kwargs: Any,
    ) -> None:
        """Create template for entry of TOC "text:table-of-content-entry-template".

        Arguments:

            style -- str
        """
        super().__init__(**kwargs)
        if self._do_init:
            if style:
                self.style = style
            if outline_level:
                self.outline_level = outline_level

    @property
    def outline_level(self) -> int | None:
        return self.get_attribute_integer("text:outline-level")

    @outline_level.setter
    def outline_level(self, level: int) -> None:
        self.set_attribute("text:outline-level", str(level))

    def complete_defaults(self) -> None:
        self.append(Element.from_tag("text:index-entry-chapter"))
        self.append(Element.from_tag("text:index-entry-text"))
        self.append(Element.from_tag("text:index-entry-text"))
        ts = Element.from_tag("text:index-entry-text")
        ts.set_style_attribute("style:type", "right")
        ts.set_style_attribute("style:leader-char", ".")
        self.append(ts)
        self.append(Element.from_tag("text:index-entry-page-number"))

outline_level property writable

outline_level: int | None

style instance-attribute

style = style

__init__

__init__(
    style: str | None = None,
    outline_level: int | None = None,
    **kwargs: Any,
) -> None

Create template for entry of TOC “text:table-of-content-entry-template”.

Arguments:

style -- str
Source code in odfdo/toc.py
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
def __init__(
    self,
    style: str | None = None,
    outline_level: int | None = None,
    **kwargs: Any,
) -> None:
    """Create template for entry of TOC "text:table-of-content-entry-template".

    Arguments:

        style -- str
    """
    super().__init__(**kwargs)
    if self._do_init:
        if style:
            self.style = style
        if outline_level:
            self.outline_level = outline_level

complete_defaults

complete_defaults() -> None
Source code in odfdo/toc.py
518
519
520
521
522
523
524
525
526
def complete_defaults(self) -> None:
    self.append(Element.from_tag("text:index-entry-chapter"))
    self.append(Element.from_tag("text:index-entry-text"))
    self.append(Element.from_tag("text:index-entry-text"))
    ts = Element.from_tag("text:index-entry-text")
    ts.set_style_attribute("style:type", "right")
    ts.set_style_attribute("style:leader-char", ".")
    self.append(ts)
    self.append(Element.from_tag("text:index-entry-page-number"))

TrackedChanges

Bases: MDZap, Element

A tracked change, “text:tracked-changes”.

The TrackedChanges “text:tracked-changes” element acts as a container for TextChangedRegion elements that represent changes in a certain scope of an OpenDocument document. This scope is the element in which the TrackedChanges element occurs. Changes in this scope shall be tracked by TextChangedRegion elements contained in the TrackedChanges element in this scope. If a TrackedChanges element is absent, there are no tracked changes in the corresponding scope. In this case, all change mark elements in this scope shall be ignored.

Methods:

Name Description
get_changed_region
get_changed_regions
Source code in odfdo/tracked_changes.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class TrackedChanges(MDZap, Element):
    """A tracked change, "text:tracked-changes".

    The TrackedChanges "text:tracked-changes" element acts as a container
    for TextChangedRegion elements that represent changes in a certain
    scope of an OpenDocument document. This scope is the element in which
    the TrackedChanges element occurs. Changes in this scope shall be
    tracked by TextChangedRegion elements contained in the
    TrackedChanges element in this scope. If a TrackedChanges
    element is absent, there are no tracked changes in the corresponding
    scope. In this case, all change mark elements in this scope shall be
    ignored.
    """

    _tag = "text:tracked-changes"

    def get_changed_regions(
        self,
        creator: str | None = None,
        date: datetime | None = None,
        content: str | None = None,
        role: str | None = None,
    ) -> list[Element]:
        changed_regions = self._filtered_elements(
            "text:changed-region",
            dc_creator=creator,
            dc_date=date,
            content=content,
        )
        if role is None:
            return changed_regions
        result: list[Element] = []
        for region in changed_regions:
            changed = region.get_change_element()  # type: ignore
            if not changed:
                continue  # pragma: nocover
            if changed.tag.endswith(role):
                result.append(region)
        return result

    def get_changed_region(
        self,
        position: int = 0,
        text_id: str | None = None,
        creator: str | None = None,
        date: datetime | None = None,
        content: str | None = None,
    ) -> Element | None:
        return self._filtered_element(
            "text:changed-region",
            position,
            text_id=text_id,
            dc_creator=creator,
            dc_date=date,
            content=content,
        )

get_changed_region

get_changed_region(
    position: int = 0,
    text_id: str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
) -> Element | None
Source code in odfdo/tracked_changes.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def get_changed_region(
    self,
    position: int = 0,
    text_id: str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
) -> Element | None:
    return self._filtered_element(
        "text:changed-region",
        position,
        text_id=text_id,
        dc_creator=creator,
        dc_date=date,
        content=content,
    )

get_changed_regions

get_changed_regions(
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
    role: str | None = None,
) -> list[Element]
Source code in odfdo/tracked_changes.py
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def get_changed_regions(
    self,
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
    role: str | None = None,
) -> list[Element]:
    changed_regions = self._filtered_elements(
        "text:changed-region",
        dc_creator=creator,
        dc_date=date,
        content=content,
    )
    if role is None:
        return changed_regions
    result: list[Element] = []
    for region in changed_regions:
        changed = region.get_change_element()  # type: ignore
        if not changed:
            continue  # pragma: nocover
        if changed.tag.endswith(role):
            result.append(region)
    return result

UserDefined

Bases: ElementTyped

A user defined field, “text:user-defined”.

Methods:

Name Description
__init__

Create a user defined field “text:user-defined”.

Attributes:

Name Type Description
name
style
text
Source code in odfdo/user_field.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class UserDefined(ElementTyped):
    """A user defined field, "text:user-defined"."""

    _tag = "text:user-defined"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "style:data-style-name"),
    )

    def __init__(
        self,
        name: str = "",
        value: Any = None,
        value_type: str | None = None,
        text: str | None = None,
        style: str | None = None,
        from_document: Document | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a user defined field "text:user-defined".

        If the current document is provided, try to extract
        the content of the meta user defined field of same name.

        Arguments:

            name -- str, name of the user defined field

            value -- python typed value, value of the field

            value_type -- str, office:value-type known type

            text -- str

            style -- str

            from_document -- ODF document
        """
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            if style:
                self.style = style
            if from_document is not None:
                meta_infos = from_document.meta
                content = meta_infos.get_user_defined_metadata_of_name(name)
                if content is not None:
                    value = content.get("value", None)
                    value_type = content.get("value_type", None)
                    text = content.get("text", None)
            text = self.set_value_and_type(
                value=value, value_type=value_type, text=text
            )
            self.text = text  # type: ignore

name instance-attribute

name = name

style instance-attribute

style = style

text instance-attribute

text = text

__init__

__init__(
    name: str = "",
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    from_document: Document | None = None,
    **kwargs: Any,
) -> None

Create a user defined field “text:user-defined”.

If the current document is provided, try to extract the content of the meta user defined field of same name.

Arguments:

name -- str, name of the user defined field

value -- python typed value, value of the field

value_type -- str, office:value-type known type

text -- str

style -- str

from_document -- ODF document
Source code in odfdo/user_field.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def __init__(
    self,
    name: str = "",
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    from_document: Document | None = None,
    **kwargs: Any,
) -> None:
    """Create a user defined field "text:user-defined".

    If the current document is provided, try to extract
    the content of the meta user defined field of same name.

    Arguments:

        name -- str, name of the user defined field

        value -- python typed value, value of the field

        value_type -- str, office:value-type known type

        text -- str

        style -- str

        from_document -- ODF document
    """
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        if style:
            self.style = style
        if from_document is not None:
            meta_infos = from_document.meta
            content = meta_infos.get_user_defined_metadata_of_name(name)
            if content is not None:
                value = content.get("value", None)
                value_type = content.get("value_type", None)
                text = content.get("text", None)
        text = self.set_value_and_type(
            value=value, value_type=value_type, text=text
        )
        self.text = text  # type: ignore

UserFieldDecl

Bases: ElementTyped

Declaration of a user field, “text:user-field-decl”.

Methods:

Name Description
__init__

Create a user field “text:user-field-decl”.

set_value

Attributes:

Name Type Description
name
Source code in odfdo/user_field.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class UserFieldDecl(ElementTyped):
    """Declaration of a user field, "text:user-field-decl"."""

    _tag = "text:user-field-decl"
    _properties = (PropDef("name", "text:name"),)

    def __init__(
        self,
        name: str | None = None,
        value: Any = None,
        value_type: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a user field "text:user-field-decl"."""
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            self.set_value_and_type(value=value, value_type=value_type)

    def set_value(self, value: Any) -> None:
        name = self.get_attribute("text:name")
        self.clear()
        self.set_value_and_type(value=value)
        self.set_attribute("text:name", name)

name instance-attribute

name = name

__init__

__init__(
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    **kwargs: Any,
) -> None

Create a user field “text:user-field-decl”.

Source code in odfdo/user_field.py
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a user field "text:user-field-decl"."""
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        self.set_value_and_type(value=value, value_type=value_type)

set_value

set_value(value: Any) -> None
Source code in odfdo/user_field.py
59
60
61
62
63
def set_value(self, value: Any) -> None:
    name = self.get_attribute("text:name")
    self.clear()
    self.set_value_and_type(value=value)
    self.set_attribute("text:name", name)

UserFieldDecls

Bases: Element

Container of user fields declarations, “text:user-field-decls”.

Source code in odfdo/user_field.py
33
34
35
36
class UserFieldDecls(Element):
    """Container of user fields declarations, "text:user-field-decls"."""

    _tag = "text:user-field-decls"

UserFieldGet

Bases: ElementTyped

Representation of user field getter, “text:user-field-get”.

Methods:

Name Description
__init__

Create a user field getter “text:user-field-get”.

Attributes:

Name Type Description
name
style
text
Source code in odfdo/user_field.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class UserFieldGet(ElementTyped):
    """Representation of user field getter, "text:user-field-get"."""

    _tag = "text:user-field-get"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "style:data-style-name"),
    )

    def __init__(
        self,
        name: str | None = None,
        value: Any = None,
        value_type: str | None = None,
        text: str | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a user field getter "text:user-field-get"."""
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            text = self.set_value_and_type(
                value=value, value_type=value_type, text=text
            )
            self.text = text  # type: ignore

            if style:
                self.style = style

name instance-attribute

name = name

style instance-attribute

style = style

text instance-attribute

text = text

__init__

__init__(
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Create a user field getter “text:user-field-get”.

Source code in odfdo/user_field.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def __init__(
    self,
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a user field getter "text:user-field-get"."""
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        text = self.set_value_and_type(
            value=value, value_type=value_type, text=text
        )
        self.text = text  # type: ignore

        if style:
            self.style = style

UserFieldInput

Bases: UserFieldGet

Representation of user field input, “text:user-field-input”.

Source code in odfdo/user_field.py
104
105
106
107
class UserFieldInput(UserFieldGet):
    """Representation of user field input, "text:user-field-input"."""

    _tag = "text:user-field-input"

VarChapter

Bases: Element

Variable for a chapter, “text:chapter”.

Methods:

Name Description
__init__

Create a variable for a chapter “text:chapter”.

Attributes:

Name Type Description
DISPLAY_VALUE_CHOICE ClassVar
display
outline_level
Source code in odfdo/variable.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
class VarChapter(Element):
    """Variable for a chapter, "text:chapter"."""

    _tag = "text:chapter"
    _properties = (
        PropDef("display", "text:display"),
        PropDef("outline_level", "text:outline-level"),
    )
    DISPLAY_VALUE_CHOICE: ClassVar = {
        "number",
        "name",
        "number-and-name",
        "plain-number",
        "plain-number-and-name",
    }

    def __init__(
        self,
        display: str | None = "name",
        outline_level: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable for a chapter "text:chapter".

        display can be: 'number', 'name', 'number-and-name', 'plain-number' or
        'plain-number-and-name'
        """
        super().__init__(**kwargs)
        if self._do_init:
            if display not in VarChapter.DISPLAY_VALUE_CHOICE:
                raise ValueError(f"Unknown display value: '{display}'")
            self.display = display
            if outline_level is not None:
                self.outline_level = outline_level

DISPLAY_VALUE_CHOICE class-attribute instance-attribute

DISPLAY_VALUE_CHOICE: ClassVar = {
    "number",
    "name",
    "number-and-name",
    "plain-number",
    "plain-number-and-name",
}

display instance-attribute

display = display

outline_level instance-attribute

outline_level = outline_level

__init__

__init__(
    display: str | None = "name",
    outline_level: str | None = None,
    **kwargs: Any,
) -> None

Create a variable for a chapter “text:chapter”.

display can be: ‘number’, ‘name’, ‘number-and-name’, ‘plain-number’ or ‘plain-number-and-name’

Source code in odfdo/variable.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def __init__(
    self,
    display: str | None = "name",
    outline_level: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable for a chapter "text:chapter".

    display can be: 'number', 'name', 'number-and-name', 'plain-number' or
    'plain-number-and-name'
    """
    super().__init__(**kwargs)
    if self._do_init:
        if display not in VarChapter.DISPLAY_VALUE_CHOICE:
            raise ValueError(f"Unknown display value: '{display}'")
        self.display = display
        if outline_level is not None:
            self.outline_level = outline_level

VarCreationDate

Bases: Element

Variable for the creation date, “text:creation-date”.

Methods:

Name Description
__init__

Create a variable for the creation date “text:creation-date”.

Attributes:

Name Type Description
data_style
fixed
Source code in odfdo/variable.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
class VarCreationDate(Element):
    """Variable for the creation date, "text:creation-date"."""

    _tag = "text:creation-date"
    _properties = (
        PropDef("fixed", "text:fixed"),
        PropDef("data_style", "style:data-style-name"),
    )

    def __init__(
        self,
        fixed: bool = False,
        data_style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable for the creation date "text:creation-date"."""
        super().__init__(**kwargs)
        if self._do_init:
            if fixed:
                self.fixed = True
            if data_style:
                self.data_style = data_style

data_style instance-attribute

data_style = data_style

fixed instance-attribute

fixed = True

__init__

__init__(
    fixed: bool = False,
    data_style: str | None = None,
    **kwargs: Any,
) -> None

Create a variable for the creation date “text:creation-date”.

Source code in odfdo/variable.py
390
391
392
393
394
395
396
397
398
399
400
401
402
def __init__(
    self,
    fixed: bool = False,
    data_style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable for the creation date "text:creation-date"."""
    super().__init__(**kwargs)
    if self._do_init:
        if fixed:
            self.fixed = True
        if data_style:
            self.data_style = data_style

VarCreationTime

Bases: Element

Variable for the creation time, “text:creation-time”.

Methods:

Name Description
__init__

Create a variable for the creation time “text:creation-time”.

Attributes:

Name Type Description
data_style
fixed
Source code in odfdo/variable.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
class VarCreationTime(Element):
    """Variable for the creation time, "text:creation-time"."""

    _tag = "text:creation-time"
    _properties = (
        PropDef("fixed", "text:fixed"),
        PropDef("data_style", "style:data-style-name"),
    )

    def __init__(
        self,
        fixed: bool = False,
        data_style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable for the creation time "text:creation-time"."""
        super().__init__(**kwargs)
        if self._do_init:
            if fixed:
                self.fixed = True
            if data_style:
                self.data_style = data_style

data_style instance-attribute

data_style = data_style

fixed instance-attribute

fixed = True

__init__

__init__(
    fixed: bool = False,
    data_style: str | None = None,
    **kwargs: Any,
) -> None

Create a variable for the creation time “text:creation-time”.

Source code in odfdo/variable.py
417
418
419
420
421
422
423
424
425
426
427
428
429
def __init__(
    self,
    fixed: bool = False,
    data_style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable for the creation time "text:creation-time"."""
    super().__init__(**kwargs)
    if self._do_init:
        if fixed:
            self.fixed = True
        if data_style:
            self.data_style = data_style

VarDate

Bases: Element

Variable for a date, “text:date”.

Methods:

Name Description
__init__

create avariable for a date “text:date”.

Attributes:

Name Type Description
data_style
date
date_adjust
fixed
text
Source code in odfdo/variable.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class VarDate(Element):
    """Variable for a date, "text:date"."""

    _tag = "text:date"
    _properties = (
        PropDef("date", "text:date-value"),
        PropDef("fixed", "text:fixed"),
        PropDef("data_style", "style:data-style-name"),
        PropDef("date_adjust", "text:date-adjust"),
    )

    def __init__(
        self,
        date: datetime | None = None,
        fixed: bool = False,
        data_style: str | None = None,
        text: str | None = None,
        date_adjust: timedelta | None = None,
        **kwargs: Any,
    ) -> None:
        """create avariable for a date "text:date"."""
        super().__init__(**kwargs)
        if self._do_init:
            if date:
                self.date = DateTime.encode(date)
            if fixed:
                self.fixed = True
            if data_style is not None:
                self.data_style = data_style
            if text is None and date is not None:
                text = Date.encode(date)
            self.text = text
            if date_adjust is not None:
                self.date_adjust = Duration.encode(date_adjust)

data_style instance-attribute

data_style = data_style

date instance-attribute

date = encode(date)

date_adjust instance-attribute

date_adjust = encode(date_adjust)

fixed instance-attribute

fixed = True

text instance-attribute

text = text

__init__

__init__(
    date: datetime | None = None,
    fixed: bool = False,
    data_style: str | None = None,
    text: str | None = None,
    date_adjust: timedelta | None = None,
    **kwargs: Any,
) -> None

create avariable for a date “text:date”.

Source code in odfdo/variable.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def __init__(
    self,
    date: datetime | None = None,
    fixed: bool = False,
    data_style: str | None = None,
    text: str | None = None,
    date_adjust: timedelta | None = None,
    **kwargs: Any,
) -> None:
    """create avariable for a date "text:date"."""
    super().__init__(**kwargs)
    if self._do_init:
        if date:
            self.date = DateTime.encode(date)
        if fixed:
            self.fixed = True
        if data_style is not None:
            self.data_style = data_style
        if text is None and date is not None:
            text = Date.encode(date)
        self.text = text
        if date_adjust is not None:
            self.date_adjust = Duration.encode(date_adjust)

VarDecl

Bases: Element

A variable declaration, “text:variable-decl”.

Methods:

Name Description
__init__

Create a variable declaration “text:variable-decl”.

Attributes:

Name Type Description
name
value_type
Source code in odfdo/variable.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class VarDecl(Element):
    """A variable declaration, "text:variable-decl"."""

    _tag = "text:variable-decl"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("value_type", "office:value-type"),
    )

    def __init__(
        self,
        name: str | None = None,
        value_type: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable declaration "text:variable-decl"."""
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            if value_type:
                self.value_type = value_type

name instance-attribute

name = name

value_type instance-attribute

value_type = value_type

__init__

__init__(
    name: str | None = None,
    value_type: str | None = None,
    **kwargs: Any,
) -> None

Create a variable declaration “text:variable-decl”.

Source code in odfdo/variable.py
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(
    self,
    name: str | None = None,
    value_type: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable declaration "text:variable-decl"."""
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        if value_type:
            self.value_type = value_type

VarDecls

Bases: Element

Container of variables declarations, “text:variable-decls”.

Source code in odfdo/variable.py
42
43
44
45
class VarDecls(Element):
    """Container of variables declarations, "text:variable-decls"."""

    _tag = "text:variable-decls"

VarDescription

Bases: VarInitialCreator

Variable for the text description, “text:description”.

Source code in odfdo/variable.py
435
436
437
438
class VarDescription(VarInitialCreator):
    """Variable for the text description, "text:description"."""

    _tag = "text:description"

VarFileName

Bases: Element

Variable for the file name, “text:file-name”.

Methods:

Name Description
__init__

Create a variable for the file name “text:file-name”.

Attributes:

Name Type Description
DISPLAY_VALUE_CHOICE ClassVar
display
fixed
Source code in odfdo/variable.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
class VarFileName(Element):
    """Variable for the file name, "text:file-name"."""

    _tag = "text:file-name"
    _properties = (
        PropDef("display", "text:display"),
        PropDef("fixed", "text:fixed"),
    )
    DISPLAY_VALUE_CHOICE: ClassVar = {
        "full",
        "path",
        "name",
        "name-and-extension",
    }

    def __init__(
        self,
        display: str | None = "full",
        fixed: bool = False,
        **kwargs: Any,
    ) -> None:
        """Create a variable for the file name "text:file-name".

        display can be: 'full', 'path', 'name' or 'name-and-extension'
        """
        super().__init__(**kwargs)
        if self._do_init:
            if display not in VarFileName.DISPLAY_VALUE_CHOICE:
                raise ValueError(f"Unknown display value: '{display}'")
            self.display = display
            if fixed:
                self.fixed = True

DISPLAY_VALUE_CHOICE class-attribute instance-attribute

DISPLAY_VALUE_CHOICE: ClassVar = {
    "full",
    "path",
    "name",
    "name-and-extension",
}

display instance-attribute

display = display

fixed instance-attribute

fixed = True

__init__

__init__(
    display: str | None = "full",
    fixed: bool = False,
    **kwargs: Any,
) -> None

Create a variable for the file name “text:file-name”.

display can be: ‘full’, ‘path’, ‘name’ or ‘name-and-extension’

Source code in odfdo/variable.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def __init__(
    self,
    display: str | None = "full",
    fixed: bool = False,
    **kwargs: Any,
) -> None:
    """Create a variable for the file name "text:file-name".

    display can be: 'full', 'path', 'name' or 'name-and-extension'
    """
    super().__init__(**kwargs)
    if self._do_init:
        if display not in VarFileName.DISPLAY_VALUE_CHOICE:
            raise ValueError(f"Unknown display value: '{display}'")
        self.display = display
        if fixed:
            self.fixed = True

VarGet

Bases: ElementTyped

Representation of a variable getter, “text:variable-get”.

Methods:

Name Description
__init__

Create a variable getter “text:variable-get”.

Attributes:

Name Type Description
name
style
text
Source code in odfdo/variable.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class VarGet(ElementTyped):
    """Representation of a variable getter, "text:variable-get"."""

    _tag = "text:variable-get"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "style:data-style-name"),
    )

    def __init__(
        self,
        name: str | None = None,
        value: Any = None,
        value_type: str | None = None,
        text: str | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable getter "text:variable-get"."""
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            if style:
                self.style = style
            text = self.set_value_and_type(
                value=value, value_type=value_type, text=text
            )
            self.text = text

name instance-attribute

name = name

style instance-attribute

style = style

text instance-attribute

text = text

__init__

__init__(
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Create a variable getter “text:variable-get”.

Source code in odfdo/variable.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def __init__(
    self,
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable getter "text:variable-get"."""
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        if style:
            self.style = style
        text = self.set_value_and_type(
            value=value, value_type=value_type, text=text
        )
        self.text = text

VarInitialCreator

Bases: Element

Variable for the initial creator, “text:initial-creator”.

Methods:

Name Description
__init__

Create a variable for the initial creator “text:initial-creator”.

Attributes:

Name Type Description
fixed
Source code in odfdo/variable.py
365
366
367
368
369
370
371
372
373
374
375
class VarInitialCreator(Element):
    """Variable for the initial creator, "text:initial-creator"."""

    _tag = "text:initial-creator"
    _properties = (PropDef("fixed", "text:fixed"),)

    def __init__(self, fixed: bool = False, **kwargs: Any) -> None:
        """Create a variable for the initial creator "text:initial-creator"."""
        super().__init__(**kwargs)
        if self._do_init and fixed:
            self.fixed = True

fixed instance-attribute

fixed = True

__init__

__init__(fixed: bool = False, **kwargs: Any) -> None

Create a variable for the initial creator “text:initial-creator”.

Source code in odfdo/variable.py
371
372
373
374
375
def __init__(self, fixed: bool = False, **kwargs: Any) -> None:
    """Create a variable for the initial creator "text:initial-creator"."""
    super().__init__(**kwargs)
    if self._do_init and fixed:
        self.fixed = True

VarKeywords

Bases: VarInitialCreator

Variable for the keywords, “text:keywords”.

Source code in odfdo/variable.py
462
463
464
465
class VarKeywords(VarInitialCreator):
    """Variable for the keywords, "text:keywords"."""

    _tag = "text:keywords"

VarPageCount

Bases: Element

Variable for page count, “text:page-count”.

Source code in odfdo/variable.py
194
195
196
197
class VarPageCount(Element):
    """Variable for page count, "text:page-count"."""

    _tag = "text:page-count"

VarPageNumber

Bases: Element

Variable for page number, “text:page-number”.

Methods:

Name Description
__init__

Create a variable for page number “text:page-number”.

Attributes:

Name Type Description
page_adjust
select_page
Source code in odfdo/variable.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
class VarPageNumber(Element):
    """Variable for page number, "text:page-number"."""

    _tag = "text:page-number"
    _properties = (
        PropDef("select_page", "text:select-page"),
        PropDef("page_adjust", "text:page-adjust"),
    )

    def __init__(
        self,
        select_page: str | None = None,
        page_adjust: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable for page number "text:page-number".

        select_page -- string in ('previous', 'current', 'next')

        page_adjust -- int (to add or subtract to the page number)
        """
        super().__init__(**kwargs)
        if self._do_init:
            if select_page is None:
                select_page = "current"
            self.select_page = select_page
            if page_adjust is not None:
                self.page_adjust = page_adjust

page_adjust instance-attribute

page_adjust = page_adjust

select_page instance-attribute

select_page = select_page

__init__

__init__(
    select_page: str | None = None,
    page_adjust: str | None = None,
    **kwargs: Any,
) -> None

Create a variable for page number “text:page-number”.

select_page – string in (‘previous’, ‘current’, ‘next’)

page_adjust – int (to add or subtract to the page number)

Source code in odfdo/variable.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def __init__(
    self,
    select_page: str | None = None,
    page_adjust: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable for page number "text:page-number".

    select_page -- string in ('previous', 'current', 'next')

    page_adjust -- int (to add or subtract to the page number)
    """
    super().__init__(**kwargs)
    if self._do_init:
        if select_page is None:
            select_page = "current"
        self.select_page = select_page
        if page_adjust is not None:
            self.page_adjust = page_adjust

VarSet

Bases: ElementTyped

Representation of a variable setter, “text:variable-set”.

Methods:

Name Description
__init__

Create a variable setter, “text:variable-set”.

set_value

Attributes:

Name Type Description
display
name
style
text
Source code in odfdo/variable.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class VarSet(ElementTyped):
    """Representation of a variable setter, "text:variable-set"."""

    _tag = "text:variable-set"
    _properties = (
        PropDef("name", "text:name"),
        PropDef("style", "style:data-style-name"),
        PropDef("display", "text:display"),
    )

    def __init__(
        self,
        name: str | None = None,
        value: Any = None,
        value_type: str | None = None,
        display: str | bool = False,
        text: str | None = None,
        style: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable setter, "text:variable-set"."""
        super().__init__(**kwargs)
        if self._do_init:
            if name:
                self.name = name
            if style:
                self.style = style
            text = self.set_value_and_type(
                value=value, value_type=value_type, text=text
            )
            if not display:
                self.display = "none"
            else:
                self.text = text

    def set_value(self, value: Any) -> None:
        name = self.get_attribute("text:name")
        display = self.get_attribute("text:display")
        style = self.get_attribute("style:data-style-name")
        self.clear()
        text = self.set_value_and_type(value=value)
        self.set_attribute("text:name", name)
        self.set_attribute("style:data-style-name", style)
        if display is not None:
            self.set_attribute("text:display", display)
        if isinstance(text, str):
            self.text = text

display instance-attribute

display = 'none'

name instance-attribute

name = name

style instance-attribute

style = style

text instance-attribute

text = text

__init__

__init__(
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    display: str | bool = False,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None

Create a variable setter, “text:variable-set”.

Source code in odfdo/variable.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def __init__(
    self,
    name: str | None = None,
    value: Any = None,
    value_type: str | None = None,
    display: str | bool = False,
    text: str | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable setter, "text:variable-set"."""
    super().__init__(**kwargs)
    if self._do_init:
        if name:
            self.name = name
        if style:
            self.style = style
        text = self.set_value_and_type(
            value=value, value_type=value_type, text=text
        )
        if not display:
            self.display = "none"
        else:
            self.text = text

set_value

set_value(value: Any) -> None
Source code in odfdo/variable.py
110
111
112
113
114
115
116
117
118
119
120
121
def set_value(self, value: Any) -> None:
    name = self.get_attribute("text:name")
    display = self.get_attribute("text:display")
    style = self.get_attribute("style:data-style-name")
    self.clear()
    text = self.set_value_and_type(value=value)
    self.set_attribute("text:name", name)
    self.set_attribute("style:data-style-name", style)
    if display is not None:
        self.set_attribute("text:display", display)
    if isinstance(text, str):
        self.text = text

VarSubject

Bases: VarInitialCreator

Variable for the subject, “text:subject”.

Source code in odfdo/variable.py
453
454
455
456
class VarSubject(VarInitialCreator):
    """Variable for the subject, "text:subject"."""

    _tag = "text:subject"

VarTime

Bases: Element

Variable for a time, “text:time”.

Methods:

Name Description
__init__

Create a variable for a time “text:time”.

Attributes:

Name Type Description
data_style
fixed
text
time
time_adjust
Source code in odfdo/variable.py
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
class VarTime(Element):
    """Variable for a time, "text:time"."""

    _tag = "text:time"
    _properties = (
        PropDef("time", "text:time-value"),
        PropDef("fixed", "text:fixed"),
        PropDef("data_style", "style:data-style-name"),
        PropDef("time_adjust", "text:time-adjust"),
    )

    def __init__(
        self,
        time: datetime | dt_time | None = None,
        fixed: bool = False,
        data_style: str | None = None,
        text: str | None = None,
        time_adjust: timedelta | None = None,
        **kwargs: Any,
    ) -> None:
        """Create a variable for a time "text:time"."""
        super().__init__(**kwargs)
        if self._do_init:
            if time is None:
                time = dt_time()
            if isinstance(time, dt_time):
                # need convert to datetime
                time = datetime(
                    year=1900,
                    month=1,
                    day=1,
                    hour=time.hour,
                    minute=time.minute,
                    second=time.second,
                )
            self.time = DateTime.encode(time)
            if fixed:
                self.fixed = True
            if data_style is not None:
                self.data_style = data_style
            if text is None:
                text = time.strftime("%H:%M:%S")
            self.text = text
            if time_adjust is not None:
                self.time_adjust = Duration.encode(time_adjust)

data_style instance-attribute

data_style = data_style

fixed instance-attribute

fixed = True

text instance-attribute

text = text

time instance-attribute

time = encode(time)

time_adjust instance-attribute

time_adjust = encode(time_adjust)

__init__

__init__(
    time: datetime | time | None = None,
    fixed: bool = False,
    data_style: str | None = None,
    text: str | None = None,
    time_adjust: timedelta | None = None,
    **kwargs: Any,
) -> None

Create a variable for a time “text:time”.

Source code in odfdo/variable.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def __init__(
    self,
    time: datetime | dt_time | None = None,
    fixed: bool = False,
    data_style: str | None = None,
    text: str | None = None,
    time_adjust: timedelta | None = None,
    **kwargs: Any,
) -> None:
    """Create a variable for a time "text:time"."""
    super().__init__(**kwargs)
    if self._do_init:
        if time is None:
            time = dt_time()
        if isinstance(time, dt_time):
            # need convert to datetime
            time = datetime(
                year=1900,
                month=1,
                day=1,
                hour=time.hour,
                minute=time.minute,
                second=time.second,
            )
        self.time = DateTime.encode(time)
        if fixed:
            self.fixed = True
        if data_style is not None:
            self.data_style = data_style
        if text is None:
            text = time.strftime("%H:%M:%S")
        self.text = text
        if time_adjust is not None:
            self.time_adjust = Duration.encode(time_adjust)

VarTitle

Bases: VarInitialCreator

Variable for the title, “text:title”.

Source code in odfdo/variable.py
444
445
446
447
class VarTitle(VarInitialCreator):
    """Variable for the title, "text:title"."""

    _tag = "text:title"

XmlPart

Representation of an XML part.

Abstraction of the XML library behind.

Methods:

Name Description
__init__

Representation of an XML part.

custom_pretty_tree
delete_element
get_element
get_elements
pretty_serialize
serialize
xpath

Apply XPath query to the XML part. Return list of Element or

Attributes:

Name Type Description
body Element

Get or set the document body : ‘office:body’

clone XmlPart
container
part_name
root Element
Source code in odfdo/xmlpart.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
class XmlPart:
    """Representation of an XML part.

    Abstraction of the XML library behind.
    """

    def __init__(self, part_name: str, container: Container) -> None:
        """Representation of an XML part."""
        self.part_name = part_name
        self.container = container

        # Internal state
        self.__tree: _ElementTree | None = None
        self.__root: Element | None = None

    def _get_tree(self) -> _ElementTree:
        if self.__tree is None:
            part = self.container.get_part(self.part_name)
            self.__tree = parse(BytesIO(part))  # type: ignore
        return self.__tree

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} part_name={self.part_name}>"

    # Public API

    @property
    def root(self) -> Element:
        if self.__root is None:
            tree = self._get_tree()
            self.__root = Element.from_tag(tree.getroot())
        return self.__root

    def _get_body(self) -> Element:
        body = self.root.document_body
        if not isinstance(body, Element):
            raise TypeError(f"No body found in {self.part_name!r}")
        return body

    @property
    def body(self) -> Element:
        """Get or set the document body : 'office:body'"""
        return self._get_body()

    @body.setter
    def body(self, new_body: Element) -> None:
        body = self._get_body()
        tail = body.tail
        body.clear()
        for item in new_body.children:
            body.append(item)
        if tail:  # pragma: nocover
            body.tail = tail

    def get_elements(self, xpath_query: str) -> list[Element | EText]:
        root = self.root
        return root.xpath(xpath_query)

    def get_element(self, xpath_query: str) -> Any:
        result = self.get_elements(xpath_query)
        if not result:
            return None
        return result[0]

    def delete_element(self, child: Element) -> None:
        child.delete()

    def xpath(self, xpath_query: str) -> list[Element | EText]:
        """Apply XPath query to the XML part. Return list of Element or
        EText instances translated from the nodes found.
        """
        root = self.root
        return root.xpath(xpath_query)

    @property
    def clone(self) -> XmlPart:
        clone = object.__new__(self.__class__)
        for name in self.__dict__:
            if name == "container":
                setattr(clone, name, self.container.clone)
            elif name in ("_XmlPart__tree",):
                setattr(clone, name, None)
            else:
                value = getattr(self, name)
                value = deepcopy(value)
                setattr(clone, name, value)
        return clone

    def serialize(self, pretty: bool = False) -> bytes:
        if pretty:
            return self.pretty_serialize()
        xml_header = b'<?xml version="1.0" encoding="UTF-8"?>\n'
        tree = self._get_tree()
        bytes_tree = tostring(tree, encoding="unicode").encode("utf8")
        return xml_header + bytes_tree

    def pretty_serialize(self) -> bytes:
        xml_header = b'<?xml version="1.0" encoding="UTF-8"?>\n'
        bytes_tree = tostring(
            self.custom_pretty_tree(),
            encoding="unicode",
        ).encode("utf8")
        return xml_header + bytes_tree

    def custom_pretty_tree(self) -> _ElementTree | _Element:
        tree = self._get_tree()
        root = tree.getroot()
        return pretty_indent(root)

body property writable

body: Element

Get or set the document body : ‘office:body’

clone property

clone: XmlPart

container instance-attribute

container = container

part_name instance-attribute

part_name = part_name

root property

root: Element

__init__

__init__(part_name: str, container: Container) -> None

Representation of an XML part.

Source code in odfdo/xmlpart.py
42
43
44
45
46
47
48
49
def __init__(self, part_name: str, container: Container) -> None:
    """Representation of an XML part."""
    self.part_name = part_name
    self.container = container

    # Internal state
    self.__tree: _ElementTree | None = None
    self.__root: Element | None = None

custom_pretty_tree

custom_pretty_tree() -> _ElementTree | _Element
Source code in odfdo/xmlpart.py
140
141
142
143
def custom_pretty_tree(self) -> _ElementTree | _Element:
    tree = self._get_tree()
    root = tree.getroot()
    return pretty_indent(root)

delete_element

delete_element(child: Element) -> None
Source code in odfdo/xmlpart.py
100
101
def delete_element(self, child: Element) -> None:
    child.delete()

get_element

get_element(xpath_query: str) -> Any
Source code in odfdo/xmlpart.py
94
95
96
97
98
def get_element(self, xpath_query: str) -> Any:
    result = self.get_elements(xpath_query)
    if not result:
        return None
    return result[0]

get_elements

get_elements(xpath_query: str) -> list[Element | EText]
Source code in odfdo/xmlpart.py
90
91
92
def get_elements(self, xpath_query: str) -> list[Element | EText]:
    root = self.root
    return root.xpath(xpath_query)

pretty_serialize

pretty_serialize() -> bytes
Source code in odfdo/xmlpart.py
132
133
134
135
136
137
138
def pretty_serialize(self) -> bytes:
    xml_header = b'<?xml version="1.0" encoding="UTF-8"?>\n'
    bytes_tree = tostring(
        self.custom_pretty_tree(),
        encoding="unicode",
    ).encode("utf8")
    return xml_header + bytes_tree

serialize

serialize(pretty: bool = False) -> bytes
Source code in odfdo/xmlpart.py
124
125
126
127
128
129
130
def serialize(self, pretty: bool = False) -> bytes:
    if pretty:
        return self.pretty_serialize()
    xml_header = b'<?xml version="1.0" encoding="UTF-8"?>\n'
    tree = self._get_tree()
    bytes_tree = tostring(tree, encoding="unicode").encode("utf8")
    return xml_header + bytes_tree

xpath

xpath(xpath_query: str) -> list[Element | EText]

Apply XPath query to the XML part. Return list of Element or EText instances translated from the nodes found.

Source code in odfdo/xmlpart.py
103
104
105
106
107
108
def xpath(self, xpath_query: str) -> list[Element | EText]:
    """Apply XPath query to the XML part. Return list of Element or
    EText instances translated from the nodes found.
    """
    root = self.root
    return root.xpath(xpath_query)

PageBreak

PageBreak() -> Paragraph

Return an empty paragraph with a manual page break.

Using this function requires to register the page break style with

document.add_page_break_style()

Source code in odfdo/paragraph.py
106
107
108
109
110
111
112
def PageBreak() -> Paragraph:
    """Return an empty paragraph with a manual page break.

    Using this function requires to register the page break style with:
        document.add_page_break_style()
    """
    return Paragraph("", style="odfdopagebreak")

create_table_cell_style

create_table_cell_style(
    border: str | None = None,
    border_top: str | None = None,
    border_bottom: str | None = None,
    border_left: str | None = None,
    border_right: str | None = None,
    padding: str | None = None,
    padding_top: str | None = None,
    padding_bottom: str | None = None,
    padding_left: str | None = None,
    padding_right: str | None = None,
    background_color: str | tuple | None = None,
    shadow: str | None = None,
    color: str | tuple | None = None,
) -> Style

Return a cell style.

The borders arguments must be some style attribute strings or None, see the method ‘make_table_cell_border_string’ to generate them. If the ‘border’ argument as the value ‘default’, the default style “0.06pt solid #000000” is used for the 4 borders. If any value is used for border, it is used for the 4 borders, else any of the 4 borders can be specified by it’s own string. If all the border, border_top, border_bottom, … arguments are None, an empty border is used (ODF value is fo:border=”none”).

Padding arguments are string specifying a length (e.g. “0.5mm”)”. If ‘padding’ is provided, it is used for the 4 sides, else any of the 4 sides padding can be specified by it’s own string. Default padding is no padding.

Arguments:

border -- str, style string for borders on four sides

border_top -- str, style string for top if no 'border' argument

border_bottom -- str, style string for bottom if no 'border' argument

border_left -- str, style string for left if no 'border' argument

border_right -- str, style string for right if no 'border' argument

padding -- str, style string for padding on four sides

padding_top -- str, style string for top if no 'padding' argument

padding_bottom -- str, style string for bottom if no 'padding' argument

padding_left -- str, style string for left if no 'padding' argument

padding_right -- str, style string for right if no 'padding' argument

background_color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

shadow -- str, e.g. "#808080 0.176cm 0.176cm"

color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

Return : Style

Source code in odfdo/style.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def create_table_cell_style(
    border: str | None = None,
    border_top: str | None = None,
    border_bottom: str | None = None,
    border_left: str | None = None,
    border_right: str | None = None,
    padding: str | None = None,
    padding_top: str | None = None,
    padding_bottom: str | None = None,
    padding_left: str | None = None,
    padding_right: str | None = None,
    background_color: str | tuple | None = None,
    shadow: str | None = None,
    color: str | tuple | None = None,
) -> Style:
    """Return a cell style.

    The borders arguments must be some style attribute strings or None, see the
    method 'make_table_cell_border_string' to generate them.
    If the 'border' argument as the value 'default', the default style
    "0.06pt solid #000000" is used for the 4 borders.
    If any value is used for border, it is used for the 4 borders, else any of
    the 4 borders can be specified by it's own string. If all the border,
    border_top, border_bottom, ... arguments are None, an empty border is used
    (ODF value is fo:border="none").

    Padding arguments are string specifying a length (e.g. "0.5mm")". If
    'padding' is provided, it is used for the 4 sides, else any of
    the 4 sides padding can be specified by it's own string. Default padding is
    no padding.

    Arguments:

        border -- str, style string for borders on four sides

        border_top -- str, style string for top if no 'border' argument

        border_bottom -- str, style string for bottom if no 'border' argument

        border_left -- str, style string for left if no 'border' argument

        border_right -- str, style string for right if no 'border' argument

        padding -- str, style string for padding on four sides

        padding_top -- str, style string for top if no 'padding' argument

        padding_bottom -- str, style string for bottom if no 'padding' argument

        padding_left -- str, style string for left if no 'padding' argument

        padding_right -- str, style string for right if no 'padding' argument

        background_color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

        shadow -- str, e.g. "#808080 0.176cm 0.176cm"

        color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

    Return : Style
    """
    if border == "default":
        border = make_table_cell_border_string()  # default border
    if border is not None:
        # use the border value for 4 sides.
        border_bottom = border_top = border_left = border_right = None
    if (
        border is None
        and border_bottom is None
        and border_top is None
        and border_left is None
        and border_right is None
    ):
        border = "none"
    if padding is not None:
        # use the padding value for 4 sides.
        padding_bottom = padding_top = padding_left = padding_right = None
    cell_style = Style(
        "table-cell",
        area="table-cell",
        border=border,
        border_top=border_top,
        border_bottom=border_bottom,
        border_left=border_left,
        border_right=border_right,
        padding=padding,
        padding_top=padding_top,
        padding_bottom=padding_bottom,
        padding_left=padding_left,
        padding_right=padding_right,
        background_color=background_color,
        shadow=shadow,
    )
    if color:
        cell_style.set_properties(area="text", color=color)
    return cell_style

default_boolean_style

default_boolean_style() -> Element

Return a default boolean style.

Source code in odfdo/style.py
1191
1192
1193
1194
1195
1196
1197
def default_boolean_style() -> Element:
    """Return a default boolean style."""
    return Element.from_tag(
        """<number:boolean-style style:name="lpod-default-boolean-style">
           <number:boolean/>
           </number:boolean-style>"""
    )

default_currency_style

default_currency_style() -> Element

Return a default currency style (€).

Source code in odfdo/style.py
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def default_currency_style() -> Element:
    """Return a default currency style (€)."""
    return Element.from_tag(
        """<number:currency-style style:name="lpod-default-currency-style">
            <number:text>-</number:text>
            <number:number number:decimal-places="2"
             number:min-integer-digits="1"
             number:grouping="true"/>
            <number:text> </number:text>
            <number:currency-symbol
             number:language="fr"
             number:country="FR">€</number:currency-symbol>
           </number:currency-style>"""
    )

default_date_style

default_date_style() -> Element

Return a default time style Y-M-D.

Source code in odfdo/style.py
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
def default_date_style() -> Element:
    """Return a default time style Y-M-D."""
    return Element.from_tag(
        """
           <number:date-style style:name="lpod-default-date-style">
           <number:year number:style="long"/>
           <number:text>-</number:text>
           <number:month number:style="long"/>
           <number:text>-</number:text>
           <number:day number:style="long"/>
           </number:date-style>"""
    )

default_frame_position_style

default_frame_position_style(
    name: str = "FramePosition",
    horizontal_pos: str = "from-left",
    vertical_pos: str = "from-top",
    horizontal_rel: str = "paragraph",
    vertical_rel: str = "paragraph",
) -> Style

Generate a style for positioning frames in desktop applications.

Default arguments should be enough.

Use the returned Style as the frame style or build a new graphic style with this style as the parent.

Source code in odfdo/frame.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def default_frame_position_style(
    name: str = "FramePosition",
    horizontal_pos: str = "from-left",
    vertical_pos: str = "from-top",
    horizontal_rel: str = "paragraph",
    vertical_rel: str = "paragraph",
) -> Style:
    """Generate a style for positioning frames in desktop applications.

    Default arguments should be enough.

    Use the returned Style as the frame style or build a new graphic style
    with this style as the parent.
    """
    return Style(
        family="graphic",
        name=name,
        horizontal_pos=horizontal_pos,
        horizontal_rel=horizontal_rel,
        vertical_pos=vertical_pos,
        vertical_rel=vertical_rel,
    )

default_number_style

default_number_style() -> Element

Return a default number style with two decimals.

Source code in odfdo/style.py
1142
1143
1144
1145
1146
1147
1148
1149
def default_number_style() -> Element:
    """Return a default number style with two decimals."""
    return Element.from_tag(
        """<number:number-style style:name="lpod-default-number-style">
           <number:number number:decimal-places="2"
            number:min-integer-digits="1"/>
           </number:number-style>"""
    )

default_percentage_style

default_percentage_style() -> Element

Return a default percentage style with two decimals.

Source code in odfdo/style.py
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
def default_percentage_style() -> Element:
    """Return a default percentage style with two decimals."""
    return Element.from_tag(
        """<number:percentage-style
            style:name="lpod-default-percentage-style">
           <number:number number:decimal-places="2"
            number:min-integer-digits="1"/>
           <number:text>%</number:text>
           </number:percentage-style>"""
    )

default_time_style

default_time_style() -> Element

Return a default time style.

Source code in odfdo/style.py
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
def default_time_style() -> Element:
    """Return a default time style."""
    return Element.from_tag(
        """<number:time-style style:name="lpod-default-time-style">
           <number:hours number:style="long"/>
           <number:text>:</number:text>
           <number:minutes number:style="long"/>
           <number:text>:</number:text>
           <number:seconds number:style="long"/>
           </number:time-style>"""
    )

default_toc_level_style

default_toc_level_style(level: int) -> Style

Generate an automatic default style for the given TOC level.

Source code in odfdo/toc.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def default_toc_level_style(level: int) -> Style:
    """Generate an automatic default style for the given TOC level."""
    tab_stop = TabStopStyle(style_type="right", leader_style="dotted", leader_text=".")
    position = 17.5 - (0.5 * level)
    tab_stop.style_position = f"{position}cm"
    tab_stops = Element.from_tag("style:tab-stops")
    tab_stops.append(tab_stop)
    properties = Element.from_tag("style:paragraph-properties")
    properties.append(tab_stops)
    toc_style_level = Style(
        family="paragraph",
        name=_toc_entry_style_name(level),
        parent=f"Contents_20_{level}",
    )
    toc_style_level.append(properties)
    return toc_style_level

hex2rgb

hex2rgb(color: str) -> tuple[int, int, int]

Convert “#RRGGBB” hexadecimal representation into (R, G, B) tuple.

Arguments:

color -- str

Return: tuple

Source code in odfdo/utils/color.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def hex2rgb(color: str) -> tuple[int, int, int]:
    """Convert "#RRGGBB" hexadecimal representation into (R, G, B) tuple.

    Arguments:

        color -- str

    Return: tuple
    """
    code = color[1:]
    if not (len(color) == 7 and color[0] == "#" and code.isalnum()):
        raise ValueError(f'"{color}" is not a valid color')
    red = int(code[:2], 16)
    green = int(code[2:4], 16)
    blue = int(code[4:6], 16)
    return (red, green, blue)

hexa_color

hexa_color(
    color: str | tuple[int, int, int] | None = None,
) -> str | None

Safe conversion from color tuple or string to hexadecimal representation.

Empty string is converted to black. None is converted to None.

Arguments:

color -- str or tuple or None

Return: str or None

Source code in odfdo/utils/color.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def hexa_color(color: str | tuple[int, int, int] | None = None) -> str | None:
    """Safe conversion from color tuple or string to hexadecimal representation.

    Empty string is converted to black.
    None is converted to None.

    Arguments:

        color -- str or tuple or None

    Return: str or None
    """
    if color is None:
        return None
    if isinstance(color, tuple):
        return rgb2hex(color)
    if not isinstance(color, str):
        raise TypeError(f'Invalid color argument "{color!r}"')
    color = color.strip()
    if not color:
        return "#000000"
    if color.startswith("#"):
        return color
    return rgb2hex(color)

make_table_cell_border_string

make_table_cell_border_string(
    thick: str | float | int | None = None,
    line: str | None = None,
    color: str | tuple | None = None,
) -> str

Returns a string for “style:table-cell-properties” “fo:border”.

With default: “0.06pt solid #000000”

Arguments:

thick -- str or float or int

line -- str

color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'
Source code in odfdo/style.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def make_table_cell_border_string(
    thick: str | float | int | None = None,
    line: str | None = None,
    color: str | tuple | None = None,
) -> str:
    """Returns a string for "style:table-cell-properties" "fo:border".

    With default: "0.06pt solid #000000"

    Arguments:

        thick -- str or float or int

        line -- str

        color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'
    """
    thick_string = _make_thick_string(thick)
    line_string = _make_line_string(line)
    color_string = hexa_color(color) or "#000000"
    return " ".join((thick_string, line_string, color_string))

rgb2hex

rgb2hex(color: str | tuple[int, int, int]) -> str

Convert color name or (R, G, B) tuple into “#RRGGBB” hexadecimal.

Arguments:

color -- str or tuple

Return: str

Examples::

>>> rgb2hex('yellow')
'#FFFF00'
>>> rgb2hex((238, 130, 238))
'#EE82EE'
Source code in odfdo/utils/color.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def rgb2hex(color: str | tuple[int, int, int]) -> str:
    """Convert color name or (R, G, B) tuple into "#RRGGBB" hexadecimal.

    Arguments:

        color -- str or tuple

    Return: str

    Examples::

        >>> rgb2hex('yellow')
        '#FFFF00'
        >>> rgb2hex((238, 130, 238))
        '#EE82EE'
    """
    if isinstance(color, str):
        try:
            code = CSS3_COLORMAP[color.lower()]
        except KeyError as e:
            raise KeyError(f'Color "{color}" is unknown in CSS color list') from e
    elif isinstance(color, tuple):
        if len(color) != 3:
            raise ValueError("Color must be a 3-tuple")
        code = color
    else:
        raise TypeError(f'Invalid color "{color}"')
    for channel in code:
        if not 0 <= channel <= 255:
            raise ValueError(
                f'Invalid color "{color}", channel must be between 0 and 255'
            )
    return f"#{code[0]:02X}{code[1]:02X}{code[2]:02X}"