patternfly_yew/components/
truncate.rs

1use yew::html::IntoPropValue;
2use yew::prelude::*;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum TruncateContent {
6    Default(String),
7    Middle(String, String),
8    Start(String),
9}
10
11impl TruncateContent {
12    pub fn middle<S: Into<String>, E: Into<String>>(start: S, end: E) -> Self {
13        Self::Middle(start.into(), end.into())
14    }
15
16    pub fn start<S: Into<String>>(start: S) -> Self {
17        Self::Start(start.into())
18    }
19}
20
21impl From<String> for TruncateContent {
22    fn from(value: String) -> Self {
23        Self::Default(value)
24    }
25}
26
27impl From<&str> for TruncateContent {
28    fn from(value: &str) -> Self {
29        Self::Default(value.to_string())
30    }
31}
32
33impl IntoPropValue<TruncateContent> for String {
34    fn into_prop_value(self) -> TruncateContent {
35        TruncateContent::Default(self)
36    }
37}
38
39impl IntoPropValue<TruncateContent> for &str {
40    fn into_prop_value(self) -> TruncateContent {
41        TruncateContent::Default(self.to_string())
42    }
43}
44
45/// Helps creating content for [`Truncate`].
46pub trait IntoTruncateContent {
47    /// Truncate at the start of the content.
48    fn truncate_start(self) -> TruncateContent;
49
50    /// Truncate `num` characters before the end of the string.
51    fn truncate_before(self, num: usize) -> TruncateContent;
52}
53
54impl<T: ToString> IntoTruncateContent for T {
55    fn truncate_start(self) -> TruncateContent {
56        TruncateContent::Start(self.to_string())
57    }
58
59    /// This function is supposed to truncate `num` characters before the end of the string.
60    ///
61    /// ## Bytes, Code Points, and Grapheme Clusters
62    ///
63    /// However, what it actually does is to truncate the string at the next Unicode code point,
64    /// after `num` bytes (not characters). This is quick and should work reasonably well with
65    /// the Latin 1 character set (or, UTF-8 characters which are represented by a single byte).
66    ///
67    /// Given a string with multi-byte code points, or even grapheme clusters (user-perceived
68    /// characters, which may consists of multiple Unicode code points), this will split at the
69    /// wrong location.
70    ///
71    /// It will still split, and not skip any data. But it might lead to an unexpected (shorter)
72    /// end section.
73    ///
74    /// What about an actual correct implementation? That would be possible by using an additional
75    /// dependency. It would also need to count all code points and grapheme clusters from the
76    /// start of the string. The question is: is that worth it? Maybe, maybe not!?
77    fn truncate_before(self, num: usize) -> TruncateContent {
78        let s = self.to_string();
79        let len = s.len();
80
81        if num == 0 {
82            return TruncateContent::Default(s);
83        }
84
85        if num > len {
86            return TruncateContent::Start(s);
87        }
88
89        let mut end = len - num;
90        loop {
91            if end == 0 {
92                return TruncateContent::Start(s);
93            }
94
95            if s.is_char_boundary(end) {
96                break;
97            }
98
99            // we can't get negative, as we exit the loop when end == 0
100            end -= 1;
101        }
102
103        let (start, end) = s.split_at(end);
104        TruncateContent::Middle(start.to_string(), end.to_string())
105    }
106}
107
108/// Properties for [`Truncate`].
109#[derive(PartialEq, Properties)]
110pub struct TruncateProperties {
111    pub content: TruncateContent,
112
113    #[prop_or_default]
114    pub id: Option<AttrValue>,
115    #[prop_or_default]
116    pub style: Option<AttrValue>,
117    #[prop_or_default]
118    pub class: Classes,
119    #[prop_or_default]
120    pub start_class: Classes,
121    #[prop_or_default]
122    pub end_class: Classes,
123}
124
125/// Truncate component
126///
127/// A **truncate** is a tool used to shorten numeric and non-numeric character strings, typically when the string overflows its container.
128///
129/// See: <https://www.patternfly.org/components/truncate>
130///
131/// ## Properties
132///
133/// Defined by [`TruncateProperties`].
134#[function_component(Truncate)]
135pub fn truncate(props: &TruncateProperties) -> Html {
136    let class = classes!("pf-v5-c-truncate", props.class.clone());
137    let start_class = classes!("pf-v5-c-truncate__start", props.start_class.clone());
138    let end_class = classes!("pf-v5-c-truncate__end", props.end_class.clone());
139
140    html!(
141        <span
142            {class}
143            style={props.style.clone()}
144            id={props.id.clone()}
145        >
146            {
147                match &props.content {
148                    TruncateContent::Default(value) => html!(
149                        <span class={start_class}>{ &value }</span>
150                    ),
151                    TruncateContent::Middle(start, end) => html!(<>
152                        <span class={start_class}>{ &start }</span>
153                        <span class={end_class}>{ &end }</span>
154                    </>),
155                    TruncateContent::Start(value) => html!(<>
156                        <span class={end_class}>{ &value }{ "\u{200E}" }</span>
157                    </>),
158                }
159            }
160        </span>
161    )
162}
163
164#[cfg(test)]
165mod test {
166    use super::*;
167
168    #[test]
169    pub fn test_mid_basic() {
170        let content = "0123456789".truncate_before(5);
171        assert_eq!(
172            TruncateContent::Middle("01234".to_string(), "56789".to_string()),
173            content
174        );
175    }
176
177    #[test]
178    pub fn test_mid_empty() {
179        let content = "".truncate_before(5);
180        assert_eq!(TruncateContent::Start("".to_string()), content);
181    }
182
183    #[test]
184    pub fn test_mid_over() {
185        let content = "0123456789".truncate_before(20);
186        assert_eq!(TruncateContent::Start("0123456789".to_string()), content);
187    }
188
189    #[test]
190    pub fn test_mid_zero() {
191        let content = "0123456789".truncate_before(0);
192        assert_eq!(TruncateContent::Default("0123456789".to_string()), content);
193    }
194}