zng_txt/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! String type optimized for sharing.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::{borrow::Cow, fmt, hash::Hash, mem, ops::Deref, sync::Arc};
13
14const INLINE_MAX: usize = (mem::size_of::<usize>() * 3) - 1;
15
16fn inline_to_str(d: &[u8; INLINE_MAX]) -> &str {
17    let utf8 = if let Some(i) = d.iter().position(|&b| b == b'\0') {
18        &d[..i]
19    } else {
20        &d[..]
21    };
22    std::str::from_utf8(utf8).unwrap()
23}
24fn str_to_inline(s: &str) -> [u8; INLINE_MAX] {
25    let mut inline = [b'\0'; INLINE_MAX];
26    inline[..s.len()].copy_from_slice(s.as_bytes());
27    inline
28}
29
30#[derive(Clone)]
31#[repr(u8)]
32enum TxtData {
33    Static(&'static str),
34    Inline([u8; INLINE_MAX]),
35    Arc(Arc<str>),
36    // TxtData is 24 bytes
37    #[allow(clippy::box_collection)]
38    String(Box<String>),
39}
40impl fmt::Debug for TxtData {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        if f.alternate() {
43            match self {
44                Self::Static(s) => write!(f, "Static({s:?})"),
45                Self::Inline(d) => write!(f, "Inline({:?})", inline_to_str(d)),
46                Self::String(s) => write!(f, "String({s:?})"),
47                Self::Arc(s) => write!(f, "Arc({s:?})"),
48            }
49        } else {
50            write!(f, "{:?}", self.deref())
51        }
52    }
53}
54impl fmt::Display for TxtData {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{}", self.deref())
57    }
58}
59impl PartialEq for TxtData {
60    fn eq(&self, other: &Self) -> bool {
61        self.deref() == other.deref()
62    }
63}
64impl Eq for TxtData {}
65impl Hash for TxtData {
66    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
67        Hash::hash(&self.deref(), state)
68    }
69}
70impl Deref for TxtData {
71    type Target = str;
72
73    fn deref(&self) -> &str {
74        match self {
75            TxtData::Static(s) => s,
76            TxtData::Inline(d) => inline_to_str(d),
77            TxtData::String(s) => s.as_str(),
78            TxtData::Arc(s) => s,
79        }
80    }
81}
82
83/// Identifies how a [`Txt`] is currently storing the string data.
84///
85/// Use [`Txt::repr`] to retrieve.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum TxtRepr {
88    /// Text data is stored as a `&'static str`.
89    Static,
90    /// Text data is a small string stored as a null terminated `[u8; {size_of::<usize>() * 3}]`.
91    Inline,
92    /// Text data is stored as a `String`.
93    String,
94    /// Text data is stored as an `Arc<str>`.
95    Arc,
96}
97
98/// Text string type, can be one of multiple internal representations, mostly optimized for sharing and one for editing.
99///
100/// This type dereferences to [`str`] so you can use all methods of that type.
101///
102/// For editing some mutable methods are provided, you can also call [`Txt::to_mut`]
103/// to access all mutating methods of [`String`]. After editing you can call [`Txt::end_mut`] to convert
104/// back to an inner representation optimized for sharing.
105///
106/// See [`Txt::repr`] for more details about the inner representations.
107#[derive(PartialEq, Eq, Hash)]
108pub struct Txt(TxtData);
109/// Clones the text.
110///
111/// If the inner representation is [`TxtRepr::String`] the returned value is in a representation optimized
112/// for sharing, either a static empty, an inlined short or an `Arc<str>` long string.
113impl Clone for Txt {
114    fn clone(&self) -> Self {
115        Self(match &self.0 {
116            TxtData::Static(s) => TxtData::Static(s),
117            TxtData::Inline(d) => TxtData::Inline(*d),
118            TxtData::String(s) => return Self::from_str(s),
119            TxtData::Arc(s) => TxtData::Arc(Arc::clone(s)),
120        })
121    }
122}
123impl Txt {
124    /// New text that is a `&'static str`.
125    pub const fn from_static(s: &'static str) -> Txt {
126        Txt(TxtData::Static(s))
127    }
128
129    /// New text from a [`String`] optimized for editing.
130    ///
131    /// If you don't plan to edit the text after this call consider using [`from_str`] instead.
132    ///
133    /// [`from_str`]: Self::from_str
134    pub fn from_string(s: String) -> Txt {
135        Txt(TxtData::String(Box::new(s)))
136    }
137
138    /// New cloned from `s`.
139    ///
140    /// The text will be internally optimized for sharing, if you plan to edit the text after this call
141    /// consider using [`from_string`] instead.
142    ///
143    /// [`from_string`]: Self::from_string
144    #[expect(clippy::should_implement_trait)] // have implemented trait, this one is infallible.
145    pub fn from_str(s: &str) -> Txt {
146        if s.is_empty() {
147            Self::from_static("")
148        } else if s.len() <= INLINE_MAX && !s.contains('\0') {
149            Self(TxtData::Inline(str_to_inline(s)))
150        } else {
151            Self(TxtData::Arc(Arc::from(s)))
152        }
153    }
154
155    /// New from a shared arc str.
156    ///
157    /// Note that the text can outlive the `Arc`, by cloning the string data when modified or
158    /// to use a more optimal representation, you cannot use the reference count of `s` to track
159    /// the lifetime of the text.
160    ///
161    /// [`from_string`]: Self::from_string
162    pub fn from_arc(s: Arc<str>) -> Txt {
163        if s.is_empty() {
164            Self::from_static("")
165        } else if s.len() <= INLINE_MAX && !s.contains('\0') {
166            Self(TxtData::Inline(str_to_inline(&s)))
167        } else {
168            Self(TxtData::Arc(s))
169        }
170    }
171
172    /// New text that is an inlined `char`.
173    pub fn from_char(c: char) -> Txt {
174        #[allow(clippy::assertions_on_constants)]
175        const _: () = assert!(4 <= INLINE_MAX, "cannot inline char");
176
177        let mut buf = [0u8; 4];
178        let s = c.encode_utf8(&mut buf);
179
180        if s.contains('\0') {
181            return Txt(TxtData::Arc(Arc::from(&*s)));
182        }
183
184        Txt(TxtData::Inline(str_to_inline(s)))
185    }
186
187    /// New text from [`format_args!`], avoids allocation if the text is static (no args) or can fit the inlined representation.
188    pub fn from_fmt(args: std::fmt::Arguments) -> Txt {
189        if let Some(s) = args.as_str() {
190            Txt::from_static(s)
191        } else {
192            let mut r = Txt(TxtData::Inline([b'\0'; INLINE_MAX]));
193            std::fmt::write(&mut r, args).unwrap();
194            r
195        }
196    }
197
198    /// Identifies how the text is currently stored.
199    pub const fn repr(&self) -> TxtRepr {
200        match &self.0 {
201            TxtData::Static(_) => TxtRepr::Static,
202            TxtData::Inline(_) => TxtRepr::Inline,
203            TxtData::String(_) => TxtRepr::String,
204            TxtData::Arc(_) => TxtRepr::Arc,
205        }
206    }
207
208    /// Acquires a mutable reference to a [`String`] buffer.
209    ///
210    /// Converts the text to an internal representation optimized for editing, you can call [`end_mut`] after
211    /// editing to re-optimize the text for sharing.
212    ///
213    /// [`end_mut`]: Self::end_mut
214    pub fn to_mut(&mut self) -> &mut String {
215        self.0 = match mem::replace(&mut self.0, TxtData::Static("")) {
216            TxtData::String(s) => TxtData::String(s),
217            TxtData::Static(s) => TxtData::String(Box::new(s.to_owned())),
218            TxtData::Inline(d) => TxtData::String(Box::new(inline_to_str(&d).to_owned())),
219            TxtData::Arc(s) => TxtData::String(Box::new((*s).to_owned())),
220        };
221
222        if let TxtData::String(s) = &mut self.0 { s } else { unreachable!() }
223    }
224
225    /// Convert the inner representation of the string to not be [`String`]. After
226    /// this call the text can be cheaply cloned.
227    pub fn end_mut(&mut self) {
228        match mem::replace(&mut self.0, TxtData::Static("")) {
229            TxtData::String(s) => {
230                *self = Self::from_str(&s);
231            }
232            already => self.0 = already,
233        }
234    }
235
236    /// Extracts the owned string.
237    ///
238    /// Turns the text to owned if it was borrowed.
239    pub fn into_owned(self) -> String {
240        match self.0 {
241            TxtData::String(s) => *s,
242            TxtData::Static(s) => s.to_owned(),
243            TxtData::Inline(d) => inline_to_str(&d).to_owned(),
244            TxtData::Arc(s) => (*s).to_owned(),
245        }
246    }
247
248    /// Calls [`String::clear`] if the text is owned, otherwise
249    /// replaces `self` with an empty str (`""`).
250    pub fn clear(&mut self) {
251        match &mut self.0 {
252            TxtData::String(s) => s.clear(),
253            d => *d = TxtData::Static(""),
254        }
255    }
256
257    /// Removes the last character from the text and returns it.
258    ///
259    /// Returns None if this `Txt` is empty.
260    ///
261    /// This method only converts to [`TxtRepr::String`] if the
262    /// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
263    pub fn pop(&mut self) -> Option<char> {
264        match &mut self.0 {
265            TxtData::String(s) => s.pop(),
266            TxtData::Static(s) => {
267                if let Some((i, c)) = s.char_indices().last() {
268                    *s = &s[..i];
269                    Some(c)
270                } else {
271                    None
272                }
273            }
274            TxtData::Inline(d) => {
275                let s = inline_to_str(d);
276                if let Some((i, c)) = s.char_indices().last() {
277                    if i > 0 {
278                        *d = str_to_inline(&s[..i]);
279                    } else {
280                        self.0 = TxtData::Static("");
281                    }
282                    Some(c)
283                } else {
284                    None
285                }
286            }
287            TxtData::Arc(_) => self.to_mut().pop(),
288        }
289    }
290
291    /// Shortens this `Txt` to the specified length.
292    ///
293    /// If `new_len` is greater than the text's current length, this has no
294    /// effect.
295    ///
296    /// This method only converts to [`TxtRepr::String`] if the
297    /// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
298    pub fn truncate(&mut self, new_len: usize) {
299        match &mut self.0 {
300            TxtData::String(s) => s.truncate(new_len),
301            TxtData::Static(s) => {
302                if new_len <= s.len() {
303                    assert!(s.is_char_boundary(new_len));
304                    *s = &s[..new_len];
305                }
306            }
307            TxtData::Inline(d) => {
308                if new_len == 0 {
309                    self.0 = TxtData::Static("");
310                } else {
311                    let s = inline_to_str(d);
312                    if new_len < s.len() {
313                        assert!(s.is_char_boundary(new_len));
314                        d[new_len..].iter_mut().for_each(|b| *b = b'\0');
315                    }
316                }
317            }
318            TxtData::Arc(_) => self.to_mut().truncate(new_len),
319        }
320    }
321
322    /// Splits the text into two at the given index.
323    ///
324    /// Returns a new `Txt`. `self` contains bytes `[0, at)`, and
325    /// the returned `Txt` contains bytes `[at, len)`. `at` must be on the
326    /// boundary of a UTF-8 code point.
327    ///
328    /// This method only converts to [`TxtRepr::String`] if the
329    /// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
330    pub fn split_off(&mut self, at: usize) -> Txt {
331        match &mut self.0 {
332            TxtData::String(s) => Txt::from_string(s.split_off(at)),
333            TxtData::Static(s) => {
334                assert!(s.is_char_boundary(at));
335                let other = &s[at..];
336                *s = &s[..at];
337                Txt(TxtData::Static(other))
338            }
339            TxtData::Inline(d) => {
340                let s = inline_to_str(d);
341                assert!(s.is_char_boundary(at));
342                let a_len = at;
343                let b_len = s.len() - at;
344
345                let r = Txt(if b_len == 0 {
346                    TxtData::Static("")
347                } else {
348                    TxtData::Inline(str_to_inline(&s[at..]))
349                });
350
351                if a_len == 0 {
352                    self.0 = TxtData::Static("");
353                } else {
354                    *d = str_to_inline(&s[..at]);
355                }
356
357                r
358            }
359            TxtData::Arc(_) => Txt::from_string(self.to_mut().split_off(at)),
360        }
361    }
362
363    /// Push the character to the end of the text.
364    ///
365    /// This method avoids converting to [`TxtRepr::String`] when the current text
366    /// plus char can fit inlined.
367    pub fn push(&mut self, c: char) {
368        match &mut self.0 {
369            TxtData::String(s) => s.push(c),
370            TxtData::Inline(inlined) => {
371                if let Some(len) = inlined.iter().position(|&c| c == b'\0') {
372                    let c_len = c.len_utf8();
373                    if len + c_len <= INLINE_MAX && c != '\0' {
374                        let mut buf = [0u8; 4];
375                        let s = c.encode_utf8(&mut buf);
376                        inlined[len..len + c_len].copy_from_slice(s.as_bytes());
377                        return;
378                    }
379                }
380                self.to_mut().push(c)
381            }
382            _ => {
383                let len = self.len();
384                let c_len = c.len_utf8();
385                if len + c_len <= INLINE_MAX && c != '\0' {
386                    let mut inlined = str_to_inline(self.as_str());
387                    let mut buf = [0u8; 4];
388                    let s = c.encode_utf8(&mut buf);
389                    inlined[len..len + c_len].copy_from_slice(s.as_bytes());
390
391                    self.0 = TxtData::Inline(inlined);
392                } else {
393                    self.to_mut().push(c)
394                }
395            }
396        }
397    }
398
399    /// Push the string to the end of the text.
400    ///
401    /// This method avoids converting to [`TxtRepr::String`] when the current text
402    /// plus char can fit inlined.
403    pub fn push_str(&mut self, s: &str) {
404        if s.is_empty() {
405            return;
406        }
407
408        match &mut self.0 {
409            TxtData::String(str) => str.push_str(s),
410            TxtData::Inline(inlined) => {
411                if let Some(len) = inlined.iter().position(|&c| c == b'\0')
412                    && len + s.len() <= INLINE_MAX
413                    && !s.contains('\0')
414                {
415                    inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
416                    return;
417                }
418                self.to_mut().push_str(s)
419            }
420            _ => {
421                let len = self.len();
422                if len + s.len() <= INLINE_MAX && !s.contains('\0') {
423                    let mut inlined = str_to_inline(self.as_str());
424                    inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
425
426                    self.0 = TxtData::Inline(inlined);
427                } else {
428                    self.to_mut().push_str(s)
429                }
430            }
431        }
432    }
433
434    /// Borrow the text as a string slice.
435    pub fn as_str(&self) -> &str {
436        self.0.deref()
437    }
438
439    /// Copy the inner static `str` if this text represents one.
440    pub fn as_static_str(&self) -> Option<&'static str> {
441        match self.0 {
442            TxtData::Static(s) => Some(s),
443            _ => None,
444        }
445    }
446}
447impl fmt::Debug for Txt {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        fmt::Debug::fmt(&self.0, f)
450    }
451}
452impl fmt::Display for Txt {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        fmt::Display::fmt(&self.0, f)
455    }
456}
457impl Default for Txt {
458    /// Empty.
459    fn default() -> Self {
460        Self::from_static("")
461    }
462}
463impl std::str::FromStr for Txt {
464    type Err = ();
465
466    fn from_str(s: &str) -> Result<Self, Self::Err> {
467        Ok(Txt::from_str(s))
468    }
469}
470impl From<&'static str> for Txt {
471    fn from(value: &'static str) -> Self {
472        Txt(TxtData::Static(value))
473    }
474}
475impl From<String> for Txt {
476    fn from(value: String) -> Self {
477        Txt(TxtData::String(Box::new(value)))
478    }
479}
480impl From<Cow<'static, str>> for Txt {
481    fn from(value: Cow<'static, str>) -> Self {
482        match value {
483            Cow::Borrowed(s) => Txt(TxtData::Static(s)),
484            Cow::Owned(s) => Txt(TxtData::String(Box::new(s))),
485        }
486    }
487}
488impl From<char> for Txt {
489    fn from(value: char) -> Self {
490        Txt::from_char(value)
491    }
492}
493impl From<Txt> for String {
494    fn from(value: Txt) -> Self {
495        value.into_owned()
496    }
497}
498impl From<Txt> for Cow<'static, str> {
499    fn from(value: Txt) -> Self {
500        match value.0 {
501            TxtData::Static(s) => Cow::Borrowed(s),
502            TxtData::String(s) => Cow::Owned(*s),
503            TxtData::Inline(d) => Cow::Owned(inline_to_str(&d).to_owned()),
504            TxtData::Arc(s) => Cow::Owned((*s).to_owned()),
505        }
506    }
507}
508impl From<Txt> for std::path::PathBuf {
509    fn from(value: Txt) -> Self {
510        value.into_owned().into()
511    }
512}
513impl From<Txt> for Box<dyn std::error::Error> {
514    fn from(err: Txt) -> Self {
515        err.into_owned().into()
516    }
517}
518impl From<Txt> for Box<dyn std::error::Error + Send + Sync> {
519    fn from(err: Txt) -> Self {
520        err.into_owned().into()
521    }
522}
523impl From<Txt> for std::ffi::OsString {
524    fn from(value: Txt) -> Self {
525        String::from(value).into()
526    }
527}
528impl std::ops::Deref for Txt {
529    type Target = str;
530
531    fn deref(&self) -> &Self::Target {
532        self.0.deref()
533    }
534}
535impl AsRef<str> for Txt {
536    fn as_ref(&self) -> &str {
537        self.0.as_ref()
538    }
539}
540impl AsRef<std::path::Path> for Txt {
541    fn as_ref(&self) -> &std::path::Path {
542        self.0.as_ref()
543    }
544}
545impl AsRef<std::ffi::OsStr> for Txt {
546    fn as_ref(&self) -> &std::ffi::OsStr {
547        self.0.as_ref()
548    }
549}
550impl std::borrow::Borrow<str> for Txt {
551    fn borrow(&self) -> &str {
552        self.as_str()
553    }
554}
555impl<'a> std::ops::Add<&'a str> for Txt {
556    type Output = Txt;
557
558    fn add(mut self, rhs: &'a str) -> Self::Output {
559        self += rhs;
560        self
561    }
562}
563impl std::ops::AddAssign<&str> for Txt {
564    fn add_assign(&mut self, rhs: &str) {
565        self.push_str(rhs);
566    }
567}
568impl PartialEq<&str> for Txt {
569    fn eq(&self, other: &&str) -> bool {
570        self.as_str().eq(*other)
571    }
572}
573impl PartialEq<str> for Txt {
574    fn eq(&self, other: &str) -> bool {
575        self.as_str().eq(other)
576    }
577}
578impl PartialEq<String> for Txt {
579    fn eq(&self, other: &String) -> bool {
580        self.as_str().eq(other)
581    }
582}
583impl PartialEq<Txt> for &str {
584    fn eq(&self, other: &Txt) -> bool {
585        other.as_str().eq(*self)
586    }
587}
588impl PartialEq<Txt> for str {
589    fn eq(&self, other: &Txt) -> bool {
590        other.as_str().eq(self)
591    }
592}
593impl PartialEq<Txt> for String {
594    fn eq(&self, other: &Txt) -> bool {
595        other.as_str().eq(self)
596    }
597}
598impl serde::Serialize for Txt {
599    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600    where
601        S: serde::Serializer,
602    {
603        serializer.serialize_str(self.as_str())
604    }
605}
606impl<'de> serde::Deserialize<'de> for Txt {
607    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
608    where
609        D: serde::Deserializer<'de>,
610    {
611        String::deserialize(deserializer).map(Txt::from)
612    }
613}
614impl AsRef<[u8]> for Txt {
615    fn as_ref(&self) -> &[u8] {
616        self.as_str().as_ref()
617    }
618}
619impl std::fmt::Write for Txt {
620    fn write_str(&mut self, s: &str) -> fmt::Result {
621        self.push_str(s);
622        Ok(())
623    }
624}
625impl PartialOrd for Txt {
626    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
627        Some(self.cmp(other))
628    }
629}
630impl Ord for Txt {
631    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
632        self.as_str().cmp(other.as_str())
633    }
634}
635impl FromIterator<char> for Txt {
636    fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> Txt {
637        String::from_iter(iter).into()
638    }
639}
640impl<'a> FromIterator<&'a char> for Txt {
641    fn from_iter<I: IntoIterator<Item = &'a char>>(iter: I) -> Txt {
642        String::from_iter(iter).into()
643    }
644}
645impl<'a> FromIterator<&'a str> for Txt {
646    fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Txt {
647        String::from_iter(iter).into()
648    }
649}
650impl FromIterator<String> for Txt {
651    fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Txt {
652        String::from_iter(iter).into()
653    }
654}
655impl<'a> FromIterator<Cow<'a, str>> for Txt {
656    fn from_iter<I: IntoIterator<Item = Cow<'a, str>>>(iter: I) -> Txt {
657        String::from_iter(iter).into()
658    }
659}
660
661impl FromIterator<Txt> for Txt {
662    fn from_iter<I: IntoIterator<Item = Txt>>(iter: I) -> Txt {
663        let mut iterator = iter.into_iter();
664
665        match iterator.next() {
666            None => Txt::from_static(""),
667            Some(mut buf) => {
668                buf.extend(iterator);
669                buf
670            }
671        }
672    }
673}
674
675impl Extend<char> for Txt {
676    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
677        if let TxtData::String(s) = &mut self.0 {
678            s.extend(iter);
679        } else {
680            let iter = iter.into_iter();
681            let (lower_bound, _) = iter.size_hint();
682
683            if self.len() + lower_bound < INLINE_MAX {
684                // avoid alloc
685                for c in iter {
686                    self.push(c);
687                }
688            } else {
689                self.to_mut().extend(iter);
690            }
691        }
692    }
693}
694impl<'a> Extend<&'a char> for Txt {
695    fn extend<I: IntoIterator<Item = &'a char>>(&mut self, iter: I) {
696        self.extend(iter.into_iter().cloned());
697    }
698}
699impl<'a> Extend<&'a str> for Txt {
700    fn extend<I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
701        iter.into_iter().for_each(move |s| self.push_str(s));
702    }
703}
704impl Extend<String> for Txt {
705    fn extend<I: IntoIterator<Item = String>>(&mut self, iter: I) {
706        iter.into_iter().for_each(move |s| self.push_str(&s));
707    }
708}
709impl<'a> Extend<Cow<'a, str>> for Txt {
710    fn extend<I: IntoIterator<Item = Cow<'a, str>>>(&mut self, iter: I) {
711        iter.into_iter().for_each(move |s| self.push_str(&s));
712    }
713}
714
715impl Extend<Txt> for Txt {
716    fn extend<I: IntoIterator<Item = Txt>>(&mut self, iter: I) {
717        iter.into_iter().for_each(move |s| self.push_str(&s));
718    }
719}
720
721/// A trait for converting a value to a [`Txt`].
722///
723/// This trait is automatically implemented for any type that implements the [`ToString`] trait.
724///
725/// You can use [`formatx!`](macro.formatx.html) to `format!` a text.
726pub trait ToTxt {
727    /// Converts the given value to an owned [`Txt`].
728    ///
729    /// # Examples
730    ///
731    /// Basic usage:
732    ///
733    /// ```
734    /// use zng_txt::*;
735    ///
736    /// let expected = formatx!("10");
737    /// let actual = 10.to_txt();
738    ///
739    /// assert_eq!(expected, actual);
740    /// ```
741    fn to_txt(&self) -> Txt;
742}
743impl<T: ToString> ToTxt for T {
744    fn to_txt(&self) -> Txt {
745        self.to_string().into()
746    }
747}
748
749///<span data-del-macro-root></span> Creates a [`Txt`] by formatting using the [`format_args!`] syntax.
750///
751/// Note that this behaves like a [`format!`] for [`Txt`], but it can be more performant because the
752/// text type can represent `&'static str` and can i
753///
754/// # Examples
755///
756/// ```
757/// # use zng_txt::formatx;
758/// let text = formatx!("Hello {}", "World!");
759/// ```
760#[macro_export]
761macro_rules! formatx {
762    ($($tt:tt)*) => {
763        {
764            let res = $crate::Txt::from_fmt(format_args!($($tt)*));
765            res
766        }
767    };
768}