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