Skip to content

Reference

AnimPar

Bases: Element

A container for SMIL Presentation Animations.

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
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
class AnimPar(Element):
    """A container for SMIL Presentation Animations.

    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
    """

    _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:
        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

TA container for SMIL Presentation Animations. 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class AnimSeq(Element):
    """TA container for SMIL Presentation Animations. 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
    """

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

    def __init__(
        self,
        presentation_node_type: str | None = None,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        if self._do_init and presentation_node_type:
            self.presentation_node_type = presentation_node_type

AnimTransFilter

Bases: Element

Class to make a beautiful transition between two frames.

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
 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
class AnimTransFilter(Element):
    """
    Class to make a beautiful transition between two frames.

    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
    """

    _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:
        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

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/note.py
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
class Annotation(MDTail, Element, DcCreatorMixin, DcDateMixin):
    """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
    """

    _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:
        # 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:
            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
        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}"

dc_creator property writable

Alias for self.creator property.

dc_date property writable

Alias for self.date property.

end property

Return the corresponding annotation-end tag or None.

start property

Return self.

delete(child=None, keep_tail=True)

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/note.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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(as_text=False, no_header=True, clean=True)

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/note.py
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
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
    return body.get_between(
        self, end, as_text=as_text, no_header=no_header, clean=clean
    )

AnnotationEnd

Bases: MDTail, Element

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.

Source code in odfdo/note.py
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
class AnnotationEnd(MDTail, Element):
    """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:
        """Initialize an AnnotationEnd element. Either annotation or name must be
        provided to have proper reference for the annotation-end.

        Arguments:

            annotation -- odf_annotation element

            name -- str
        """
        # 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")
        body = self.document_body
        if not body:
            body = parent
        return body.get_annotation(name=name)

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

end property

Return self.

start property

Return the corresponding annotation starting tag or None.

__init__(annotation=None, name=None, **kwargs)

Initialize an AnnotationEnd element. Either annotation or name must be provided to have proper reference for the annotation-end.

Arguments:

annotation -- odf_annotation element

name -- str
Source code in odfdo/note.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def __init__(
    self,
    annotation: Element | None = None,
    name: str | None = None,
    **kwargs: Any,
) -> None:
    """Initialize an AnnotationEnd element. Either annotation or name must be
    provided to have proper reference for the annotation-end.

    Arguments:

        annotation -- odf_annotation element

        name -- str
    """
    # 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

Body

Bases: Element

Body, specialized class of Element in charge of actual content management.

Source code in odfdo/body.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
85
86
87
88
89
class Body(Element):
    """Body, specialized class of Element in charge of actual content
    management.
    """

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

    def get_tables(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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[Element]:
        """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,
    ) -> Element | 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

Return all the tables.

Return: list of Table

get_table(position=0, name=None, content=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
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
def get_table(
    self,
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> Element | 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(style=None, content=None)

Return all the tables that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Table

Source code in odfdo/body.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def get_tables(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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 for ODF “text:bookmark”

Arguments:

name -- str
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 for ODF "text:bookmark"

    Arguments:

        name -- str
    """

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

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

BookmarkEnd

Bases: Element

BookmarkEnd class for ODF “text:bookmark-end”

Arguments:

name -- str
Source code in odfdo/bookmark.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class BookmarkEnd(Element):
    """
    BookmarkEnd class for ODF "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

BookmarkStart

Bases: Element

BookmarkStart class for ODF “text:bookmark-start”

Arguments:

name -- str
Source code in odfdo/bookmark.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class BookmarkStart(Element):
    """
    BookmarkStart class for ODF "text:bookmark-start"

    Arguments:

        name -- str
    """

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

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

Cell

Bases: ElementTyped

“table:table-cell” table cell element.

Source code in odfdo/cell.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
 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
class Cell(ElementTyped):
    """ "table:table-cell" table cell element."""

    _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:
        """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):
            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)
        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":
                break
            child = upper
        # fixme : need to optimize this
        if isinstance(upper, Element) and upper._tag == "table:table-row":
            upper._compute_row_cache()

    @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_spanned():
            return False
        if not aggressive and self.style is not None:  # noqa: SIM103
            return False
        return True

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

        Returns: True | False
        """
        if self.tag == "table:covered-table-cell":
            return True
        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

bool property writable

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

currency property writable

Get / set the currency used for monetary values.

Return: str | None

date property writable

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

datetime property writable

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

decimal property writable

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

duration property writable

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

float property writable

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

formula property writable

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

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

repeated property writable

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

Always None when using the table API.

Return: int or None

string property writable

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

style property writable

Get / set the style of the cell itself.

Return: str | None

type property writable

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

Return: str | None

value property writable

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.

__init__(value=None, text=None, cell_type=None, currency=None, formula=None, repeated=None, style=None, **kwargs)

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
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:
    """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_empty(aggressive=False)

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
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
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_spanned():
        return False
    if not aggressive and self.style is not None:  # noqa: SIM103
        return False
    return True

is_spanned()

Return whether the cell is spanned over several cells.

Returns: True | False

Source code in odfdo/cell.py
512
513
514
515
516
517
518
519
520
521
522
523
def is_spanned(self) -> bool:
    """Return whether the cell is spanned over several cells.

    Returns: True | False
    """
    if self.tag == "table:covered-table-cell":
        return True
    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(value, text=None, cell_type=None, currency=None, formula=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

ChangeInfo

Bases: Element, DcCreatorMixin, DcDateMixin

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

Arguments:

creator – str (or None)

date – datetime (or None)

Source code in odfdo/tracked_changes.py
 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
class ChangeInfo(Element, DcCreatorMixin, DcDateMixin):
    """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

      Arguments:

         creator -- str (or None)

         date -- datetime (or None)
    """

    _tag = "office:change-info"

    def __init__(
        self,
        creator: str | None = None,
        date: datetime | None = None,
        **kwargs: Any,
    ) -> 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
        if content is None:
            content = []
        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)

get_comments(joined=True)

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
    if content is None:
        content = []
    text = [para.get_formatted_text(simple=True) for para in content]  # type: ignore
    if joined:
        return "\n".join(text)
    return text

set_comments(text='', replace=True)

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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
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

Chart, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
92
93
94
95
96
97
98
class Chart(Body):
    """Chart, specialized class of Element in charge of actual content
    management.
    """

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

Column

Bases: Element

ODF table column “table:table-column”

Source code in odfdo/table.py
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
class Column(Element):
    """ODF table column "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:
        """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

            repeated -- int

            style -- str
        """
        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 = Element.clone.fget(self)  # type: ignore
        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: Element | 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: Element | 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 isinstance(upper, Table):
                break
            current = upper
        # fixme : need to optimize this
        if isinstance(upper, Table):
            upper._compute_table_cache()
            if hasattr(self, "_cmap"):
                del self._cmap[:]
                self._cmap.extend(upper._cmap)
            else:
                self._cmap = upper._cmap

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

        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)

default_cell_style property writable

Get or set the default cell style for column.

repeated property writable

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

Always None when using the table API.

Return: int or None

style property writable

Get /set the style of the column itself.

Return: str

__init__(default_cell_style=None, repeated=None, style=None, **kwargs)

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

repeated -- int

style -- str
Source code in odfdo/table.py
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
def __init__(
    self,
    default_cell_style: str | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """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

        repeated -- int

        style -- str
    """
    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 or set the default cell style for column.

(See also self.default_cell_style property.)

Source code in odfdo/table.py
223
224
225
226
227
228
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(style)

Get or set the default cell style for column.

(See also self.default_cell_style property.)

Source code in odfdo/table.py
230
231
232
233
234
235
def set_default_cell_style(self, style: Element | 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

Create a Connector shape.

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
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
class ConnectorShape(ShapeBase):
    """Create a Connector shape.

    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)
    """

    _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:
        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

Representation of the ODF file.

Source code in odfdo/container.py
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
class Container:
    """Representation of the ODF file."""

    def __init__(self, path: Path | str | io.BytesIO | None = None) -> 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  # type: ignore
        ):
            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

Make a copy of this container with no path.

mimetype property writable

Return str value of mimetype of the document.

parts property

Get the list of members.

del_part(path)

Mark a part for deletion.

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

get_part(path)

Get the bytes of a part of the ODF.

Source code in odfdo/container.py
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
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 the list of members.

Source code in odfdo/container.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
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(path_or_file)

Load the content of an ODF file.

Source code in odfdo/container.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
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  # type: ignore
    ):
        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(target, packaging=None, backup=False, pretty=False)

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
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
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(path, data)

Replace or add a new part.

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

Content

Bases: XmlPart

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):
    # The following two seem useless but they match styles API

    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(family, name_or_element=None, display_name=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(family=None)

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

Database, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
101
102
103
104
105
106
107
class Database(Body):
    """Database, specialized class of Element in charge of actual content
    management.
    """

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

Document

Bases: MDDocument

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
 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
class Document(MDDocument):
    """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.
    """

    def __init__(
        self,
        target: str | bytes | Path | Container | io.BytesIO | None = "text",
    ) -> None:
        # 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) :])  # type: ignore
            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]:  # type: ignore
        """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:  # type: ignore
        """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

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

clone property

Return an exact copy of the document.

Return: Document

manifest property

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

meta property

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

parts property

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

path property writable

Shortcut to Document.Container.path.

add_file(path_or_file)

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
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
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()

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
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
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(path)

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

Source code in odfdo/document.py
366
367
368
369
370
371
372
373
374
375
376
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()

Remove all style information from content and all styles.

Return: number of deleted styles

Source code in odfdo/document.py
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
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(table, coord, default='#ffffff')

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
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
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(table, coord)

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
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
def get_cell_style_properties(
    self, table: str | int, coord: tuple | list | str
) -> dict[str, str]:  # type: ignore
    """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()

Return meta informations as text, with some formatting.

(Redirection to new implementation for compatibility.)

Source code in odfdo/document.py
553
554
555
556
557
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(rst_mode=False)

Return content as text, with some formatting.

Source code in odfdo/document.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
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_part(path)

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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
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()

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

Source code in odfdo/document.py
308
309
310
311
312
313
314
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(family, name_or_element=None, display_name=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
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
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(family, name, area=None)

Return the properties of the required style as a dict.

Source code in odfdo/document.py
1180
1181
1182
1183
1184
1185
1186
1187
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(name='')

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
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
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_table_displayed(table)

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
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
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(table)

Return the Style instance the table.

Arguments:

table -- name or index of the table
Source code in odfdo/document.py
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
def get_table_style(
    self,
    table: str | int,
) -> Style | None:  # type: ignore
    """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 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
390
391
392
393
394
395
396
397
398
399
400
401
402
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(style, name='', automatic=False, default=False)

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
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
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(document)

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
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
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(template='text') classmethod

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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
@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(target=None, packaging=ZIP, pretty=None, backup=False)

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
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
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(path, data)

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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
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(table, displayed)

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
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
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

DrawFillImage

Bases: DrawImage

Source code in odfdo/image.py
 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
class DrawFillImage(DrawImage):
    _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:
        """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 = ""

__init__(name=None, display_name=None, height=None, width=None, **kwargs)

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
 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
def __init__(
    self,
    name: str | None = None,
    display_name: str | None = None,
    height: str | None = None,
    width: str | None = None,
    **kwargs: Any,
) -> None:
    """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

The DrawGroup “draw:g” element represents a group of drawing shapes.

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”.

Source code in odfdo/shapes.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
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
class DrawGroup(Element, AnchorMix, ZMix, PosMix):
    """The DrawGroup "draw:g" element represents a group of drawing shapes.

    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:
        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

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. 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().

Source code in odfdo/image.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
class DrawImage(Element):
    """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.
    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().
    """

    _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:
        """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

__init__(url='', xlink_type='simple', show='embed', actuate='onLoad', filter_name=None, **kwargs)

Initialisation of an DrawImage.

Arguments:

url -- str

type -- str

show -- str

actuate -- str

filter_name -- str
Source code in odfdo/image.py
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
def __init__(
    self,
    url: str = "",
    xlink_type: str = "simple",
    show: str = "embed",
    actuate: str = "onLoad",
    filter_name: str | None = None,
    **kwargs: Any,
) -> None:
    """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 “draw:page”, for pages of presentation and drawings.

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
class DrawPage(Element):
    """ODF draw page "draw:page", for pages of presentation and drawings."""

    _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:
        """
        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 set_transition(
        self,
        smil_type: str,
        subtype: str | None = None,
        dur: str = "2s",
    ) -> None:
        # Create the new animation
        anim_page = AnimPar(presentation_node_type="timing-root")
        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'
        existing = self.get_element("anim:par")
        if existing:
            self.delete(existing)
        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)

__init__(draw_id=None, name=None, master_page=None, presentation_page_layout=None, style=None, **kwargs)

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
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:
    """
    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

Drawing

Bases: Body

Drawing, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
110
111
112
113
114
115
116
class Drawing(Body):
    """Drawing, specialized class of Element in charge of actual content
    management.
    """

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

EText

Bases: str

Representation of an XML text node. Created to hide the specifics of lxml in searching text nodes using XPath.

Constructed like any str object but only accepts lxml text objects.

Source code in odfdo/element.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
class EText(str):
    """Representation of an XML text node. 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: str | bytes,
    ) -> None:
        self.__parent = text_result.getparent()  # type: ignore
        self.__is_text = text_result.is_text
        self.__is_tail = 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

Element

Bases: MDBase

Super class of all ODF classes.

Representation of an XML element. Abstraction of the XML library behind.

