Skip to content

Tracked Changes

TrackedChanges class for “text:tracked-changes” and related classes (ChangeInfo, TextInsertion, TextChange…).

Classes:

Name Description
ChangeInfo

Information about a change, “office:change-info”.

TextChange

A text change position, “text:change”.

TextChangeEnd

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

TextChangeStart

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

TextChangedRegion

A container for a single tracked change, “text:changed-region”.

TextDeletion

Represents a text deletion, “text:deletion”.

TextFormatChange

Represents a change in text formatting, “text:format-change”.

TextInsertion

Represents a text insertion, “text:insertion”.

TrackedChanges

The main container for all tracked changes, “text:tracked-changes”.

TrackedChangesMixin

Mixin class for classes containing TrackedChanges.

ChangeInfo

Bases: Element, DcCreatorMixin, DcDateMixin

Information about a change, “office:change-info”.

This element stores metadata about a change, including the author, timestamp, and any associated comments.

Comments are stored as one or more “text:p” (Paragraph) elements and can be accessed as Paragraph objects or as plain text.

Attributes:

Name Type Description
creator str

The name of the author who made the change.

date datetime

The date and time when the change was made.

Methods:

Name Description
__init__

Initializes the ChangeInfo element.

get_comments

Gets the text content of the comments.

set_comments

Sets the text content of the comments.

Source code in odfdo/tracked_changes.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class ChangeInfo(Element, DcCreatorMixin, DcDateMixin):
    """Information about a change, "office:change-info".

    This element stores metadata about a change, including the author,
    timestamp, and any associated comments.

    Comments are stored as one or more "text:p" (Paragraph) elements and can
    be accessed as Paragraph objects or as plain text.

    Attributes:
        creator (str): The name of the author who made the change.
        date (datetime): The date and time when the change was made.
    """

    _tag = "office:change-info"

    def __init__(
        self,
        creator: str | None = None,
        date: datetime | None = None,
        **kwargs: Any,
    ) -> None:
        """Initializes the ChangeInfo element.

        Args:
            creator: The name of the author of the change. Defaults to "Unknown".
            date: The date and time of the change. Defaults to the current time if not provided.
        """
        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]:
        """Gets the text content of the comments.

        Args:
            joined: If True (the default), concatenates the text of
                all comment paragraphs into a single string. If False,
                returns a list of strings, one for each paragraph.

        Returns:
            str | list[str]: The comment text as a single string or a list
                of strings.
        """
        content = self.paragraphs
        text = [para.get_formatted_text(simple=True) for para in content]
        if joined:
            return "\n".join(text)
        return text

    def set_comments(self, text: str = "", replace: bool = True) -> None:
        """Sets the text content of the comments.

        Args:
            text: The new text for the comments.
            replace: If True (the default), the new text replaces any
                existing comments. If False, it is appended as a new
                paragraph.
        """
        if replace:
            for para in self.paragraphs:
                self.delete(para)
        para = Paragraph()
        para.append_plain_text(text)
        self.insert(para, xmlposition=LAST_CHILD)

_tag class-attribute instance-attribute

_tag = 'office:change-info'

creator instance-attribute

creator = creator or 'Unknown'

date instance-attribute

date = date

__init__

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

Initializes the ChangeInfo element.

Parameters:

Name Type Description Default
creator str | None

The name of the author of the change. Defaults to “Unknown”.

None
date datetime | None

The date and time of the change. Defaults to the current time if not provided.

None
Source code in odfdo/tracked_changes.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def __init__(
    self,
    creator: str | None = None,
    date: datetime | None = None,
    **kwargs: Any,
) -> None:
    """Initializes the ChangeInfo element.

    Args:
        creator: The name of the author of the change. Defaults to "Unknown".
        date: The date and time of the change. Defaults to the current time if not provided.
    """
    super().__init__(**kwargs)
    if self._do_init:
        self.creator = creator or "Unknown"
        self.date = date

get_comments

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

Gets the text content of the comments.

Parameters:

Name Type Description Default
joined bool

If True (the default), concatenates the text of all comment paragraphs into a single string. If False, returns a list of strings, one for each paragraph.

True

Returns:

Type Description
str | list[str]

str | list[str]: The comment text as a single string or a list of strings.

Source code in odfdo/tracked_changes.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def get_comments(self, joined: bool = True) -> str | list[str]:
    """Gets the text content of the comments.

    Args:
        joined: If True (the default), concatenates the text of
            all comment paragraphs into a single string. If False,
            returns a list of strings, one for each paragraph.

    Returns:
        str | list[str]: The comment text as a single string or a list
            of strings.
    """
    content = self.paragraphs
    text = [para.get_formatted_text(simple=True) for para in content]
    if joined:
        return "\n".join(text)
    return text

set_comments

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

Sets the text content of the comments.

Parameters:

Name Type Description Default
text str

The new text for the comments.

''
replace bool

If True (the default), the new text replaces any existing comments. If False, it is appended as a new paragraph.

True
Source code in odfdo/tracked_changes.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def set_comments(self, text: str = "", replace: bool = True) -> None:
    """Sets the text content of the comments.

    Args:
        text: The new text for the comments.
        replace: If True (the default), the new text replaces any
            existing comments. If False, it is appended as a new
            paragraph.
    """
    if replace:
        for para in self.paragraphs:
            self.delete(para)
    para = Paragraph()
    para.append_plain_text(text)
    self.insert(para, xmlposition=LAST_CHILD)

TextChange

Bases: Element

A text change position, “text:change”.

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

Methods:

Name Description
get_change_element

Gets the underlying change element for this marker.

get_change_info

Gets the ‘office:change-info’ for the change this marker refers to.

get_changed_region

Finds the ‘text:changed-region’ this marker is associated with.

get_deleted

Gets the deleted content associated with this change marker.

get_end

Returns None, as this is a single-point marker.

get_id

Gets the ID of the change this marker refers to.

get_inserted

Returns None, as ‘text:change’ marks a deletion point.

get_start

Returns None, as this is a single-point marker.

set_id

Sets the ID of the change this marker refers to.

