Skip to main content

docling_core/
revisions.rs

1//! Tracked changes (#851): the policy a reader applies to a document's
2//! revisions — Pandoc's `--track-changes=accept|reject|all` — and the typed
3//! revision a text run carries under `all`.
4
5use std::fmt;
6use std::str::FromStr;
7
8/// How tracked changes are read (#851), Pandoc's `--track-changes`.
9///
10/// `Accept` shows insertions and leaves deletions out — the text as it
11/// stands once every change is accepted, what the DOCX and ODT readers give
12/// without a policy. `Reject` shows deletions and leaves insertions out.
13/// `All` keeps both as typed revisions ([`Revision`]) with their author and
14/// date. The reader of a format a mode is not implemented for refuses it
15/// (`ConversionError::UnsupportedOption` in docling) rather than answering
16/// with another view.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum TrackChanges {
19    #[default]
20    Accept,
21    Reject,
22    All,
23}
24
25impl TrackChanges {
26    /// The option's accepted spellings, for error messages.
27    pub const ACCEPTED: &'static str = "accept, reject, all";
28
29    /// Parse the wire spelling (case-insensitive); `None` for any other.
30    pub fn parse(s: &str) -> Option<Self> {
31        match s.trim().to_ascii_lowercase().as_str() {
32            "accept" | "accepted" => Some(Self::Accept),
33            "reject" | "rejected" => Some(Self::Reject),
34            "all" => Some(Self::All),
35            _ => None,
36        }
37    }
38
39    pub fn as_str(self) -> &'static str {
40        match self {
41            Self::Accept => "accept",
42            Self::Reject => "reject",
43            Self::All => "all",
44        }
45    }
46}
47
48impl fmt::Display for TrackChanges {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(self.as_str())
51    }
52}
53
54impl FromStr for TrackChanges {
55    type Err = String;
56
57    fn from_str(s: &str) -> Result<Self, String> {
58        Self::parse(s)
59            .ok_or_else(|| format!("track_changes must be one of {}; got {s:?}", Self::ACCEPTED))
60    }
61}
62
63/// What a tracked change did to the run it marks.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum RevisionKind {
66    /// Inserted text, or the destination of a tracked move.
67    Insertion,
68    /// Deleted text, or the source of a tracked move.
69    Deletion,
70}
71
72impl RevisionKind {
73    /// Pandoc's class for the span (`insertion` / `deletion`).
74    pub fn as_str(self) -> &'static str {
75        match self {
76            Self::Insertion => "insertion",
77            Self::Deletion => "deletion",
78        }
79    }
80}
81
82/// A typed revision on a text run (#851), kept under [`TrackChanges::All`]:
83/// the Pandoc AST writes it as `Span .insertion` / `.deletion` with
84/// `author` and `date` attributes, Pandoc's docx reader's shape.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Revision {
87    pub kind: RevisionKind,
88    /// The reviewer (`w:author`), when the file names one.
89    pub author: Option<String>,
90    /// The change's timestamp (`w:date`), as written in the file.
91    pub date: Option<String>,
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn spellings() {
100        assert_eq!("Accept".parse::<TrackChanges>(), Ok(TrackChanges::Accept));
101        assert_eq!(" all ".parse::<TrackChanges>(), Ok(TrackChanges::All));
102        assert_eq!(TrackChanges::parse("rejected"), Some(TrackChanges::Reject));
103        assert!("keep".parse::<TrackChanges>().is_err());
104        assert_eq!(TrackChanges::default().as_str(), "accept");
105    }
106}