Skip to main content

odox_core/edit/
format.rs

1//! Bold, italic, underline and strikethrough over a range of a paragraph's
2//! text. DESIGN.md ยง11.
3//!
4//! The range's ends are cut into the nodes they fall inside, and what lies
5//! between is given the mark in one of two ways: a span the range holds whole
6//! has its style changed, and any other run of nodes is wrapped in a new span,
7//! inside whatever holds it, whose style says only the one thing. The spans
8//! the document already had are never taken apart, and a property is written
9//! only where it changes what is drawn, so that taking a mark off the text it
10//! was put on gives back the paragraph it was.
11//!
12//! A span's style is an automatic style in `content.xml`. One the document
13//! already holds is used again where it says the same thing; otherwise one is
14//! written, named `T` and the first number no text style has.
15//
16// Author: David M. Anderson
17// Built with AI assistance (Claude, Anthropic)
18
19use std::collections::HashSet;
20use std::ops::Range;
21
22use super::{
23    Kind, Refused, is_inline_container, is_paragraph, normalize, parent_of, segments, set_count,
24    text,
25};
26use crate::style::{Family, Styles, TextProperties};
27use crate::xml::{Attribute, Element, Name, Node, Ns};
28
29/// Formatting a range of text is given or has taken off.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Mark {
32    /// Bold.
33    Bold,
34    /// Italic.
35    Italic,
36    /// Underlined.
37    Underline,
38    /// Struck through.
39    Strike,
40}
41
42impl Mark {
43    fn of(self, properties: &TextProperties) -> Option<bool> {
44        match self {
45            Self::Bold => properties.bold,
46            Self::Italic => properties.italic,
47            Self::Underline => properties.underline,
48            Self::Strike => properties.strike,
49        }
50    }
51
52    /// Every attribute of `style:text-properties` that says something about
53    /// the mark: what is cleared before the mark is written.
54    fn attributes(self) -> Vec<(Ns, &'static str)> {
55        let style = |names: &[&'static str]| names.iter().map(|n| (Ns::Style, *n)).collect();
56        match self {
57            Self::Bold => vec![
58                (Ns::Fo, "font-weight"),
59                (Ns::Style, "font-weight-asian"),
60                (Ns::Style, "font-weight-complex"),
61            ],
62            Self::Italic => vec![
63                (Ns::Fo, "font-style"),
64                (Ns::Style, "font-style-asian"),
65                (Ns::Style, "font-style-complex"),
66            ],
67            Self::Underline => style(&[
68                "text-underline-type",
69                "text-underline-style",
70                "text-underline-width",
71                "text-underline-color",
72                "text-underline-mode",
73            ]),
74            Self::Strike => style(&[
75                "text-line-through-type",
76                "text-line-through-style",
77                "text-line-through-width",
78                "text-line-through-color",
79                "text-line-through-text",
80                "text-line-through-text-style",
81                "text-line-through-mode",
82            ]),
83        }
84    }
85
86    /// What is written to give the mark or take it off. Bold and italic are
87    /// written for Asian and complex scripts too, or text in those scripts
88    /// would not change in an application that reads them.
89    fn written(self, on: bool) -> Vec<(Ns, &'static str, &'static str)> {
90        match self {
91            Self::Bold => {
92                let weight = if on { "bold" } else { "normal" };
93                vec![
94                    (Ns::Fo, "font-weight", weight),
95                    (Ns::Style, "font-weight-asian", weight),
96                    (Ns::Style, "font-weight-complex", weight),
97                ]
98            }
99            Self::Italic => {
100                let style = if on { "italic" } else { "normal" };
101                vec![
102                    (Ns::Fo, "font-style", style),
103                    (Ns::Style, "font-style-asian", style),
104                    (Ns::Style, "font-style-complex", style),
105                ]
106            }
107            Self::Underline if on => vec![
108                (Ns::Style, "text-underline-style", "solid"),
109                (Ns::Style, "text-underline-width", "auto"),
110                (Ns::Style, "text-underline-color", "font-color"),
111            ],
112            Self::Underline => vec![(Ns::Style, "text-underline-style", "none")],
113            Self::Strike => vec![(
114                Ns::Style,
115                "text-line-through-style",
116                if on { "solid" } else { "none" },
117            )],
118        }
119    }
120}
121
122/// Whether the characters of a range carry a mark: `Some` when all of them
123/// say the same, `None` when they differ. A range of no characters answers
124/// for the character before it, or after it at the paragraph's start, which
125/// is the one text typed there takes its formatting from; a paragraph with no
126/// characters answers for itself.
127pub fn marked(
128    paragraph: &Element,
129    range: Range<usize>,
130    mark: Mark,
131    styles: &Styles,
132) -> Option<bool> {
133    let base = paragraph_mark(paragraph, mark, styles);
134    let (start, end) = if range.start < range.end {
135        (range.start, range.end)
136    } else if range.start > 0 {
137        (range.start - 1, range.start)
138    } else {
139        (0, 1)
140    };
141    let mut answer = None;
142    for segment in segments(paragraph) {
143        let segment_end = segment.start + segment.kind.len();
144        if segment.kind.len() == 0 || segment_end <= start || end <= segment.start {
145            continue;
146        }
147        let value = mark_at(paragraph, &segment.path, base, mark, styles);
148        if answer.is_some_and(|a| a != value) {
149            return None;
150        }
151        answer = Some(value);
152    }
153    Some(answer.unwrap_or(base))
154}
155
156/// Give a range of the text of the paragraph at a path under the content root
157/// a mark, or take it off, writing into the root's automatic styles whatever
158/// style that needs and telling `styles` of it.
159///
160/// A document with no `office:automatic-styles` is given one before its body,
161/// as the format orders them, which moves the body one place along: a path
162/// held from before the edit is to be taken again.
163///
164/// # Errors
165///
166/// The path does not lead to a paragraph, or the content root does not
167/// declare a namespace the mark is written in. Each is refused before
168/// anything changes.
169pub fn format(
170    content: &mut Element,
171    path: &[usize],
172    range: Range<usize>,
173    mark: Mark,
174    on: bool,
175    styles: &mut Styles,
176) -> Result<(), Refused> {
177    let paragraph = content
178        .at(path)
179        .filter(|e| is_paragraph(e))
180        .ok_or(Refused::NotFound)?;
181    let needs_fo = matches!(mark, Mark::Bold | Mark::Italic);
182    if !content.declares(&Ns::Style) || needs_fo && !content.declares(&Ns::Fo) {
183        return Err(Refused::Namespace);
184    }
185    let len = text(paragraph).chars().count();
186    let end = range.end.min(len);
187    let start = range.start.min(end);
188    if start == end {
189        return Ok(());
190    }
191
192    let mut edited = paragraph.clone();
193    cut_at(&mut edited, end);
194    cut_at(&mut edited, start);
195    // What the range holds: every character inside it, and every element of
196    // no length strictly inside it. One at either end stays outside a span.
197    let selected = segments(&edited)
198        .into_iter()
199        .filter(|s| match s.kind.len() {
200            0 => start < s.start && s.start < end,
201            n => start <= s.start && s.start + n <= end,
202        })
203        .map(|s| s.path)
204        .collect();
205    let base = paragraph_mark(&edited, mark, styles);
206    let root = Element {
207        attrs: content.attrs.clone(),
208        ..element(content.name.clone())
209    };
210    let mut formatter = Formatter {
211        mark,
212        on,
213        selected,
214        automatic: automatic_text_styles(content),
215        text: edited.name.prefix.as_deref().unwrap_or("text").to_owned(),
216        root,
217        styles,
218        minted: Vec::new(),
219    };
220    formatter.within(&mut edited, &mut Vec::new(), base);
221    normalize(&mut edited);
222    let minted = formatter.minted;
223
224    // The paragraph first: writing a new automatic-styles container would
225    // move the body, and the path with it.
226    *content.at_mut(path).ok_or(Refused::NotFound)? = edited;
227    if !minted.is_empty() {
228        let container = automatic_styles(content);
229        container
230            .children
231            .extend(minted.into_iter().map(Node::Element));
232        container.self_closing = false;
233    }
234    Ok(())
235}
236
237/// The name a paragraph, a span or a link is resolved by, as the renderer
238/// resolves it.
239fn style_name(element: &Element) -> &str {
240    element.attr(&Ns::Text, "style-name").unwrap_or("Standard")
241}
242
243/// What the paragraph's own style says, which everything in it reads unless
244/// a span says otherwise.
245fn paragraph_mark(paragraph: &Element, mark: Mark, styles: &Styles) -> bool {
246    mark.of(&styles
247        .resolve(&Family::Paragraph, style_name(paragraph))
248        .text)
249        .unwrap_or(false)
250}
251
252/// What an element's own style says about the mark: a span's or a link's, and
253/// nothing for anything else inside a paragraph.
254fn own(element: &Element, mark: Mark, styles: &Styles) -> Option<bool> {
255    if element.is(&Ns::Text, "span") || element.is(&Ns::Text, "a") {
256        mark.of(&styles.resolve(&Family::Text, style_name(element)).text)
257    } else {
258        None
259    }
260}
261
262/// Whether the node at a path under a paragraph reads the mark: the nearest
263/// container above it that says so, or the paragraph.
264fn mark_at(paragraph: &Element, path: &[usize], base: bool, mark: Mark, styles: &Styles) -> bool {
265    (1..path.len())
266        .filter_map(|depth| paragraph.at(&path[..depth]))
267        .fold(base, |value, e| own(e, mark, styles).unwrap_or(value))
268}
269
270/// Cut the node whose characters surround an offset in two, so that the
271/// offset falls between nodes.
272fn cut_at(paragraph: &mut Element, at: usize) {
273    let Some(segment) = segments(paragraph)
274        .into_iter()
275        .find(|s| s.start < at && at < s.start + s.kind.len())
276    else {
277        return;
278    };
279    let offset = at - segment.start;
280    let Some((parent, index)) = parent_of(paragraph, &segment.path) else {
281        return;
282    };
283    let byte = |t: &str| t.char_indices().nth(offset).map_or(t.len(), |(b, _)| b);
284    let second = match (&mut parent.children[index], segment.kind) {
285        (Node::Text(t), _) => Node::Text(t.split_off(byte(t))),
286        (Node::CData(t), _) => Node::CData(t.split_off(byte(t))),
287        (Node::Element(space), Kind::Spaces(count)) => {
288            let mut second = space.clone();
289            set_count(space, offset);
290            set_count(&mut second, count - offset);
291            Node::Element(second)
292        }
293        _ => return,
294    };
295    parent.children.insert(index + 1, second);
296}
297
298/// The text styles `content.xml` holds among its automatic styles.
299fn automatic_text_styles(content: &Element) -> Vec<Element> {
300    content
301        .child(&Ns::Office, "automatic-styles")
302        .into_iter()
303        .flat_map(Element::elements)
304        .filter(|e| e.is(&Ns::Style, "style") && e.attr(&Ns::Style, "family") == Some("text"))
305        .cloned()
306        .collect()
307}
308
309/// The content root's `office:automatic-styles`, written before the body where
310/// the document has none.
311pub(super) fn automatic_styles(content: &mut Element) -> &mut Element {
312    let found = |local: &'static str| move |n: &Node| matches!(n, Node::Element(e) if e.is(&Ns::Office, local));
313    let at = if let Some(at) = content.children.iter().position(found("automatic-styles")) {
314        at
315    } else {
316        let before = content
317            .children
318            .iter()
319            .position(|n| found("master-styles")(n) || found("body")(n))
320            .unwrap_or(content.children.len());
321        let container = element(content.name_for(&Ns::Office, "automatic-styles"));
322        content.children.insert(before, Node::Element(container));
323        content.self_closing = false;
324        before
325    };
326    let Node::Element(container) = &mut content.children[at] else {
327        unreachable!("found or written as an element");
328    };
329    container
330}
331
332pub(super) fn element(name: Name) -> Element {
333    Element {
334        name,
335        attrs: Vec::new(),
336        children: Vec::new(),
337        self_closing: true,
338    }
339}
340
341/// How much of a node the range holds.
342#[derive(Clone, Copy, PartialEq, Eq)]
343enum Held {
344    /// All of it.
345    Whole,
346    /// Some of what is inside it.
347    Part,
348    /// None of it.
349    Outside,
350    /// A comment or a processing instruction, which a run passes over.
351    Neutral,
352}
353
354/// One range being given a mark, or having it taken off.
355struct Formatter<'a> {
356    mark: Mark,
357    on: bool,
358    /// The paths from the paragraph of what the range holds.
359    selected: HashSet<Vec<usize>>,
360    /// The text styles among the automatic styles, with those written here.
361    automatic: Vec<Element>,
362    /// The prefix the paragraph spells the text namespace with.
363    text: String,
364    /// The content root without its children: what a name written into the
365    /// styles is spelled by.
366    root: Element,
367    styles: &'a mut Styles,
368    /// The styles written here, to go into the tree.
369    minted: Vec<Element>,
370}
371
372impl Formatter<'_> {
373    fn held(&self, node: &Node, path: &mut Vec<usize>) -> Held {
374        match node {
375            Node::Comment(_) | Node::ProcessingInstruction(_) => Held::Neutral,
376            Node::Element(e) if is_inline_container(e) => {
377                let (mut whole, mut some) = (true, false);
378                for (index, child) in e.children.iter().enumerate() {
379                    path.push(index);
380                    match self.held(child, path) {
381                        Held::Whole => some = true,
382                        Held::Part => (some, whole) = (true, false),
383                        Held::Outside => whole = false,
384                        Held::Neutral => {}
385                    }
386                    path.pop();
387                }
388                match (some, whole) {
389                    (true, true) => Held::Whole,
390                    (true, false) => Held::Part,
391                    _ => Held::Outside,
392                }
393            }
394            _ if self.selected.contains(path) => Held::Whole,
395            _ => Held::Outside,
396        }
397    }
398
399    /// Give the mark to what the range holds under an element whose contents
400    /// read `context`. Runs of what it holds whole are wrapped where they read
401    /// otherwise; a span it holds whole whose own style says otherwise is
402    /// restyled; and whatever it holds part of is gone into.
403    fn within(&mut self, element: &mut Element, path: &mut Vec<usize>, context: bool) {
404        let held: Vec<Held> = (0..element.children.len())
405            .map(|index| {
406                path.push(index);
407                let held = self.held(&element.children[index], path);
408                path.pop();
409                held
410            })
411            .collect();
412        let mut out = Vec::with_capacity(element.children.len());
413        let mut run = Vec::new();
414        // Comments after the run so far, which join it if more follows.
415        let mut waiting = Vec::new();
416        for (index, (node, held)) in std::mem::take(&mut element.children)
417            .into_iter()
418            .zip(held)
419            .enumerate()
420        {
421            path.push(index);
422            let contrary = matches!(&node, Node::Element(e)
423                if is_inline_container(e) && own(e, self.mark, self.styles) == Some(!self.on));
424            match (held, node) {
425                (Held::Neutral, node) if !run.is_empty() => waiting.push(node),
426                (Held::Whole, mut node) if !contrary => {
427                    // A container inside a run reads the mark once the run
428                    // does; something deeper in it may still say otherwise.
429                    if let Node::Element(e) = &mut node
430                        && is_inline_container(e)
431                    {
432                        self.within(e, path, self.on);
433                    }
434                    run.append(&mut waiting);
435                    run.push(node);
436                }
437                (held, node) => {
438                    self.close(&mut run, &mut out, context);
439                    out.append(&mut waiting);
440                    match (held, node) {
441                        (Held::Whole, Node::Element(mut e)) if e.is(&Ns::Text, "span") => {
442                            self.within(&mut e, path, self.on);
443                            if self.restyle(&mut e, context) {
444                                out.extend(e.children);
445                            } else {
446                                out.push(Node::Element(e));
447                            }
448                        }
449                        // A link whose own style says otherwise: its contents
450                        // are wrapped inside it.
451                        (Held::Whole, Node::Element(mut e)) => {
452                            self.within(&mut e, path, !self.on);
453                            out.push(Node::Element(e));
454                        }
455                        (Held::Part, Node::Element(mut e)) => {
456                            let inner = own(&e, self.mark, self.styles).unwrap_or(context);
457                            self.within(&mut e, path, inner);
458                            out.push(Node::Element(e));
459                        }
460                        (_, node) => out.push(node),
461                    }
462                }
463            }
464            path.pop();
465        }
466        self.close(&mut run, &mut out, context);
467        out.append(&mut waiting);
468        element.children = out;
469    }
470
471    /// Give a run the mark where its surroundings do not: a lone span by its
472    /// style, anything else by a new span around it.
473    fn close(&mut self, run: &mut Vec<Node>, out: &mut Vec<Node>, context: bool) {
474        if run.is_empty() {
475            return;
476        }
477        if context != self.on {
478            if let [Node::Element(span)] = run.as_mut_slice()
479                && span.is(&Ns::Text, "span")
480            {
481                if own(span, self.mark, self.styles) != Some(self.on) {
482                    // The mark is written, so the span is never left empty.
483                    self.restyle(span, context);
484                }
485            } else {
486                let mut style = self.blank(None);
487                self.write_mark(&mut style);
488                let name = self.name(style);
489                let mut span = element(Name::new(&self.text, "span", Ns::Text));
490                span.set_attr(Name::new(&self.text, "style-name", Ns::Text), name);
491                span.children = std::mem::take(run);
492                span.self_closing = false;
493                out.push(Node::Element(span));
494                return;
495            }
496        }
497        out.append(run);
498    }
499
500    /// Change a span's style so that the span reads the mark, in a parent
501    /// whose contents read `context`. Answers whether the span is left saying
502    /// nothing at all, and should give way to its contents.
503    ///
504    /// The mark can be said two ways: written into the span's style, or left
505    /// out of it where what the span inherits already says it. Left out is
506    /// the smaller, and is taken unless the document has a style that says it
507    /// the written way and none that says it the other, which is what brings
508    /// a span back to the style it had when a mark is put on and taken off.
509    fn restyle(&mut self, span: &mut Element, context: bool) -> bool {
510        let current = span.attr(&Ns::Text, "style-name").map(ToOwned::to_owned);
511        let style = match current.as_deref().and_then(|n| self.automatic(n)) {
512            Some(existing) => existing.clone(),
513            None => self.blank(current.as_deref()),
514        };
515        let mut written = style.clone();
516        self.write_mark(&mut written);
517        let mut bare = style;
518        if let Some(properties) = bare.child_mut(&Ns::Style, "text-properties") {
519            for (ns, local) in self.mark.attributes() {
520                properties.remove_attr(&ns, local);
521            }
522        }
523        bare.children.retain(|n| {
524            !matches!(n, Node::Element(e)
525                if e.is(&Ns::Style, "text-properties") && e.attrs.is_empty() && e.children.is_empty())
526        });
527        // What the span reads with no word of its own on the mark: its
528        // style's parent, or with no parent the family's default, which is
529        // what a name the document does not have resolves to.
530        let parent = bare.attr(&Ns::Style, "parent-style-name");
531        let bare_reads = self
532            .mark
533            .of(&self
534                .styles
535                .resolve(&Family::Text, parent.unwrap_or(""))
536                .text)
537            .unwrap_or(context);
538
539        let style = if bare_reads != self.on {
540            written
541        } else if parent.is_none() && bare.elements().next().is_none() {
542            span.remove_attr(&Ns::Text, "style-name");
543            return span.attrs.is_empty();
544        } else if self.existing(&bare).is_none() && self.existing(&written).is_some() {
545            written
546        } else {
547            bare
548        };
549        let name = self.name(style);
550        match span
551            .attrs
552            .iter_mut()
553            .find(|a| a.name.is(&Ns::Text, "style-name"))
554        {
555            Some(attr) => attr.value = name,
556            None => span.set_attr(Name::new(&self.text, "style-name", Ns::Text), name),
557        }
558        false
559    }
560
561    /// Write the mark into a style's text properties, which it is given
562    /// where it has none. An attribute the style has is changed where it
563    /// stands, and one the mark does not write is taken out.
564    fn write_mark(&self, style: &mut Element) {
565        if style.child(&Ns::Style, "text-properties").is_none() {
566            let properties = element(self.root.name_for(&Ns::Style, "text-properties"));
567            style.children.push(Node::Element(properties));
568            style.self_closing = false;
569        }
570        let Some(properties) = style.child_mut(&Ns::Style, "text-properties") else {
571            return;
572        };
573        let written = self.mark.written(self.on);
574        for (ns, local) in self.mark.attributes() {
575            if !written.iter().any(|(n, l, _)| *n == ns && *l == local) {
576                properties.remove_attr(&ns, local);
577            }
578        }
579        for (ns, local, value) in written {
580            match properties.attrs.iter_mut().find(|a| a.name.is(&ns, local)) {
581                Some(attr) => value.clone_into(&mut attr.value),
582                None => properties.set_attr(self.root.name_for(&ns, local), value),
583            }
584        }
585    }
586
587    /// A text style that says nothing yet, inheriting from `parent`.
588    fn blank(&self, parent: Option<&str>) -> Element {
589        let mut style = element(self.root.name_for(&Ns::Style, "style"));
590        style.set_attr(self.root.name_for(&Ns::Style, "family"), "text");
591        if let Some(parent) = parent {
592            style.set_attr(self.root.name_for(&Ns::Style, "parent-style-name"), parent);
593        }
594        style
595    }
596
597    fn automatic(&self, name: &str) -> Option<&Element> {
598        self.automatic
599            .iter()
600            .find(|s| s.attr(&Ns::Style, "name") == Some(name))
601    }
602
603    /// The name of an automatic style that says what this one does.
604    fn existing(&self, style: &Element) -> Option<&str> {
605        let wanted = unnamed(style);
606        self.automatic
607            .iter()
608            .find(|s| unnamed(s) == wanted)
609            .and_then(|s| s.attr(&Ns::Style, "name"))
610    }
611
612    /// The name of an automatic style that says what this one does, written
613    /// into the styles where none is there yet.
614    fn name(&mut self, mut style: Element) -> String {
615        if let Some(name) = self.existing(&style) {
616            return name.to_owned();
617        }
618        let taken =
619            |n: &str| self.styles.style(&Family::Text, n).is_some() || self.automatic(n).is_some();
620        let mut number = 1;
621        while taken(&format!("T{number}")) {
622            number += 1;
623        }
624        let name = format!("T{number}");
625        style.remove_attr(&Ns::Style, "name");
626        style.attrs.insert(
627            0,
628            Attribute {
629                name: self.root.name_for(&Ns::Style, "name"),
630                value: name.clone(),
631            },
632        );
633        self.styles.add(&style);
634        self.automatic.push(style.clone());
635        self.minted.push(style);
636        name
637    }
638}
639
640/// A style with its name taken off, to compare with another by what it says.
641pub(super) fn unnamed(style: &Element) -> Element {
642    let mut style = style.clone();
643    style.remove_attr(&Ns::Style, "name");
644    style
645}