Skip to main content

duat_core/text/
builder.rs

1//! [`Text`] building macros
2//!
3//! This module defines the [`txt!`] macro, alongside its [`Builder`]
4//! companion struct. This macro is very convenient for the creation
5//! of [`Text`]s, since its syntax is just a superset of the natural
6//! syntax of [`format_args!`], also allowing for the addition of
7//! [`Form`]s through specialized arguments.
8//!
9//! [`Form`]: crate::form::Form
10//! [`txt!`]: crate::text::txt
11use std::{
12    fmt::{Display, Write},
13    marker::PhantomData,
14    path::Path,
15};
16
17use super::{Change, Ns, Text};
18use crate::{
19    buffer::PathKind,
20    form::FormId,
21    text::{FormTag, Inlay, Mask, Spacer},
22};
23
24/// Builds and modifies a [`Text`], based on replacements applied
25/// to it.
26///
27/// This struct is meant to be used alongside the [`txt!`] macro, as
28/// you can just push more `Text` to the `Builder` py pushing
29/// another `Builder`, which can be returned by the `txt!` macro:
30///
31/// ```rust
32/// # use duat_core::text::{Text, txt};
33/// fn is_more_than_two(num: usize) -> Text {
34///     let mut builder = Text::builder();
35///     builder.push(txt!("The number [a]{num}[] is"));
36///     if num > 2 {
37///         builder.push(txt!("[a]more[] than 2."));
38///     } else {
39///         builder.push(txt!("[a]not more[] than 2."));
40///     }
41///     builder.build()
42/// }
43/// ```
44///
45/// In the above call, you can see that `num` was interpolated, just
46/// like with [`println!`], there are also [`Form`]s being applied to
47/// the `Text`. Each `[]` pair denotes a `Form`. These pairs
48/// follow the following rule:
49///
50/// - `[]`: Will push the `"default"` `Form`, which is actually just
51///   removing prior `Form`s.
52/// - `[a]`: Will push the `"accent"` `Form`.
53/// - `[{form}]`: Will push the `"{form}"`, where `{form}` can be any
54///   sequence of valid identifiers, separated by `"."`s.
55///
56/// [`impl Display`]: std::fmt::Display
57/// [`Form`]: crate::form::Form
58/// [`txt!`]: crate::text::txt
59#[derive(Clone)]
60pub struct Builder {
61    text: Text,
62    last_form: Option<(usize, FormTag)>,
63    last_mask: Option<(usize, Mask)>,
64    buffer: String,
65    last_was_empty: bool,
66    /// Wether to collapse `" "`s after an empty element is pushed
67    pub no_space_after_empty: bool,
68}
69
70impl Builder {
71    /// Returns a new instance of [`Builder`]
72    ///
73    /// Use [`Text::builder`] if you don't want to bring `Builder`
74    /// into scope.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Finish construction and returns the [`Text`]
80    ///
81    /// Will also finish the last [`Form`] and alignments pushed to
82    /// the [builder], as well as pushing a `'\n'` at the end, much
83    /// like with regular `Text` construction.
84    ///
85    /// [`Form`]: crate::form::Form
86    /// [builder]: Builder
87    /// [`Builder::into::<Text>`]: Into::into
88    /// [`Widget`]: crate::ui::Widget
89    /// [`StatusLine`]: https://docs.rs/duat/latest/duat/widgets/struct.StatusLine.html
90    pub fn build(mut self) -> Text {
91        if let Some((b, id)) = self.last_form
92            && b < self.text.last_point().byte()
93        {
94            self.text.insert_tag(Ns::basic(), b.., id);
95        }
96
97        if let Some((b, id)) = self.last_mask
98            && b < self.text.last_point().byte()
99        {
100            self.text.insert_tag(Ns::basic(), b.., id);
101        }
102
103        self.text
104    }
105
106    /// Builds the [`Text`], removing any double `\n`s at the end
107    ///
108    /// When building multi-line `Text`, it is common to just insert
109    /// every piece of `Text` with a `\n` at the end. This ends up
110    /// resulting in a `Text` that has two `\n`s at the end, since
111    /// there is always at least one final `\n`.
112    ///
113    /// This function lets you construct the `Text` taking that effect
114    /// into account.
115    pub fn build_no_double_nl(self) -> Text {
116        let mut text = self.build();
117        if let Some(last_last_byte) = text.len().checked_sub(2)
118            && let Some(strs) = text.get(last_last_byte..)
119            && strs == "\n\n"
120        {
121            text.replace_range(last_last_byte..last_last_byte + 1, "");
122        }
123
124        text
125    }
126
127    /// Pushes a part of the text
128    ///
129    /// This can be an [`impl Display`] type, an [`impl Tag`], a
130    /// [`FormId`], or even a [`Path`].
131    ///
132    /// [`impl Display`]: std::fmt::Display
133    /// [`impl Tag`]: super::Tag
134    /// [`Path`]: std::path::Path
135    pub fn push<D: Display, _T>(&mut self, part: impl AsBuilderPart<D, _T>) {
136        self.push_builder_part(part.as_builder_part());
137    }
138
139    #[doc(hidden)]
140    pub fn push_builder_part<_T>(&mut self, part: BuilderPart<impl Display, _T>) {
141        fn push_simple(builder: &mut Builder, part: BuilderPart) {
142            use BuilderPart as BP;
143
144            let end = builder.text.last_point().byte();
145            let ns = Ns::basic();
146
147            match part {
148                BP::Text(text) => builder.push_text(text),
149                BP::Builder(other) => builder.push_builder(other),
150                BP::Path(path) => builder.push_str(path.to_string_lossy()),
151                BP::PathKind(text) => builder.push_text(&text),
152                BP::Form(tag) => {
153                    let last_form = if tag == crate::form::DEFAULT_ID.to_tag(0) {
154                        builder.last_form.take()
155                    } else {
156                        builder.last_form.replace((end, tag))
157                    };
158
159                    if let Some((b, tag)) = last_form
160                        && b < end
161                    {
162                        builder.text.insert_tag(ns, b..end, tag);
163                    }
164                }
165                BP::Spacer(_) => builder.text.insert_tag(ns, end, Spacer),
166                BP::Inlay(ghost) => builder.text.insert_tag(ns, end, ghost.clone()),
167                BP::ToString(_) => unsafe { std::hint::unreachable_unchecked() },
168                BP::Mask(mask) => {
169                    let last_form = if mask == Mask::no_mask() {
170                        builder.last_mask.take()
171                    } else {
172                        builder.last_mask.replace((end, mask))
173                    };
174
175                    if let Some((b, tag)) = last_form
176                        && b < end
177                    {
178                        builder.text.insert_tag(ns, b..end, tag);
179                    }
180                }
181            }
182        }
183
184        match part.try_to_basic() {
185            Ok(part_ref) => push_simple(self, part_ref),
186            Err(BuilderPart::ToString(display)) => self.push_str(display),
187            Err(_) => unsafe { std::hint::unreachable_unchecked() },
188        }
189    }
190
191    /// Whether or not the last added piece was empty
192    ///
193    /// This happens when an empty [`String`] or an empty [`Text`] is
194    /// pushed.
195    pub fn last_was_empty(&self) -> bool {
196        self.last_was_empty
197    }
198
199    /// Pushes an [`impl Display`] to the [`Text`]
200    ///
201    /// If `builder.no_space_after_empty` is set to `true` and
202    /// the argument is equal to `" "`, then it won't be added if
203    /// the previous argument was empty. This is useful especially
204    /// in situations where you expect to be constructing a `Text`
205    /// with spaces in between the elements (like in a status line),
206    /// but you don't want an empty element to just leave to space
207    /// wide gap in between two non empty elements.
208    ///
209    /// [`impl Display`]: std::fmt::Display
210    pub fn push_str<D: Display>(&mut self, d: D) {
211        self.buffer.clear();
212        write!(self.buffer, "{d}").unwrap();
213        if self.buffer.is_empty()
214            || (self.no_space_after_empty && self.buffer == " " && self.last_was_empty)
215        {
216            self.last_was_empty = true;
217        } else {
218            self.last_was_empty = false;
219            let end = self.text.last_point();
220            self.text
221                .apply_change(0, Change::str_insert(&self.buffer, end));
222        }
223    }
224
225    /// Resets the [`Form`]
226    ///
227    /// This is equivalent to pushing the `default` `Form`.
228    ///
229    /// [`Form`]: crate::form::Form
230    pub fn reset_form(&mut self) {
231        let end = self.text.last_point().byte();
232        if let Some((b, last_form)) = self.last_form.take() {
233            self.text.insert_tag(Ns::basic(), b..end, last_form);
234        }
235    }
236
237    /// Pushes [`Text`] directly
238    fn push_text(&mut self, text: &Text) {
239        self.last_was_empty = text.is_empty();
240        self.text.insert_text(self.text.last_point(), text);
241    }
242
243    /// Pushes [`Text`] directly
244    fn push_builder(&mut self, other: &Builder) {
245        self.last_was_empty = other.text.is_empty();
246
247        let offset = self.text.last_point().byte();
248        self.text.insert_text(offset, &other.text);
249        let end = self.text.last_point().byte();
250
251        if let Some((b, id)) = other.last_form
252            && b < other.text.last_point().byte()
253        {
254            self.text.insert_tag(Ns::basic(), offset + b..end, id);
255        }
256    }
257}
258
259impl Default for Builder {
260    fn default() -> Self {
261        Builder {
262            text: Text::new(),
263            last_form: None,
264            last_mask: None,
265            buffer: String::with_capacity(50),
266            last_was_empty: false,
267            no_space_after_empty: false,
268        }
269    }
270}
271
272impl std::fmt::Debug for Builder {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.debug_struct("Builder")
275            .field("text", &self.text)
276            .finish_non_exhaustive()
277    }
278}
279
280impl From<Builder> for Text {
281    fn from(value: Builder) -> Self {
282        value.build()
283    }
284}
285
286/// A part to be pushed to a [`Builder`] by a macro
287#[derive(Clone)]
288pub enum BuilderPart<'a, D: Display = String, _T = ()> {
289    /// Text to be pushed
290    ///
291    /// # Note
292    ///
293    /// Every [`Text`] struct has a `\n` attached at the end,
294    /// but when pushing it to a [`Builder`], said `\n` is
295    /// automatically removed. If you want to keep a `\n` at the
296    /// end, push an additional one.
297    Text(&'a Text),
298    /// A Text Builder
299    ///
300    /// Much like the [`Text`], normally, the [`Builder`] finishes
301    /// with a `\n`, but when pushed to another `Builder`, that `\n`
302    /// is removed as well.
303    Builder(&'a Builder),
304    /// An [`impl Display`](std::fmt::Display) type
305    ToString(&'a D),
306    /// An [`Path`] reference
307    Path(&'a std::path::Path),
308    /// A [`PathKind`]
309    PathKind(Text),
310    /// A [`FormId`]
311    Form(FormTag),
312    /// A spacer for more advanced alignment
313    Spacer(PhantomData<_T>),
314    /// Inlay [`Text`] that is separate from the real thing
315    Inlay(&'a Inlay),
316    /// A mask, which maps applied [`Form`]s.
317    ///
318    /// [`Form`]: crate::form::Form
319    Mask(Mask),
320}
321
322impl<'a, D: Display, _T> BuilderPart<'a, D, _T> {
323    fn try_to_basic(self) -> Result<BuilderPart<'a>, Self> {
324        match self {
325            Self::Text(text) => Ok(BuilderPart::Text(text)),
326            Self::Builder(builder) => Ok(BuilderPart::Builder(builder)),
327            Self::ToString(_) => Err(self),
328            Self::Path(path) => Ok(BuilderPart::Path(path)),
329            Self::PathKind(text) => Ok(BuilderPart::PathKind(text)),
330            Self::Form(form_id) => Ok(BuilderPart::Form(form_id)),
331            Self::Spacer(_) => Ok(BuilderPart::Spacer(PhantomData)),
332            Self::Inlay(ghost) => Ok(BuilderPart::Inlay(ghost)),
333            Self::Mask(mask) => Ok(BuilderPart::Mask(mask)),
334        }
335    }
336}
337
338/// Defines which types can be pushed onto a [`Builder`]
339///
340/// Every [`Tag`], as well as any [`Display`] and [`AsRef<Path>`]
341/// types can be pushed to a `Builder`.
342///
343/// [`Tag`]: super::Tag
344pub trait AsBuilderPart<D: Display = String, _T = ()> {
345    /// Gets a [`BuilderPart`] fro this value
346    fn as_builder_part(&self) -> BuilderPart<'_, D, _T>;
347}
348
349macro_rules! implAsBuilderPart {
350    ($type:ident, $value:ident, $result:expr) => {
351        impl AsBuilderPart for $type {
352            fn as_builder_part(&self) -> BuilderPart<'_> {
353                let $value = self;
354                $result
355            }
356        }
357    };
358}
359
360implAsBuilderPart!(Builder, builder, BuilderPart::Builder(builder));
361implAsBuilderPart!(FormId, form_id, BuilderPart::Form(form_id.to_tag(0)));
362implAsBuilderPart!(FormTag, form_tag, BuilderPart::Form(*form_tag));
363implAsBuilderPart!(Spacer, _spacer, BuilderPart::Spacer(PhantomData));
364implAsBuilderPart!(Inlay, ghost, BuilderPart::Inlay(ghost));
365implAsBuilderPart!(Text, text, BuilderPart::Text(text));
366implAsBuilderPart!(Path, path, BuilderPart::Path(path));
367implAsBuilderPart!(PathKind, path, BuilderPart::PathKind(path.name_txt()));
368implAsBuilderPart!(Mask, mask, BuilderPart::Mask(*mask));
369
370impl<D: Display> AsBuilderPart<D, D> for D {
371    fn as_builder_part(&self) -> BuilderPart<'_, D, D> {
372        BuilderPart::ToString(self)
373    }
374}
375
376/// The standard [`Text`] construction macro
377///
378/// TODO: Docs
379///
380/// [`Text`]: super::Text
381#[macro_export]
382macro_rules! txt {
383    ($($parts:tt)+) => {{
384        #[allow(unused_imports)]
385        use $crate::{
386            __parse_arg__, __parse_form__, __parse_str__, private_exports::format_like
387        };
388
389        let mut builder = $crate::text::Builder::new();
390        let _ = format_like!(
391            __parse_str__,
392            [('{', __parse_arg__, false), ('[', __parse_form__, true)],
393            &mut builder,
394            $($parts)*
395        );
396
397        builder.build()
398    }};
399}
400
401#[macro_export]
402#[doc(hidden)]
403macro_rules! __log__ {
404    ($lvl:expr, $location:expr, $($arg:tt)*) => {{
405        #[allow(unused_must_use)]
406        let text = $crate::text::txt!($($arg)*);
407
408        $crate::context::logs().push_record($crate::context::Record::new(
409            text,
410            $lvl,
411            $location
412        ));
413    }}
414}
415
416#[macro_export]
417#[doc(hidden)]
418macro_rules! __parse_str__ {
419    ($builder:expr, $str:literal) => {{
420        let builder = $builder;
421        builder.push_str($str);
422        builder
423    }};
424}
425
426#[macro_export]
427#[doc(hidden)]
428macro_rules! __parse_arg__ {
429    ($builder:expr,"", $arg:expr) => {{
430        use $crate::text::AsBuilderPart;
431        let builder = $builder;
432        builder.push_builder_part($arg.as_builder_part());
433        builder
434    }};
435    ($builder:expr, $modif:literal, $arg:expr) => {{
436        let builder = $builder;
437        builder.push_str(format!(concat!("{:", $modif, "}"), &$arg));
438        builder
439    }};
440}
441
442#[macro_export]
443#[doc(hidden)]
444macro_rules! __parse_form__ {
445    ($builder:expr, $priority:literal,) => {{
446        use $crate::text::AsBuilderPart;
447        const PRIORITY: u8 = $crate::priority($priority);
448        let builder = $builder;
449        let id = $crate::form::DEFAULT_ID;
450        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
451        builder
452    }};
453    ($builder:expr, $priority:literal, a) => {{
454        use $crate::text::AsBuilderPart;
455        const PRIORITY: u8 = $crate::priority($priority);
456        let builder = $builder;
457        let id = $crate::form::ACCENT_ID;
458        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
459        builder
460    }};
461    ($builder:expr, $priority:literal, $($form:tt)*) => {{
462        use $crate::text::AsBuilderPart;
463        const PRIORITY: u8 = $crate::priority($priority);
464        let builder = $builder;
465        let id = $crate::form::id_of!(concat!($(stringify!($form)),*));
466        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
467        builder
468    }};
469}