bread/
lib.rs

1extern crate term;
2
3use std::io::Write;
4use State::{Beginning, Tag, Inside, InsideColor, InsideBool};
5use Token::{Attribute,
6            Reset,
7            Literal,
8            Partial,
9};
10use term::{StdoutTerminal, Attr};
11pub use term::Attr::{
12    Bold,
13    Dim,
14    Italic,
15    Underline,
16    Blink,
17    Standout,
18    Reverse,
19    Secure,
20    ForegroundColor,
21    BackgroundColor,
22};
23use PartialToken as PT;
24pub use term::color::Color as Color;
25pub use term::color::{
26    BLACK,
27    BLUE,
28    BRIGHT_BLACK,
29    BRIGHT_BLUE,
30    BRIGHT_CYAN,
31    BRIGHT_GREEN,
32    BRIGHT_MAGENTA,
33    BRIGHT_RED,
34    BRIGHT_WHITE,
35    BRIGHT_YELLOW,
36    CYAN,
37    GREEN,
38    MAGENTA,
39    RED,
40    WHITE,
41    YELLOW,
42};
43
44pub type FullTerminal = Box<StdoutTerminal>;
45
46#[derive(Debug)]
47enum State {
48    Beginning,
49    Tag,
50    Inside,
51    InsideColor,
52    InsideBool,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum PartialToken {
57    Fg,
58    Bg,
59    Italic,
60    Underline,
61    Standout,
62}
63
64#[derive(Debug, PartialEq, Eq)]
65pub enum Token {
66    Partial(PartialToken),
67    Attribute(Attr),
68    Reset,
69    Literal(String),
70}
71
72fn parse(s: &str) -> Result<Vec<Token>, String> {
73    let mut state = Beginning;
74    let mut current = String::new();
75    let mut tokens = vec![];
76    for i in s.chars() {
77        match state {
78            Beginning => {
79                match try!(parse_literal(i, &current)) {
80                    (maybe_token, s, action) => {
81                        match action {
82                            Action::Grow(string) => current.push_str(&*string),
83                            Action::Break(string) => current = string,
84                        }
85                        match maybe_token {
86                            Some(token) => tokens.push(token),
87                            None => (),
88                        }
89                        state = s;
90                    }
91                }
92            }
93            Tag => match i {
94                'a'...'z' | '-' => {
95                    state = Tag;
96                    current.push(i);
97                },
98                '(' => {
99                    match try!(parse_tag(&*current)) {
100                        (token, s) => {
101                            tokens.push(token);
102                            state = s;
103                        }
104                    }
105                    current = String::new();
106                }
107                  _ => {
108                      return Err(format!("Expected lowercase letter or '(', found {}. Current: {}, state: {:?}, tokens: {:?}", i, current, state, tokens));
109                  }
110                },
111                Inside => {
112                    if &*current != "" {
113                        return Err(format!("Expected no arguments, found {}", current));
114                    }
115                    match i {
116                        ')' => {
117                            state = Beginning;
118                        }
119                    _   => {
120                            return Err(format!("Expected ')', found {}", i));
121                        }
122                    }
123                }
124                InsideBool => {
125                    match i {
126                        ')' => {
127                        state = Beginning;
128                        let value = match &*current {
129                            "true" => true,
130                            "false" => false,
131                            _ => return Err(format!("Expected bool, found {}", current)),
132                        };
133                        let maybe_last = tokens.pop();
134                        current = String::new();
135                        tokens.push(
136                            match maybe_last {
137                                None => return Err(format!("Expected a tag token in array, found {:?}", maybe_last)),
138                                Some(token) => match token {
139                                    Partial(PT::Italic) => Attribute(Italic(value)),
140                                    Partial(PT::Underline) => Attribute(Underline(value)),
141                                    Partial(PT::Standout) => Attribute(Standout(value)),
142                                    _ => return Err(format!("Expected tag, found {:?}", token)),
143                                }
144                            })
145                        }
146                        _ => {
147                            state = InsideBool;
148                            current.push(i);
149                        },
150                    }
151                }
152                InsideColor => {
153                    match i {
154                        ')' => {
155                        state = Beginning;
156                        let color = try!(get_color_by_name(&*current));
157                        let maybe_last = tokens.pop();
158                        tokens.push(match maybe_last {
159                            None => return Err(format!("Expected a tag token in array, found {:?}", maybe_last)),
160                            Some(token) => {
161                                match token {
162                                    Partial(PT::Fg) => Attribute(ForegroundColor(color)),
163                                    Partial(PT::Bg) => Attribute(BackgroundColor(color)),
164                                    _ => unreachable!(),
165                                }
166                            }
167                        });
168                        current = String::new();
169                    },
170                    _ => {
171                        state = InsideColor;
172                        current.push(i);
173                    },
174                }
175            },
176
177        }
178    }
179    match state {
180        Tag => return Err(format!("Expected lowercase letter or '(', found EOF")),
181        Inside => return Err(format!("Expected ')', found EOF")),
182        InsideColor => return Err(format!("Expected ')', found EOF")),
183        InsideBool => return Err(format!("Expected ')', found EOF")),
184        Beginning => if &*current != "" {
185            tokens.push(Literal(current.clone()))
186        }
187    }
188
189    Ok(tokens)
190}
191
192enum Action {
193    Grow(String),
194    Break(String),
195}
196
197fn parse_literal(next: char, current: &String)
198 -> Result<(Option<Token>, State, Action), String> {
199    match next {
200        '^' => {
201            let maybe_token =
202                if *current != "" {
203                    Some(Literal(current.clone()))
204                } else {
205                    None
206                };
207            Ok((maybe_token, Tag, Action::Break(String::new())))
208        },
209        _ => {
210            let mut s = String::new();
211            s.push(next);
212            Ok((None, Beginning, Action::Grow(s)))
213        },
214    }
215}
216
217fn parse_tag(current: &str) -> Result<(Token, State), String> {
218    match &*current {
219        "fg" => {
220            Ok((Partial(PT::Fg), InsideColor))
221        }
222        "bg" => {
223            Ok((Partial(PT::Bg), InsideColor))
224        }
225        "bold" => {
226            Ok((Attribute(Bold), Inside))
227        }
228        "dim" => {
229            Ok((Attribute(Dim), Inside))
230        }
231        "italic" => {
232            Ok((Partial(PT::Italic), InsideBool))
233        }
234        "underline" => {
235            Ok((Partial(PT::Underline), InsideBool))
236        }
237        "blink" => {
238            Ok((Attribute(Blink), Inside))
239        }
240        "standout" => {
241            Ok((Partial(PT::Standout), InsideBool))
242        }
243        "reverse" => {
244            Ok((Attribute(Reverse), Inside))
245        }
246        "secure" => {
247            Ok((Attribute(Secure), Inside))
248        }
249        "reset" => {
250            Ok((Reset, Inside))
251        }
252        _ => {
253            Err(format!("Expected a tag, found {}", current))
254        }
255    }
256}
257
258fn get_color_by_name(color: &str) -> Result<Color, String> {
259    match &*color {
260        "black" => Ok(term::color::BLACK),
261        "blue" => Ok(term::color::BLUE),
262        "bright-black" => Ok(term::color::BRIGHT_BLACK),
263        "bright-blue" => Ok(term::color::BRIGHT_BLUE),
264        "bright-cyan" => Ok(term::color::BRIGHT_CYAN),
265        "bright-green" => Ok(term::color::BRIGHT_GREEN),
266        "bright-magenta" => Ok(term::color::BRIGHT_MAGENTA),
267        "bright-red" => Ok(term::color::BRIGHT_RED),
268        "bright-white" => Ok(term::color::BRIGHT_WHITE),
269        "bright-yellow" => Ok(term::color::BRIGHT_YELLOW),
270        "cyan" => Ok(term::color::CYAN),
271        "green" => Ok(term::color::GREEN),
272        "magenta" => Ok(term::color::MAGENTA),
273        "red" => Ok(term::color::RED),
274        "white" => Ok(term::color::WHITE),
275        "yellow" => Ok(term::color::YELLOW),
276
277        _ => return Err(format!("Expected color name, found {}", color)),
278    }
279}
280
281pub fn render(trm: &mut FullTerminal, tokens: &[Token]) {
282    for t in tokens.iter() {
283        match *t {
284            Literal(ref string) => {
285                write!(trm, "{}", string).unwrap();
286            }
287            Attribute(value) => {
288                trm.attr(value).unwrap();
289            }
290            Reset => {
291                trm.reset().unwrap();
292            }
293            Partial(_) => unreachable!(),
294        }
295    }
296    trm.reset().unwrap();
297}
298
299pub fn render_str(term: &mut FullTerminal, s: &str) -> Result<(), String> {
300    let tokens = try!(parse(s));
301    Ok(render(term, &*tokens))
302}
303
304#[test]
305fn parse_fg_two_colors() {
306    let input = "^fg(red)I'm red text ^fg(blue)I am blue";
307    println!("{:?}", parse(input));
308    assert!(parse(input)
309         == Ok(
310             vec![Attribute(ForegroundColor(term::color::RED)),
311                  Literal("I'm red text ".to_string()),
312                  Attribute(ForegroundColor(term::color::BLUE)),
313                  Literal("I am blue".to_string())]))
314}
315
316#[test]
317fn parse_fg_colors_bright() {
318    let input = "^fg(bright-green)I'm bright green text ^fg(bright-magenta)I am bright magenta";
319    println!("{:?}", parse(input));
320    assert!(parse(input)
321         == Ok(
322             vec![Attribute(ForegroundColor(term::color::BRIGHT_GREEN)),
323                  Literal("I'm bright green text ".to_string()),
324                  Attribute(ForegroundColor(term::color::BRIGHT_MAGENTA)),
325                  Literal("I am bright magenta".to_string())]))
326}
327
328#[test]
329fn parse_fg_bg_colors() {
330    let input = "^fg(bright-green)^bg(blue)I'm bright green text ^bg(bright-black)^fg(bright-magenta)I am bright magenta";
331    println!("{:?}", parse(input));
332    assert!(parse(input)
333         == Ok(
334             vec![Attribute(ForegroundColor(term::color::BRIGHT_GREEN)),
335                  Attribute(BackgroundColor(term::color::BLUE)),
336                  Literal("I'm bright green text ".to_string()),
337                  Attribute(BackgroundColor(term::color::BRIGHT_BLACK)),
338                  Attribute(ForegroundColor(term::color::BRIGHT_MAGENTA)),
339                  Literal("I am bright magenta".to_string())]))
340}
341
342#[test]
343fn parse_fg_bg_bold_colors() {
344    let input = "^fg(bright-green)^bg(blue)^bold()I'm bold bright green text";
345    println!("{:?}", parse(input));
346    assert!(parse(input)
347         == Ok(
348             vec![Attribute(ForegroundColor(term::color::BRIGHT_GREEN)),
349                  Attribute(BackgroundColor(term::color::BLUE)),
350                  Attribute(Bold),
351                  Literal("I'm bold bright green text".to_string()),
352                  ]))
353}
354
355#[test]
356fn parse_dim() {
357    let input = "^dim()I'm just dim text";
358    println!("{:?}", parse(input));
359    assert!(parse(input)
360         == Ok(
361             vec![Attribute(Dim),
362                  Literal("I'm just dim text".to_string()),
363                  ]))
364}
365
366#[test]
367fn parse_reset() {
368    let input = "^fg(red)I'm just dim text^reset()";
369    println!("{:?}", parse(input));
370    assert!(parse(input)
371         == Ok(
372             vec![Attribute(ForegroundColor(term::color::RED)),
373                  Literal("I'm just dim text".to_string()),
374                  Reset,
375                  ]))
376}
377
378#[test]
379fn parse_italic() {
380    let input = "^italic(true)I'm just dim text^italic(false)";
381    println!("{:?}", parse(input));
382    assert!(parse(input)
383         == Ok(
384             vec![Attribute(Italic(true)),
385                  Literal("I'm just dim text".to_string()),
386                  Attribute(Italic(false)),
387                  ]))
388}
389
390#[test]
391fn parse_underline() {
392    let input = "^underline(true)I'm underlined text^underline(false)";
393    println!("{:?}", parse(input));
394    assert!(parse(input)
395         == Ok(
396             vec![Attribute(Underline(true)),
397                  Literal("I'm underlined text".to_string()),
398                  Attribute(Underline(false)),
399                  ]))
400}
401
402#[test]
403fn parse_blink() {
404    let input = "^blink()I'm blinking text^reset()";
405    println!("{:?}", parse(input));
406    assert!(parse(input)
407         == Ok(
408             vec![Attribute(Blink),
409                  Literal("I'm blinking text".to_string()),
410                  Reset,
411                  ]))
412}
413
414#[test]
415fn parse_standout() {
416    let input = "^standout(true)I'm standing out text^standout(false)";
417    println!("{:?}", parse(input));
418    assert!(parse(input)
419         == Ok(
420             vec![Attribute(Standout(true)),
421                  Literal("I'm standing out text".to_string()),
422                  Attribute(Standout(false)),
423                  ]))
424}
425
426#[test]
427fn parse_reverse() {
428    let input = "^reverse()I'm reversed text^reset()";
429    println!("{:?}", parse(input));
430    assert!(parse(input)
431         == Ok(
432             vec![Attribute(Reverse),
433                  Literal("I'm reversed text".to_string()),
434                  Reset,
435                  ]))
436}
437
438#[test]
439fn parse_secure() {
440    let input = "^secure()I'm secure text^reset()";
441    println!("{:?}", parse(input));
442    assert!(parse(input)
443         == Ok(
444             vec![Attribute(Secure),
445                  Literal("I'm secure text".to_string()),
446                  Reset,
447                  ]))
448}