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, Tagger, Text};
18use crate::{
19    buffer::PathKind,
20    form::FormId,
21    text::{FormTag, Ghost, Spacer, SwapChar},
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().checked_sub(2)
111            && let Some(strs) = text.get(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::SwapChar(tag) => builder.text.insert_tag(tagger, end, tag),
161                BP::ToString(_) => unsafe { std::hint::unreachable_unchecked() },
162            }
163        }
164
165        match part.try_to_basic() {
166            Ok(part_ref) => push_simple(self, part_ref),
167            Err(BuilderPart::ToString(display)) => self.push_str(display),
168            Err(_) => unsafe { std::hint::unreachable_unchecked() },
169        }
170    }
171
172    /// Whether or not the last added piece was empty
173    ///
174    /// This happens when an empty [`String`] or an empty [`Text`] is
175    /// pushed.
176    pub fn last_was_empty(&self) -> bool {
177        self.last_was_empty
178    }
179
180    /// Pushes an [`impl Display`] to the [`Text`]
181    ///
182    /// If `builder.no_space_after_empty` is set to `true` and
183    /// the argument is equal to `" "`, then it won't be added if
184    /// the previous argument was empty. This is useful especially
185    /// in situations where you expect to be constructing a `Text`
186    /// with spaces in between the elements (like in a status line),
187    /// but you don't want an empty element to just leave to space
188    /// wide gap in between two non empty elements.
189    ///
190    /// [`impl Display`]: std::fmt::Display
191    pub fn push_str<D: Display>(&mut self, d: D) {
192        self.buffer.clear();
193        write!(self.buffer, "{d}").unwrap();
194        if self.buffer.is_empty()
195            || (self.no_space_after_empty && self.buffer == " " && self.last_was_empty)
196        {
197            self.last_was_empty = true;
198        } else {
199            self.last_was_empty = false;
200            let end = self.text.last_point();
201            self.text
202                .apply_change(0, Change::str_insert(&self.buffer, end));
203        }
204    }
205
206    /// Resets the [`Form`]
207    ///
208    /// This is equivalent to pushing the `default` `Form`.
209    ///
210    /// [`Form`]: crate::form::Form
211    pub fn reset_form(&mut self) {
212        let end = self.text.last_point().byte();
213        if let Some((b, last_form)) = self.last_form.take() {
214            self.text.insert_tag(Tagger::basic(), b..end, last_form);
215        }
216    }
217
218    /// Pushes [`Text`] directly
219    fn push_text(&mut self, text: &Text) {
220        self.last_was_empty = text.is_empty();
221        self.text.insert_text(self.text.last_point(), text);
222    }
223
224    /// Pushes [`Text`] directly
225    fn push_builder(&mut self, other: &Builder) {
226        self.last_was_empty = other.text.is_empty();
227
228        let offset = self.text.last_point().byte();
229        self.text.insert_text(offset, &other.text);
230        let end = self.text.last_point().byte();
231
232        if let Some((b, id)) = other.last_form
233            && b < other.text.last_point().byte()
234        {
235            self.text.insert_tag(Tagger::basic(), offset + b..end, id);
236        }
237    }
238}
239
240impl Default for Builder {
241    fn default() -> Self {
242        Builder {
243            text: Text::new(),
244            last_form: None,
245            buffer: String::with_capacity(50),
246            last_was_empty: false,
247            no_space_after_empty: false,
248        }
249    }
250}
251
252impl std::fmt::Debug for Builder {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        f.debug_struct("Builder")
255            .field("text", &self.text)
256            .finish_non_exhaustive()
257    }
258}
259
260impl From<Builder> for Text {
261    fn from(value: Builder) -> Self {
262        value.build()
263    }
264}
265
266/// A part to be pushed to a [`Builder`] by a macro
267#[derive(Clone)]
268pub enum BuilderPart<'a, D: Display = String, _T = ()> {
269    /// Text to be pushed
270    ///
271    /// # Note
272    ///
273    /// Every [`Text`] struct has a `\n` attached at the end,
274    /// but when pushing it to a [`Builder`], said `\n` is
275    /// automatically removed. If you want to keep a `\n` at the
276    /// end, push an additional one.
277    Text(&'a Text),
278    /// A Text Builder
279    ///
280    /// Much like the [`Text`], normally, the [`Builder`] finishes
281    /// with a `\n`, but when pushed to another `Builder`, that `\n`
282    /// is removed as well.
283    Builder(&'a Builder),
284    /// An [`impl Display`](std::fmt::Display) type
285    ToString(&'a D),
286    /// An [`Path`] reference
287    Path(&'a std::path::Path),
288    /// A [`PathKind`]
289    PathKind(Text),
290    /// A [`FormId`]
291    Form(FormTag),
292    /// A spacer for more advanced alignment
293    Spacer(PhantomData<_T>),
294    /// Ghost [`Text`] that is separate from the real thing
295    Ghost(&'a Ghost),
296    /// A [`SwapChar`] tag
297    SwapChar(SwapChar),
298}
299
300impl<'a, D: Display, _T> BuilderPart<'a, D, _T> {
301    fn try_to_basic(self) -> Result<BuilderPart<'a>, Self> {
302        match self {
303            BuilderPart::Text(text) => Ok(BuilderPart::Text(text)),
304            BuilderPart::Builder(builder) => Ok(BuilderPart::Builder(builder)),
305            BuilderPart::ToString(_) => Err(self),
306            BuilderPart::Path(path) => Ok(BuilderPart::Path(path)),
307            BuilderPart::PathKind(text) => Ok(BuilderPart::PathKind(text)),
308            BuilderPart::Form(form_id) => Ok(BuilderPart::Form(form_id)),
309            BuilderPart::Spacer(_) => Ok(BuilderPart::Spacer(PhantomData)),
310            BuilderPart::Ghost(ghost) => Ok(BuilderPart::Ghost(ghost)),
311            BuilderPart::SwapChar(replacement) => Ok(BuilderPart::SwapChar(replacement)),
312        }
313    }
314}
315
316/// Defines which types can be pushed onto a [`Builder`]
317///
318/// Every [`Tag`], as well as any [`Display`] and [`AsRef<Path>`]
319/// types can be pushed to a `Builder`.
320///
321/// [`Tag`]: super::Tag
322pub trait AsBuilderPart<D: Display = String, _T = ()> {
323    /// Gets a [`BuilderPart`] fro this value
324    fn as_builder_part(&self) -> BuilderPart<'_, D, _T>;
325}
326
327macro_rules! implAsBuilderPart {
328    ($type:ident, $value:ident, $result:expr) => {
329        impl AsBuilderPart for $type {
330            fn as_builder_part(&self) -> BuilderPart<'_> {
331                let $value = self;
332                $result
333            }
334        }
335    };
336}
337
338implAsBuilderPart!(Builder, builder, BuilderPart::Builder(builder));
339implAsBuilderPart!(FormId, form_id, BuilderPart::Form(form_id.to_tag(0)));
340implAsBuilderPart!(FormTag, form_tag, BuilderPart::Form(*form_tag));
341implAsBuilderPart!(Spacer, _spacer, BuilderPart::Spacer(PhantomData));
342implAsBuilderPart!(Ghost, ghost, BuilderPart::Ghost(ghost));
343implAsBuilderPart!(SwapChar, replace, BuilderPart::SwapChar(*replace));
344implAsBuilderPart!(Text, text, BuilderPart::Text(text));
345implAsBuilderPart!(Path, path, BuilderPart::Path(path));
346implAsBuilderPart!(PathKind, path, BuilderPart::PathKind(path.name_txt()));
347
348impl<D: Display> AsBuilderPart<D, D> for D {
349    fn as_builder_part(&self) -> BuilderPart<'_, D, D> {
350        BuilderPart::ToString(self)
351    }
352}
353
354/// The standard [`Text`] construction macro
355///
356/// TODO: Docs
357///
358/// [`Text`]: super::Text
359#[macro_export]
360macro_rules! txt {
361    ($($parts:tt)+) => {{
362        #[allow(unused_imports)]
363        use $crate::{
364            __parse_arg__, __parse_form__, __parse_str__, private_exports::format_like
365        };
366
367        let mut builder = $crate::text::Builder::new();
368        let _ = format_like!(
369            __parse_str__,
370            [('{', __parse_arg__, false), ('[', __parse_form__, true)],
371            &mut builder,
372            $($parts)*
373        );
374
375        builder.build()
376    }};
377}
378
379#[macro_export]
380#[doc(hidden)]
381macro_rules! __log__ {
382    ($lvl:expr, $location:expr, $($arg:tt)*) => {{
383        #[allow(unused_must_use)]
384        let text = $crate::text::txt!($($arg)*);
385
386        $crate::context::logs().push_record($crate::context::Record::new(
387            text,
388            $lvl,
389            $location
390        ));
391    }}
392}
393
394#[macro_export]
395#[doc(hidden)]
396macro_rules! __parse_str__ {
397    ($builder:expr, $str:literal) => {{
398        let builder = $builder;
399        builder.push_str($str);
400        builder
401    }};
402}
403
404#[macro_export]
405#[doc(hidden)]
406macro_rules! __parse_arg__ {
407    ($builder:expr,"", $arg:expr) => {{
408        use $crate::text::AsBuilderPart;
409        let builder = $builder;
410        builder.push_builder_part($arg.as_builder_part());
411        builder
412    }};
413    ($builder:expr, $modif:literal, $arg:expr) => {{
414        let builder = $builder;
415        builder.push_str(format!(concat!("{:", $modif, "}"), &$arg));
416        builder
417    }};
418}
419
420#[macro_export]
421#[doc(hidden)]
422macro_rules! __parse_form__ {
423    ($builder:expr, $priority:literal,) => {{
424        use $crate::text::AsBuilderPart;
425        const PRIORITY: u8 = $crate::priority($priority);
426        let builder = $builder;
427        let id = $crate::form::DEFAULT_ID;
428        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
429        builder
430    }};
431    ($builder:expr, $priority:literal, a) => {{
432        use $crate::text::AsBuilderPart;
433        const PRIORITY: u8 = $crate::priority($priority);
434        let builder = $builder;
435        let id = $crate::form::ACCENT_ID;
436        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
437        builder
438    }};
439    ($builder:expr, $priority:literal, $($form:tt)*) => {{
440        use $crate::text::AsBuilderPart;
441        const PRIORITY: u8 = $crate::priority($priority);
442        let builder = $builder;
443        let id = $crate::form::id_of!(concat!($(stringify!($form)),*));
444        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
445        builder
446    }};
447}