Skip to main content

rink_core/output/
fmt.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::{borrow::Cow, fmt, iter::Peekable};
6
7use serde_derive::Serialize;
8
9/// Represents a node in a token tree. Each token is tagged with a hint
10/// for how it should be displayed.
11///
12/// To process a list of these objects, you should iterate through it,
13/// building up your final format string as you go. This allows the
14/// addition of color, bold, and other formatting to replies, when
15/// outputting to a format where those are available.
16///
17/// # Example
18///
19/// ```rs
20/// fn write_string<'a>(string: &mut String, obj: &'a dyn TokenFmt<'a>) {
21///     let spans = obj.to_spans();
22///     for span in spans {
23///         match span {
24///             // In most cases you would apply varying formatting based on
25///             // the FmtToken, as otherwise simply using Display would
26///             // suffice.
27///             Span::Content { text, token } => string.push_str(&text),
28///             Span::Child(obj) => write_string(string, obj),
29///         }
30///     }
31/// }
32/// ```
33#[derive(Clone)]
34pub enum Span<'a> {
35    Content { text: Cow<'a, str>, token: FmtToken },
36    Child(&'a dyn TokenFmt<'a>),
37}
38
39impl<'a> Span<'a> {
40    /// Creates a new span given the text and token it should represent.
41    /// This is a leaf in the tree.
42    pub fn new(text: impl Into<Cow<'a, str>>, token: FmtToken) -> Span<'a> {
43        Span::Content {
44            text: text.into(),
45            token,
46        }
47    }
48
49    /// Creates a new span with FmtToken::Plain
50    pub fn plain(text: impl Into<Cow<'a, str>>) -> Span<'a> {
51        Span::new(text, FmtToken::Plain)
52    }
53
54    /// Creates a new span with FmtToken::Error
55    pub fn error(text: impl Into<Cow<'a, str>>) -> Span<'a> {
56        Span::new(text, FmtToken::Error)
57    }
58
59    /// Creates a new span with FmtToken::Unit
60    pub fn unit(text: impl Into<Cow<'a, str>>) -> Span<'a> {
61        Span::new(text, FmtToken::Unit)
62    }
63
64    /// Creates a new span with FmtToken::Quantity
65    pub fn quantity(text: impl Into<Cow<'a, str>>) -> Span<'a> {
66        Span::new(text, FmtToken::Quantity)
67    }
68
69    /// Creates a new span with FmtToken::Number
70    pub fn number(text: impl Into<Cow<'a, str>>) -> Span<'a> {
71        Span::new(text, FmtToken::Number)
72    }
73
74    /// Creates a new span with FmtToken::PropName
75    pub fn prop_name(text: impl Into<Cow<'a, str>>) -> Span<'a> {
76        Span::new(text, FmtToken::PropName)
77    }
78
79    /// Creates a new span with FmtToken::UserInput
80    pub fn user_input(text: impl Into<Cow<'a, str>>) -> Span<'a> {
81        Span::new(text, FmtToken::UserInput)
82    }
83
84    /// Creates a new span with FmtToken::ListBegin
85    pub fn list_begin(text: impl Into<Cow<'a, str>>) -> Span<'a> {
86        Span::new(text, FmtToken::ListBegin)
87    }
88
89    /// Creates a new span with FmtToken::ListSep
90    pub fn list_sep(text: impl Into<Cow<'a, str>>) -> Span<'a> {
91        Span::new(text, FmtToken::ListSep)
92    }
93
94    /// Creates a new span with FmtToken::DocString
95    pub fn doc_string(text: impl Into<Cow<'a, str>>) -> Span<'a> {
96        Span::new(text, FmtToken::DocString)
97    }
98
99    /// Creates a new span with FmtToken::Pow
100    pub fn pow(text: impl Into<Cow<'a, str>>) -> Span<'a> {
101        Span::new(text, FmtToken::Pow)
102    }
103
104    /// Creates a new span with FmtToken::DateTime
105    pub fn date_time(text: impl Into<Cow<'a, str>>) -> Span<'a> {
106        Span::new(text, FmtToken::DateTime)
107    }
108
109    /// Creates a node with children in the token tree, given any object
110    /// that implements TokenFmt.
111    pub fn child(obj: &'a dyn TokenFmt<'a>) -> Span<'a> {
112        Span::Child(obj)
113    }
114
115    /// Creates a new span with FmtToken::Link
116    pub fn link(text: impl Into<Cow<'a, str>>) -> Span<'a> {
117        Span::new(text, FmtToken::Link)
118    }
119
120    // Creates a new span with FmtToken::Keyword
121    pub fn keyword(text: impl Into<Cow<'a, str>>) -> Span<'a> {
122        Span::new(text, FmtToken::Keyword)
123    }
124
125    // Creates a new span with FmtToken::TimeZone
126    pub fn timezone(text: impl Into<Cow<'a, str>>) -> Span<'a> {
127        Span::new(text, FmtToken::TimeZone)
128    }
129
130    pub fn is_ws(&self) -> bool {
131        if let Span::Content { text, .. } = self {
132            text.ends_with(" ")
133        } else {
134            false
135        }
136    }
137}
138
139impl<'a> fmt::Debug for Span<'a> {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            Span::Content { text, token } => write!(f, "({:?}, {:?})", text, token),
143            Span::Child(obj) => {
144                let spans = obj.to_spans();
145                spans.fmt(f)
146            }
147        }
148    }
149}
150
151/// Provides a hint for how a string should be displayed, based on its
152/// contents.
153#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize)]
154#[serde(rename_all = "snake_case")]
155#[non_exhaustive]
156pub enum FmtToken {
157    /// Indicator text that isn't based on user input.
158    /// Generally displayed without any formatting.
159    Plain,
160    /// Similar to plain, but indicates text that is used in an error context.
161    /// Generally displayed in red.
162    Error,
163    /// The name of a unit, like `kilogram`.
164    Unit,
165    /// A quantity like length or time.
166    Quantity,
167    /// A number in any context.
168    Number,
169    /// A string that's derived in some way from user input, but doesn't
170    /// have a more specific usage. This is used for unit not found
171    /// errors.
172    UserInput,
173    /// Text indicating the start of a list, usually a string followed
174    /// by a colon.
175    ListBegin,
176    /// A separator between items in a list, usually a comma or
177    /// semicolon. When a lot of vertical space is available, these can
178    /// be turned into a bulleted list.
179    ListSep,
180    /// A documentation string.
181    DocString,
182    /// A number raised to a power, like `^32`.
183    /// When possible, replace with superscript.
184    Pow,
185    /// The name of a property in a substance.
186    PropName,
187    /// A date time, either being printed, or from user input.
188    /// Sometimes found inside doc strings.
189    /// Suggested parsing:
190    /// 1. ISO 8601 timestamp (yyyy-mm-ddThh:mm:ss +oo:oo)
191    /// 2. ISO 8601 date (yyyy-mm-dd)
192    /// 3. Fallback to displaying original text
193    DateTime,
194    /// A URL, typically found inside of a doc string.
195    /// Intended to be made clickable.
196    /// Generally formatted like `http://example.com`.
197    Link,
198    /// Reserved words in Rink's query language
199    Keyword,
200    /// Timezone conversions in syntax highlighted queries
201    TimeZone,
202}
203
204impl FmtToken {
205    pub fn as_str(self) -> &'static str {
206        match self {
207            FmtToken::Plain => "plain",
208            FmtToken::Error => "error",
209            FmtToken::Unit => "unit",
210            FmtToken::Quantity => "quantity",
211            FmtToken::Number => "number",
212            FmtToken::UserInput => "user_input",
213            FmtToken::ListBegin => "list_begin",
214            FmtToken::ListSep => "list_sep",
215            FmtToken::DocString => "doc_string",
216            FmtToken::Pow => "pow",
217            FmtToken::PropName => "prop_name",
218            FmtToken::DateTime => "date_time",
219            FmtToken::Link => "link",
220            FmtToken::Keyword => "keyword",
221            FmtToken::TimeZone => "time_zone",
222        }
223    }
224}
225
226pub(crate) fn write_spans_string(out: &mut String, spans: &[Span]) {
227    for span in spans {
228        match span {
229            Span::Content { text, .. } => out.push_str(text),
230            Span::Child(child) => write_spans_string(out, &child.to_spans()),
231        }
232    }
233}
234
235/// Allows an object to be converted into a token tree.
236pub trait TokenFmt<'a> {
237    fn to_spans(&'a self) -> Vec<Span<'a>>;
238
239    fn spans_to_string(&'a self) -> String {
240        let mut string = String::new();
241        write_spans_string(&mut string, &self.to_spans());
242        string
243    }
244
245    fn display(&'a self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
246        for span in self.to_spans() {
247            match span {
248                Span::Content { text, .. } => write!(fmt, "{}", text)?,
249                Span::Child(child) => child.display(fmt)?,
250            }
251        }
252        Ok(())
253    }
254}
255
256pub(crate) struct JoinIter<'a, I>
257where
258    I: Iterator,
259{
260    iter: Peekable<I>,
261    sep: Span<'a>,
262    last_was_sep: bool,
263}
264
265pub(crate) fn join<'a, I>(iter: I, sep: Span<'a>) -> impl Iterator<Item = Span<'a>>
266where
267    I: Iterator<Item = Span<'a>>,
268{
269    JoinIter {
270        iter: iter.peekable(),
271        sep,
272        last_was_sep: true,
273    }
274}
275
276impl<'a, I> Iterator for JoinIter<'a, I>
277where
278    I: Iterator<Item = Span<'a>>,
279{
280    type Item = Span<'a>;
281
282    fn next(&mut self) -> Option<Self::Item> {
283        if self.iter.peek().is_some() {
284            if self.last_was_sep {
285                self.last_was_sep = false;
286                self.iter.next()
287            } else {
288                self.last_was_sep = true;
289                Some(self.sep.clone())
290            }
291        } else {
292            None
293        }
294    }
295}
296
297pub(crate) struct FlatJoinIter<'a, I, I2>
298where
299    I: Iterator<Item = I2>,
300    I2: IntoIterator<Item = Span<'a>>,
301{
302    iter: Peekable<I>,
303    sep: Span<'a>,
304    last_was_sep: bool,
305    current: Option<I2::IntoIter>,
306}
307
308pub(crate) fn flat_join<'a, I, I2>(iter: I, sep: Span<'a>) -> impl Iterator<Item = Span<'a>>
309where
310    I: Iterator<Item = I2>,
311    I2: IntoIterator<Item = Span<'a>>,
312{
313    FlatJoinIter {
314        iter: iter.peekable(),
315        sep,
316        last_was_sep: true,
317        current: None,
318    }
319}
320
321impl<'a, I, I2> Iterator for FlatJoinIter<'a, I, I2>
322where
323    I: Iterator<Item = I2>,
324    I2: IntoIterator<Item = Span<'a>>,
325{
326    type Item = Span<'a>;
327
328    fn next(&mut self) -> Option<Self::Item> {
329        if let Some(ref mut current) = self.current {
330            if let Some(next) = current.next() {
331                return Some(next);
332            }
333        }
334        if self.iter.peek().is_some() {
335            if self.last_was_sep {
336                self.last_was_sep = false;
337                let mut new_current = self.iter.next().unwrap().into_iter();
338                let next = new_current.next();
339                self.current = Some(new_current);
340                next
341            } else {
342                self.last_was_sep = true;
343                Some(self.sep.clone())
344            }
345        } else {
346            None
347        }
348    }
349}