Source code in odfdo/tracked_changes.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
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
class TextChange(Element):
    """A text change position, "text:change".

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

    _tag = "text:change"

    def get_id(self) -> str | None:
        """Gets the ID of the change this marker refers to.

        Returns:
            str | None: The 'text:change-id' attribute value.
        """
        return self.get_attribute_string("text:change-id")

    def set_id(self, idx: str) -> None:
        """Sets the ID of the change this marker refers to.

        Args:
            idx: The ID to set as 'text:change-id'.
        """
        self.set_attribute("text:change-id", idx)

    def _get_tracked_changes(self) -> TrackedChanges | None:
        body: Text | None = self.document_body  # type: ignore[assignment]
        if body and body.tag == "office:text":
            return body.get_tracked_changes()
        raise ValueError

    def get_changed_region(
        self,
        tracked_changes: TrackedChanges | None = None,
    ) -> Element | None:
        """Finds the 'text:changed-region' this marker is associated with.

        Args:
            tracked_changes: The parent tracked
                changes container to search in. If not provided, it is
                inferred from the document.

        Returns:
            Element | None: The associated TextChangedRegion element, or
                None if not found.
        """
        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: TrackedChanges | None = None,
    ) -> Element | None:
        """Gets the 'office:change-info' for the change this marker refers to.

        Args:
            tracked_changes: The parent tracked changes container to search in.

        Returns:
            Element | None: The ChangeInfo element, or None if not found.
        """
        changed_region = self.get_changed_region(tracked_changes=tracked_changes)
        if not changed_region:
            return None  # pragma: nocover
        return changed_region.get_change_info()  # type: ignore

    def get_change_element(
        self,
        tracked_changes: TrackedChanges | None = None,
    ) -> Element | None:
        """Gets the underlying change element for this marker.

        This will be one of TextInsertion, TextDeletion, or TextFormatChange.

        Args:
            tracked_changes: The parent tracked changes container to search in.

        Returns:
            Element | None: The change element, or None if not found.
        """
        changed_region = self.get_changed_region(tracked_changes=tracked_changes)
        if not changed_region:
            return None  # pragma: nocover
        return changed_region.get_change_element()  # type: ignore

    def get_deleted(
        self,
        tracked_changes: TrackedChanges | None = None,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> Element | None:
        """Gets the deleted content associated with this change marker.

        This is a shortcut to find the related TextDeletion element and
        retrieve its content.

        Args:
            tracked_changes: The parent tracked changes container.
            as_text: Return content as a plain text string.
            no_header: Convert headings to paragraphs.
            clean: Ignored.

        Returns:
            Element | None: The deleted content.
        """
        changed = self.get_change_element(tracked_changes=tracked_changes)
        if not changed:
            return None  # pragma: nocover
        return changed.get_deleted(  # type: ignore
            as_text=as_text,
            no_header=no_header,
        )

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Returns None, as 'text:change' marks a deletion point.

        Args:
            as_text: unused.
            no_header: unused.
            clean: unused.

        Returns:
            str | Element | list[Element] | None: Always returns None.
        """
        return None

    def get_start(self) -> TextChangeStart | None:
        """Returns None, as this is a single-point marker.

        Returns:
            TextChangeStart | None: Always returns None.
        """
        return None

    def get_end(self) -> TextChangeEnd | None:
        """Returns None, as this is a single-point marker.

        Returns:
            TextChangeEnd | None: Always returns None.
        """
        return None

_tag class-attribute instance-attribute

_tag = 'text:change'

_get_tracked_changes

_get_tracked_changes() -> TrackedChanges | None
Source code in odfdo/tracked_changes.py
573
574
575
576
577
def _get_tracked_changes(self) -> TrackedChanges | None:
    body: Text | None = self.document_body  # type: ignore[assignment]
    if body and body.tag == "office:text":
        return body.get_tracked_changes()
    raise ValueError

get_change_element

get_change_element(
    tracked_changes: TrackedChanges | None = None,
) -> Element | None

Gets the underlying change element for this marker.

This will be one of TextInsertion, TextDeletion, or TextFormatChange.

Parameters:

Name Type Description Default
tracked_changes TrackedChanges | None

The parent tracked changes container to search in.

None

Returns:

Type Description
Element | None

Element | None: The change element, or None if not found.

Source code in odfdo/tracked_changes.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def get_change_element(
    self,
    tracked_changes: TrackedChanges | None = None,
) -> Element | None:
    """Gets the underlying change element for this marker.

    This will be one of TextInsertion, TextDeletion, or TextFormatChange.

    Args:
        tracked_changes: The parent tracked changes container to search in.

    Returns:
        Element | None: The change element, or None if not found.
    """
    changed_region = self.get_changed_region(tracked_changes=tracked_changes)
    if not changed_region:
        return None  # pragma: nocover
    return changed_region.get_change_element()  # type: ignore

get_change_info

get_change_info(
    tracked_changes: TrackedChanges | None = None,
) -> Element | None

Gets the ‘office:change-info’ for the change this marker refers to.

Parameters:

Name Type Description Default
tracked_changes TrackedChanges | None

The parent tracked changes container to search in.

None

Returns:

Type Description
Element | None

Element | None: The ChangeInfo element, or None if not found.

Source code in odfdo/tracked_changes.py
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def get_change_info(
    self,
    tracked_changes: TrackedChanges | None = None,
) -> Element | None:
    """Gets the 'office:change-info' for the change this marker refers to.

    Args:
        tracked_changes: The parent tracked changes container to search in.

    Returns:
        Element | None: The ChangeInfo element, or None if not found.
    """
    changed_region = self.get_changed_region(tracked_changes=tracked_changes)
    if not changed_region:
        return None  # pragma: nocover
    return changed_region.get_change_info()  # type: ignore

get_changed_region

get_changed_region(
    tracked_changes: TrackedChanges | None = None,
) -> Element | None

Finds the ‘text:changed-region’ this marker is associated with.

Parameters:

Name Type Description Default
tracked_changes TrackedChanges | None

The parent tracked changes container to search in. If not provided, it is inferred from the document.

None

Returns:

Type Description
Element | None

Element | None: The associated TextChangedRegion element, or None if not found.

Source code in odfdo/tracked_changes.py
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
def get_changed_region(
    self,
    tracked_changes: TrackedChanges | None = None,
) -> Element | None:
    """Finds the 'text:changed-region' this marker is associated with.

    Args:
        tracked_changes: The parent tracked
            changes container to search in. If not provided, it is
            inferred from the document.

    Returns:
        Element | None: The associated TextChangedRegion element, or
            None if not found.
    """
    if not tracked_changes:
        tracked_changes = self._get_tracked_changes()
    idx = self.get_id()
    return tracked_changes.get_changed_region(text_id=idx)  # type: ignore

get_deleted

