Skip to main content

ephemeris_documents/markdown/
reconcile.rs

1//! Splicing selector replacements into an existing Markdown document.
2
3use std::ops::Range;
4
5use super::ast::{MarkdownBlock, MarkdownInline, MarkdownSelector};
6use super::scan::{Targets, Text};
7use crate::error::{DocumentError, InternalError, MarkdownError};
8use crate::source::Source;
9
10impl MarkdownSelector {
11    /// Check the selector on its own.
12    ///
13    /// # Errors
14    /// Returns an error for heading content that spans lines.
15    fn validate(&self) -> Result<(), MarkdownError> {
16        match self {
17            Self::Heading { level: _, content } if content.contains(['\r', '\n']) => {
18                Err(MarkdownError::HeadingContentMultiline)
19            }
20            Self::Heading {
21                level: _,
22                content: _,
23            }
24            | Self::Marker { name: _, blocks: _ } => Ok(()),
25        }
26    }
27
28    /// Fail if this marker renders a heading that one of `others` selects.
29    fn check_shadowing(&self, others: &[Self]) -> Result<(), MarkdownError> {
30        let Self::Marker { name, blocks } = self else {
31            return Ok(());
32        };
33        for other in others {
34            if let Self::Heading { level, content: _ } = other
35                && blocks.iter().any(|block| block.contains_heading(*level))
36            {
37                return Err(MarkdownError::HeadingInsideMarker {
38                    level: level.get(),
39                    name: String::from(name),
40                });
41            }
42        }
43        Ok(())
44    }
45
46    /// Check the selectors without reading any source.
47    ///
48    /// # Errors
49    /// Returns an error for invalid selectors and marker blocks that shadow a
50    /// heading selector.
51    pub(super) fn validate_all(selectors: &[Self]) -> Result<(), MarkdownError> {
52        selectors.iter().try_for_each(Self::validate)?;
53        selectors
54            .iter()
55            .try_for_each(|selector| selector.check_shadowing(selectors))
56    }
57
58    /// Reconcile selected Markdown regions while preserving all source
59    /// outside them. The output uses the line ending of `source`.
60    ///
61    /// # Errors
62    /// Returns an error for malformed Markdown, invalid selectors, marker
63    /// blocks that shadow a heading selector, or overlapping selections.
64    pub(super) fn reconcile_all(
65        source: &Source<'_>,
66        selectors: &[Self],
67    ) -> Result<Vec<u8>, DocumentError> {
68        let Some(text) = source.text() else {
69            return Err(MarkdownError::DocumentMissing.into());
70        };
71        Self::validate_all(selectors)?;
72        let text = Text::new(text);
73        let targets = Targets::scan(text)?;
74        let edits = selectors
75            .iter()
76            .map(|selector| selector.resolve(&targets))
77            .collect::<Result<Vec<_>, _>>()?;
78        let edits = Edits(edits);
79        edits.check_overlap()?;
80        Ok(source.write(&edits.splice(text)?))
81    }
82
83    /// The source range this selector replaces and its rendered replacement.
84    fn resolve(&self, targets: &Targets<'_>) -> Result<Edit, DocumentError> {
85        match self {
86            Self::Heading { level, content } => Ok(Edit {
87                range: targets.heading(*level)?,
88                replacement: MarkdownBlock::render_all(&[MarkdownBlock::Heading {
89                    level: *level,
90                    content: vec![MarkdownInline::Text {
91                        value: content.clone(),
92                    }],
93                }])?,
94            }),
95            Self::Marker { name, blocks } => {
96                let rendered = MarkdownBlock::render_all(blocks)?;
97                Ok(Edit {
98                    range: targets.marker(&String::from(name))?,
99                    replacement: if rendered.is_empty() {
100                        "\n".to_owned()
101                    } else {
102                        format!("\n{rendered}\n")
103                    },
104                })
105            }
106        }
107    }
108}
109
110/// A replacement for a range of the source.
111struct Edit {
112    range: Range<usize>,
113    replacement: String,
114}
115
116/// The replacements of all selectors.
117struct Edits(Vec<Edit>);
118
119impl Edits {
120    /// Fail if two replacements touch the same source.
121    fn check_overlap(&self) -> Result<(), MarkdownError> {
122        for (index, left) in self.0.iter().enumerate() {
123            let left = &left.range;
124            let mut rest = self.0.iter().skip(index.saturating_add(1));
125            if rest.any(|edit| {
126                let right = &edit.range;
127                left == right || (left.start < right.end && right.start < left.end)
128            }) {
129                return Err(MarkdownError::SelectorsOverlapping);
130            }
131        }
132        Ok(())
133    }
134
135    /// The text with every replacement applied.
136    fn splice(mut self, text: Text<'_>) -> Result<String, InternalError> {
137        self.0.sort_by_key(|edit| edit.range.start);
138        let mut output = String::with_capacity(text.len());
139        let mut cursor = 0;
140        for edit in self.0 {
141            output.push_str(text.slice(cursor..edit.range.start)?);
142            output.push_str(&edit.replacement);
143            cursor = edit.range.end;
144        }
145        output.push_str(text.slice(cursor..)?);
146        Ok(output)
147    }
148}
149
150#[cfg(test)]
151mod tests;