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

            
19
use colored::Colorize;
20

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

            
23
/// A String with style
24
#[derive(Clone, Debug, Default, PartialEq, Eq)]
25
#[allow(unused)]
26
pub struct StyledString {
27
    tokens: Vec<Token>,
28
}
29

            
30
#[allow(unused)]
31
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
32
pub enum Format {
33
    Plain,
34
    Ansi,
35
}
36

            
37
#[allow(unused)]
38
#[derive(Clone, Debug, PartialEq, Eq)]
39
struct Token {
40
    content: String,
41
    style: Style,
42
}
43

            
44
#[allow(unused)]
45
impl StyledString {
46
88040
    pub fn new() -> StyledString {
47
88040
        StyledString { tokens: vec![] }
48
    }
49

            
50
121020
    pub fn push(&mut self, content: &str) {
51
121020
        self.push_with(content, Style::new());
52
    }
53

            
54
218735
    pub fn push_with(&mut self, content: &str, style: Style) {
55
218735
        let token = Token::new(content, style);
56
218735
        self.push_token(token);
57
    }
58

            
59
308740
    fn push_token(&mut self, token: Token) {
60
        // Concatenate content to last token if it has the same style
61
308740
        if let Some(last) = self.tokens.last_mut() {
62
220785
            if last.style == token.style {
63
45400
                last.content.push_str(&token.content);
64
45400
                return;
65
            }
66
        }
67
263340
        self.tokens.push(token);
68
    }
69

            
70
51605
    pub fn to_string(&self, format: Format) -> String {
71
51605
        self.tokens
72
51605
            .iter()
73
212401
            .map(|token| token.to_string(format))
74
51605
            .collect::<Vec<String>>()
75
51605
            .join("")
76
    }
77

            
78
48450
    pub fn append(&mut self, other: StyledString) {
79
138455
        for token in other.tokens {
80
90005
            self.push_token(token);
81
        }
82
    }
83

            
84
4865
    pub fn split(&self, delimiter: char) -> Vec<StyledString> {
85
4865
        let mut items = vec![];
86
4865
        let mut item = StyledString::new();
87
19255
        for token in &self.tokens {
88
14390
            let mut substrings = token.content.split(delimiter).collect::<Vec<&str>>();
89
14390
            let first = substrings.remove(0);
90
14390
            if !first.is_empty() {
91
12040
                item.push_with(first, token.style);
92
            }
93
21650
            for substring in substrings {
94
7260
                items.push(item);
95
7260
                item = StyledString::new();
96
7260
                if !substring.is_empty() {
97
                    item.push_with(substring, token.style);
98
                }
99
            }
100
        }
101
4865
        items.push(item);
102
4865
        items
103
    }
104

            
105
4780
    pub fn ends_with(&self, value: &str) -> bool {
106
4780
        self.to_string(Format::Plain).ends_with(value)
107
    }
108

            
109
    /// Returns the length of visible chars.
110
215
    pub fn len(&self) -> usize {
111
241
        self.tokens.iter().fold(0, |acc, t| acc + t.content.len())
112
    }
113

            
114
    /// Checks if this string is empty.
115
215
    pub fn is_empty(&self) -> bool {
116
215
        self.len() == 0
117
    }
118

            
119
    /// Add newlines so each lines of this string has a maximum of `max_width` chars.
120
    pub fn wrap(&self, max_width: usize) -> StyledString {
121
        let mut string = StyledString::new();
122
        let mut width = 0;
123

            
124
        for token in &self.tokens {
125
            let mut chunk = String::new();
126
            let mut it = token.content.chars().peekable();
127

            
128
            // Iterate over each chars of the current token, splitting the current
129
            // token if necessary
130
            while let Some(c) = it.next() {
131
                chunk.push(c);
132
                width += 1;
133

            
134
                if width >= max_width {
135
                    let token = Token::new(&chunk, token.style);
136
                    string.push_token(token);
137
                    if it.peek().is_some() {
138
                        // New lines are always plain
139
                        let nl = Token::new("\n", Style::new());
140
                        string.push_token(nl);
141
                    }
142
                    chunk = String::new();
143
                    width = 0;
144
                }
145
            }
146

            
147
            // Append the last chunk
148
            if !chunk.is_empty() {
149
                let token = Token::new(&chunk, token.style);
150
                string.push_token(token);
151
            }
152
        }
153
        string
154
    }
155
}
156

            
157
impl Token {
158
218735
    pub fn new(content: &str, style: Style) -> Token {
159
218735
        let content = content.to_string();
160
218735
        Token { content, style }
161
    }
162

            
163
202080
    pub fn to_string(&self, format: Format) -> String {
164
202080
        match format {
165
197945
            Format::Plain => self.plain(),
166
4135
            Format::Ansi => self.ansi(),
167
        }
168
    }
169

            
170
197945
    fn plain(&self) -> String {
171
197945
        self.content.to_string()
172
    }
173

            
174
4135
    fn ansi(&self) -> String {
175
4135
        let mut s = self.content.to_string();
176
4135
        if let Some(color) = &self.style.fg {
177
2160
            s = match color {
178
                Color::Blue => {
179
1280
                    if self.style.bold {
180
1280
                        s.blue().bold().to_string()
181
                    } else {
182
                        s.blue().to_string()
183
                    }
184
                }
185
                Color::BrightBlack => {
186
305
                    if self.style.bold {
187
                        s.bright_black().bold().to_string()
188
                    } else {
189
305
                        s.bright_black().to_string()
190
                    }
191
                }
192
                Color::Cyan => {
193
130
                    if self.style.bold {
194
130
                        s.cyan().bold().to_string()
195
                    } else {
196
                        s.cyan().to_string()
197
                    }
198
                }
199
                Color::Green => {
200
30
                    if self.style.bold {
201
20
                        s.green().bold().to_string()
202
                    } else {
203
10
                        s.green().to_string()
204
                    }
205
                }
206
                Color::Magenta => {
207
                    if self.style.bold {
208
                        s.magenta().bold().to_string()
209
                    } else {
210
                        s.magenta().to_string()
211
                    }
212
                }
213
                Color::Purple => {
214
                    if self.style.bold {
215
                        s.purple().bold().to_string()
216
                    } else {
217
                        s.purple().to_string()
218
                    }
219
                }
220
                Color::Red => {
221
405
                    if self.style.bold {
222
400
                        s.red().bold().to_string()
223
                    } else {
224
5
                        s.red().to_string()
225
                    }
226
                }
227
                Color::Yellow => {
228
10
                    if self.style.bold {
229
5
                        s.yellow().bold().to_string()
230
                    } else {
231
5
                        s.yellow().to_string()
232
                    }
233
                }
234
            };
235
1975
        } else if self.style.bold {
236
195
            s = s.bold().to_string();
237
        }
238
4135
        s
239
    }
240
}
241

            
242
#[cfg(test)]
243
mod tests {
244
    use super::*;
245

            
246
    #[test]
247
    fn test_hello() {
248
        crate::text::init_crate_colored();
249

            
250
        let mut message = StyledString::new();
251
        message.push("Hello ");
252
        message.push_with("Bob", Style::new().red());
253
        message.push("!");
254
        assert_eq!(message.to_string(Format::Plain), "Hello Bob!");
255
        assert_eq!(
256
            message.to_string(Format::Ansi),
257
            "Hello \u{1b}[31mBob\u{1b}[0m!"
258
        );
259
    }
260

            
261
    #[test]
262
    fn test_push() {
263
        let mut message = StyledString::new();
264
        message.push("Hello");
265
        message.push(" ");
266
        message.push_with("Bob", Style::new().red());
267
        message.push("!");
268

            
269
        assert_eq!(
270
            message,
271
            StyledString {
272
                tokens: vec![
273
                    Token {
274
                        content: "Hello ".to_string(),
275
                        style: Style::new()
276
                    },
277
                    Token {
278
                        content: "Bob".to_string(),
279
                        style: Style::new().red()
280
                    },
281
                    Token {
282
                        content: "!".to_string(),
283
                        style: Style::new()
284
                    },
285
                ],
286
            }
287
        );
288
    }
289

            
290
    #[test]
291
    fn test_append() {
292
        let mut message1 = StyledString::new();
293
        message1.push("Hello ");
294
        message1.push_with("Bob", Style::new().red());
295
        message1.push("!");
296
        let mut message2 = StyledString::new();
297
        message2.push("Hi ");
298
        message2.push_with("Bill", Style::new().red());
299
        message2.push("!");
300

            
301
        let mut messages = StyledString::new();
302
        messages.push("Hello ");
303
        messages.push_with("Bob", Style::new().red());
304
        messages.push("!");
305
        messages.push("Hi ");
306
        messages.push_with("Bill", Style::new().red());
307
        messages.push("!");
308

            
309
        message1.append(message2);
310
        assert_eq!(message1, messages);
311
    }
312

            
313
    #[test]
314
    fn test_split() {
315
        let mut line = StyledString::new();
316
        line.push("Hello,Hi,");
317
        line.push_with("Hola", Style::new().red());
318
        line.push(",Bye,");
319
        line.push_with("Adios", Style::new().red());
320

            
321
        let mut item0 = StyledString::new();
322
        item0.push("Hello");
323
        let mut item1 = StyledString::new();
324
        item1.push("Hi");
325
        let mut item2 = StyledString::new();
326
        item2.push_with("Hola", Style::new().red());
327
        let mut item3 = StyledString::new();
328
        item3.push("Bye");
329
        let mut item4 = StyledString::new();
330
        item4.push_with("Adios", Style::new().red());
331
        assert_eq!(line.split(','), vec![item0, item1, item2, item3, item4]);
332

            
333
        // Test empty items
334
        let mut line = StyledString::new();
335
        line.push("0,,2,");
336

            
337
        let mut item0 = StyledString::new();
338
        item0.push("0");
339
        let item1 = StyledString::new();
340
        let mut item2 = StyledString::new();
341
        item2.push("2");
342
        let item3 = StyledString::new();
343
        assert_eq!(line.split(','), vec![item0, item1, item2, item3]);
344
    }
345

            
346
    #[test]
347
    fn test_ends_with() {
348
        let mut line = StyledString::new();
349
        line.push("Hello,Hi,");
350
        assert!(line.ends_with(","));
351
        assert!(!line.ends_with("\n"));
352
    }
353

            
354
    #[test]
355
    fn compare_with_crate_colored() {
356
        // These tests are used to check regression against the [colored crate](https://crates.io/crates/colored).
357
        // A short-term objective is to remove the colored crates to manage ansi colors.
358
        let mut message = StyledString::new();
359
        message.push_with("foo", Style::new().red().bold());
360
        assert_eq!(
361
            "foo".red().bold().to_string(),
362
            message.to_string(Format::Ansi),
363
        );
364

            
365
        let mut message = StyledString::new();
366
        message.push_with("bar", Style::new().bold());
367
        assert_eq!("bar".bold().to_string(), message.to_string(Format::Ansi),);
368
    }
369

            
370
    #[test]
371
    fn wrap_single_plain_token() {
372
        let mut line = StyledString::new();
373
        line.push("aaaabbbbcccc");
374

            
375
        let mut wrapped = StyledString::new();
376
        wrapped.push("aaaa\nbbbb\ncccc");
377

            
378
        assert_eq!(line.wrap(4), wrapped);
379
        assert_eq!(line.len(), 12);
380
        assert_eq!(line.wrap(4).len(), 14);
381
    }
382

            
383
    #[test]
384
    fn wrap_single_styled_token() {
385
        let mut line = StyledString::new();
386
        line.push_with("aaaabbbbcccc", Style::new().blue());
387

            
388
        let mut wrapped = StyledString::new();
389
        wrapped.push_with("aaaa", Style::new().blue());
390
        wrapped.push("\n");
391
        wrapped.push_with("bbbb", Style::new().blue());
392
        wrapped.push("\n");
393
        wrapped.push_with("cccc", Style::new().blue());
394

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

            
400
    #[test]
401
    fn wrap_multi_styled_token() {
402
        let mut line = StyledString::new();
403
        line.push_with("aaa", Style::new().blue());
404
        line.push_with("ab", Style::new().green());
405
        line.push_with("bbbccc", Style::new().yellow());
406
        line.push_with("cee", Style::new().purple());
407

            
408
        let mut wrapped = StyledString::new();
409
        wrapped.push_with("aaa", Style::new().blue());
410
        wrapped.push_with("a", Style::new().green());
411
        wrapped.push("\n");
412
        wrapped.push_with("b", Style::new().green());
413
        wrapped.push_with("bbb", Style::new().yellow());
414
        wrapped.push("\n");
415
        wrapped.push_with("ccc", Style::new().yellow());
416
        wrapped.push_with("c", Style::new().purple());
417
        wrapped.push("\n");
418
        wrapped.push_with("ee", Style::new().purple());
419

            
420
        assert_eq!(line.wrap(4), wrapped);
421
    }
422
}