1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use colored::Colorize;
19

            
20
use crate::text::style::{Color, Style};
21

            
22
/// A String with style.
23
///
24
/// A styled string can be composed of styled parts (tokens). A token has a style (an optional
25
/// foreground color with a bold attribute) and a string content.
26
///
27
/// A styled string can be rendered as plain text, or as a string with
28
/// [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). It's useful to make text
29
/// that can be colored or plain at runtime.
30
#[derive(Clone, Debug, Default, PartialEq, Eq)]
31
pub struct StyledString {
32
    /// A list of tokens (styled parts).
33
    tokens: Vec<Token>,
34
}
35

            
36
/// Represents part of a [`StyledString`].
37
#[derive(Clone, Debug, PartialEq, Eq)]
38
struct Token {
39
    /// The string content of the token.
40
    content: String,
41
    /// The style of the token, a foreground color with a bold attribute.
42
    style: Style,
43
}
44

            
45
/// The format in which a [`StyledString`] can be rendered.
46
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
47
pub enum Format {
48
    Plain,
49
    Ansi,
50
}
51

            
52
impl StyledString {
53
    /// Creates an empty instance of a styled string.
54
89820
    pub fn new() -> StyledString {
55
89820
        StyledString { tokens: vec![] }
56
    }
57

            
58
    /// Appends a given `content` without any style onto this end of this `StyledString`
59
129595
    pub fn push(&mut self, content: &str) {
60
129595
        self.push_with(content, Style::new());
61
    }
62

            
63
    /// Appends a given `content` with a specific `style`.
64
233570
    pub fn push_with(&mut self, content: &str, style: Style) {
65
233570
        let token = Token::new(content, style);
66
233570
        self.push_token(token);
67
    }
68

            
69
327750
    fn push_token(&mut self, token: Token) {
70
        // Concatenate content to last token if it has the same style
71
327750
        if let Some(last) = self.tokens.last_mut() {
72
238015
            if last.style == token.style {
73
46160
                last.content.push_str(&token.content);
74
46160
                return;
75
            }
76
        }
77
281590
        self.tokens.push(token);
78
    }
79

            
80
    /// Renders a styled string given a `format`.
81
53215
    pub fn to_string(&self, format: Format) -> String {
82
53215
        self.tokens
83
53215
            .iter()
84
230623
            .map(|token| token.to_string(format))
85
53215
            .collect::<Vec<String>>()
86
53215
            .join("")
87
    }
88

            
89
    /// Appends a styled string.
90
52355
    pub fn append(&mut self, other: StyledString) {
91
146535
        for token in other.tokens {
92
94180
            self.push_token(token);
93
        }
94
    }
95

            
96
    /// Splits a styled string to a given list of [`StyledString`], given a `delimiter`.
97
4885
    pub fn split(&self, delimiter: char) -> Vec<StyledString> {
98
4885
        let mut items = vec![];
99
4885
        let mut item = StyledString::new();
100
19375
        for token in &self.tokens {
101
14490
            let mut substrings = token.content.split(delimiter).collect::<Vec<&str>>();
102
14490
            let first = substrings.remove(0);
103
14490
            if !first.is_empty() {
104
12110
                item.push_with(first, token.style);
105
            }
106
21800
            for substring in substrings {
107
7310
                items.push(item);
108
7310
                item = StyledString::new();
109
7310
                if !substring.is_empty() {
110
                    item.push_with(substring, token.style);
111
                }
112
            }
113
        }
114
4885
        items.push(item);
115
4885
        items
116
    }
117

            
118
    /// Tests if this styled string ends with a given `value`, no matter what's the style of the string.
119
4800
    pub fn ends_with(&self, value: &str) -> bool {
120
4800
        self.to_string(Format::Plain).ends_with(value)
121
    }
122

            
123
    /// Returns the length of visible chars.
124
215
    pub fn len(&self) -> usize {
125
241
        self.tokens.iter().fold(0, |acc, t| acc + t.content.len())
126
    }
127

            
128
    /// Checks if this string is empty.
129
215
    pub fn is_empty(&self) -> bool {
130
215
        self.len() == 0
131
    }
132

            
133
    /// Add newlines so each lines of this string has a maximum of `max_width` chars.
134
    pub fn wrap(&self, max_width: usize) -> StyledString {
135
        let mut string = StyledString::new();
136
        let mut width = 0;
137

            
138
        for token in &self.tokens {
139
            let mut chunk = String::new();
140
            let mut it = token.content.chars().peekable();
141

            
142
            // Iterate over each chars of the current token, splitting the current
143
            // token if necessary
144
            while let Some(c) = it.next() {
145
                chunk.push(c);
146
                width += 1;
147

            
148
                if width >= max_width {
149
                    let token = Token::new(&chunk, token.style);
150
                    string.push_token(token);
151
                    if it.peek().is_some() {
152
                        // New lines are always plain
153
                        let nl = Token::new("\n", Style::new());
154
                        string.push_token(nl);
155
                    }
156
                    chunk = String::new();
157
                    width = 0;
158
                }
159
            }
160

            
161
            // Append the last chunk
162
            if !chunk.is_empty() {
163
                let token = Token::new(&chunk, token.style);
164
                string.push_token(token);
165
            }
166
        }
167
        string
168
    }
169
}
170

            
171
/// Represents part of a styled string.
172
impl Token {
173
233570
    fn new(content: &str, style: Style) -> Token {
174
233570
        let content = content.to_string();
175
233570
        Token { content, style }
176
    }
177

            
178
219980
    fn to_string(&self, format: Format) -> String {
179
219980
        match format {
180
215220
            Format::Plain => self.plain(),
181
4760
            Format::Ansi => self.ansi(),
182
        }
183
    }
184

            
185
215220
    fn plain(&self) -> String {
186
215220
        self.content.to_string()
187
    }
188

            
189
4760
    fn ansi(&self) -> String {
190
4760
        let mut s = self.content.to_string();
191
4760
        if let Some(color) = &self.style.fg {
192
2495
            s = match color {
193
                Color::Blue => {
194
1425
                    if self.style.bold {
195
1425
                        s.blue().bold().to_string()
196
                    } else {
197
                        s.blue().to_string()
198
                    }
199
                }
200
                Color::BrightBlack => {
201
335
                    if self.style.bold {
202
                        s.bright_black().bold().to_string()
203
                    } else {
204
335
                        s.bright_black().to_string()
205
                    }
206
                }
207
                Color::Cyan => {
208
220
                    if self.style.bold {
209
220
                        s.cyan().bold().to_string()
210
                    } else {
211
                        s.cyan().to_string()
212
                    }
213
                }
214
                Color::Green => {
215
45
                    if self.style.bold {
216
35
                        s.green().bold().to_string()
217
                    } else {
218
10
                        s.green().to_string()
219
                    }
220
                }
221
                Color::Magenta => {
222
                    if self.style.bold {
223
                        s.magenta().bold().to_string()
224
                    } else {
225
                        s.magenta().to_string()
226
                    }
227
                }
228
                Color::Purple => {
229
                    if self.style.bold {
230
                        s.purple().bold().to_string()
231
                    } else {
232
                        s.purple().to_string()
233
                    }
234
                }
235
                Color::Red => {
236
460
                    if self.style.bold {
237
455
                        s.red().bold().to_string()
238
                    } else {
239
5
                        s.red().to_string()
240
                    }
241
                }
242
                Color::Yellow => {
243
10
                    if self.style.bold {
244
5
                        s.yellow().bold().to_string()
245
                    } else {
246
5
                        s.yellow().to_string()
247
                    }
248
                }
249
            };
250
2265
        } else if self.style.bold {
251
215
            s = s.bold().to_string();
252
        }
253
4760
        s
254
    }
255
}
256

            
257
#[cfg(test)]
258
mod tests {
259
    use super::*;
260

            
261
    #[test]
262
    fn test_hello() {
263
        crate::text::init_crate_colored();
264

            
265
        let mut message = StyledString::new();
266
        message.push("Hello ");
267
        message.push_with("Bob", Style::new().red());
268
        message.push("!");
269
        assert_eq!(message.to_string(Format::Plain), "Hello Bob!");
270
        assert_eq!(
271
            message.to_string(Format::Ansi),
272
            "Hello \u{1b}[31mBob\u{1b}[0m!"
273
        );
274
    }
275

            
276
    #[test]
277
    fn test_push() {
278
        let mut message = StyledString::new();
279
        message.push("Hello");
280
        message.push(" ");
281
        message.push_with("Bob", Style::new().red());
282
        message.push("!");
283

            
284
        assert_eq!(
285
            message,
286
            StyledString {
287
                tokens: vec![
288
                    Token {
289
                        content: "Hello ".to_string(),
290
                        style: Style::new()
291
                    },
292
                    Token {
293
                        content: "Bob".to_string(),
294
                        style: Style::new().red()
295
                    },
296
                    Token {
297
                        content: "!".to_string(),
298
                        style: Style::new()
299
                    },
300
                ],
301
            }
302
        );
303
    }
304

            
305
    #[test]
306
    fn test_append() {
307
        let mut message1 = StyledString::new();
308
        message1.push("Hello ");
309
        message1.push_with("Bob", Style::new().red());
310
        message1.push("!");
311
        let mut message2 = StyledString::new();
312
        message2.push("Hi ");
313
        message2.push_with("Bill", Style::new().red());
314
        message2.push("!");
315

            
316
        let mut messages = StyledString::new();
317
        messages.push("Hello ");
318
        messages.push_with("Bob", Style::new().red());
319
        messages.push("!");
320
        messages.push("Hi ");
321
        messages.push_with("Bill", Style::new().red());
322
        messages.push("!");
323

            
324
        message1.append(message2);
325
        assert_eq!(message1, messages);
326
    }
327

            
328
    #[test]
329
    fn test_split() {
330
        let mut line = StyledString::new();
331
        line.push("Hello,Hi,");
332
        line.push_with("Hola", Style::new().red());
333
        line.push(",Bye,");
334
        line.push_with("Adios", Style::new().red());
335

            
336
        let mut item0 = StyledString::new();
337
        item0.push("Hello");
338
        let mut item1 = StyledString::new();
339
        item1.push("Hi");
340
        let mut item2 = StyledString::new();
341
        item2.push_with("Hola", Style::new().red());
342
        let mut item3 = StyledString::new();
343
        item3.push("Bye");
344
        let mut item4 = StyledString::new();
345
        item4.push_with("Adios", Style::new().red());
346
        assert_eq!(line.split(','), vec![item0, item1, item2, item3, item4]);
347

            
348
        // Test empty items
349
        let mut line = StyledString::new();
350
        line.push("0,,2,");
351

            
352
        let mut item0 = StyledString::new();
353
        item0.push("0");
354
        let item1 = StyledString::new();
355
        let mut item2 = StyledString::new();
356
        item2.push("2");
357
        let item3 = StyledString::new();
358
        assert_eq!(line.split(','), vec![item0, item1, item2, item3]);
359
    }
360

            
361
    #[test]
362
    fn test_ends_with() {
363
        let mut line = StyledString::new();
364
        line.push("Hello,Hi,");
365
        assert!(line.ends_with(","));
366
        assert!(!line.ends_with("\n"));
367
    }
368

            
369
    #[test]
370
    fn compare_with_crate_colored() {
371
        // These tests are used to check regression against the [colored crate](https://crates.io/crates/colored).
372
        // A short-term objective is to remove the colored crates to manage ansi colors.
373
        let mut message = StyledString::new();
374
        message.push_with("foo", Style::new().red().bold());
375
        assert_eq!(
376
            "foo".red().bold().to_string(),
377
            message.to_string(Format::Ansi),
378
        );
379

            
380
        let mut message = StyledString::new();
381
        message.push_with("bar", Style::new().bold());
382
        assert_eq!("bar".bold().to_string(), message.to_string(Format::Ansi),);
383
    }
384

            
385
    #[test]
386
    fn wrap_single_plain_token() {
387
        let mut line = StyledString::new();
388
        line.push("aaaabbbbcccc");
389

            
390
        let mut wrapped = StyledString::new();
391
        wrapped.push("aaaa\nbbbb\ncccc");
392

            
393
        assert_eq!(line.wrap(4), wrapped);
394
        assert_eq!(line.len(), 12);
395
        assert_eq!(line.wrap(4).len(), 14);
396
    }
397

            
398
    #[test]
399
    fn wrap_single_styled_token() {
400
        let mut line = StyledString::new();
401
        line.push_with("aaaabbbbcccc", Style::new().blue());
402

            
403
        let mut wrapped = StyledString::new();
404
        wrapped.push_with("aaaa", Style::new().blue());
405
        wrapped.push("\n");
406
        wrapped.push_with("bbbb", Style::new().blue());
407
        wrapped.push("\n");
408
        wrapped.push_with("cccc", Style::new().blue());
409

            
410
        assert_eq!(line.wrap(4), wrapped);
411
        assert_eq!(line.len(), 12);
412
        assert_eq!(line.wrap(4).len(), 14);
413
    }
414

            
415
    #[test]
416
    fn wrap_multi_styled_token() {
417
        let mut line = StyledString::new();
418
        line.push_with("aaa", Style::new().blue());
419
        line.push_with("ab", Style::new().green());
420
        line.push_with("bbbccc", Style::new().yellow());
421
        line.push_with("cee", Style::new().purple());
422

            
423
        let mut wrapped = StyledString::new();
424
        wrapped.push_with("aaa", Style::new().blue());
425
        wrapped.push_with("a", Style::new().green());
426
        wrapped.push("\n");
427
        wrapped.push_with("b", Style::new().green());
428
        wrapped.push_with("bbb", Style::new().yellow());
429
        wrapped.push("\n");
430
        wrapped.push_with("ccc", Style::new().yellow());
431
        wrapped.push_with("c", Style::new().purple());
432
        wrapped.push("\n");
433
        wrapped.push_with("ee", Style::new().purple());
434

            
435
        assert_eq!(line.wrap(4), wrapped);
436
    }
437
}