Source code in odfdo/element.py
 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
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
class Element(MDBase):
    """Super class of all ODF classes.

    Representation of an XML element. Abstraction of the XML library behind.
    """

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

    def __init__(self, **kwargs: Any) -> None:
        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 = klass(tag_or_elem=tree_element)
        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)  # type: ignore
                    parent.insert(index + 1, wrapper)  # type: ignore
                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)  # type: ignore
        parent.insert(index + 1, wrapper)  # type: ignore

        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  # type: ignore
            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]:
        cache: tuple | None = None
        element = self.__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):
            raise TypeError("Bad XPath result")
        return [
            Element.from_tag_for_clone(e, cache)
            for e in result
            if isinstance(e, _Element)
        ]

    # fixme : need original get_element as wrapper of get_elements

    def get_element(self, xpath_query: XPath | str) -> Element | None:
        element = self.__element
        result = element.xpath(f"({xpath_query})[1]", namespaces=ODF_NAMESPACES)
        if result:
            return Element.from_tag(result[0])  # type:ignore
        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])  # type:ignore
        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])  # type:ignore
        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:
        """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

    @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  # type: ignore
                else:
                    container.tail = new_text  # type: ignore
                if formatted and container.tag in {  # type; ignore
                    "text:h",
                    "text:p",
                    "text:span",
                }:
                    container.append_plain_text("")  # type; ignore
                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
        """
        return self.__element.index(child.__element)

    @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 | 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[:]
        element.text = 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  # type: ignore
                        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)  # type: ignore
                    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)  # type: ignore
                    target = new_target
                    current = current.children[0]
                    continue
                # collect
                target.__append(current.clone)  # type: ignore
                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 | Element | 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)  # type: ignore
            parent.insert(index + 1, lx_element)  # type: ignore
        elif xmlposition is PREV_SIBLING:
            parent = current.getparent()
            index = parent.index(current)  # type: ignore
            parent.insert(index, lx_element)  # type: ignore
        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:  # type: ignore
                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 = 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) -> Element | None:
        """Return the first children of document body if any: 'office:body/*[1]'"""
        return self.get_element("//office:body/*[1]")

    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
        """
        # 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[Element]:
        """Return all the sections that match the criteria.

        Arguments:

            style -- str

            content -- str regex

        Return: list of Element
        """
        return self._filtered_elements(
            "text:section", text_style=style, content=content
        )

    @property
    def sections(
        self,
    ) -> list[Element]:
        """Return all the sections.

        Return: list of Element
        """
        return self.get_elements("text:section")

    def get_section(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | None:
        """Return the section that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:section", position, content=content
        )

    # Paragraphs

    def get_paragraphs(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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
        )

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

        Return: list of Paragraph
        """
        return self.get_elements("descendant::text:p")

    def get_paragraph(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | 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)

    # Span

    def get_spans(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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
        )

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

        Return: list of Span
        """
        return self.get_elements("descendant::text:span")

    def get_span(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | 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
        )

    # Headers

    def get_headers(
        self,
        style: str | None = None,
        outline_level: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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,
        )

    @property
    def headers(self) -> list[Element]:
        """Return all the Headers.

        Return: list of Header
        """
        return self.get_elements("descendant::text:h")

    def get_header(
        self,
        position: int = 0,
        outline_level: str | None = None,
        content: str | None = None,
    ) -> Element | 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,
        )

    # Lists

    def get_lists(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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
        )

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

        Return: list of List
        """
        return self.get_elements("descendant::text:list")

    def get_list(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | 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
        )

    # 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[Element]:
        """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,
        )

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

        Return: list of Frame
        """
        return self.get_elements("descendant::draw:frame")

    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,
    ) -> Element | 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,
        )

    # Images

    def get_images(
        self,
        style: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the images matching the criteria.

        Arguments:

            style -- str

            url -- str regex

            content -- str regex

        Return: list of Element
        """
        return self._filtered_elements(
            "descendant::draw:image", text_style=style, url=url, content=content
        )

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

        Return: list of Element
        """
        return self.get_elements("descendant::draw:image")

    def get_image(
        self,
        position: int = 0,
        name: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> Element | None:
        """Return the image matching the criteria.

        Arguments:

            position -- int

            name -- str

            url -- str regex

            content -- str regex

        Return: Element 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")
        return self._filtered_element(
            "descendant::draw:image", position, url=url, content=content
        )

    # Named Range

    def get_named_ranges(self) -> list[Element]:
        """Return all the tables named ranges.

        Return: list of odf_named_range
        """
        named_ranges = self.get_elements(
            "descendant::table:named-expressions/table:named-range"
        )
        return named_ranges

    def get_named_range(self, name: str) -> Element | 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]
        else:
            return None

    def append_named_range(self, named_range: Element) -> 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]'  # type:ignore
        )
        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[Element]:
        """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
        )

    def get_note(
        self,
        position: int = 0,
        note_id: str | None = None,
        note_class: str | None = None,
        content: str | None = None,
    ) -> Element | 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,
        )

    # Annotations

    def get_annotations(
        self,
        creator: str | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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 = []
        for annotation in self._filtered_elements(
            "descendant::office:annotation", content=content
        ):
            if creator is not None and creator != annotation.dc_creator:
                continue
            date = annotation.date
            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)
        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,
    ) -> Element | 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
            )
        annotations = 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[Element]:
        """Return all the annotation ends.

        Return: list of Element
        """
        return self._filtered_elements("descendant::office:annotation-end")

    def get_annotation_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | None:
        """Return the annotation end that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::office:annotation-end", position, office_name=name
        )

    # 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) -> Element:
        """Return the container for variable declarations. Created if not
        found.

        Return: Element
        """
        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

    def get_variable_decl_list(self) -> list[Element]:
        """Return all the variable declarations.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:variable-decl")

    def get_variable_decl(self, name: str, position: int = 0) -> Element | None:
        """return the variable declaration for the given name.

        Arguments:

            name -- str

            position -- int

        return: Element or none if not found
        """
        return self._filtered_element(
            "descendant::text:variable-decl", position, text_name=name
        )

    def get_variable_sets(self, name: str | None = None) -> list[Element]:
        """Return all the variable sets that match the criteria.

        Arguments:

            name -- str

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:variable-set", text_name=name)

    def get_variable_set(self, name: str, position: int = -1) -> Element | None:
        """Return the variable set for the given name (last one by default).

        Arguments:

            name -- str

            position -- int

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:variable-set", position, text_name=name
        )

    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

    # User fields

    def get_user_field_decls(self) -> Element | None:
        """Return the container for user field declarations. Created if not
        found.

        Return: Element
        """
        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

    def get_user_field_decl_list(self) -> list[Element]:
        """Return all the user field declarations.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:user-field-decl")

    def get_user_field_decl(self, name: str, position: int = 0) -> Element | 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
        )

    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
        return user_field_decl.get_value(value_type)  # type: ignore

    # User defined fields
    # They are fields who should contain a copy of a user defined medtadata

    def get_user_defined_list(self) -> list[Element]:
        """Return all the user defined field declarations.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:user-defined")

    @property
    def user_defined_list(self) -> list[Element]:
        """Return all the user defined field declarations.

        Return: list of Element
        """
        return self.get_user_defined_list()

    def get_user_defined(self, name: str, position: int = 0) -> Element | None:
        """return the user defined declaration for the given name.

        return: Element or none if not found
        """
        return self._filtered_element(
            "descendant::text:user-defined", position, text_name=name
        )

    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

    # Draw Pages

    def get_draw_pages(
        self,
        style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """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
        )

    def get_draw_page(
        self,
        position: int = 0,
        name: str | None = None,
        content: str | None = None,
    ) -> Element | 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
        )

    # Links

    def get_links(
        self,
        name: str | None = None,
        title: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the links that match the criteria.

        Arguments:

            name -- str

            title -- str

            url -- str regex

            content -- str regex

        Return: list of Element
        """
        return self._filtered_elements(
            "descendant::text:a",
            office_name=name,
            office_title=title,
            url=url,
            content=content,
        )

    def get_link(
        self,
        position: int = 0,
        name: str | None = None,
        title: str | None = None,
        url: str | None = None,
        content: str | None = None,
    ) -> Element | None:
        """Return the link that matches the criteria.

        Arguments:

            position -- int

            name -- str

            title -- str

            url -- str regex

            content -- str regex

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:a",
            position,
            office_name=name,
            office_title=title,
            url=url,
            content=content,
        )

    # Bookmarks

    def get_bookmarks(self) -> list[Element]:
        """Return all the bookmarks.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:bookmark")

    def get_bookmark(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | 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
        )

    def get_bookmark_starts(self) -> list[Element]:
        """Return all the bookmark starts.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:bookmark-start")

    def get_bookmark_start(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | None:
        """Return the bookmark start that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:bookmark-start", position, text_name=name
        )

    def get_bookmark_ends(self) -> list[Element]:
        """Return all the bookmark ends.

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:bookmark-end")

    def get_bookmark_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | None:
        """Return the bookmark end that matches the criteria.

        Arguments:

            position -- int

            name -- str

        Return: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:bookmark-end", position, text_name=name
        )

    # Reference marks

    def get_reference_marks_single(self) -> list[Element]:
        """Return all the reference marks. Search only the tags
        text:reference-mark.
        Consider using : get_reference_marks()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:reference-mark")

    def get_reference_mark_single(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark", position, text_name=name
        )

    def get_reference_mark_starts(self) -> list[Element]:
        """Return all the reference mark starts. Search only the tags
        text:reference-mark-start.
        Consider using : get_reference_marks()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:reference-mark-start")

    def get_reference_mark_start(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark-start", position, text_name=name
        )

    def get_reference_mark_ends(self) -> list[Element]:
        """Return all the reference mark ends. Search only the tags
        text:reference-mark-end.
        Consider using : get_reference_marks()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:reference-mark-end")

    def get_reference_mark_end(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:reference-mark-end", position, text_name=name
        )

    def get_reference_marks(self) -> list[Element]:
        """Return all the reference marks, either single position reference
        (text:reference-mark) or start of range reference
        (text:reference-mark-start).

        Return: list of Element
        """
        return self._filtered_elements(
            "descendant::text:reference-mark-start | descendant::text:reference-mark"
        )

    def get_reference_mark(
        self,
        position: int = 0,
        name: str | None = None,
    ) -> Element | 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: Element 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)
        request = (
            "descendant::text:reference-mark-start | descendant::text:reference-mark"
        )
        return self._filtered_element(request, position)

    def get_references(self, name: str | None = None) -> list[Element]:
        """Return all the references (text:reference-ref). If name is
        provided, returns the references of that name.

        Return: list of Element

        Arguments:

            name -- str or None
        """
        if name is None:
            return self._filtered_elements("descendant::text:reference-ref")
        request = f'descendant::text:reference-ref[@text:ref-name="{name}"]'
        return self._filtered_elements(request)

    # Shapes elements

    # Groups

    def get_draw_groups(
        self,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        return self._filtered_elements(
            "descendant::draw:g",
            svg_title=title,
            svg_desc=description,
            content=content,
        )

    def get_draw_group(
        self,
        position: int = 0,
        name: str | None = None,
        title: str | None = None,
        description: str | None = None,
        content: str | None = None,
    ) -> Element | None:
        return self._filtered_element(
            "descendant::draw:g",
            position,
            draw_name=name,
            svg_title=title,
            svg_desc=description,
            content=content,
        )

    # Lines

    def get_draw_lines(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the draw lines that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of odf_shape
        """
        return self._filtered_elements(
            "descendant::draw:line",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )

    def get_draw_line(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> Element | None:
        """Return the draw line that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: odf_shape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:line", position, draw_id=id, content=content
        )

    # Rectangles

    def get_draw_rectangles(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the draw rectangles that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of odf_shape
        """
        return self._filtered_elements(
            "descendant::draw:rect",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )

    def get_draw_rectangle(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> Element | None:
        """Return the draw rectangle that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: odf_shape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:rect", position, draw_id=id, content=content
        )

    # Ellipse

    def get_draw_ellipses(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the draw ellipses that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of odf_shape
        """
        return self._filtered_elements(
            "descendant::draw:ellipse",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )

    def get_draw_ellipse(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> Element | None:
        """Return the draw ellipse that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: odf_shape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:ellipse", position, draw_id=id, content=content
        )

    # Connectors

    def get_draw_connectors(
        self,
        draw_style: str | None = None,
        draw_text_style: str | None = None,
        content: str | None = None,
    ) -> list[Element]:
        """Return all the draw connectors that match the criteria.

        Arguments:

            draw_style -- str

            draw_text_style -- str

            content -- str regex

        Return: list of odf_shape
        """
        return self._filtered_elements(
            "descendant::draw:connector",
            draw_style=draw_style,
            draw_text_style=draw_text_style,
            content=content,
        )

    def get_draw_connector(
        self,
        position: int = 0,
        id: str | None = None,  # noqa:A002
        content: str | None = None,
    ) -> Element | None:
        """Return the draw connector that matches the criteria.

        Arguments:

            position -- int

            id -- str

            content -- str regex

        Return: odf_shape or None if not found
        """
        return self._filtered_element(
            "descendant::draw:connector", position, draw_id=id, content=content
        )

    def get_orphan_draw_connectors(self) -> list[Element]:
        """Return a list of connectors which don't have any shape connected
        to them.
        """
        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) -> Element | None:
        """Return the tracked-changes part in the text body.

        Return: Element or None
        """
        return self.get_element("//text:tracked-changes")

    @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[Element]:
        """Return all the text changes of deletion kind: the tags text:change.
        Consider using : get_text_changes()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:text:change")

    def get_text_change_deletion(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:change", position, change_id=idx
        )

    def get_text_change_starts(self) -> list[Element]:
        """Return all the text change-start. Search only for the tags
        text:change-start.
        Consider using : get_text_changes()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:change-start")

    def get_text_change_start(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:change-start", position, change_id=idx
        )

    def get_text_change_ends(self) -> list[Element]:
        """Return all the text change-end. Search only the tags
        text:change-end.
        Consider using : get_text_changes()

        Return: list of Element
        """
        return self._filtered_elements("descendant::text:change-end")

    def get_text_change_end(
        self,
        position: int = 0,
        idx: str | None = None,
    ) -> Element | 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: Element or None if not found
        """
        return self._filtered_element(
            "descendant::text:change-end", position, change_id=idx
        )

    def get_text_changes(self) -> list[Element]:
        """Return all the text changes, either single deletion
        (text:change) or start of range of changes (text:change-start).

        Return: list of Element
        """
        request = "descendant::text:change-start | descendant::text:change"
        return self._filtered_elements(request)

    @property
    def text_changes(self) -> list[Element]:
        """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,
    ) -> Element | 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)
        request = "descendant::text:change-start | descendant::text:change"
        return self._filtered_element(request, position)

    # Table Of Content

    def get_tocs(self) -> list[Element]:
        """Return all the tables of contents.

        Return: list of odf_toc
        """
        return self.get_elements("text:table-of-content")

    @property
    def tocs(self) -> list[Element]:
        """Return all the tables of contents.

        Return: list of odf_toc
        """
        return self.get_elements("text:table-of-content")

    def get_toc(
        self,
        position: int = 0,
        content: str | None = None,
    ) -> Element | None:
        """Return the table of contents that matches the criteria.

        Arguments:

            position -- int

            content -- str regex

        Return: odf_toc or None if not found
        """
        return self._filtered_element(
            "text:table-of-content", position, content=content
        )

    @property
    def toc(self) -> Element | 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,
    ) -> Element | 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: odf_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
            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,
            )
        else:
            return self._filtered_element(
                tagname,
                0,
                draw_name=style_name or display_name,
                family=family,
            )

    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

document_body property