get_deleted(
    tracked_changes: TrackedChanges | None = None,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> Element | None

Gets the deleted content associated with this change marker.

This is a shortcut to find the related TextDeletion element and retrieve its content.

Parameters:

Name Type Description Default
tracked_changes TrackedChanges | None

The parent tracked changes container.

None
as_text bool

Return content as a plain text string.

False
no_header bool

Convert headings to paragraphs.

False
clean bool

Ignored.

True

Returns:

Type Description
Element | None

Element | None: The deleted content.

Source code in odfdo/tracked_changes.py
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
def get_deleted(
    self,
    tracked_changes: TrackedChanges | None = None,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> Element | None:
    """Gets the deleted content associated with this change marker.

    This is a shortcut to find the related TextDeletion element and
    retrieve its content.

    Args:
        tracked_changes: The parent tracked changes container.
        as_text: Return content as a plain text string.
        no_header: Convert headings to paragraphs.
        clean: Ignored.

    Returns:
        Element | None: The deleted content.
    """
    changed = self.get_change_element(tracked_changes=tracked_changes)
    if not changed:
        return None  # pragma: nocover
    return changed.get_deleted(  # type: ignore
        as_text=as_text,
        no_header=no_header,
    )

get_end

get_end() -> TextChangeEnd | None

Returns None, as this is a single-point marker.

Returns:

Type Description
TextChangeEnd | None

TextChangeEnd | None: Always returns None.

Source code in odfdo/tracked_changes.py
690
691
692
693
694
695
696
def get_end(self) -> TextChangeEnd | None:
    """Returns None, as this is a single-point marker.

    Returns:
        TextChangeEnd | None: Always returns None.
    """
    return None

get_id

get_id() -> str | None

Gets the ID of the change this marker refers to.

Returns:

Type Description
str | None

str | None: The ‘text:change-id’ attribute value.

Source code in odfdo/tracked_changes.py
557
558
559
560
561
562
563
def get_id(self) -> str | None:
    """Gets the ID of the change this marker refers to.

    Returns:
        str | None: The 'text:change-id' attribute value.
    """
    return self.get_attribute_string("text:change-id")

get_inserted

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

Returns None, as ‘text:change’ marks a deletion point.

Parameters:

Name Type Description Default
as_text bool

unused.

False
no_header bool

unused.

False
clean bool

unused.

True

Returns:

Type Description
str | Element | list[Element] | None

str | Element | list[Element] | None: Always returns None.

Source code in odfdo/tracked_changes.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Returns None, as 'text:change' marks a deletion point.

    Args:
        as_text: unused.
        no_header: unused.
        clean: unused.

    Returns:
        str | Element | list[Element] | None: Always returns None.
    """
    return None

get_start

get_start() -> TextChangeStart | None

Returns None, as this is a single-point marker.

Returns:

Type Description
TextChangeStart | None

TextChangeStart | None: Always returns None.

Source code in odfdo/tracked_changes.py
682
683
684
685
686
687
688
def get_start(self) -> TextChangeStart | None:
    """Returns None, as this is a single-point marker.

    Returns:
        TextChangeStart | None: Always returns None.
    """
    return None

set_id

set_id(idx: str) -> None

Sets the ID of the change this marker refers to.

Parameters:

Name Type Description Default
idx str

The ID to set as ‘text:change-id’.

required
Source code in odfdo/tracked_changes.py
565
566
567
568
569
570
571
def set_id(self, idx: str) -> None:
    """Sets the ID of the change this marker refers to.

    Args:
        idx: The ID to set as 'text:change-id'.
    """
    self.set_attribute("text:change-id", idx)

TextChangeEnd

Bases: TextChange

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

The TextChangeEnd “text:change-end” element marks the end of a region with content where text has been inserted or the format has been changed.

Methods:

Name Description
get_deleted

Returns None, as this marks an insertion or format change.

get_end

Returns self, as this is the end marker.

get_inserted

Gets the content between the start and end change markers.

get_start

Gets the corresponding ‘text:change-start’ tag.

Source code in odfdo/tracked_changes.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
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
class TextChangeEnd(TextChange):
    """End of a changed region, "text:change-end".

    The TextChangeEnd "text:change-end" element marks the end of a region with
    content where text has been inserted or the format has been changed.
    """

    _tag = "text:change-end"

    def get_start(self) -> TextChangeStart | None:
        """Gets the corresponding 'text:change-start' tag.

        Returns:
            TextChangeStart | None: The start marker with the same ID, or
                None if not found.
        """
        idx = self.get_id()
        parent = self.parent
        if parent is None:
            raise ValueError(
                "Can not find end tag: no parent available."
            )  # pragma: nocover
        body: Body | Element = self.document_body or self.root
        return body.get_text_change_start(idx=idx)

    def get_end(self) -> TextChangeEnd | None:
        """Returns self, as this is the end marker.

        Returns:
            TextChangeEnd: This element.
        """
        return self

    def get_deleted(self, *args: Any, **kwargs: Any) -> Element | None:
        """Returns None, as this marks an insertion or format change.

        Returns:
            None: Always returns None.
        """
        return None

    def get_inserted(
        self,
        as_text: bool = False,
        no_header: bool = False,
        clean: bool = True,
    ) -> str | Element | list[Element] | None:
        """Gets the content between the start and end change markers.

        Args:
            as_text: If True, returns the content as a plain text string.
            no_header: If True, converts any 'text:h' (heading) elements to 'text:p' (paragraph) elements.
            clean: If True, filters out unwanted elements from the result.

        Returns:
            str | Element | list[Element] | None: The inserted content.
        """

        # 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: Body | Element = self.document_body or self.root
        return elements_between(
            body, start, end, as_text=as_text, no_header=no_header, clean=clean
        )

_tag class-attribute instance-attribute

_tag = 'text:change-end'

get_deleted

get_deleted(*args: Any, **kwargs: Any) -> Element | None

Returns None, as this marks an insertion or format change.

Returns:

Name Type Description
None Element | None

Always returns None.

Source code in odfdo/tracked_changes.py
732
733
734
735
736
737
738
def get_deleted(self, *args: Any, **kwargs: Any) -> Element | None:
    """Returns None, as this marks an insertion or format change.

    Returns:
        None: Always returns None.
    """
    return None

get_end

get_end() -> TextChangeEnd | None

Returns self, as this is the end marker.

Returns:

Name Type Description
TextChangeEnd TextChangeEnd | None

This element.

Source code in odfdo/tracked_changes.py
724
725
726
727
728
729
730
def get_end(self) -> TextChangeEnd | None:
    """Returns self, as this is the end marker.

    Returns:
        TextChangeEnd: This element.
    """
    return self

get_inserted

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

Gets the content between the start and end change markers.

Parameters:

Name Type Description Default
as_text bool

If True, returns the content as a plain text string.

False
no_header bool

If True, converts any ‘text:h’ (heading) elements to ‘text:p’ (paragraph) elements.

False
clean bool

If True, filters out unwanted elements from the result.

True

Returns:

Type Description
str | Element | list[Element] | None

str | Element | list[Element] | None: The inserted content.

Source code in odfdo/tracked_changes.py
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
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Gets the content between the start and end change markers.

    Args:
        as_text: If True, returns the content as a plain text string.
        no_header: If True, converts any 'text:h' (heading) elements to 'text:p' (paragraph) elements.
        clean: If True, filters out unwanted elements from the result.

    Returns:
        str | Element | list[Element] | None: The inserted content.
    """

    # 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: Body | Element = self.document_body or self.root
    return elements_between(
        body, start, end, as_text=as_text, no_header=no_header, clean=clean
    )

get_start

get_start() -> TextChangeStart | None

Gets the corresponding ‘text:change-start’ tag.

Returns:

Type Description
TextChangeStart | None

TextChangeStart | None: The start marker with the same ID, or None if not found.

Source code in odfdo/tracked_changes.py
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
def get_start(self) -> TextChangeStart | None:
    """Gets the corresponding 'text:change-start' tag.

    Returns:
        TextChangeStart | None: The start marker with the same ID, or
            None if not found.
    """
    idx = self.get_id()
    parent = self.parent
    if parent is None:
        raise ValueError(
            "Can not find end tag: no parent available."
        )  # pragma: nocover
    body: Body | Element = self.document_body or self.root
    return body.get_text_change_start(idx=idx)

TextChangeStart

Bases: TextChangeEnd

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

The TextChangeStart “text:change-start” element marks the start of a region with content where text has been inserted or the format has been changed.

Methods:

Name Description
delete

Deletes the element from the XML tree.

get_end

Gets the corresponding ‘text:change-end’ tag.

get_start

Returns self, as this is the start marker.

Source code in odfdo/tracked_changes.py
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
class TextChangeStart(TextChangeEnd):
    """Start of a changed region, "text:change-start".

    The TextChangeStart "text:change-start" element marks the start of a region
    with content where text has been inserted or the format has been changed.
    """

    _tag = "text:change-start"

    def get_start(self) -> TextChangeStart:
        """Returns self, as this is the start marker.

        Returns:
            TextChangeStart: This element.
        """
        return self

    def get_end(self) -> TextChangeEnd:
        """Gets the corresponding 'text:change-end' tag.

        Returns:
            TextChangeEnd | None: The end marker with the same ID, or
                None if not found.
        """
        idx = self.get_id()
        parent = self.parent
        if parent is None:
            raise ValueError(
                "Can not find end tag: no parent available."
            )  # pragma: nocover
        body: Body | Element = self.document_body or self.root
        return body.get_text_change_end(idx=idx)  # type: ignore

    def delete(
        self,
        child: Element | None = None,
        keep_tail: bool = True,
    ) -> None:
        """Deletes the element from the XML tree.

        For a TextChangeStart, this also deletes the corresponding
        'text:change-end' tag if it exists.

        Args:
            child: A specific child to delete. If None,
                the element itself is deleted.
            keep_tail: If True, the text that follows the element
                is preserved. Defaults to True.
        """
        if child is not None:  # act like normal delete
            return super().delete(child, keep_tail)  # pragma: nocover
        idx = self.get_id()
        if self.parent is None:
            raise ValueError("cannot delete the root element")  # pragma: nocover
        body: Body | Element = self.document_body or self.root
        end = body.get_text_change_end(idx=idx)
        if end:  # pragma: nocover
            end.delete()
        # act like normal delete
        super().delete()

_tag class-attribute instance-attribute

_tag = 'text:change-start'

delete

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

Deletes the element from the XML tree.

For a TextChangeStart, this also deletes the corresponding ‘text:change-end’ tag if it exists.

Parameters:

Name Type Description Default
child Element | None

A specific child to delete. If None, the element itself is deleted.

None
keep_tail bool

If True, the text that follows the element is preserved. Defaults to True.

True
Source code in odfdo/tracked_changes.py
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
def delete(
    self,
    child: Element | None = None,
    keep_tail: bool = True,
) -> None:
    """Deletes the element from the XML tree.

    For a TextChangeStart, this also deletes the corresponding
    'text:change-end' tag if it exists.

    Args:
        child: A specific child to delete. If None,
            the element itself is deleted.
        keep_tail: If True, the text that follows the element
            is preserved. Defaults to True.
    """
    if child is not None:  # act like normal delete
        return super().delete(child, keep_tail)  # pragma: nocover
    idx = self.get_id()
    if self.parent is None:
        raise ValueError("cannot delete the root element")  # pragma: nocover
    body: Body | Element = self.document_body or self.root
    end = body.get_text_change_end(idx=idx)
    if end:  # pragma: nocover
        end.delete()
    # act like normal delete
    super().delete()

get_end

get_end() -> TextChangeEnd

Gets the corresponding ‘text:change-end’ tag.

Returns:

Type Description
TextChangeEnd

TextChangeEnd | None: The end marker with the same ID, or None if not found.

Source code in odfdo/tracked_changes.py
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
def get_end(self) -> TextChangeEnd:
    """Gets the corresponding 'text:change-end' tag.

    Returns:
        TextChangeEnd | None: The end marker with the same ID, or
            None if not found.
    """
    idx = self.get_id()
    parent = self.parent
    if parent is None:
        raise ValueError(
            "Can not find end tag: no parent available."
        )  # pragma: nocover
    body: Body | Element = self.document_body or self.root
    return body.get_text_change_end(idx=idx)  # type: ignore

get_start

get_start() -> TextChangeStart

Returns self, as this is the start marker.

Returns:

Name Type Description
TextChangeStart TextChangeStart

This element.

Source code in odfdo/tracked_changes.py
779
780
781
782
783
784
785
def get_start(self) -> TextChangeStart:
    """Returns self, as this is the start marker.

    Returns:
        TextChangeStart: This element.
    """
    return self

TextChangedRegion

Bases: Element

A container for a single tracked change, “text:changed-region”.

This element links a change marker in the document body (like “text:change-start”) to the details of the change (like “text:insertion” or “text:deletion”). It contains exactly one of: TextInsertion, TextDeletion, or TextFormatChange.

The ‘xml:id’ or ‘text:id’ of this element is referenced by the corresponding change marker elements.

Warning

This implementation expects that a ‘text:changed-region’ is referenced only once, which differs from the ODF 1.2 specification.

Methods:

Name Description
get_change_element

Gets the underlying change element.

get_change_info

Gets the ChangeInfo from the underlying change element.

get_id

Gets the “text:id” attribute of the region.

set_change_info

Sets the ChangeInfo on the underlying change element.

set_id

Sets both the “text:id” and “xml:id” attributes to the same value.

Source code in odfdo/tracked_changes.py
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
class TextChangedRegion(Element):
    """A container for a single tracked change, "text:changed-region".

    This element links a change marker in the document body (like
    "text:change-start") to the details of the change (like
    "text:insertion" or "text:deletion"). It contains exactly one of:
    TextInsertion, TextDeletion, or TextFormatChange.

    The 'xml:id' or 'text:id' of this element is referenced by the
    corresponding change marker elements.

    Warning:
        This implementation expects that a 'text:changed-region' is
        referenced only once, which differs from the ODF 1.2 specification.
    """

    _tag = "text:changed-region"

    def get_change_info(self) -> ChangeInfo | None:
        """Gets the ChangeInfo from the underlying change element.

        This is a shortcut to access the 'office:change-info' of the
        child (e.g., TextInsertion).

        Returns:
            ChangeInfo | None: The ChangeInfo element, or None if not found.
        """
        return cast(
            None | ChangeInfo, 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:
        """Sets the ChangeInfo on the underlying change element.

        This is a shortcut to set the 'office:change-info' of the child
        (e.g., TextInsertion). See `TextInsertion.set_change_info()` for
        more details.

        Args:
            change_info: An existing ChangeInfo element.
            creator: The author's name.
            date: The date of the change.
            comments: Comments to add.
        """
        child = self.get_change_element()
        if not child:
            raise ValueError("Empty TextChangedRegion")
        child.set_change_info(  # type: ignore
            change_info=change_info, creator=creator, date=date, comments=comments
        )

    def get_change_element(self) -> Element | None:
        """Gets the underlying change element.

        This will be one of TextInsertion, TextDeletion, or TextFormatChange.

        Returns:
            Element | None: The change element, or None if not found.
        """
        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:
        """Gets the "text:id" attribute of the region.

        Returns:
            str | None: The ID of the changed region.
        """
        return self._get_text_id()

    def set_id(self, idx: str) -> None:
        """Sets both the "text:id" and "xml:id" attributes to the same value.

        Args:
            idx: The ID to set.
        """
        self._set_text_id(idx)
        self._set_xml_id(idx)

_tag class-attribute instance-attribute

_tag = 'text:changed-region'

_get_text_id

_get_text_id() -> str | None
Source code in odfdo/tracked_changes.py
439
440
def _get_text_id(self) -> str | None:
    return self.get_attribute_string("text:id")

_get_xml_id

_get_xml_id() -> str | None
Source code in odfdo/tracked_changes.py
445
446
def _get_xml_id(self) -> str | None:
    return self.get_attribute_string("xml:id")

_set_text_id

_set_text_id(text_id: str) -> None
Source code in odfdo/tracked_changes.py
442
443
def _set_text_id(self, text_id: str) -> None:
    self.set_attribute("text:id", text_id)

_set_xml_id

_set_xml_id(xml_id: str) -> None
Source code in odfdo/tracked_changes.py
448
449
def _set_xml_id(self, xml_id: str) -> None:
    self.set_attribute("xml:id", xml_id)

get_change_element

get_change_element() -> Element | None

Gets the underlying change element.

This will be one of TextInsertion, TextDeletion, or TextFormatChange.

Returns:

Type Description
Element | None

Element | None: The change element, or None if not found.

Source code in odfdo/tracked_changes.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def get_change_element(self) -> Element | None:
    """Gets the underlying change element.

    This will be one of TextInsertion, TextDeletion, or TextFormatChange.

    Returns:
        Element | None: The change element, or None if not found.
    """
    request = (
        "descendant::text:insertion "
        "| descendant::text:deletion"
        "| descendant::text:format-change"
    )
    return self._filtered_element(request, 0)

get_change_info

get_change_info() -> ChangeInfo | None

Gets the ChangeInfo from the underlying change element.

This is a shortcut to access the ‘office:change-info’ of the child (e.g., TextInsertion).

Returns:

Type Description
ChangeInfo | None

ChangeInfo | None: The ChangeInfo element, or None if not found.

Source code in odfdo/tracked_changes.py
385
386
387
388
389
390
391
392
393
394
395
396
def get_change_info(self) -> ChangeInfo | None:
    """Gets the ChangeInfo from the underlying change element.

    This is a shortcut to access the 'office:change-info' of the
    child (e.g., TextInsertion).

    Returns:
        ChangeInfo | None: The ChangeInfo element, or None if not found.
    """
    return cast(
        None | ChangeInfo, self.get_element("descendant::office:change-info")
    )

get_id

get_id() -> str | None

Gets the “text:id” attribute of the region.

Returns:

Type Description
str | None

str | None: The ID of the changed region.

Source code in odfdo/tracked_changes.py
451
452
453
454
455
456
457
def get_id(self) -> str | None:
    """Gets the "text:id" attribute of the region.

    Returns:
        str | None: The ID of the changed region.
    """
    return self._get_text_id()

set_change_info

set_change_info(
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None

Sets the ChangeInfo on the underlying change element.

This is a shortcut to set the ‘office:change-info’ of the child (e.g., TextInsertion). See TextInsertion.set_change_info() for more details.

Parameters:

Name Type Description Default
change_info Element | None

An existing ChangeInfo element.

None
creator str | None

The author’s name.

None
date datetime | None

The date of the change.

None
comments Element | list[Element] | None

Comments to add.

None
Source code in odfdo/tracked_changes.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def set_change_info(
    self,
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None:
    """Sets the ChangeInfo on the underlying change element.

    This is a shortcut to set the 'office:change-info' of the child
    (e.g., TextInsertion). See `TextInsertion.set_change_info()` for
    more details.

    Args:
        change_info: An existing ChangeInfo element.
        creator: The author's name.
        date: The date of the change.
        comments: Comments to add.
    """
    child = self.get_change_element()
    if not child:
        raise ValueError("Empty TextChangedRegion")
    child.set_change_info(  # type: ignore
        change_info=change_info, creator=creator, date=date, comments=comments
    )

set_id

set_id(idx: str) -> None

Sets both the “text:id” and “xml:id” attributes to the same value.

Parameters:

Name Type Description Default
idx str

The ID to set.

required
Source code in odfdo/tracked_changes.py
459
460
461
462
463
464
465
466
def set_id(self, idx: str) -> None:
    """Sets both the "text:id" and "xml:id" attributes to the same value.

    Args:
        idx: The ID to set.
    """
    self._set_text_id(idx)
    self._set_xml_id(idx)

TextDeletion

Bases: ListMixin, TocMixin, SectionMixin, TextInsertion

Represents a text deletion, “text:deletion”.

This element contains metadata about a deletion (author, date, comments) and stores the actual content that was deleted. The position in the document where the deletion occurred is marked by a “text:change” element.

Methods:

Name Description
get_deleted

Gets the content that was deleted.

get_inserted

Returns None, as this is a text deletion.

set_deleted

Sets the content that was deleted.

Source code in odfdo/tracked_changes.py
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
class TextDeletion(ListMixin, TocMixin, SectionMixin, TextInsertion):
    """Represents a text deletion, "text:deletion".

    This element contains metadata about a deletion (author, date, comments)
    and stores the actual content that was deleted. The position in the
    document where the deletion occurred is marked by a "text:change"
    element.
    """

    _tag = "text:deletion"

    def get_deleted(
        self,
        as_text: bool = False,
        no_header: bool = False,
    ) -> str | list[Element] | None:
        """Gets the content that was deleted.

        Args:
            as_text: If True, returns the content as a plain text
                string.
            no_header: If True, converts any 'text:h' (heading)
                elements to 'text:p' (paragraph) elements.

        Returns:
            str | list[Element] | None: The deleted content, which is
                typically a list of Paragraph or Header elements, a string,
                or None.
        """
        children = self.children
        inner = [elem for elem in children if elem.tag != "office:change-info"]
        if no_header:  # crude replace t:h by t:p
            print("noheaders")
            new_inner = []
            for element in inner:
                if element.tag == "text:h":
                    children = element.children
                    text = element.text
                    para = Element.from_tag("text:p")
                    para.text = text
                    for child in children:
                        para.append(child)  # pragma: nocover
                    new_inner.append(para)
                else:
                    new_inner.append(element)
            inner = new_inner
        if as_text:
            return "\n".join([elem.get_formatted_text(context=None) for elem in inner])
        return inner

    def set_deleted(self, paragraph_or_list: Element | list[Element]) -> None:
        """Sets the content that was deleted.

        This method replaces any existing deleted content within this element.

        Args:
            paragraph_or_list: A Paragraph,
                Header, or a list of such elements representing the deleted
                content.
        """
        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:
        """Returns None, as this is a text deletion.

        This method is for consistency with other change types.

        Args:
            as_text: Ignored.
            no_header: Ignored.
            clean: Ignored.

        Returns:
            None | str: Always None for list return, empty string for text.
        """
        if as_text:
            return ""
        return None

_tag class-attribute instance-attribute

_tag = 'text:deletion'

get_deleted

get_deleted(
    as_text: bool = False, no_header: bool = False
) -> str | list[Element] | None

Gets the content that was deleted.

Parameters:

Name Type Description Default
as_text bool

If True, returns the content as a plain text string.

False
no_header bool

If True, converts any ‘text:h’ (heading) elements to ‘text:p’ (paragraph) elements.

False

Returns:

Type Description
str | list[Element] | None

str | list[Element] | None: The deleted content, which is typically a list of Paragraph or Header elements, a string, or None.

Source code in odfdo/tracked_changes.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def get_deleted(
    self,
    as_text: bool = False,
    no_header: bool = False,
) -> str | list[Element] | None:
    """Gets the content that was deleted.

    Args:
        as_text: If True, returns the content as a plain text
            string.
        no_header: If True, converts any 'text:h' (heading)
            elements to 'text:p' (paragraph) elements.

    Returns:
        str | list[Element] | None: The deleted content, which is
            typically a list of Paragraph or Header elements, a string,
            or None.
    """
    children = self.children
    inner = [elem for elem in children if elem.tag != "office:change-info"]
    if no_header:  # crude replace t:h by t:p
        print("noheaders")
        new_inner = []
        for element in inner:
            if element.tag == "text:h":
                children = element.children
                text = element.text
                para = Element.from_tag("text:p")
                para.text = text
                for child in children:
                    para.append(child)  # pragma: nocover
                new_inner.append(para)
            else:
                new_inner.append(element)
        inner = new_inner
    if as_text:
        return "\n".join([elem.get_formatted_text(context=None) for elem in inner])
    return inner

get_inserted

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

Returns None, as this is a text deletion.

This method is for consistency with other change types.

Parameters:

Name Type Description Default
as_text bool

Ignored.

False
no_header bool

Ignored.

False
clean bool

Ignored.

True

Returns:

Type Description
str | Element | list[Element] | None

None | str: Always None for list return, empty string for text.

Source code in odfdo/tracked_changes.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Returns None, as this is a text deletion.

    This method is for consistency with other change types.

    Args:
        as_text: Ignored.
        no_header: Ignored.
        clean: Ignored.

    Returns:
        None | str: Always None for list return, empty string for text.
    """
    if as_text:
        return ""
    return None

set_deleted

set_deleted(
    paragraph_or_list: Element | list[Element],
) -> None

Sets the content that was deleted.

This method replaces any existing deleted content within this element.

Parameters:

Name Type Description Default
paragraph_or_list Element | list[Element]

A Paragraph, Header, or a list of such elements representing the deleted content.

required
Source code in odfdo/tracked_changes.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
def set_deleted(self, paragraph_or_list: Element | list[Element]) -> None:
    """Sets the content that was deleted.

    This method replaces any existing deleted content within this element.

    Args:
        paragraph_or_list: A Paragraph,
            Header, or a list of such elements representing the deleted
            content.
    """
    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

Represents a change in text formatting, “text:format-change”.

This element marks a change in formatting attributes. The actual region of the change is defined by “text:change-start” and “text:change-end” elements.

Note: This element itself does not contain the specific formatting changes, only metadata about the change event.

Source code in odfdo/tracked_changes.py
353
354
355
356
357
358
359
360
361
362
363
364
class TextFormatChange(TextInsertion):
    """Represents a change in text formatting, "text:format-change".

    This element marks a change in formatting attributes. The actual region
    of the change is defined by "text:change-start" and "text:change-end"
    elements.

    Note: This element itself does not contain the specific formatting
    changes, only metadata about the change event.
    """

    _tag = "text:format-change"

_tag class-attribute instance-attribute

_tag = 'text:format-change'

TextInsertion

Bases: Element

Represents a text insertion, “text:insertion”.

This element contains metadata about an insertion, including the author, date, and optional comments. It is linked to the actual inserted content in the document body via its parent “text:changed-region”.

Methods:

Name Description
get_change_info

Gets the ‘office:change-info’ child of this element.

get_deleted

Returns None, as this is a text insertion.

get_inserted

Gets the content that was inserted.

set_change_info

Sets the ‘office:change-info’ for this insertion.

Source code in odfdo/tracked_changes.py
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
class TextInsertion(Element):
    """Represents a text insertion, "text:insertion".

    This element contains metadata about an insertion, including the author,
    date, and optional comments. It is linked to the actual inserted content
    in the document body via its parent "text:changed-region".
    """

    _tag = "text:insertion"

    def get_deleted(
        self,
        as_text: bool = False,
        no_header: bool = False,
    ) -> str | list[Element] | None:
        """Returns None, as this is a text insertion.

        This method is for consistency with other change types.

        Args:
            as_text: Ignored.
            no_header: Ignored.

        Returns:
            None | str: Always None for list return, empty string for text.
        """
        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:
        """Gets the content that was inserted.

        This is a shortcut to find the corresponding 'text:change-start' and
        'text:change-end' tags and return the content between them.

        Args:
            as_text: If True, returns the content as a plain text string.
            no_header: If True, converts any 'text:h' (heading)
                elements to 'text:p' (paragraph) elements.
            clean: If True, filters out unwanted elements from the result.

        Returns:
            str | Element | list[Element] | None: The inserted content,
                which can be a single element, a list of elements, a string,
                or None if not found.
        """
        current = self.parent  # text:changed-region
        if not isinstance(current, TextChangedRegion):
            raise TypeError("Missing parent TextChangedRegion")
        idx = current.get_id()
        body: Body | Element = self.document_body or self.root
        text_change = body.get_text_change_start(idx=idx)
        if not text_change:
            raise ValueError  # pragma: nocover
        return text_change.get_inserted(
            as_text=as_text, no_header=no_header, clean=clean
        )

    def get_change_info(self) -> ChangeInfo | None:
        """Gets the 'office:change-info' child of this element.

        Returns:
            ChangeInfo | None: The ChangeInfo element, or None if not found.
        """
        return cast(
            None | ChangeInfo, 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:
        """Sets the 'office:change-info' for this insertion.

        If `change_info` is not provided, a new one is created using the
        other arguments. Any existing change info is replaced.

        Args:
            change_info: An existing ChangeInfo element
                to set.
            creator: The name of the author. Defaults to
                'Unknown'.
            date: The date and time of the change.
                Defaults to the current time.
            comments: A Paragraph or list of Paragraphs to add as comments.
        """
        if change_info is None:
            new_change_info = ChangeInfo(creator, date)
            if comments is not None:
                if isinstance(comments, Element):
                    # single paragraph 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)

_tag class-attribute instance-attribute

_tag = 'text:insertion'

get_change_info

get_change_info() -> ChangeInfo | None

Gets the ‘office:change-info’ child of this element.

Returns:

Type Description
ChangeInfo | None

ChangeInfo | None: The ChangeInfo element, or None if not found.

Source code in odfdo/tracked_changes.py
207
208
209
210
211
212
213
214
215
def get_change_info(self) -> ChangeInfo | None:
    """Gets the 'office:change-info' child of this element.

    Returns:
        ChangeInfo | None: The ChangeInfo element, or None if not found.
    """
    return cast(
        None | ChangeInfo, self.get_element("descendant::office:change-info")
    )

get_deleted

get_deleted(
    as_text: bool = False, no_header: bool = False
) -> str | list[Element] | None

Returns None, as this is a text insertion.

This method is for consistency with other change types.

Parameters:

Name Type Description Default
as_text bool

Ignored.

False
no_header bool

Ignored.

False

Returns:

Type Description
str | list[Element] | None

None | str: Always None for list return, empty string for text.

Source code in odfdo/tracked_changes.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def get_deleted(
    self,
    as_text: bool = False,
    no_header: bool = False,
) -> str | list[Element] | None:
    """Returns None, as this is a text insertion.

    This method is for consistency with other change types.

    Args:
        as_text: Ignored.
        no_header: Ignored.

    Returns:
        None | str: Always None for list return, empty string for text.
    """
    if as_text:
        return ""
    return None

get_inserted

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

Gets the content that was inserted.

This is a shortcut to find the corresponding ‘text:change-start’ and ‘text:change-end’ tags and return the content between them.

Parameters:

Name Type Description Default
as_text bool

If True, returns the content as a plain text string.

False
no_header bool

If True, converts any ‘text:h’ (heading) elements to ‘text:p’ (paragraph) elements.

False
clean bool

If True, filters out unwanted elements from the result.

True

Returns:

Type Description
str | Element | list[Element] | None

str | Element | list[Element] | None: The inserted content, which can be a single element, a list of elements, a string, or None if not found.

Source code in odfdo/tracked_changes.py
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
def get_inserted(
    self,
    as_text: bool = False,
    no_header: bool = False,
    clean: bool = True,
) -> str | Element | list[Element] | None:
    """Gets the content that was inserted.

    This is a shortcut to find the corresponding 'text:change-start' and
    'text:change-end' tags and return the content between them.

    Args:
        as_text: If True, returns the content as a plain text string.
        no_header: If True, converts any 'text:h' (heading)
            elements to 'text:p' (paragraph) elements.
        clean: If True, filters out unwanted elements from the result.

    Returns:
        str | Element | list[Element] | None: The inserted content,
            which can be a single element, a list of elements, a string,
            or None if not found.
    """
    current = self.parent  # text:changed-region
    if not isinstance(current, TextChangedRegion):
        raise TypeError("Missing parent TextChangedRegion")
    idx = current.get_id()
    body: Body | Element = self.document_body or self.root
    text_change = body.get_text_change_start(idx=idx)
    if not text_change:
        raise ValueError  # pragma: nocover
    return text_change.get_inserted(
        as_text=as_text, no_header=no_header, clean=clean
    )

set_change_info

set_change_info(
    change_info: Element | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    comments: Element | list[Element] | None = None,
) -> None

Sets the ‘office:change-info’ for this insertion.

If change_info is not provided, a new one is created using the other arguments. Any existing change info is replaced.

Parameters:

Name Type Description Default
change_info Element | None

An existing ChangeInfo element to set.

None
creator str | None

The name of the author. Defaults to ‘Unknown’.

None
date datetime | None

The date and time of the change. Defaults to the current time.

None
comments Element | list[Element] | None

A Paragraph or list of Paragraphs to add as comments.

None
Source code in odfdo/tracked_changes.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
249
250
251
252
253
254
255
256
257
258
259
260
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:
    """Sets the 'office:change-info' for this insertion.

    If `change_info` is not provided, a new one is created using the
    other arguments. Any existing change info is replaced.

    Args:
        change_info: An existing ChangeInfo element
            to set.
        creator: The name of the author. Defaults to
            'Unknown'.
        date: The date and time of the change.
            Defaults to the current time.
        comments: A Paragraph or list of Paragraphs to add as comments.
    """
    if change_info is None:
        new_change_info = ChangeInfo(creator, date)
        if comments is not None:
            if isinstance(comments, Element):
                # single paragraph 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)

TrackedChanges

Bases: MDZap, Element

The main container for all tracked changes, “text:tracked-changes”.

This element holds all the ‘text:changed-region’ elements that describe the individual changes (insertions, deletions, format changes) within its scope (e.g., the document body or a header).

If this element is absent, change tracking is considered disabled for that scope, and any change markers should be ignored.

Methods:

Name Description
get_changed_region

Gets a single ‘text:changed-region’ element by position or criteria.

get_changed_regions

Gets a list of ‘text:changed-region’ elements matching criteria.

Source code in odfdo/tracked_changes.py
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
class TrackedChanges(MDZap, Element):
    """The main container for all tracked changes, "text:tracked-changes".

    This element holds all the 'text:changed-region' elements that describe
    the individual changes (insertions, deletions, format changes) within
    its scope (e.g., the document body or a header).

    If this element is absent, change tracking is considered disabled for that
    scope, and any change markers should 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]:
        """Gets a list of 'text:changed-region' elements matching criteria.

        Args:
            creator: Filter by the author's name.
            date: Filter by the date of the change.
            content: Filter by a regex match in the content.
            role: Filter by the type of change ('insertion', 'deletion', 'format-change').

        Returns:
            list[Element]: A list of matching TextChangedRegion elements.
        """
        changed_regions = self._filtered_elements(
            "text:changed-region",
            dc_creator=creator,
            dc_date=date,
            content=content,
        )
        if role is None:
            return changed_regions
        result: list[Element] = []
        for region in changed_regions:
            changed = region.get_change_element()  # type: ignore
            if not changed:
                continue  # pragma: nocover
            if changed.tag.endswith(role):
                result.append(region)
        return result

    def get_changed_region(
        self,
        position: int = 0,
        text_id: str | None = None,
        creator: str | None = None,
        date: datetime | None = None,
        content: str | None = None,
    ) -> Element | None:
        """Gets a single 'text:changed-region' element by position or criteria.

        Args:
            position: The index of the region to retrieve.
            text_id: Get the region with this specific ID.
            creator: Filter by the author's name.
            date: Filter by the date of the change.
            content: Filter by a regex match in the content.

        Returns:
            Element | None: The matching TextChangedRegion element, or None
                if not found.
        """
        return self._filtered_element(
            "text:changed-region",
            position,
            text_id=text_id,
            dc_creator=creator,
            dc_date=date,
            content=content,
        )

_tag class-attribute instance-attribute

_tag = 'text:tracked-changes'

get_changed_region

get_changed_region(
    position: int = 0,
    text_id: str | None = None,
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
) -> Element | None

Gets a single ‘text:changed-region’ element by position or criteria.

Parameters:

Name Type Description Default
position int

The index of the region to retrieve.

0
text_id str | None

Get the region with this specific ID.

None
creator str | None

Filter by the author’s name.

None
date datetime | None

Filter by the date of the change.

None
content str | None

Filter by a regex match in the content.

None

Returns:

Type Description
Element | None

Element | None: The matching TextChangedRegion element, or None if not found.

Source code in odfdo/tracked_changes.py
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 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:
    """Gets a single 'text:changed-region' element by position or criteria.

    Args:
        position: The index of the region to retrieve.
        text_id: Get the region with this specific ID.
        creator: Filter by the author's name.
        date: Filter by the date of the change.
        content: Filter by a regex match in the content.

    Returns:
        Element | None: The matching TextChangedRegion element, or None
            if not found.
    """
    return self._filtered_element(
        "text:changed-region",
        position,
        text_id=text_id,
        dc_creator=creator,
        dc_date=date,
        content=content,
    )

get_changed_regions

get_changed_regions(
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
    role: str | None = None,
) -> list[Element]

Gets a list of ‘text:changed-region’ elements matching criteria.

Parameters:

Name Type Description Default
creator str | None

Filter by the author’s name.

None
date datetime | None

Filter by the date of the change.

None
content str | None

Filter by a regex match in the content.

None
role str | None

Filter by the type of change (‘insertion’, ‘deletion’, ‘format-change’).

None

Returns:

Type Description
list[Element]

list[Element]: A list of matching TextChangedRegion elements.

Source code in odfdo/tracked_changes.py
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
def get_changed_regions(
    self,
    creator: str | None = None,
    date: datetime | None = None,
    content: str | None = None,
    role: str | None = None,
) -> list[Element]:
    """Gets a list of 'text:changed-region' elements matching criteria.

    Args:
        creator: Filter by the author's name.
        date: Filter by the date of the change.
        content: Filter by a regex match in the content.
        role: Filter by the type of change ('insertion', 'deletion', 'format-change').

    Returns:
        list[Element]: A list of matching TextChangedRegion elements.
    """
    changed_regions = self._filtered_elements(
        "text:changed-region",
        dc_creator=creator,
        dc_date=date,
        content=content,
    )
    if role is None:
        return changed_regions
    result: list[Element] = []
    for region in changed_regions:
        changed = region.get_change_element()  # type: ignore
        if not changed:
            continue  # pragma: nocover
        if changed.tag.endswith(role):
            result.append(region)
    return result

TrackedChangesMixin

Bases: Element

Mixin class for classes containing TrackedChanges.

Used by the following classes: “office:text”, “style:footer”, “style:footer-first”, “style:footer-left”, “style:header”, “style:header-first” and “style:header-left”.

Methods:

Name Description
get_tracked_changes

Return the tracked-changes part in the text body.

Attributes:

Name Type Description
tracked_changes TrackedChanges | None

The ‘text:tracked-changes’ element in the body.

Source code in odfdo/tracked_changes.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
class TrackedChangesMixin(Element):
    """Mixin class for classes containing TrackedChanges.

    Used by the following classes:  "office:text", "style:footer",
    "style:footer-first", "style:footer-left", "style:header",
    "style:header-first" and "style:header-left".
    """

    def get_tracked_changes(self) -> TrackedChanges | None:
        """Return the tracked-changes part in the text body.

        Returns:
            TrackedChanges or None: The tracked changes element, or None if
                not found.
        """
        return cast(
            None | TrackedChanges, self.get_element("//text:tracked-changes")
        )

    @property
    def tracked_changes(self) -> TrackedChanges | None:
        """The 'text:tracked-changes' element in the body.

        This property provides access to the main container for tracked
        changes within the element's scope.
        """
        return self.get_tracked_changes()

tracked_changes property

tracked_changes: TrackedChanges | None

The ‘text:tracked-changes’ element in the body.

This property provides access to the main container for tracked changes within the element’s scope.

get_tracked_changes

get_tracked_changes() -> TrackedChanges | None

Return the tracked-changes part in the text body.

Returns:

Type Description
TrackedChanges | None

TrackedChanges or None: The tracked changes element, or None if not found.

Source code in odfdo/tracked_changes.py
54
55
56
57
58
59
60
61
62
63
def get_tracked_changes(self) -> TrackedChanges | None:
    """Return the tracked-changes part in the text body.

    Returns:
        TrackedChanges or None: The tracked changes element, or None if
            not found.
    """
    return cast(
        None | TrackedChanges, self.get_element("//text:tracked-changes")
    )