substrait_validator/output/
comment.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Module for comments.
4//!
5//! [`Comment`]s can be added to nodes between the child edges to attach
6//! additional miscellaneous information that doesn't fit in any of the more
7//! structured types, intended purely to be formatted for and interpreted by
8//! humans.
9
10use crate::output::path;
11
12/// Representation of a comment message intended only for human consumption.
13/// Includes basic formatting information.
14#[derive(Clone, Debug, PartialEq, Eq, Default)]
15pub struct Comment {
16    /// Formatting elements and spans that make up the comment.
17    elements: Vec<Element>,
18}
19
20impl Comment {
21    /// Creates an empty comment.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Adds a piece of plain text to the comment.
27    pub fn plain<S: ToString>(mut self, text: S) -> Self {
28        self.push(Element::Span(text.to_string().into()));
29        self
30    }
31
32    /// Adds a piece of text to the comment that links to the given path.
33    pub fn link<S: ToString>(mut self, text: S, path: path::PathBuf) -> Self {
34        self.push(Element::Span(Span {
35            text: text.to_string(),
36            link: Some(Link::Path(path)),
37        }));
38        self
39    }
40
41    /// Adds a piece of text to the comment that links to the given URL.
42    pub fn url<S: ToString, U: ToString>(mut self, text: S, url: U) -> Self {
43        self.push(Element::Span(Span {
44            text: text.to_string(),
45            link: Some(Link::Url(url.to_string())),
46        }));
47        self
48    }
49
50    /// Adds a newline/paragraph break.
51    pub fn nl(mut self) -> Self {
52        self.push(Element::NewLine);
53        self
54    }
55
56    /// Opens a list.
57    pub fn lo(mut self) -> Self {
58        self.push(Element::ListOpen);
59        self
60    }
61
62    /// Advances to the next list item.
63    pub fn li(mut self) -> Self {
64        self.push(Element::ListNext);
65        self
66    }
67
68    /// Closes the current list.
69    pub fn lc(mut self) -> Self {
70        self.push(Element::ListClose);
71        self
72    }
73
74    /// Pushes an element into this comment.
75    pub fn push(&mut self, element: Element) {
76        // Some pairs of element types should never follow each other, because
77        // one implies the other.
78        match self.elements.pop() {
79            None => self.elements.push(element),
80            Some(Element::Span(s1)) => {
81                if let Element::Span(s2) = element {
82                    let (s1, maybe_s2) = merge_spans(s1, s2);
83                    self.elements.push(Element::Span(s1));
84                    if let Some(s2) = maybe_s2 {
85                        self.elements.push(Element::Span(s2));
86                    }
87                } else {
88                    self.elements.push(Element::Span(s1));
89                    self.elements.push(element);
90                }
91            }
92            Some(Element::NewLine) => {
93                if matches!(element, Element::Span(_)) {
94                    self.elements.push(Element::NewLine);
95                }
96                self.elements.push(element);
97            }
98            Some(Element::ListOpen) => {
99                self.elements.push(Element::ListOpen);
100                if !matches!(element, Element::ListNext) {
101                    self.elements.push(element);
102                }
103            }
104            Some(Element::ListNext) => {
105                self.elements.push(Element::ListNext);
106                if !matches!(element, Element::ListNext) {
107                    self.elements.push(element);
108                }
109            }
110            Some(Element::ListClose) => {
111                self.elements.push(Element::ListClose);
112                if !matches!(element, Element::NewLine) {
113                    self.elements.push(element);
114                }
115            }
116        }
117    }
118
119    /// Pushes a whole other comment's worth of elements into this comment.
120    pub fn extend(&mut self, other: Comment) {
121        let mut it = other.elements.into_iter();
122
123        // The first element of other may need to be merged with its new
124        // predecessor.
125        if let Some(element) = it.next() {
126            self.push(element);
127        }
128
129        // The rest of the elements would already have been merged, so we can
130        // just copy them over.
131        self.elements.extend(it);
132    }
133
134    /// Returns the slice of elements that comprise the comment.
135    ///
136    /// This list is "minimal:"
137    ///  - there are no consecutive newlines, list item tags, or spans with
138    ///    equal formatting (they are merged together);
139    ///  - there are no empty lists, and there is never a list item immediately
140    ///    following a list open tag (as this is redundant).
141    pub fn elements(&self) -> &[Element] {
142        &self.elements
143    }
144}
145
146impl std::fmt::Display for Comment {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        let mut indent = 0;
149        for element in self.elements.iter() {
150            match element {
151                Element::Span(span) => span.fmt(f),
152                Element::NewLine => write!(f, "\n\n{: >1$}", "", indent),
153                Element::ListOpen => {
154                    indent += 3;
155                    write!(f, "\n\n{: >1$}", "- ", indent)
156                }
157                Element::ListNext => {
158                    write!(f, "\n\n{: >1$}", "- ", indent)
159                }
160                Element::ListClose => {
161                    indent -= 3;
162                    write!(f, "\n\n{: >1$}", "", indent)
163                }
164            }?;
165        }
166        Ok(())
167    }
168}
169
170impl From<String> for Comment {
171    fn from(text: String) -> Self {
172        Self {
173            elements: vec![Element::Span(text.into())],
174        }
175    }
176}
177
178/// A comment element.
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub enum Element {
181    /// A span of text. Should not include newlines.
182    Span(Span),
183
184    /// A newline/paragraph break.
185    NewLine,
186
187    /// Starts a new list. Subsequent spans form the text for the first item.
188    ListOpen,
189
190    /// Advances to the next list item.
191    ListNext,
192
193    /// Closes a list.
194    ListClose,
195}
196
197/// Like Comment, but single-line.
198#[derive(Clone, Debug, PartialEq, Default, Eq)]
199pub struct Brief {
200    /// Spans that make up the comment. These are simply concatenated, but
201    /// spans may contain optional formatting information.
202    spans: Vec<Span>,
203}
204
205impl Brief {
206    /// Creates an empty comment.
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Adds a piece of plain text to the comment.
212    pub fn plain<S: ToString>(mut self, text: S) -> Self {
213        self.push(text.to_string().into());
214        self
215    }
216
217    /// Adds a piece of text to the comment that links to the given path.
218    pub fn link<S: ToString>(mut self, text: S, path: path::PathBuf) -> Self {
219        self.push(Span {
220            text: text.to_string(),
221            link: Some(Link::Path(path)),
222        });
223        self
224    }
225
226    /// Adds a piece of text to the comment that links to the given URL.
227    pub fn url<S: ToString, U: ToString>(mut self, text: S, url: U) -> Self {
228        self.push(Span {
229            text: text.to_string(),
230            link: Some(Link::Url(url.to_string())),
231        });
232        self
233    }
234
235    /// Pushes a span into this brief.
236    pub fn push(&mut self, span: Span) {
237        if let Some(s1) = self.spans.pop() {
238            let s2 = span;
239            let (s1, maybe_s2) = merge_spans(s1, s2);
240            self.spans.push(s1);
241            if let Some(s2) = maybe_s2 {
242                self.spans.push(s2);
243            }
244        } else {
245            self.spans.push(span);
246        }
247    }
248
249    /// Pushes a whole other brief's worth of elements into this brief.
250    pub fn extend(&mut self, other: Brief) {
251        let mut it = other.spans.into_iter();
252
253        // The first span of other may need to be merged with its new
254        // predecessor.
255        if let Some(element) = it.next() {
256            self.push(element);
257        }
258
259        // The rest of the spans would already have been merged, so we can
260        // just copy them over.
261        self.spans.extend(it);
262    }
263
264    /// Returns the slice of spans that comprise the brief.
265    pub fn spans(&self) -> &[Span] {
266        &self.spans
267    }
268}
269
270impl std::fmt::Display for Brief {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        for span in self.spans.iter() {
273            span.fmt(f)?;
274        }
275        Ok(())
276    }
277}
278
279impl From<String> for Brief {
280    fn from(text: String) -> Self {
281        Self {
282            spans: vec![text.into()],
283        }
284    }
285}
286
287impl From<Brief> for Comment {
288    fn from(brief: Brief) -> Self {
289        Self {
290            elements: brief.spans.into_iter().map(Element::Span).collect(),
291        }
292    }
293}
294
295/// A span of text within a comment.
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub struct Span {
298    /// The span of text.
299    pub text: String,
300
301    /// Whether this span of text should link to something.
302    pub link: Option<Link>,
303}
304
305impl std::fmt::Display for Span {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(f, "{}", self.text)
308    }
309}
310
311impl From<String> for Span {
312    fn from(text: String) -> Self {
313        Span { text, link: None }
314    }
315}
316
317/// Merges two spans together, if possible. A space is inserted between the
318/// spans if there isn't one already.
319fn merge_spans(mut a: Span, b: Span) -> (Span, Option<Span>) {
320    if b.text.is_empty() {
321        return (a, None);
322    }
323    if !a.text.ends_with(' ') && !b.text.starts_with(' ') {
324        a.text.push(' ');
325    }
326    if a.link == b.link {
327        a.text += &b.text;
328        return (a, None);
329    }
330    (a, Some(b))
331}
332
333/// A link to something.
334#[derive(Clone, Debug, PartialEq, Eq)]
335pub enum Link {
336    /// Link to another node in the tree, via an absolute node path.
337    Path(path::PathBuf),
338
339    /// Link to some external URL.
340    Url(String),
341}