Return the first children of document body if any: ‘office:body/*[1]’

frames property

Return all the frames.

Return: list of Frame

headers property

Return all the Headers.

Return: list of Header

images property

Return all the images.

Return: list of Element

lists property

Return all the lists.

Return: list of List

paragraphs property

Return all the paragraphs.

Return: list of Paragraph

sections property

Return all the sections.

Return: list of Element

spans property

Return all the spans.

Return: list of Span

tag property writable

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

Get / set the text immediately following the element.

text property writable

Get / set the text content of the element.

text_changes property

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

Get / set the text of the embedded paragraphs, including embeded annotations, cells…

Set does create a paragraph if missing.

toc property

Return the first table of contents.

Return: odf_toc or None if not found

tocs property

Return all the tables of contents.

Return: list of odf_toc

tracked_changes property

Return the tracked-changes part in the text body.

Return: Element or None

user_defined_list property

Return all the user defined field declarations.

Return: list of Element

__append(str_or_element)

Insert element or text in the last position.

Source code in odfdo/element.py
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
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_named_range(named_range)

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
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
def append_named_range(self, named_range: Element) -> 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]'  # type:ignore
    )
    if current:
        named_expressions.delete(current)
    named_expressions.__append(named_range)

clear()

Remove text, children and attributes from the element.

Source code in odfdo/element.py
1665
1666
1667
def clear(self) -> None:
    """Remove text, children and attributes from the element."""
    self.__element.clear()

delete(child=None, keep_tail=True)

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
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
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(name)

Delete the Named Range of specified name from the spreadsheet.

Arguments:

name -- str
Source code in odfdo/element.py
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
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(xpath_instance, name)

Utility method for table module.

Source code in odfdo/element.py
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
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(odf_elements)

Fast append elements at the end of ourself using extend.

Source code in odfdo/element.py
1429
1430
1431
1432
1433
1434
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(tag_or_elem) classmethod

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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
@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)

get_annotation(position=0, creator=None, start_date=None, end_date=None, content=None, name=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
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
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,
) -> Element | 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
        )
    annotations = 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(position=0, name=None)

Return the annotation end that matches the criteria.

Arguments:

position -- int

name -- str

Return: Element or None if not found

Source code in odfdo/element.py
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
def get_annotation_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | None:
    """Return the annotation end that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::office:annotation-end", position, office_name=name
    )

get_annotation_ends()

Return all the annotation ends.

Return: list of Element

Source code in odfdo/element.py
2357
2358
2359
2360
2361
2362
def get_annotation_ends(self) -> list[Element]:
    """Return all the annotation ends.

    Return: list of Element
    """
    return self._filtered_elements("descendant::office:annotation-end")

get_annotations(creator=None, start_date=None, end_date=None, content=None)

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
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
def get_annotations(
    self,
    creator: str | None = None,
    start_date: datetime | None = None,
    end_date: datetime | None = None,
    content: str | None = None,
) -> list[Element]:
    """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 = []
    for annotation in self._filtered_elements(
        "descendant::office:annotation", content=content
    ):
        if creator is not None and creator != annotation.dc_creator:
            continue
        date = annotation.date
        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)
    return annotations

get_attribute(name)

Return the attribute value as type str | bool | None.

Source code in odfdo/element.py
795
796
797
798
799
800
801
802
803
804
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(name)

Return either the attribute as type int, or None.

Source code in odfdo/element.py
806
807
808
809
810
811
812
813
814
815
816
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(name)

Return either the attribute as type str, or None.

Source code in odfdo/element.py
818
819
820
821
822
823
824
825
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(tag1, tag2, as_text=False, clean=True, no_header=True)

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
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
def get_between(
    self,
    tag1: Element,
    tag2: Element,
    as_text: bool = False,
    clean: bool = True,
    no_header: bool = True,
) -> list | Element | 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(position=0, name=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
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
def get_bookmark(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | 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
    )

get_bookmark_end(position=0, name=None)

Return the bookmark end that matches the criteria.

Arguments:

position -- int

name -- str

Return: Element or None if not found

Source code in odfdo/element.py
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
def get_bookmark_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | None:
    """Return the bookmark end that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:bookmark-end", position, text_name=name
    )

get_bookmark_ends()

Return all the bookmark ends.

Return: list of Element

Source code in odfdo/element.py
2743
2744
2745
2746
2747
2748
def get_bookmark_ends(self) -> list[Element]:
    """Return all the bookmark ends.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:bookmark-end")

get_bookmark_start(position=0, name=None)

Return the bookmark start that matches the criteria.

Arguments:

position -- int

name -- str

Return: Element or None if not found

Source code in odfdo/element.py
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
def get_bookmark_start(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | None:
    """Return the bookmark start that matches the criteria.

    Arguments:

        position -- int

        name -- str

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:bookmark-start", position, text_name=name
    )

get_bookmark_starts()

Return all the bookmark starts.

Return: list of Element

Source code in odfdo/element.py
2717
2718
2719
2720
2721
2722
def get_bookmark_starts(self) -> list[Element]:
    """Return all the bookmark starts.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:bookmark-start")

get_bookmarks()

Return all the bookmarks.

Return: list of Element

Source code in odfdo/element.py
2691
2692
2693
2694
2695
2696
def get_bookmarks(self) -> list[Element]:
    """Return all the bookmarks.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:bookmark")

get_changes_ids()

Return a list of ids that refers to a change region in the tracked changes list.

Source code in odfdo/element.py
3176
3177
3178
3179
3180
3181
3182
3183
3184
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(position=0, id=None, content=None)

Return the draw connector that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: odf_shape or None if not found

Source code in odfdo/element.py
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
def get_draw_connector(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> Element | None:
    """Return the draw connector that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: odf_shape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:connector", position, draw_id=id, content=content
    )

get_draw_connectors(draw_style=None, draw_text_style=None, content=None)

Return all the draw connectors that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of odf_shape

Source code in odfdo/element.py
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
def get_draw_connectors(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the draw connectors that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of odf_shape
    """
    return self._filtered_elements(
        "descendant::draw:connector",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )

get_draw_ellipse(position=0, id=None, content=None)

Return the draw ellipse that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: odf_shape or None if not found

Source code in odfdo/element.py
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
def get_draw_ellipse(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> Element | None:
    """Return the draw ellipse that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: odf_shape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:ellipse", position, draw_id=id, content=content
    )

get_draw_ellipses(draw_style=None, draw_text_style=None, content=None)

Return all the draw ellipses that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of odf_shape

Source code in odfdo/element.py
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
def get_draw_ellipses(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the draw ellipses that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of odf_shape
    """
    return self._filtered_elements(
        "descendant::draw:ellipse",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )

get_draw_line(position=0, id=None, content=None)

Return the draw line that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: odf_shape or None if not found

Source code in odfdo/element.py
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
def get_draw_line(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> Element | None:
    """Return the draw line that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: odf_shape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:line", position, draw_id=id, content=content
    )

get_draw_lines(draw_style=None, draw_text_style=None, content=None)

Return all the draw lines that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of odf_shape

Source code in odfdo/element.py
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
def get_draw_lines(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the draw lines that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of odf_shape
    """
    return self._filtered_elements(
        "descendant::draw:line",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )

get_draw_page(position=0, name=None, content=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
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
def get_draw_page(
    self,
    position: int = 0,
    name: str | None = None,
    content: str | None = None,
) -> Element | 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
    )

get_draw_pages(style=None, content=None)

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
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
def get_draw_pages(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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
    )

get_draw_rectangle(position=0, id=None, content=None)

Return the draw rectangle that matches the criteria.

Arguments:

position -- int

id -- str

content -- str regex

Return: odf_shape or None if not found

Source code in odfdo/element.py
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
def get_draw_rectangle(
    self,
    position: int = 0,
    id: str | None = None,  # noqa:A002
    content: str | None = None,
) -> Element | None:
    """Return the draw rectangle that matches the criteria.

    Arguments:

        position -- int

        id -- str

        content -- str regex

    Return: odf_shape or None if not found
    """
    return self._filtered_element(
        "descendant::draw:rect", position, draw_id=id, content=content
    )

get_draw_rectangles(draw_style=None, draw_text_style=None, content=None)

Return all the draw rectangles that match the criteria.

Arguments:

draw_style -- str

draw_text_style -- str

content -- str regex

Return: list of odf_shape

Source code in odfdo/element.py
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
def get_draw_rectangles(
    self,
    draw_style: str | None = None,
    draw_text_style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the draw rectangles that match the criteria.

    Arguments:

        draw_style -- str

        draw_text_style -- str

        content -- str regex

    Return: list of odf_shape
    """
    return self._filtered_elements(
        "descendant::draw:rect",
        draw_style=draw_style,
        draw_text_style=draw_text_style,
        content=content,
    )

get_formatted_text(context=None)

This function should return a beautiful version of the text.

Source code in odfdo/element.py
1703
1704
1705
def get_formatted_text(self, context: dict | None = None) -> str:
    """This function should return a beautiful version of the text."""
    return ""

get_frame(position=0, name=None, presentation_class=None, title=None, description=None, content=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
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
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,
) -> Element | 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,
    )

get_frames(presentation_class=None, style=None, title=None, description=None, content=None)

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
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
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[Element]:
    """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,
    )

get_header(position=0, outline_level=None, content=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
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
def get_header(
    self,
    position: int = 0,
    outline_level: str | None = None,
    content: str | None = None,
) -> Element | 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,
    )

get_headers(style=None, outline_level=None, content=None)

Return all the Headers that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Header

Source code in odfdo/element.py
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
def get_headers(
    self,
    style: str | None = None,
    outline_level: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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,
    )

get_image(position=0, name=None, url=None, content=None)

Return the image matching the criteria.

Arguments:

position -- int

name -- str

url -- str regex

content -- str regex

Return: Element or None if not found

Source code in odfdo/element.py
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
def get_image(
    self,
    position: int = 0,
    name: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Element | None:
    """Return the image matching the criteria.

    Arguments:

        position -- int

        name -- str

        url -- str regex

        content -- str regex

    Return: Element 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")
    return self._filtered_element(
        "descendant::draw:image", position, url=url, content=content
    )

get_images(style=None, url=None, content=None)

Return all the images matching the criteria.

Arguments:

style -- str

url -- str regex

content -- str regex

Return: list of Element

Source code in odfdo/element.py
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
def get_images(
    self,
    style: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the images matching the criteria.

    Arguments:

        style -- str

        url -- str regex

        content -- str regex

    Return: list of Element
    """
    return self._filtered_elements(
        "descendant::draw:image", text_style=style, url=url, content=content
    )

Return the link that matches the criteria.

Arguments:

position -- int

name -- str

title -- str

url -- str regex

content -- str regex

Return: Element or None if not found

Source code in odfdo/element.py
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
def get_link(
    self,
    position: int = 0,
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> Element | None:
    """Return the link that matches the criteria.

    Arguments:

        position -- int

        name -- str

        title -- str

        url -- str regex

        content -- str regex

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:a",
        position,
        office_name=name,
        office_title=title,
        url=url,
        content=content,
    )

Return all the links that match the criteria.

Arguments:

name -- str

title -- str

url -- str regex

content -- str regex

Return: list of Element

Source code in odfdo/element.py
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
def get_links(
    self,
    name: str | None = None,
    title: str | None = None,
    url: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the links that match the criteria.

    Arguments:

        name -- str

        title -- str

        url -- str regex

        content -- str regex

    Return: list of Element
    """
    return self._filtered_elements(
        "descendant::text:a",
        office_name=name,
        office_title=title,
        url=url,
        content=content,
    )

get_list(position=0, content=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
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
def get_list(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | 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
    )

get_lists(style=None, content=None)

Return all the lists that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of List

Source code in odfdo/element.py
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
def get_lists(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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
    )

get_named_range(name)

Return the named range of specified name, or None if not found.

Arguments:

name -- str

Return: NamedRange

Source code in odfdo/element.py
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
def get_named_range(self, name: str) -> Element | 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]
    else:
        return None

get_named_ranges()

Return all the tables named ranges.

Return: list of odf_named_range

Source code in odfdo/element.py
2157
2158
2159
2160
2161
2162
2163
2164
2165
def get_named_ranges(self) -> list[Element]:
    """Return all the tables named ranges.

    Return: list of odf_named_range
    """
    named_ranges = self.get_elements(
        "descendant::table:named-expressions/table:named-range"
    )
    return named_ranges

get_note(position=0, note_id=None, note_class=None, content=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
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
def get_note(
    self,
    position: int = 0,
    note_id: str | None = None,
    note_class: str | None = None,
    content: str | None = None,
) -> Element | 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,
    )

get_notes(note_class=None, content=None)

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
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
def get_notes(
    self,
    note_class: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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
    )

get_office_names()

Return all the used office:name tags values of the element.

Return: list of unique str

Source code in odfdo/element.py
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
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()

Return a list of connectors which don’t have any shape connected to them.

Source code in odfdo/element.py
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
def get_orphan_draw_connectors(self) -> list[Element]:
    """Return a list of connectors which don't have any shape connected
    to them.
    """
    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(position=0, content=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
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
def get_paragraph(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | 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)

get_paragraphs(style=None, content=None)

Return all the paragraphs that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Paragraph

Source code in odfdo/element.py
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
def get_paragraphs(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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
    )

get_reference_mark(position=0, name=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: Element or None if not found

Source code in odfdo/element.py
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
def get_reference_mark(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | 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: Element 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)
    request = (
        "descendant::text:reference-mark-start | descendant::text:reference-mark"
    )
    return self._filtered_element(request, position)

get_reference_mark_end(position=0, name=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: Element or None if not found

Source code in odfdo/element.py
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
def get_reference_mark_end(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark-end", position, text_name=name
    )

get_reference_mark_ends()

Return all the reference mark ends. Search only the tags text:reference-mark-end. Consider using : get_reference_marks()

Return: list of Element

Source code in odfdo/element.py
2831
2832
2833
2834
2835
2836
2837
2838
def get_reference_mark_ends(self) -> list[Element]:
    """Return all the reference mark ends. Search only the tags
    text:reference-mark-end.
    Consider using : get_reference_marks()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:reference-mark-end")

get_reference_mark_single(position=0, name=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: Element or None if not found

Source code in odfdo/element.py
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
def get_reference_mark_single(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark", position, text_name=name
    )

get_reference_mark_start(position=0, name=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: Element or None if not found

Source code in odfdo/element.py
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
def get_reference_mark_start(
    self,
    position: int = 0,
    name: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:reference-mark-start", position, text_name=name
    )

get_reference_mark_starts()

Return all the reference mark starts. Search only the tags text:reference-mark-start. Consider using : get_reference_marks()

Return: list of Element

Source code in odfdo/element.py
2801
2802
2803
2804
2805
2806
2807
2808
def get_reference_mark_starts(self) -> list[Element]:
    """Return all the reference mark starts. Search only the tags
    text:reference-mark-start.
    Consider using : get_reference_marks()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:reference-mark-start")

get_reference_marks()

Return all the reference marks, either single position reference (text:reference-mark) or start of range reference (text:reference-mark-start).

Return: list of Element

Source code in odfdo/element.py
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
def get_reference_marks(self) -> list[Element]:
    """Return all the reference marks, either single position reference
    (text:reference-mark) or start of range reference
    (text:reference-mark-start).

    Return: list of Element
    """
    return self._filtered_elements(
        "descendant::text:reference-mark-start | descendant::text:reference-mark"
    )

get_reference_marks_single()

Return all the reference marks. Search only the tags text:reference-mark. Consider using : get_reference_marks()

Return: list of Element

Source code in odfdo/element.py
2771
2772
2773
2774
2775
2776
2777
2778
def get_reference_marks_single(self) -> list[Element]:
    """Return all the reference marks. Search only the tags
    text:reference-mark.
    Consider using : get_reference_marks()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:reference-mark")

get_references(name=None)

Return all the references (text:reference-ref). If name is provided, returns the references of that name.

Return: list of Element

Arguments:

name -- str or None
Source code in odfdo/element.py
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
def get_references(self, name: str | None = None) -> list[Element]:
    """Return all the references (text:reference-ref). If name is
    provided, returns the references of that name.

    Return: list of Element

    Arguments:

        name -- str or None
    """
    if name is None:
        return self._filtered_elements("descendant::text:reference-ref")
    request = f'descendant::text:reference-ref[@text:ref-name="{name}"]'
    return self._filtered_elements(request)

get_section(position=0, content=None)

Return the section that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: Element or None if not found

Source code in odfdo/element.py
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
def get_section(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | None:
    """Return the section that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:section", position, content=content
    )

get_sections(style=None, content=None)

Return all the sections that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Element

Source code in odfdo/element.py
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
def get_sections(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """Return all the sections that match the criteria.

    Arguments:

        style -- str

        content -- str regex

    Return: list of Element
    """
    return self._filtered_elements(
        "text:section", text_style=style, content=content
    )

get_span(position=0, content=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
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
def get_span(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | 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
    )

get_spans(style=None, content=None)

Return all the spans that match the criteria.

Arguments:

style -- str

content -- str regex

Return: list of Span

Source code in odfdo/element.py
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
def get_spans(
    self,
    style: str | None = None,
    content: str | None = None,
) -> list[Element]:
    """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
    )

get_style(family, name_or_element=None, display_name=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: odf_style or None if not found

Source code in odfdo/element.py
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
def get_style(
    self,
    family: str,
    name_or_element: str | Element | None = None,
    display_name: str | None = None,
) -> Element | 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: odf_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
        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,
        )
    else:
        return self._filtered_element(
            tagname,
            0,
            draw_name=style_name or display_name,
            family=family,
        )

get_styled_elements(name='')

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/element.py
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
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
    """
    # 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_text_change(position=0, idx=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
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
def get_text_change(
    self,
    position: int = 0,
    idx: str | None = None,
) -> Element | 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)
    request = "descendant::text:change-start | descendant::text:change"
    return self._filtered_element(request, position)

get_text_change_deletion(position=0, idx=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: Element or None if not found

Source code in odfdo/element.py
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
def get_text_change_deletion(
    self,
    position: int = 0,
    idx: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:change", position, change_id=idx
    )

get_text_change_deletions()

Return all the text changes of deletion kind: the tags text:change. Consider using : get_text_changes()

Return: list of Element

Source code in odfdo/element.py
3186
3187
3188
3189
3190
3191
3192
def get_text_change_deletions(self) -> list[Element]:
    """Return all the text changes of deletion kind: the tags text:change.
    Consider using : get_text_changes()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:text:change")

get_text_change_end(position=0, idx=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: Element or None if not found

Source code in odfdo/element.py
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
def get_text_change_end(
    self,
    position: int = 0,
    idx: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:change-end", position, change_id=idx
    )

get_text_change_ends()

Return all the text change-end. Search only the tags text:change-end. Consider using : get_text_changes()

Return: list of Element

Source code in odfdo/element.py
3245
3246
3247
3248
3249
3250
3251
3252
def get_text_change_ends(self) -> list[Element]:
    """Return all the text change-end. Search only the tags
    text:change-end.
    Consider using : get_text_changes()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:change-end")

get_text_change_start(position=0, idx=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: Element or None if not found

Source code in odfdo/element.py
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
def get_text_change_start(
    self,
    position: int = 0,
    idx: str | None = None,
) -> Element | 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: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:change-start", position, change_id=idx
    )

get_text_change_starts()

Return all the text change-start. Search only for the tags text:change-start. Consider using : get_text_changes()

Return: list of Element

Source code in odfdo/element.py
3215
3216
3217
3218
3219
3220
3221
3222
def get_text_change_starts(self) -> list[Element]:
    """Return all the text change-start. Search only for the tags
    text:change-start.
    Consider using : get_text_changes()

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:change-start")

get_text_changes()

Return all the text changes, either single deletion (text:change) or start of range of changes (text:change-start).

Return: list of Element

Source code in odfdo/element.py
3275
3276
3277
3278
3279
3280
3281
3282
def get_text_changes(self) -> list[Element]:
    """Return all the text changes, either single deletion
    (text:change) or start of range of changes (text:change-start).

    Return: list of Element
    """
    request = "descendant::text:change-start | descendant::text:change"
    return self._filtered_elements(request)

get_toc(position=0, content=None)

Return the table of contents that matches the criteria.

Arguments:

position -- int

content -- str regex

Return: odf_toc or None if not found

Source code in odfdo/element.py
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
def get_toc(
    self,
    position: int = 0,
    content: str | None = None,
) -> Element | None:
    """Return the table of contents that matches the criteria.

    Arguments:

        position -- int

        content -- str regex

    Return: odf_toc or None if not found
    """
    return self._filtered_element(
        "text:table-of-content", position, content=content
    )

get_tocs()

Return all the tables of contents.

Return: list of odf_toc

Source code in odfdo/element.py
3323
3324
3325
3326
3327
3328
def get_tocs(self) -> list[Element]:
    """Return all the tables of contents.

    Return: list of odf_toc
    """
    return self.get_elements("text:table-of-content")

get_tracked_changes()

Return the tracked-changes part in the text body.

Return: Element or None

Source code in odfdo/element.py
3161
3162
3163
3164
3165
3166
def get_tracked_changes(self) -> Element | None:
    """Return the tracked-changes part in the text body.

    Return: Element or None
    """
    return self.get_element("//text:tracked-changes")

get_user_defined(name, position=0)

return the user defined declaration for the given name.

return: Element or none if not found

Source code in odfdo/element.py
2554
2555
2556
2557
2558
2559
2560
2561
def get_user_defined(self, name: str, position: int = 0) -> Element | None:
    """return the user defined declaration for the given name.

    return: Element or none if not found
    """
    return self._filtered_element(
        "descendant::text:user-defined", position, text_name=name
    )

get_user_defined_list()

Return all the user defined field declarations.

Return: list of Element

Source code in odfdo/element.py
2539
2540
2541
2542
2543
2544
def get_user_defined_list(self) -> list[Element]:
    """Return all the user defined field declarations.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:user-defined")

get_user_defined_value(name, value_type=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
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
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

get_user_field_decl(name, position=0)

return the user field declaration for the given name.

return: Element or none if not found

Source code in odfdo/element.py
2508
2509
2510
2511
2512
2513
2514
2515
def get_user_field_decl(self, name: str, position: int = 0) -> Element | 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
    )

get_user_field_decl_list()

Return all the user field declarations.

Return: list of Element

Source code in odfdo/element.py
2501
2502
2503
2504
2505
2506
def get_user_field_decl_list(self) -> list[Element]:
    """Return all the user field declarations.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:user-field-decl")

get_user_field_decls()

Return the container for user field declarations. Created if not found.

Return: Element

Source code in odfdo/element.py
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
def get_user_field_decls(self) -> Element | None:
    """Return the container for user field declarations. Created if not
    found.

    Return: Element
    """
    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

get_user_field_value(name, value_type=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
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
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
    return user_field_decl.get_value(value_type)  # type: ignore

get_variable_decl(name, position=0)

return the variable declaration for the given name.

Arguments:

name -- str

position -- int

return: Element or none if not found

Source code in odfdo/element.py
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
def get_variable_decl(self, name: str, position: int = 0) -> Element | None:
    """return the variable declaration for the given name.

    Arguments:

        name -- str

        position -- int

    return: Element or none if not found
    """
    return self._filtered_element(
        "descendant::text:variable-decl", position, text_name=name
    )

get_variable_decl_list()

Return all the variable declarations.

Return: list of Element

Source code in odfdo/element.py
2414
2415
2416
2417
2418
2419
def get_variable_decl_list(self) -> list[Element]:
    """Return all the variable declarations.

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:variable-decl")

get_variable_decls()

Return the container for variable declarations. Created if not found.

Return: Element

Source code in odfdo/element.py
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
def get_variable_decls(self) -> Element:
    """Return the container for variable declarations. Created if not
    found.

    Return: Element
    """
    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

get_variable_set(name, position=-1)

Return the variable set for the given name (last one by default).

Arguments:

name -- str

position -- int

Return: Element or None if not found

Source code in odfdo/element.py
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
def get_variable_set(self, name: str, position: int = -1) -> Element | None:
    """Return the variable set for the given name (last one by default).

    Arguments:

        name -- str

        position -- int

    Return: Element or None if not found
    """
    return self._filtered_element(
        "descendant::text:variable-set", position, text_name=name
    )

get_variable_set_value(name, value_type=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
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
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

get_variable_sets(name=None)

Return all the variable sets that match the criteria.

Arguments:

name -- str

Return: list of Element

Source code in odfdo/element.py
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
def get_variable_sets(self, name: str | None = None) -> list[Element]:
    """Return all the variable sets that match the criteria.

    Arguments:

        name -- str

    Return: list of Element
    """
    return self._filtered_elements("descendant::text:variable-set", text_name=name)

index(child)

Return the position of the child in this element.

Inspired by lxml

Source code in odfdo/element.py
1089
1090
1091
1092
1093
1094
def index(self, child: Element) -> int:
    """Return the position of the child in this element.

    Inspired by lxml
    """
    return self.__element.index(child.__element)

insert(element, xmlposition=None, position=None, start=False)

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
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
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)  # type: ignore
        parent.insert(index + 1, lx_element)  # type: ignore
    elif xmlposition is PREV_SIBLING:
        parent = current.getparent()
        index = parent.index(current)  # type: ignore
        parent.insert(index, lx_element)  # type: ignore
    else:
        raise ValueError("(xml)position must be defined")

is_empty()

Check if the element is empty : no text, no children, no tail.

Return: Boolean

Source code in odfdo/element.py
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
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

match(pattern)

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
976
977
978
979
980
981
982
983
984
985
986
987
988
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(pattern, new=None, formatted=False)

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
 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
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  # type: ignore
            else:
                container.tail = new_text  # type: ignore
            if formatted and container.tag in {  # type; ignore
                "text:h",
                "text:p",
                "text:span",
            }:
                container.append_plain_text("")  # type; ignore
            count += number
    return count

replace_element(old_element, new_element)

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
1516
1517
1518
1519
1520
1521
1522
1523
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(pattern)

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
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
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(pattern)

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
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
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(pattern)

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
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
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(pretty=False, with_ns=False)

Return text serialization of XML element.

Source code in odfdo/element.py
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
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 = tostring(
        native, with_tail=False, pretty_print=pretty, encoding="unicode"
    )
    if with_ns:
        return data
    # Remove namespaces
    return self._strip_namespaces(data)

set_style_attribute(name, value)

Shortcut to accept a style object as a value.

Source code in odfdo/element.py
845
846
847
848
849
def set_style_attribute(self, name: str, value: Element | str) -> 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(sub_elements)

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
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
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=None, protect=None, default='text:p')

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
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
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(start, end=None)

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
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
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_query)

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
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
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:  # type: ignore
            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

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
class ElementTyped(Element):
    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)
        elif value is not None:
            raise TypeError(f"Type unknown: '{value!r}'")

        if value_type is not None:
            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(
        self,
        value_type: str | None = None,
        try_get_text: bool = True,
    ) -> tuple[Any, str | None]:
        """Return Python typed value.

        Only for "with office:value-type" elements, not for meta fields."""
        value: Decimal | str | bool | None = None
        if value_type is None:
            read_value_type = self.get_attribute("office:value-type")
            if isinstance(read_value_type, bool):
                raise TypeError(
                    f'Wrong type for "office:value-type": {type(read_value_type)}'
                )
            value_type = read_value_type
        # value_type = to_str(value_type)
        if value_type == "boolean":
            value = self.get_attribute("office:boolean-value")
            return (value, value_type)
        if value_type in {"float", "percentage", "currency"}:
            read_number = self.get_attribute("office:value")
            if not isinstance(read_number, (Decimal, str)):
                raise TypeError(f'Wrong type for "office:value": {type(read_number)}')
            value = Decimal(read_number)
            # Return 3 instead of 3.0 if possible
            with contextlib.suppress(ValueError):
                if int(value) == value:
                    return (int(value), value_type)
            return (value, value_type)
        if value_type == "date":
            read_attribute = self.get_attribute("office:date-value")
            if not isinstance(read_attribute, str):
                raise TypeError(
                    f'Wrong type for "office:date-value": {type(read_attribute)}'
                )
            if "T" in read_attribute:
                return (DateTime.decode(read_attribute), value_type)
            return (Date.decode(read_attribute), value_type)
        if value_type == "string":
            value = self.get_attribute("office:string-value")
            if value is not None:
                return (str(value), value_type)
            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), value_type)
            return (None, value_type)
        if value_type == "time":
            read_value = self.get_attribute("office:time-value")
            if not isinstance(read_value, str):
                raise TypeError(
                    f'Wrong type for "office:time-value": {type(read_value)}'
                )
            time_value = Duration.decode(read_value)
            return (time_value, value_type)
        if value_type is None:
            return (None, None)
        raise ValueError(f'Unexpected value type: "{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."""
        if get_type:
            return self._get_typed_value(
                value_type=value_type,
                try_get_text=try_get_text,
            )
        return self._get_typed_value(
            value_type=value_type,
            try_get_text=try_get_text,
        )[0]

get_value(value_type=None, try_get_text=True, get_type=False)

Return Python typed value.

Only for “with office:value-type” elements, not for meta fields.

Source code in odfdo/element_typed.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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."""
    if get_type:
        return self._get_typed_value(
            value_type=value_type,
            try_get_text=try_get_text,
        )
    return self._get_typed_value(
        value_type=value_type,
        try_get_text=try_get_text,
    )[0]

EllipseShape

Bases: ShapeBase

Create a ellipse shape.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

position -- (str, str)

size -- (str, str)
Source code in odfdo/shapes.py
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
class EllipseShape(ShapeBase):
    """Create a ellipse shape.

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        position -- (str, str)

        size -- (str, str)

    """

    _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:
        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. You should consider calling Frame.image_frame() or Frame.text_frame directly.

Source code in odfdo/frame.py
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
class Frame(MDDrawFrame, Element, AnchorMix, PosMix, ZMix, SizeMix):
    """ODF Frame "draw:frame"

    Frames are not useful by themselves. You should 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:
        """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_or_element: Element | str) -> 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_or_element, Element):
            text_box.clear()
            text_box.append(text_or_element)
        else:
            text_box.text_content = text_or_element

    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)

__init__(name=None, draw_id=None, style=None, position=None, size=('1cm', '1cm'), z_index=0, presentation_class=None, anchor_type=None, anchor_page=None, layer=None, presentation_style=None, **kwargs)

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
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
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:
    """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

image_frame(image, text=None, name=None, draw_id=None, style=None, position=None, size=('1cm', '1cm'), z_index=0, presentation_class=None, anchor_type=None, anchor_page=None, layer=None, presentation_style=None, **kwargs) classmethod

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
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
@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

text_frame(text_or_element, text_style=None, name=None, draw_id=None, style=None, position=None, size=('1cm', '1cm'), z_index=0, presentation_class=None, anchor_type=None, anchor_page=None, layer=None, presentation_style=None, **kwargs) classmethod

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
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
@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

Specialised paragraph for headings “text:h”.

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
class Header(Paragraph, MDHeader):
    """Specialised paragraph for headings "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:
        """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)  # type:ignore
                else:
                    self.text = self._unformatted(text)  # type:ignore
            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)

__init__(level=1, text=None, restart_numbering=False, start_value=None, suppress_numbering=False, style=None, formatted=True, **kwargs)

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
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:
    """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)  # type:ignore
            else:
                self.text = self._unformatted(text)  # type:ignore
        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

Image

Bases: Body

Image, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
119
120
121
122
123
124
125
class Image(Body):
    """Image, specialized class of Element in charge of actual content
    management.
    """

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

IndexTitle

Bases: Element

The “text:index-title” element contains the title of an index.

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
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 IndexTitle(Element):
    """The "text:index-title" element contains the title of an index.

    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:
        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)

IndexTitleTemplate

Bases: Element

ODF “text:index-title-template”

Arguments:

style -- str
Source code in odfdo/toc.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
class IndexTitleTemplate(Element):
    """ODF "text:index-title-template"

    Arguments:

        style -- str
    """

    _tag = "text:index-title-template"
    _properties = (PropDef("style", "text:style-name"),)

    def __init__(self, style: str | None = None, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        if self._do_init and style:
            self.style = style

LineBreak

Bases: MDLineBreak, Element

This element represents a line break “text:line-break”

Source code in odfdo/paragraph_base.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
class LineBreak(MDLineBreak, Element):
    """This element represents a line break "text:line-break" """

    _tag = "text:line-break"

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    def __str__(self) -> str:
        return "\n"

    @property
    def text(self) -> str:
        return "\n"

LineShape

Bases: ShapeBase

Create a line shape.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

p1 -- (str, str)

p2 -- (str, str)
Source code in odfdo/shapes.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
135
136
137
138
139
class LineShape(ShapeBase):
    """Create a line shape.

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        p1 -- (str, str)

        p2 -- (str, str)
    """

    _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:
        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, ParagraphBase

Link class, “text:a” ODF element.

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
class Link(MDLink, ParagraphBase):
    """Link class, "text:a" ODF element."""

    _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:
        """
        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})"

__init__(url='', name=None, title=None, text=None, target_frame=None, style=None, visited_style=None, **kwargs)

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
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:
    """
    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

ODF List “text:list”.

Source code in odfdo/list.py
 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
class List(MDList, Element):
    """ODF List "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:
        """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 = {}
        rst_mode = context["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
                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)

__init__(list_content=None, style=None, **kwargs)

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
 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
def __init__(
    self,
    list_content: str | Element | Iterable[str | Element] | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """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

get_item(position=0, content=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
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
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(content=None)

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
108
109
110
111
112
113
114
115
116
117
118
119
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)

ListItem

Bases: MDListItem, Element

ODF element “text:list-item”, item of a List.

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
class ListItem(MDListItem, Element):
    """ODF element "text:list-item", item of a List."""

    _tag = "text:list-item"

    def __init__(
        self,
        text_or_element: str | Element | None = None,
        **kwargs: Any,
    ) -> None:
        """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("Expected str or Element")

    def __str__(self) -> str:
        self._md_initialize_level()
        return "\n".join(self._md_collect())

__init__(text_or_element=None, **kwargs)

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
def __init__(
    self,
    text_or_element: str | Element | None = None,
    **kwargs: Any,
) -> None:
    """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("Expected str or Element")

Manifest

Bases: XmlPart

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
class Manifest(XmlPart):
    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):
                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)

get_media_type(full_path)

Get the media type of an existing path.

Return: str

Source code in odfdo/manifest.py
66
67
68
69
70
71
72
73
74
75
76
77
78
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()

Return the list of (full_path, media_type) pairs in the manifest.

Return: list of str tuples

Source code in odfdo/manifest.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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):
            continue
        result.append(
            (
                file_entry.get_attribute_string("manifest:full-path"),
                file_entry.get_attribute_string("manifest:media-type"),
            )
        )
    return result

get_paths()

Return the list of full paths in the manifest.

Return: list of str

Source code in odfdo/manifest.py
31
32
33
34
35
36
37
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)

set_media_type(full_path, media_type)

Set the media type of an existing path.

Arguments:

full_path -- str

media_type -- str
Source code in odfdo/manifest.py
80
81
82
83
84
85
86
87
88
89
90
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

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
class Meta(XmlPart, DcCreatorMixin, DcDateMixin):
    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

Get the MetaAutoReload element or None.

creation_date property writable

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

Get or set the description of a document .

Return: str (or None if inexistant)

editing_cycles property writable

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

Get or set the total time spent editing a document .

Return: timedelta (or None if inexistant)

generator property writable

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 the MetaHyperlinkBehaviour element or None.

initial_creator property writable

Get or set the initial creator of a document .

Return: str (or None if inexistant)

keyword property writable

Get or set some keyword(s) keyword pertaining to a document .

Return: str (or None if inexistant)

language property writable

Get or set the default language of the document .

Return: str (or None if inexistant)

print_date property writable

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

Get or set the name of the last person who printed a document.

Return: str (or None if inexistant)

statistic property writable

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

Get or set the subject of a document .

Return: str (or None if inexistant)

template property

Get the MetaTemplate element or None.

title property writable

Get or set the title of the document .

Return: str (or None if inexistant)

user_defined_metadata property writable

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.

as_dict(full=False)

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
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
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(full=False)

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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
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()

Return meta informations as text, with some formatting for printing.

Source code in odfdo/meta.py
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
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()

Remove all user-defined metadata.

Source code in odfdo/meta.py
792
793
794
795
796
797
798
799
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(data)

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
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
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 the MetaAutoReload element or None.

Source code in odfdo/meta.py
354
355
356
357
358
359
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 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
257
258
259
260
261
262
263
264
265
266
267
268
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 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
100
101
102
103
104
105
106
107
108
109
110
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 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
557
558
559
560
561
562
563
564
565
566
567
568
569
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 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
513
514
515
516
517
518
519
520
521
522
523
524
525
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 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
633
634
635
636
637
638
639
640
641
642
643
644
645
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 the MetaHyperlinkBehaviour element or None.

Source code in odfdo/meta.py
374
375
376
377
378
379
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 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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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 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
469
470
471
472
473
474
475
476
477
478
479
480
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 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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_statistic()

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
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
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 the subject of the document.

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

Return: str (or None if inexistant)

Source code in odfdo/meta.py
144
145
146
147
148
149
150
151
152
153
154
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 the MetaTemplate element or None.

Source code in odfdo/meta.py
329
330
331
332
333
334
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 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
57
58
59
60
61
62
63
64
65
66
67
68
69
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 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
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
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(keyname)

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
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
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(delay, href='')

Set the MetaAutoReload element.

Source code in odfdo/meta.py
366
367
368
369
370
371
372
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(date=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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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(description)

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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(cycles)

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
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
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(duration)

Set the time the document was edited.

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

Arguments:

duration -- timedelta
Source code in odfdo/meta.py
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
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(generator)

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
647
648
649
650
651
652
653
654
655
656
657
658
659
660
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 the signature of the software that generated this document to ourself.

Example::

>>> document.meta.set_generator_default()
Source code in odfdo/meta.py
662
663
664
665
666
667
668
669
670
671
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 the MetaHyperlinkBehaviour element.

Source code in odfdo/meta.py
386
387
388
389
390
391
392
393
394
395
396
397
398
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(creator)

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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
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(keywords)

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
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
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(language)

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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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(statistic)

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
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
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(subject)

Set the subject of the document.

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

Arguments:

subject -- str
Source code in odfdo/meta.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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(date=None, href='', title='')

Set the MetaTemplate element.

Source code in odfdo/meta.py
341
342
343
344
345
346
347
348
349
350
351
352
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(title)

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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

MetaAutoReload

Bases: Element

Source code in odfdo/meta_auto_reload.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
class MetaAutoReload(Element):
    _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:
        """
        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,
        }

__init__(delay=None, href='', **kwargs)

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
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
def __init__(
    self,
    delay: timedelta | None = None,
    href: str = "",
    **kwargs: Any,
) -> None:
    """
    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()

Return the MetaAutoReload attributes as a Python dict.

Source code in odfdo/meta_auto_reload.py
79
80
81
82
83
84
85
86
87
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

Source code in odfdo/meta_hyperlink_behaviour.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
class MetaHyperlinkBehaviour(Element):
    _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:
        """
        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,
        }

__init__(target_frame_name='_blank', show='replace', **kwargs)

The 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    target_frame_name: str = "_blank",
    show: str = "replace",
    **kwargs: Any,
) -> None:
    """
    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()

Return the MetaHyperlinkBehaviour attributes as a Python dict.

Source code in odfdo/meta_hyperlink_behaviour.py
68
69
70
71
72
73
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

Source code in odfdo/meta_template.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
class MetaTemplate(Element):
    _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:
        """
        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,
        }

__init__(date=None, href='', title='', **kwargs)

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
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
def __init__(
    self,
    date: datetime | None = None,
    href: str = "",
    title: str = "",
    **kwargs: Any,
) -> None:
    """
    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()

Return the MetaTemplate attributes as a Python dict.

Source code in odfdo/meta_template.py
81
82
83
84
85
86
87
88
89
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

ODF Named Range “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.
Source code in odfdo/table.py
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
class NamedRange(Element):
    """ODF Named Range "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:
        """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 = 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:
            self.table_name = ""
            self.start = None
            self.end = None
            self.crange = None
            self.usage = None
            return
        self.usage = self.get_attribute("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 | list) -> 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}"  # type: ignore

    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}:"  # type: ignore
            f".${digit_to_alpha(self.end[0])}${self.end[1] + 1}"  # type: ignore
        )

    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)  # type: ignore

    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 = body.get_table(name=self.table_name)
        if table is None:
            raise ValueError
        return table.get_value(self.start, get_type)  # type: ignore

    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(  # type: ignore
            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(  # type: ignore
            coord=self.start,
            value=value,
            cell_type=cell_type,
            currency=currency,
            style=style,
        )

name property writable

Get / set the name of the table.

__init__(name=None, crange=None, table_name=None, usage=None, **kwargs)

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/table.py
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
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:
    """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 = 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:
        self.table_name = ""
        self.start = None
        self.end = None
        self.crange = None
        self.usage = None
        return
    self.usage = self.get_attribute("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_type=False)

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/table.py
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
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 = body.get_table(name=self.table_name)
    if table is None:
        raise ValueError
    return table.get_value(self.start, get_type)  # type: ignore

get_values(cell_type=None, complete=True, get_type=False, flat=False)

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/table.py
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
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)  # type: ignore

set_range(crange)

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/table.py
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
def set_range(self, crange: str | tuple | list) -> 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(name)

Set the name of the table of the Named Range. The name is mandatory.

Arguments:

name -- str
Source code in odfdo/table.py
3079
3080
3081
3082
3083
3084
3085
3086
3087
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(usage=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/table.py
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
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(value, cell_type=None, currency=None, style=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/table.py
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
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(  # type: ignore
        coord=self.start,
        value=value,
        cell_type=cell_type,
        currency=currency,
        style=style,
    )

set_values(values, style=None, cell_type=None, currency=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/table.py
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
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(  # type: ignore
        values,
        coord=self.crange,
        style=style,
        cell_type=cell_type,
        currency=currency,
    )

Note

Bases: MDNote, Element

Either a footnote or a endnote element with the given text, optionally referencing it using the given note_id.

Arguments:

note_class -- 'footnote' or 'endnote'

note_id -- str

citation -- str

body -- str or Element
Source code in odfdo/note.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
class Note(MDNote, Element):
    """Either a footnote or a endnote element with the given text,
    optionally referencing it using the given note_id.

    Arguments:

        note_class -- 'footnote' or 'endnote'

        note_id -- str

        citation -- str

        body -- str or Element
    """

    _tag = "text:note"
    _properties = (
        PropDef("note_class", "text:note-class"),
        PropDef("note_id", "text:id"),
    )

    def __init__(
        self,
        note_class: str = "footnote",
        note_id: str | None = None,
        citation: str | None = None,
        body: str | None = None,
        **kwargs: Any,
    ) -> None:
        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  # type:ignore

    @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:
            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

Paragraph

Bases: MDParagraph, ParagraphBase

Specialised element for paragraphs “text:p”. The “text:p” element represents a paragraph, which is the basic unit of text in an OpenDocument file.

Source code in odfdo/paragraph.py
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
class Paragraph(MDParagraph, ParagraphBase):
    """Specialised element for paragraphs "text:p". The "text:p" element
    represents a paragraph, which is the basic unit of text in an OpenDocument
    file.
    """

    _tag = "text:p"

    def __init__(
        self,
        text_or_element: str | bytes | Element | None = None,
        style: str | None = None,
        formatted: bool = True,
        **kwargs: Any,
    ):
        """Create a paragraph element 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, 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"

    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):
            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
        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 = (Span._tag,)
        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

__init__(text_or_element=None, style=None, formatted=True, **kwargs)

Create a paragraph element 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, bytes or Element

style -- str

formatted -- bool
Source code in odfdo/paragraph.py
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
def __init__(
    self,
    text_or_element: str | bytes | Element | None = None,
    style: str | None = None,
    formatted: bool = True,
    **kwargs: Any,
):
    """Create a paragraph element 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, 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

append_plain_text(text='')

Append plain text to the paragraph, replacing , and multiple spaces by ODF corresponding tags.

Source code in odfdo/paragraph.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
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(annotation_element=None, before=None, after=None, position=0, content=None, body=None, creator=None, date=None)

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/paragraph.py
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
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(annotation_element, before=None, after=None, position=0)

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/paragraph.py
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
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_reference(name, ref_format='', before=None, after=None, position=0, display=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/paragraph.py
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
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
        )

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/paragraph.py
784
785
786
787
788
789
790
791
792
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)

Send back a copy of the element, without links tags.

Source code in odfdo/paragraph.py
779
780
781
782
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(spans)

Send back a copy of the element, the spans (not a clone) removed.

Arguments:

spans -- Element or list of Element
Source code in odfdo/paragraph.py
739
740
741
742
743
744
745
746
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(keep_heading=True)

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/paragraph.py
727
728
729
730
731
732
733
734
735
736
737
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 = (Span._tag,)
    if keep_heading:
        protect = ("text:h",)
    else:
        protect = None
    return self.strip_tags(strip=strip, protect=protect)

set_bookmark(name, before=None, after=None, position=0, role=None, content=None)

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/paragraph.py
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
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(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/paragraph.py
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
@_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(name, before=None, after=None, position=0, content=None)

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/paragraph.py
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
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(reference_mark, before=None, after=None, position=0)

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/paragraph.py
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
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(match, tail, style, regex=None, offset=None, length=0)

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/paragraph.py
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
@_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
    return span

Presentation

Bases: Body

Presentation, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
128
129
130
131
132
133
134
class Presentation(Body):
    """Presentation, specialized class of Element in charge of actual content
    management.
    """

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

RectangleShape

Bases: ShapeBase

Create a rectangle shape.

Arguments:

style -- str

text_style -- str

draw_id -- str

layer -- str

position -- (str, str)

size -- (str, str)
Source code in odfdo/shapes.py
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
class RectangleShape(ShapeBase):
    """Create a rectangle shape.

    Arguments:

        style -- str

        text_style -- str

        draw_id -- str

        layer -- str

        position -- (str, str)

        size -- (str, str)

    """

    _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:
        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. 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.
Source code in odfdo/reference.py
 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
class Reference(Element):
    """A reference to a content marked by a reference mark.
    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. 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
        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

__init__(name='', ref_format='', **kwargs)

Create a reference to a content marked by a reference mark. 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
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
def __init__(self, name: str = "", ref_format: str = "", **kwargs: Any) -> None:
    """Create a reference to a content marked by a reference mark. 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 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
    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. A point reference marks a position in text and is represented by a single “text:reference-mark” element.

Source code in odfdo/reference.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class ReferenceMark(Element):
    """A point reference.
    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. 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

__init__(name='', **kwargs)

A point reference. 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
200
201
202
203
204
205
206
207
208
209
210
211
def __init__(self, name: str = "", **kwargs: Any) -> None:
    """A point reference. 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

The “text:reference-mark-end” element represents the end of a range reference.

Source code in odfdo/reference.py
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
class ReferenceMarkEnd(Element):
    """The "text:reference-mark-end" element represents the end of a range
    reference.
    """

    _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

__init__(name='', **kwargs)

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
225
226
227
228
229
230
231
232
233
234
235
236
237
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()

Return the text between reference-mark-start and reference-mark-end.

Source code in odfdo/reference.py
239
240
241
242
243
244
245
246
247
248
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

The “text:reference-mark-start” element represents the start of a range reference.

Source code in odfdo/reference.py
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
class ReferenceMarkStart(Element):
    """The "text:reference-mark-start" element represents the start of a
    range reference.
    """

    _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.
        """
        name = self.name
        parent = self.parent
        if parent is None:
            raise ValueError("Reference need some upper document part")
        body = self.document_body
        if not body:
            body = parent
        end = body.get_reference_mark_end(name=name)
        if end is None:
            raise ValueError("No reference-end found")
        start = self
        return _get_referenced(body, start, end, no_header, clean, as_xml, as_list)

    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)
        name = self.name
        parent = self.parent
        if parent is None:
            raise ValueError("Can't delete the root element")
        body = self.document_body
        if not body:
            body = parent
        end = body.get_reference_mark_end(name=name)
        if end:
            end.delete()
        # act like normal delete
        return super().delete()

__init__(name='', **kwargs)

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
262
263
264
265
266
267
268
269
270
271
272
273
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(child=None, keep_tail=True)

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
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
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)
    name = self.name
    parent = self.parent
    if parent is None:
        raise ValueError("Can't delete the root element")
    body = self.document_body
    if not body:
        body = parent
    end = body.get_reference_mark_end(name=name)
    if end:
        end.delete()
    # act like normal delete
    return super().delete()

get_referenced(no_header=False, clean=True, as_xml=False, as_list=False)

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
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
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.
    """
    name = self.name
    parent = self.parent
    if parent is None:
        raise ValueError("Reference need some upper document part")
    body = self.document_body
    if not body:
        body = parent
    end = body.get_reference_mark_end(name=name)
    if end is None:
        raise ValueError("No reference-end found")
    start = self
    return _get_referenced(body, start, end, no_header, clean, as_xml, as_list)

referenced_text()

Return the text between reference-mark-start and reference-mark-end.

Source code in odfdo/reference.py
275
276
277
278
279
280
281
282
283
284
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: CachedElement

ODF table row “table:table-row”

Source code in odfdo/row.py
 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
class Row(CachedElement):
    """ODF table row "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, 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.y = None
        self._indexes = {}
        self._indexes["_rmap"] = {}
        self._compute_row_cache()
        self._tmap = []
        self._cmap = []
        if self._do_init:
            if width is not None:
                for _i in range(width):
                    self.append(Cell())  # type:ignore
            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_cells(self) -> list[Element]:
        return self.get_elements(_xpath_cell)

    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._rmap = make_cache_map(idx_repeated_seq)

    # Public API

    @property
    def clone(self) -> Row:
        clone = Element.clone.fget(self)  # type: ignore
        clone.y = self.y
        clone._rmap = self._rmap[:]
        clone._tmap = self._tmap[:]
        clone._cmap = self._cmap[:]
        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":
                break
            current = upper
        # fixme : need to optimize this
        if isinstance(upper, Element) and upper._tag == "table:table":
            upper._compute_table_cache()
            if hasattr(self, "_tmap"):
                del self._tmap[:]
                self._tmap.extend(upper._tmap)
            else:
                self._tmap = upper._tmap

    @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
        """
        try:
            value = self._rmap[-1] + 1
        except Exception:
            value = 0
        return value

    def _translate_x_from_any(self, x: str | int) -> int:
        return translate_from_any(x, self.width, 0)

    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.
        """
        idx = -1
        before = -1
        x = 0
        cell: Cell
        if start is None and end is None:
            for juska in self._rmap:
                idx += 1
                if idx in self._indexes["_rmap"]:
                    cell = self._indexes["_rmap"][idx]
                else:
                    cell = self._get_element_idx2(_xpath_cell_idx, idx)  # type: ignore
                    if not isinstance(cell, Cell):
                        raise TypeError(f"Not a cell: {cell!r}")
                    self._indexes["_rmap"][idx] = cell
                repeated = juska - before
                before = juska
                for _i in range(repeated or 1):
                    # Return a copy without the now obsolete repetition
                    if cell is None:
                        cell = Cell()
                    else:
                        cell = cell.clone
                        if repeated > 1:
                            cell.repeated = None
                    cell.y = self.y
                    cell.x = x
                    x += 1
                    yield cell
        else:
            if start is None:
                start = 0
            start = max(0, start)
            if end is None:
                try:
                    end = self._rmap[-1]
                except Exception:
                    end = -1
            start_map = find_odf_idx(self._rmap, start)
            if start_map is None:
                return
            if start_map > 0:
                before = self._rmap[start_map - 1]
            idx = start_map - 1
            before = start - 1
            x = start
            for juska in self._rmap[start_map:]:
                idx += 1
                if idx in self._indexes["_rmap"]:
                    cell = self._indexes["_rmap"][idx]
                else:
                    cell = self._get_element_idx2(_xpath_cell_idx, idx)  # type: ignore
                    if not isinstance(cell, Cell):
                        raise TypeError(f"Not a cell: {cell!r}")
                    self._indexes["_rmap"][idx] = cell
                repeated = juska - before
                before = juska
                for _i in range(repeated or 1):
                    if x <= end:
                        if cell is None:
                            cell = Cell()
                        else:
                            cell = cell.clone
                            if repeated > 1 or (x == start and start > 0):
                                cell.repeated = None
                        cell.y = self.y
                        cell.x = x
                        x += 1
                        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 = find_odf_idx(self._rmap, x)
        cell: Cell
        if idx is not None:
            if idx in self._indexes["_rmap"]:
                cell = self._indexes["_rmap"][idx]
            else:
                cell = self._get_element_idx2(_xpath_cell_idx, idx)  # type: ignore
                self._indexes["_rmap"][idx] = cell
            return cell
        return None

    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
        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
            set_item_in_vault(x, cell, self, _xpath_cell_idx, "_rmap", clone=clone)
            cell.x = x
            cell.y = self.y
            cell_back = cell
        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:
            insert_item_in_vault(x, cell, self, _xpath_cell_idx, "_rmap")
            cell.x = x
            cell.y = self.y
            cell_back = cell
        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._rmap = insert_map_once(self._rmap, len(self._rmap), _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
        delete_item_in_vault(x, self, _xpath_cell_idx, "_rmap")

    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):  # type: ignore
                break
            self.delete(cell)
        self._compute_row_cache()
        self._indexes["_rmap"] = {}

    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._indexes["_rmap"] = {}
        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]  # type: ignore
        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())  # type: ignore

cells property

Get the list of all cells.

Return: list of Cell

repeated property writable

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

Always None when using the table API.

Return: int or None

style property writable

Get /set the style of the row itself.

Return: str

width property

Get the number of expected cells in the row, i.e. addition repetitions.

Return: int

__init__(width=None, repeated=None, style=None, **kwargs)

create a 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
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,
    width: int | None = None,
    repeated: int | None = None,
    style: str | None = None,
    **kwargs: Any,
) -> None:
    """create a 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.y = None
    self._indexes = {}
    self._indexes["_rmap"] = {}
    self._compute_row_cache()
    self._tmap = []
    self._cmap = []
    if self._do_init:
        if width is not None:
            for _i in range(width):
                self.append(Cell())  # type:ignore
        if repeated:
            self.repeated = repeated
        if style is not None:
            self.style = style
        self._compute_row_cache()

append_cell(cell=None, clone=True, _repeated=None)

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
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
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._rmap = insert_map_once(self._rmap, len(self._rmap), _repeated)
    cell.x = self.width - 1
    cell.y = self.y
    return cell

delete_cell(x)

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
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
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
    delete_item_in_vault(x, self, _xpath_cell_idx, "_rmap")

force_width(width)

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
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
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(x, clone=True)

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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
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
    cell.y = self.y
    cell.x = x
    return cell

get_cells(coord=None, style=None, content=None, cell_type=None)

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
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
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_sub_elements()

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
652
653
654
655
656
657
658
659
660
661
662
663
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(x, get_type=False)

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
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
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(coord=None, cell_type=None, complete=False, get_type=False)

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
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
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(x, cell=None, clone=True)

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
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
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:
        insert_item_in_vault(x, cell, self, _xpath_cell_idx, "_rmap")
        cell.x = x
        cell.y = self.y
        cell_back = cell
    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(aggressive=False)

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
827
828
829
830
831
832
833
834
835
836
837
838
839
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())  # type: ignore

last_cell()

Return the las cell of the row.

Return Cell | None

Source code in odfdo/row.py
797
798
799
800
801
802
803
804
805
def last_cell(self) -> Cell | None:
    """Return the las cell of the row.

    Return Cell | None
    """
    try:
        return self._get_cells()[-1]  # type: ignore
    except IndexError:
        return None

minimized_width()

Return the length of the row if the last repeated sequence is reduced to one.

Return: int

Source code in odfdo/row.py
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
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._indexes["_rmap"] = {}
    return min_width

rstrip(aggressive=False)

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
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
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):  # type: ignore
            break
        self.delete(cell)
    self._compute_row_cache()
    self._indexes["_rmap"] = {}

set_cell(x, cell=None, clone=True)

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
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
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
        set_item_in_vault(x, cell, self, _xpath_cell_idx, "_rmap", clone=clone)
        cell.x = x
        cell.y = self.y
        cell_back = cell
    return cell_back

set_cells(cells=None, start=0, clone=True)

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
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
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(x, value, style=None, cell_type=None, currency=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
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
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(values, start=0, style=None, cell_type=None, currency=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
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
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(start=None, end=None)

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
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
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.
    """
    idx = -1
    before = -1
    x = 0
    cell: Cell
    if start is None and end is None:
        for juska in self._rmap:
            idx += 1
            if idx in self._indexes["_rmap"]:
                cell = self._indexes["_rmap"][idx]
            else:
                cell = self._get_element_idx2(_xpath_cell_idx, idx)  # type: ignore
                if not isinstance(cell, Cell):
                    raise TypeError(f"Not a cell: {cell!r}")
                self._indexes["_rmap"][idx] = cell
            repeated = juska - before
            before = juska
            for _i in range(repeated or 1):
                # Return a copy without the now obsolete repetition
                if cell is None:
                    cell = Cell()
                else:
                    cell = cell.clone
                    if repeated > 1:
                        cell.repeated = None
                cell.y = self.y
                cell.x = x
                x += 1
                yield cell
    else:
        if start is None:
            start = 0
        start = max(0, start)
        if end is None:
            try:
                end = self._rmap[-1]
            except Exception:
                end = -1
        start_map = find_odf_idx(self._rmap, start)
        if start_map is None:
            return
        if start_map > 0:
            before = self._rmap[start_map - 1]
        idx = start_map - 1
        before = start - 1
        x = start
        for juska in self._rmap[start_map:]:
            idx += 1
            if idx in self._indexes["_rmap"]:
                cell = self._indexes["_rmap"][idx]
            else:
                cell = self._get_element_idx2(_xpath_cell_idx, idx)  # type: ignore
                if not isinstance(cell, Cell):
                    raise TypeError(f"Not a cell: {cell!r}")
                self._indexes["_rmap"][idx] = cell
            repeated = juska - before
            before = juska
            for _i in range(repeated or 1):
                if x <= end:
                    if cell is None:
                        cell = Cell()
                    else:
                        cell = cell.clone
                        if repeated > 1 or (x == start and start > 0):
                            cell.repeated = None
                    cell.y = self.y
                    cell.x = x
                    x += 1
                    yield cell

RowGroup

Bases: Element

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

Source code in odfdo/table.py
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
class RowGroup(Element):
    """ "table:table-row-group" group rows with common properties."""

    # 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, 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)

__init__(height=None, width=None, **kwargs)

Create a group of rows, 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/table.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def __init__(
    self,
    height: int | None = None,
    width: int | None = None,
    **kwargs: Any,
) -> None:
    """Create a group of rows, 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

ODF section “text:section”

Arguments:

style -- str

name -- str
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
class Section(Element):
    """ODF section "text:section"

    Arguments:

        style -- str

        name -- str
    """

    _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:
        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)

Spacer

Bases: MDSpacer, Element

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.

Source code in odfdo/paragraph_base.py
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
class Spacer(MDSpacer, Element):
    """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):
        """
        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))

text property writable

Get / set the text content of the element.

__init__(number=1, **kwargs)

Arguments:

number -- int
Source code in odfdo/paragraph_base.py
177
178
179
180
181
182
183
184
185
186
187
188
def __init__(self, number: int | None = 1, **kwargs: Any):
    """
    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, Paragraph

Specialised paragraph for span “text:span”.

Source code in odfdo/paragraph.py
 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
class Span(MDSpan, Paragraph):
    """Specialised paragraph for span "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)  # type:ignore
                else:
                    self.text = self._unformatted(text)  # type:ignore
            if style:
                self.style = style

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

__init__(text=None, style=None, formatted=True, **kwargs)

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/paragraph.py
 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
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)  # type:ignore
            else:
                self.text = self._unformatted(text)  # type:ignore
        if style:
            self.style = style

Spreadsheet

Bases: Body

Spreadsheet, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
137
138
139
140
141
142
143
class Spreadsheet(Body):
    """Spreadsheet, specialized class of Element in charge of actual content
    management.
    """

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

Style

Bases: Element

Style class for all these tags:

‘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’, …

Source code in odfdo/style.py
 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
class Style(Element):
    """Style class for all these tags:

    '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'
        width: str | None = None,
        break_before: str | None = None,
        break_after: 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'
        """
        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
            # 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.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

__init__(family=None, name=None, display_name=None, parent_style=None, area=None, color=None, background_color=None, italic=False, bold=False, master_page=None, page_layout=None, next_style=None, data_style=None, border=None, border_top=None, border_right=None, border_bottom=None, border_left=None, padding=None, padding_top=None, padding_bottom=None, padding_left=None, padding_right=None, shadow=None, height=None, use_optimal_height=False, width=None, break_before=None, break_after=None, min_height=None, font_name=None, font_family=None, font_family_generic=None, font_pitch='variable', **kwargs)

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'
Source code in odfdo/style.py
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
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'
    width: str | None = None,
    break_before: str | None = None,
    break_after: 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'
    """
    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
        # 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(properties=None, area=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
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
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_list_style_properties()

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
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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 the element that contains the footer contents.

If None, no footer was set.

Source code in odfdo/style.py
1008
1009
1010
1011
1012
1013
1014
1015
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 the element that contains the header contents.

If None, no header was set.

Source code in odfdo/style.py
981
982
983
984
985
986
987
988
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(area=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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
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 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
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
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(color=None, url=None, position='center', repeat=None, opacity=None, filter=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
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
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_level_style(level, num_format=None, bullet_char=None, url=None, display_levels=None, prefix=None, suffix=None, start_value=None, style=None, clone=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
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
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

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
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
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(text_or_element)

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
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
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(properties=None, style=None, area=None, **kwargs)

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
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
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

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
class Styles(XmlPart):
    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
            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_style(family, name_or_element=None, display_name=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
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
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
        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(family='', automatic=False)

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
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
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

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.

Implementation: 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
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
class TOC(MDToc, Element):
    """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.


    Implementation:
    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
    """

    _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:
        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
        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):
                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 is None or 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

fill(document=None, use_default_styles=True)

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
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
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
    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):
            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 is None or 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

Tab

Bases: MDTab, Element

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

Source code in odfdo/paragraph_base.py
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
class Tab(MDTab, Element):
    """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:
        """
        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"

__init__(position=None, **kwargs)

Arguments:

position -- int
Source code in odfdo/paragraph_base.py
240
241
242
243
244
245
246
247
248
def __init__(self, position: int | None = None, **kwargs: Any) -> None:
    """
    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

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

Source code in odfdo/toc.py
 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
class TabStopStyle(Element):
    """ODF "style:tab-stop"
    Base style for a TOC entryBase style for a TOC entry
    """

    _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,
    ):
        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

Table

Bases: MDTable, CachedElement

ODF table “table:table”

Source code in odfdo/table.py
 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
class Table(MDTable, CachedElement):
    """ODF table "table:table" """

    _tag = "table:table"
    _append = Element.append

    def __init__(
        self,
        name: str | None = None,
        width: int | None = None,
        height: int | 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, 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

            height -- int

            protected -- bool

            protection_key -- str

            printable -- bool

            print_ranges -- list

            style -- str
        """
        super().__init__(**kwargs)
        self._indexes = {}
        self._indexes["_cmap"] = {}
        self._indexes["_tmap"] = {}
        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 = width or 1
                height = 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 _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(
            _xpath_row, "table:number-rows-repeated"
        )
        self._tmap = make_cache_map(idx_repeated_seq)
        idx_repeated_seq = self.elements_repeated_sequence(
            _xpath_column, "table:number-columns-repeated"
        )
        self._cmap = make_cache_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
        """
        try:
            height = self._tmap[-1] + 1
        except Exception:
            height = 0
        return 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

        try:
            width = self._cmap[-1] + 1
        except Exception:
            width = 0

        # columns = self._get_columns()
        # repeated = self.xpath(
        #        'table:table-column/@table:number-columns-repeated')
        # unrepeated = len(columns) - len(repeated)
        # ws = sum(int(r) for r in repeated) + unrepeated
        # if w != ws:
        #    print "WARNING   ws", ws, "w", w

        return 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._indexes["_tmap"] = {}
        # 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._indexes["_cmap"] = {}
        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._indexes["_tmap"] = {}

    def _optimize_width_length(self) -> int:
        return max(row.minimized_width() for row in self._get_rows())

    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._indexes["_cmap"] = {}
        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
    #

    def _get_rows(self) -> list[Row]:
        return self.get_elements(_xpath_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 = find_odf_idx(self._tmap, y)
        if idx is not None:
            if idx in self._indexes["_tmap"]:
                row = self._indexes["_tmap"][idx]
            else:
                row = self._get_element_idx2(_xpath_row_idx, idx)
                self._indexes["_tmap"][idx] = row
            return row
        return None

    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 = set_item_in_vault(  # type: ignore
                y, row, self, _xpath_row_idx, "_tmap", 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 = insert_item_in_vault(y, row, self, _xpath_row_idx, "_tmap")
        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._tmap = insert_map_once(self._tmap, len(self._tmap), _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
        delete_item_in_vault(y, self, _xpath_row_idx, "_tmap")

    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:
        return self.get_elements(_xpath_column)

    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.
        """
        idx = -1
        before = -1
        x = 0
        if start is None and end is None:
            for juska in self._cmap:
                idx += 1
                if idx in self._indexes["_cmap"]:
                    column = self._indexes["_cmap"][idx]
                else:
                    column = self._get_element_idx2(_xpath_column_idx, idx)
                    self._indexes["_cmap"][idx] = column
                repeated = juska - before
                before = juska
                for _i in range(repeated or 1):
                    # Return a copy without the now obsolete repetition
                    column = column.clone
                    column.x = x
                    x += 1
                    if repeated > 1:
                        column.repeated = None
                    yield column
        else:
            if start is None:
                start = 0
            start = max(0, start)
            if end is None:
                try:
                    end = self._cmap[-1]
                except Exception:
                    end = -1
            start_map = find_odf_idx(self._cmap, start)
            if start_map is None:
                return
            if start_map > 0:
                before = self._cmap[start_map - 1]
            idx = start_map - 1
            before = start - 1
            x = start
            for juska in self._cmap[start_map:]:
                idx += 1
                if idx in self._indexes["_cmap"]:
                    column = self._indexes["_cmap"][idx]
                else:
                    column = self._get_element_idx2(_xpath_column_idx, idx)
                    self._indexes["_cmap"][idx] = column
                repeated = juska - before
                before = juska
                for _i in range(repeated or 1):
                    if x <= end:
                        column = column.clone
                        column.x = x
                        x += 1
                        if repeated > 1 or (x == start and start > 0):
                            column.repeated = None
                        yield column

    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()
        # Inside the defined table
        odf_idx = find_odf_idx(self._cmap, x)
        if odf_idx is not None:
            column = self._get_element_idx2(_xpath_column_idx, odf_idx)
            if column is None:
                return None
            # fixme : no clone here => change doc and unit tests
            return column.clone  # type: ignore
            # return row
        return None

    @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 = set_item_in_vault(  # type: ignore
                x, column, self, _xpath_column_idx, "_cmap"
            )
        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 = insert_item_in_vault(
                x, column, self, _xpath_column_idx, "_cmap"
            )
        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
        """
        if column is None:
            column = Column()
        else:
            column = column.clone
        if not self._cmap:
            position = 0
        else:
            odf_idx = len(self._cmap) - 1
            last_column = self._get_element_idx2(_xpath_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._cmap = insert_map_once(self._cmap, len(self._cmap), _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
        delete_item_in_vault(x, self, _xpath_column_idx, "_cmap")
        # 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",
    ) -> Any:
        """Write the table as CSV in the file.

        If the file is a string, it is opened as a local path. Else an
        opened file-like is expected.

        Arguments:

            path_or_file -- str or file-like

            dialect -- str, python csv.dialect, can be 'excel', 'unix'...
        """

        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="")
        csv_writer = csv.writer(out, dialect=dialect)
        write_content(csv_writer)
        if path_or_file is None:
            return out.getvalue()
        path = Path(path_or_file)
        path.write_text(out.getvalue())
        return None

cells property

Get all cells of the table.

Return: list of list of Cell

columns property

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

Get the current height of the table.

Return: int

name property writable

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.

rows property

Get the list of all rows.

Return: list of rows

size property

Shortcut to get the current width and height of the table.

Return: (int, int)

style property writable

Get / set the style of the table

Return: str

width property

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__(name=None, width=None, height=None, protected=False, protection_key=None, printable=True, print_ranges=None, style=None, **kwargs)

Create a table element, 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

height -- int

protected -- bool

protection_key -- str

printable -- bool

print_ranges -- list

style -- str
Source code in odfdo/table.py
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
def __init__(
    self,
    name: str | None = None,
    width: int | None = None,
    height: int | 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, 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

        height -- int

        protected -- bool

        protection_key -- str

        printable -- bool

        print_ranges -- list

        style -- str
    """
    super().__init__(**kwargs)
    self._indexes = {}
    self._indexes["_cmap"] = {}
    self._indexes["_tmap"] = {}
    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 = width or 1
            height = 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(something)

Dispatch .append() call to append_row() or append_column().

Source code in odfdo/table.py
752
753
754
755
756
757
758
759
760
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(y, cell=None, clone=True)

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
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
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(column=None, _repeated=None)

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
Source code in odfdo/table.py
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
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
    """
    if column is None:
        column = Column()
    else:
        column = column.clone
    if not self._cmap:
        position = 0
    else:
        odf_idx = len(self._cmap) - 1
        last_column = self._get_element_idx2(_xpath_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._cmap = insert_map_once(self._cmap, len(self._cmap), _repeated)
    # No need to update row widths
    return column

append_row(row=None, clone=True, _repeated=None)

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
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
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._tmap = insert_map_once(self._tmap, len(self._tmap), _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

del_span(area)

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
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
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(coord)

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
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
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(x)

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
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
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
    delete_item_in_vault(x, self, _xpath_column_idx, "_cmap")
    # Update width
    width = self.width
    for row in self._get_rows():
        if row.width >= width:
            row.delete_cell(x)

delete_named_range(name)

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
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
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(y)

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
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
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
    delete_item_in_vault(y, self, _xpath_row_idx, "_tmap")

extend_rows(rows=None)

Append a list of rows at the end of the table.

Arguments:

rows -- list of Row
Source code in odfdo/table.py
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
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))

get_cell(coord, clone=True, keep_repeated=True)

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
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
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(coord=None, cell_type=None, style=None, content=None, flat=False)

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
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
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(x)

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
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
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(x, style=None, content=None, cell_type=None, complete=False)

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
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
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(x, cell_type=None, complete=True, get_type=False)

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
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
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(coord=None, style=None)

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
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
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_named_range(name)

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
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
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(table_name=None)

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
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
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(y, clone=True, create=True)

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
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
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(y)

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
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
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(y, cell_type=None, complete=True, get_type=False)

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
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
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(coord=None, style=None, content=None)

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
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
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(coord, get_type=False)

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
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
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(coord=None, cell_type=None, complete=True, get_type=False, flat=False)

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
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
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(coord, cell=None, clone=True)

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
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
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(x, column=None)

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
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
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 = insert_item_in_vault(
            x, column, self, _xpath_column_idx, "_cmap"
        )
    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(y, row=None, clone=True)

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
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
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 = insert_item_in_vault(y, row, self, _xpath_row_idx, "_tmap")
    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(x, aggressive=False)

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
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
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(aggressive=False)

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
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
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(y, aggressive=False)

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
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
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(coord=None, cell_type=None, complete=True, get_type=False)

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
 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
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()

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
1120
1121
1122
1123
1124
1125
1126
1127
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(aggressive=False)

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
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
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._indexes["_tmap"] = {}
    # 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._indexes["_cmap"] = {}
    self._compute_table_cache()

set_cell(coord, cell=None, clone=True)

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
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
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(coord, image_frame, doc_type=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
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
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(cells, coord=None, clone=True)

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
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
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(x, column=None)

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
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
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 = set_item_in_vault(  # type: ignore
            x, column, self, _xpath_column_idx, "_cmap"
        )
    return column_back

set_column_cells(x, cells)

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
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
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(x, values, cell_type=None, currency=None, style=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
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
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(name, crange, table_name=None, usage=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
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 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(y, row=None, clone=True)

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
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
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 = set_item_in_vault(  # type: ignore
            y, row, self, _xpath_row_idx, "_tmap", clone=clone
        )
    # print self.serialize(True)
    # Update width if necessary
    self._update_width(row_back)
    return row_back

set_row_cells(y, cells=None)

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
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
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(y, values, cell_type=None, currency=None, style=None)

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
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
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(area, merge=False)

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
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
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(coord, value, cell_type=None, currency=None, style=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
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
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(values, coord=None, style=None, cell_type=None, currency=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
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
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(path_or_file=None, dialect='excel')

Write the table as CSV in the file.

If the file is a string, it is opened as a local path. Else an opened file-like is expected.

Arguments:

path_or_file -- str or file-like

dialect -- str, python csv.dialect, can be 'excel', 'unix'...
Source code in odfdo/table.py
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
def to_csv(
    self,
    path_or_file: str | Path | None = None,
    dialect: str = "excel",
) -> Any:
    """Write the table as CSV in the file.

    If the file is a string, it is opened as a local path. Else an
    opened file-like is expected.

    Arguments:

        path_or_file -- str or file-like

        dialect -- str, python csv.dialect, can be 'excel', 'unix'...
    """

    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="")
    csv_writer = csv.writer(out, dialect=dialect)
    write_content(csv_writer)
    if path_or_file is None:
        return out.getvalue()
    path = Path(path_or_file)
    path.write_text(out.getvalue())
    return None

transpose(coord=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
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
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(start=None, end=None)

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
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
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(start=None, end=None)

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
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
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.
    """
    idx = -1
    before = -1
    x = 0
    if start is None and end is None:
        for juska in self._cmap:
            idx += 1
            if idx in self._indexes["_cmap"]:
                column = self._indexes["_cmap"][idx]
            else:
                column = self._get_element_idx2(_xpath_column_idx, idx)
                self._indexes["_cmap"][idx] = column
            repeated = juska - before
            before = juska
            for _i in range(repeated or 1):
                # Return a copy without the now obsolete repetition
                column = column.clone
                column.x = x
                x += 1
                if repeated > 1:
                    column.repeated = None
                yield column
    else:
        if start is None:
            start = 0
        start = max(0, start)
        if end is None:
            try:
                end = self._cmap[-1]
            except Exception:
                end = -1
        start_map = find_odf_idx(self._cmap, start)
        if start_map is None:
            return
        if start_map > 0:
            before = self._cmap[start_map - 1]
        idx = start_map - 1
        before = start - 1
        x = start
        for juska in self._cmap[start_map:]:
            idx += 1
            if idx in self._indexes["_cmap"]:
                column = self._indexes["_cmap"][idx]
            else:
                column = self._get_element_idx2(_xpath_column_idx, idx)
                self._indexes["_cmap"][idx] = column
            repeated = juska - before
            before = juska
            for _i in range(repeated or 1):
                if x <= end:
                    column = column.clone
                    column.x = x
                    x += 1
                    if repeated > 1 or (x == start and start > 0):
                        column.repeated = None
                    yield column

Text

Bases: Body

Text, specialized class of Element in charge of actual content management.

Source code in odfdo/body.py
146
147
148
149
150
151
152
class Text(Body):
    """Text, specialized class of Element in charge of actual content
    management.
    """

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

TextChange

Bases: Element

The TextChange “text:change” element marks a position in an empty region where text has been deleted.

Source code in odfdo/tracked_changes.py
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
class TextChange(Element):
    """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
        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
        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
        return changed.get_deleted(  # type: ignore
            as_text=as_text,
            no_header=no_header,
            clean=clean,
        )

    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_deleted(tracked_changes=None, as_text=False, no_header=False, clean=True)

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
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
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
    return changed.get_deleted(  # type: ignore
        as_text=as_text,
        no_header=no_header,
        clean=clean,
    )

get_end()

Return None.

Source code in odfdo/tracked_changes.py
558
559
560
def get_end(self) -> TextChangeEnd | None:
    """Return None."""
    return None

get_inserted(as_text=False, no_header=False, clean=True)

Return None.

Source code in odfdo/tracked_changes.py
545
546
547
548
549
550
551
552
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()

Return None.

Source code in odfdo/tracked_changes.py
554
555
556
def get_start(self) -> TextChangeStart | None:
    """Return None."""
    return None

TextChangeEnd

Bases: TextChange

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.

Source code in odfdo/tracked_changes.py
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
class TextChangeEnd(TextChange):
    """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.")
        body = self.document_body
        if not body:
            body = self.root
        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
        return body.get_between(
            start, end, as_text=as_text, no_header=no_header, clean=clean
        )

get_deleted(*args, **kwargs)

Return None.

Source code in odfdo/tracked_changes.py
586
587
588
def get_deleted(self, *args: Any, **kwargs: Any) -> Element | None:
    """Return None."""
    return None

get_end()

Return self.

Source code in odfdo/tracked_changes.py
582
583
584
def get_end(self) -> TextChangeEnd | None:
    """Return self."""
    return self

get_inserted(as_text=False, no_header=False, clean=True)

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
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
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
    return body.get_between(
        start, end, as_text=as_text, no_header=no_header, clean=clean
    )

get_start()

Return the corresponding annotation starting tag or None.

Source code in odfdo/tracked_changes.py
571
572
573
574
575
576
577
578
579
580
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.")
    body = self.document_body
    if not body:
        body = self.root
    return body.get_text_change_start(idx=idx)  # type: ignore

TextChangeStart

Bases: TextChangeEnd

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.

Source code in odfdo/tracked_changes.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
674
675
676
677
678
679
680
681
682
683
684
class TextChangeStart(TextChangeEnd):
    """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.")
        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)
        idx = self.get_id()
        parent = self.parent
        if parent is None:
            raise ValueError("cannot delete the root element")
        body = self.document_body
        if not body:
            body = parent
        end = body.get_text_change_end(idx=idx)
        if end:
            end.delete()
        # act like normal delete
        super().delete()

delete(child=None, keep_tail=True)

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
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
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)
    idx = self.get_id()
    parent = self.parent
    if parent is None:
        raise ValueError("cannot delete the root element")
    body = self.document_body
    if not body:
        body = parent
    end = body.get_text_change_end(idx=idx)
    if end:
        end.delete()
    # act like normal delete
    super().delete()

get_end()

Return the corresponding change-end tag or None.

Source code in odfdo/tracked_changes.py
643
644
645
646
647
648
649
650
651
652
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.")
    body = self.document_body
    if not body:
        body = self.root
    return body.get_text_change_end(idx=idx)  # type: ignore

get_start()

Return self.

Source code in odfdo/tracked_changes.py
639
640
641
def get_start(self) -> TextChangeStart:
    """Return self."""
    return self

TextChangedRegion

Bases: Element

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.

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. "
Source code in odfdo/tracked_changes.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
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
class TextChangedRegion(Element):
    """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
        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 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
384
385
386
387
388
389
390
391
392
393
394
395
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()

Shortcut to get the ChangeInfo element of the change element child.

Return: ChangeInfo element.

Source code in odfdo/tracked_changes.py
349
350
351
352
353
354
355
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 the “text:id” attribute.

Return: str

Source code in odfdo/tracked_changes.py
409
410
411
412
413
414
def get_id(self) -> str | None:
    """Get the "text:id" attribute.

    Return: str
    """
    return self._get_text_id()

set_change_info(change_info=None, creator=None, date=None, comments=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
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
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
    child.set_change_info(  # type: ignore
        change_info=change_info, creator=creator, date=date, comments=comments
    )

set_id(idx)

Set both the “text:id” and “xml:id” attributes with same value.

Source code in odfdo/tracked_changes.py
416
417
418
419
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

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.

Source code in odfdo/tracked_changes.py
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
class TextDeletion(TextInsertion):
    """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
            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)
                    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(as_text=False, no_header=False)

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
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 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
        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)
                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(as_text=False, no_header=False, clean=True)

Return None.

Source code in odfdo/tracked_changes.py
303
304
305
306
307
308
309
310
311
312
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(paragraph_or_list)

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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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

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
315
316
317
318
319
320
321
322
323
324
325
class TextFormatChange(TextInsertion):
    """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

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.

Source code in odfdo/tracked_changes.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
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
class TextInsertion(Element):
    """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 current:
            raise ValueError
        idx = current.get_id()  # type: ignore
        body = self.document_body
        if not body:
            body = self.root
        text_change = body.get_text_change_start(idx=idx)
        if not text_change:
            raise ValueError
        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 the ChangeInfo child of the element.

Return: ChangeInfo element.

Source code in odfdo/tracked_changes.py
165
166
167
168
169
170
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(as_text=False, no_header=False)

Return: None.

Source code in odfdo/tracked_changes.py
117
118
119
120
121
122
123
124
125
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(as_text=False, no_header=False, clean=True)

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
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
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 current:
        raise ValueError
    idx = current.get_id()  # type: ignore
    body = self.document_body
    if not body:
        body = self.root
    text_change = body.get_text_change_start(idx=idx)
    if not text_change:
        raise ValueError
    return text_change.get_inserted(  # type: ignore
        as_text=as_text, no_header=no_header, clean=clean
    )

set_change_info(change_info=None, creator=None, date=None, comments=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
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
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

ODF “text:table-of-content-entry-template”

Arguments:

style -- str
Source code in odfdo/toc.py
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
class TocEntryTemplate(Element):
    """ODF "text:table-of-content-entry-template"

    Arguments:

        style -- str
    """

    _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:
        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"))

TrackedChanges

Bases: MDZap, Element

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.

Source code in odfdo/tracked_changes.py
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
class TrackedChanges(MDZap, Element):
    """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 regien in changed_regions:
            changed = regien.get_change_element()  # type: ignore
            if not changed:
                continue
            if changed.tag.endswith(role):
                result.append(regien)
        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,
        )

UserDefined

Bases: ElementTyped

Return 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/variable.py
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
class UserDefined(ElementTyped):
    """Return 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
    """

    _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:
        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

VarChapter

Bases: Element

Source code in odfdo/variable.py
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
class VarChapter(Element):
    _tag = "text:chapter"
    _properties = (
        PropDef("display", "text:display"),
        PropDef("outline_level", "text:outline-level"),
    )
    DISPLAY_VALUE_CHOICE = {  # noqa: RUF012
        "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:
        """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

__init__(display='name', outline_level=None, **kwargs)

display can be: ‘number’, ‘name’, ‘number-and-name’, ‘plain-number’ or ‘plain-number-and-name’

Source code in odfdo/variable.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def __init__(
    self,
    display: str | None = "name",
    outline_level: str | None = None,
    **kwargs: Any,
) -> None:
    """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

VarFileName

Bases: Element

Source code in odfdo/variable.py
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
class VarFileName(Element):
    _tag = "text:file-name"
    _properties = (
        PropDef("display", "text:display"),
        PropDef("fixed", "text:fixed"),
    )
    DISPLAY_VALUE_CHOICE = {  # noqa: RUF012
        "full",
        "path",
        "name",
        "name-and-extension",
    }

    def __init__(
        self,
        display: str | None = "full",
        fixed: bool = False,
        **kwargs: Any,
    ) -> None:
        """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

__init__(display='full', fixed=False, **kwargs)

display can be: ‘full’, ‘path’, ‘name’ or ‘name-and-extension’

Source code in odfdo/variable.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
def __init__(
    self,
    display: str | None = "full",
    fixed: bool = False,
    **kwargs: Any,
) -> None:
    """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

VarPageNumber

Bases: Element

select_page – string in (‘previous’, ‘current’, ‘next’)

page_adjust – int (to add or subtract to the page number)

Source code in odfdo/variable.py
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
class VarPageNumber(Element):
    """
    select_page -- string in ('previous', 'current', 'next')

    page_adjust -- int (to add or subtract to the 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:
        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

XmlPart

Representation of an XML part.

Abstraction of the XML library behind.

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
class XmlPart:
    """Representation of an XML part.

    Abstraction of the XML library behind.
    """

    def __init__(self, part_name: str, container: Container) -> None:
        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

    @property
    def body(self) -> Element:
        """Get or set the document body : 'office:body'"""
        body = self.root.document_body
        if not isinstance(body, Element):
            raise TypeError(f"No body found in {self.part_name!r}")
        return body

    @body.setter
    def body(self, new_body: Element) -> None:
        body = self.root.document_body
        if not isinstance(body, Element):
            raise TypeError("//office:body not found in document")
        tail = body.tail
        body.clear()
        for item in new_body.children:
            body.append(item)
        if tail:
            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

Get or set the document body : ‘office:body’

xpath(xpath_query)

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
101
102
103
104
105
106
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()

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
1014
1015
1016
1017
1018
1019
1020
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(border=None, border_top=None, border_bottom=None, border_left=None, border_right=None, padding=None, padding_top=None, padding_bottom=None, padding_left=None, padding_right=None, background_color=None, shadow=None, color=None)

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
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
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_frame_position_style(name='FramePosition', horizontal_pos='from-left', vertical_pos='from-top', horizontal_rel='paragraph', vertical_rel='paragraph')

Helper style for positioning frames in desktop applications that need it.

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
64
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:
    """Helper style for positioning frames in desktop applications that need
    it.

    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_toc_level_style(level)

Generate an automatic default style for the given TOC level.

Source code in odfdo/toc.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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(color)

Turns a “#RRGGBB” hexadecimal color representation into a (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
41
def hex2rgb(color: str) -> tuple[int, int, int]:
    """Turns a "#RRGGBB" hexadecimal color representation into a (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(color=None)

Convert a color definition of type 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
 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
def hexa_color(color: str | tuple[int, int, int] | None = None) -> str | None:
    """Convert a color definition of type 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(thick=None, line=None, color=None)

Returns a string for style:table-cell-properties fo:border, with default : “0.06pt solid #000000”

thick -- str or float or int
line -- str
color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

Returns : str

Source code in odfdo/style.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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"

        thick -- str or float or int
        line -- str
        color -- str or rgb 3-tuple, str is 'black', 'grey', ... or '#012345'

    Returns : str
    """
    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(color)

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

Arguments:

color -- str or tuple

Return: str

Examples::

>>> rgb2hex('yellow')
'#FFFF00'
>>> rgb2hex((238, 130, 238))
'#EE82EE'
Source code in odfdo/utils/color.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
def rgb2hex(color: str | tuple[int, int, int]) -> str:
    """Turns a color name or a (R, G, B) color tuple into a "#RRGGBB"
    hexadecimal representation.

    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}"