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, ¤t)) {
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}