rs3a/
lib.rs

1/*
2    This file is part of rs3a.
3
4    rs3a is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    rs3a is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with rs3a.  If not, see <https://www.gnu.org/licenses/>.
16*/
17use std::convert::{TryFrom, Into};
18use std::cmp::PartialEq;
19use regex::Regex;
20use std::{fs, io, fmt};
21use std::sync::Arc;
22#[macro_use]
23extern crate lazy_static;
24#[cfg(test)]
25mod tests;
26
27lazy_static! {
28    pub static ref COLORS: Vec<Color> = vec![
29        Color::BLACK,
30        Color::BLUE,
31        Color::GREEN,
32        Color::CYAN,
33        Color::RED,
34        Color::MAGENTA,
35        Color::YELLOW,
36        Color::WHITE,
37        Color::GRAY,
38        Color::BRIGHT_BLUE,
39        Color::BRIGHT_GREEN,
40        Color::BRIGHT_CYAN,
41        Color::BRIGHT_RED,
42        Color::BRIGHT_MAGENTA,
43        Color::BRIGHT_YELLOW,
44        Color::BRIGHT_WHITE,
45    ];
46}
47
48const DEFAULT_DELAY: u16 = 50;
49const DEFAULT_PREVIEW: u16 = 0;
50const DEFAULT_LOOP: bool = true;
51const DEFAULT_COLORS: ColorMod = ColorMod::None;
52const DEFAULT_UTF8: bool = false;
53
54#[derive(Debug, Clone)]
55pub enum ParcingError{
56    UnknownColor(char),
57    UnknownColorMod(String),
58    InvalidWidth,
59    InvalidHeight,
60    ThereIsNoBody,
61}
62
63impl fmt::Display for ParcingError {
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        match self{
66            Self::UnknownColor(c) => {write!(f, "UnknownColor: {}", c)}
67            Self::UnknownColorMod(s) => {write!(f, "UnknownColorMod: {}", s)}
68            Self::InvalidWidth => {write!(f, "InvalidWidth")}
69            Self::InvalidHeight => {write!(f, "InvalidHeight")}
70            Self::ThereIsNoBody => {write!(f, "There is no body found")}
71        }
72    }
73}
74
75#[derive(Debug, Clone)]
76pub enum ReadingError{
77    ParcingError(ParcingError),
78    IOError(Arc<io::Error>),
79}
80
81impl fmt::Display for ReadingError {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        match self{
84            Self::ParcingError(e) => {e.fmt(f)}
85            Self::IOError(e) => {e.fmt(f)}
86        }
87    }
88}
89
90pub fn escape_comments(s: &str) -> String{
91    let re1 = Regex::new(r"(?m)^\t.*?(\n|$)").unwrap();
92    let re2 = Regex::new(r"\t.*?(\n|$)").unwrap();
93    let s = re1.replace_all(s, "").to_string();
94    let s = re2.replace_all(&s, "\n").to_string();
95    s
96}
97
98pub fn load(s: String) -> Result<Art, ParcingError>{
99    let s = escape_comments(&s);
100    let fragments: Vec<&str> = s.splitn(2, "\n\n").collect();
101    if fragments.len() < 2 {
102        return Err(ParcingError::ThereIsNoBody);
103    }
104    let header: Header = match Header::try_from(fragments[0].to_string()){
105        Ok(v) => {v}
106        Err(e) => {return Err(e);}
107    };
108    let body: Body = match Body::from_string(fragments[1].to_string(), header.clone()){
109            Ok(v) => {v}
110            Err(e) => {return Err(e);}
111    };
112    Ok(Art{header, body})
113}
114
115pub fn save(art: Art, pretify: bool) -> String{
116    let mut ret: String = art.header.into();
117    ret += "\n";
118    ret += &art.body.to_string(pretify);
119    ret
120}
121
122pub fn load_file(path: String) -> Result<Art, ReadingError>{
123    let s = match fs::read_to_string(path) {
124        Ok(s) => {s}
125        Err(ie) => {return Err(ReadingError::IOError(Arc::new(ie)))}
126    };
127    match load(s) {
128        Ok(v) => {Ok(v)}
129        Err(e) => {Err(ReadingError::ParcingError(e))}
130    }
131}
132
133pub fn save_file(art: Art, path: String, pretify: bool) -> Result<(), io::Error>{
134    let s = save(art, pretify);
135    match fs::write(path, s) {
136        Ok(_) => {Ok(())}
137        Err(e) => {Err(e)}
138    }
139}
140
141#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
142#[allow(non_camel_case_types)]
143pub enum Color{
144    BLACK,
145    BLUE,
146    GREEN,
147    CYAN,
148    RED,
149    MAGENTA,
150    YELLOW,
151    WHITE,
152    GRAY,
153    BRIGHT_BLUE,
154    BRIGHT_GREEN,
155    BRIGHT_CYAN,
156    BRIGHT_RED,
157    BRIGHT_MAGENTA,
158    BRIGHT_YELLOW,
159    BRIGHT_WHITE
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Hash)]
163pub struct RowFragment{
164    pub text: String,
165    pub fg_color: Option<Color>,
166    pub bg_color: Option<Color>,
167}
168
169pub type Row = Vec<RowFragment>;
170
171pub type Frame = Vec<Row>;
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
174pub struct Body{
175    pub frames: Vec<Frame>
176}
177
178#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
179pub enum ColorMod{
180    None,
181    Fg,
182    Bg,
183    Full
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
187pub struct Header{
188    pub width: u16,
189    pub height: u16,
190    pub delay: u16,
191    pub loop_enable: bool,
192    pub color_mod: ColorMod,
193    pub utf8: bool,
194    pub datacols: u16,
195    pub preview: u16,
196    pub audio: Option<String>,
197    pub title: Option<String>,
198    pub author: Option<String>,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Hash)]
202pub struct Art{
203    pub header: Header,
204    pub body: Body,
205}
206
207impl Body{
208    pub fn from_string(s: String, h: Header) -> Result<Self, ParcingError> {
209        let re = Regex::new(r"(\n|\t)").unwrap();
210        let s = re.replace_all(&s, "").to_string();
211        let char_vec: Vec<char> = s.chars().collect();
212        let len = char_vec.len();
213        let mut frm: usize = 0; //frame nom
214        let width = h.width as usize;
215        let height = h.height as usize;
216        let datacols = h.datacols as usize;
217        let mut frames: Vec<Frame> = Vec::new();
218        let mut next = true;
219        'outer: while next {
220            let mut frame: Frame = Vec::new();
221            for y in 0..height{
222                let mut row: Row = Vec::new();
223                let mut row_fragment = RowFragment{
224                    text: "".to_string(),
225                    fg_color: None,
226                    bg_color: None,
227                };
228                for x in 0..width{
229                    let symbol_pos: usize = (frm*width*datacols*height)+(y*width*datacols)+x;
230                    if symbol_pos >= len{
231                        next = false;
232                        break;
233                    }
234                    let symbol: char = char_vec[symbol_pos];
235                    let mut fg_color: Option<Color> = None;
236                    let mut bg_color: Option<Color> = None;
237                    match h.color_mod {
238                        ColorMod::None => {}
239                        ColorMod::Fg => {
240                            let fg_color_position = (frm*width*datacols*height)+(y*width*datacols)+width+x;
241                            if fg_color_position >= len{
242                                next = false;
243                                break;
244                            }
245                            fg_color = Some(match Color::try_from(char_vec[fg_color_position]) {
246                                Ok(c) => {c}
247                                Err(e) => {return Err(e);}
248                            });
249                        }
250                        ColorMod::Bg => {
251                            let bg_color_position = (frm*width*datacols*height)+(y*width*datacols)+width+x;
252                            if bg_color_position >= len{
253                                next = false;
254                                break;
255                            }
256                            bg_color = Some(match Color::try_from(char_vec[bg_color_position]) {
257                                Ok(c) => {c}
258                                Err(e) => {return Err(e);}
259                            });
260                        }
261                        ColorMod::Full => {
262                            let fg_color_position = (frm*width*datacols*height)+(y*width*datacols)+width+x;
263                            let bg_color_position = (frm*width*datacols*height)+(y*width*datacols)+width*2+x;
264                            if fg_color_position >= len || bg_color_position >= len {
265                                next = false;
266                                break;
267                            }
268                            fg_color = Some(match Color::try_from(char_vec[fg_color_position]) {
269                                Ok(c) => {c}
270                                Err(e) => {return Err(e);}
271                            });
272                            bg_color = Some(match Color::try_from(char_vec[bg_color_position]) {
273                                Ok(c) => {c}
274                                Err(e) => {return Err(e);}
275                            });
276                        }
277                    }
278                    if x == 0 {
279                        row_fragment.fg_color = fg_color;
280                        row_fragment.bg_color = bg_color;
281                    }else{
282                        if row_fragment.fg_color != fg_color || row_fragment.bg_color != bg_color {
283                            row.push(row_fragment);
284                            row_fragment = RowFragment{
285                                text: symbol.to_string(),
286                                fg_color: fg_color,
287                                bg_color: bg_color,
288                            };
289                            continue;
290                        }
291                    }
292                    row_fragment.text.push(symbol)
293                }
294                if row_fragment.text.len() > 0 {
295                    row.push(row_fragment);
296                }
297                if row.len() < 1 {
298                    break 'outer;
299                }
300                frame.push(row);
301            }
302            frames.push(frame);
303            frm += 1;
304        }
305        Ok(Body{frames})
306    }
307    fn generate_color_fragment(color: Option<Color>, count: usize) -> String{
308        let mut ret: String = "".to_string();
309        if let Some(color) = color {
310            let c: char = color.into();
311            for _ in 0..count {
312                ret.push(c);
313            }
314        }
315        ret
316    }
317    pub fn to_string(self, pretify: bool) -> String{
318        let mut ret: String = "".to_string();
319        for (frm, frame) in self.frames.iter().enumerate(){
320            for row in frame{
321                let mut text_col: String = "".to_string();
322                let mut color1_col: String = "".to_string();
323                let mut color2_col: String = "".to_string();
324                for fragment in row{
325                    text_col += &fragment.text;
326                    color1_col += &Self::generate_color_fragment(fragment.fg_color, fragment.text.len());
327                    color2_col += &Self::generate_color_fragment(fragment.bg_color, fragment.text.len());
328                }
329                ret += &text_col;
330                ret += &color1_col;
331                ret += &color2_col;
332                if pretify {
333                    ret.push('\n');
334                }
335            }
336            if frm < self.frames.len()-1 {
337                ret.push('\n');
338            }
339        }
340        ret
341    }
342}
343
344impl ColorMod{
345    fn to_datacols(self) -> u16{
346        match self{
347            ColorMod::None => {1}
348            ColorMod::Fg => {2}
349            ColorMod::Bg => {2}
350            ColorMod::Full => {3}
351        }
352    }
353}
354
355impl TryFrom<&str> for ColorMod{
356    type Error = ParcingError;
357    fn try_from(value: &str) -> Result<Self, Self::Error> {
358        match value{
359            "none" => {Ok(Self::None)}
360            "fg" => {Ok(Self::Fg)}
361            "bg" => {Ok(Self::Bg)}
362            "full" => {Ok(Self::Full)}
363            _ => {Err(ParcingError::UnknownColorMod(value.to_string()))}
364        }
365    }
366}
367
368impl Into<String> for ColorMod{
369    fn into(self) -> String{
370        match self{
371            ColorMod::None => {"none"}
372            ColorMod::Fg => {"fg"}
373            ColorMod::Bg => {"bg"}
374            ColorMod::Full => {"full"}
375        }.to_string()
376    }
377}
378
379impl TryFrom<char> for Color{
380    type Error = ParcingError;
381    fn try_from(value: char) -> Result<Self, Self::Error> {
382        match value{
383            '0' => {Ok(Self::BLACK)}
384            '1' => {Ok(Self::BLUE)}
385            '2' => {Ok(Self::GREEN)}
386            '3' => {Ok(Self::CYAN)}
387            '4' => {Ok(Self::RED)}
388            '5' => {Ok(Self::MAGENTA)}
389            '6' => {Ok(Self::YELLOW)}
390            '7' => {Ok(Self::WHITE)}
391            '8' => {Ok(Self::GRAY)}
392            '9' => {Ok(Self::BRIGHT_BLUE)}
393            'a' => {Ok(Self::BRIGHT_GREEN)}
394            'b' => {Ok(Self::BRIGHT_CYAN)}
395            'c' => {Ok(Self::BRIGHT_RED)}
396            'd' => {Ok(Self::BRIGHT_MAGENTA)}
397            'e' => {Ok(Self::BRIGHT_YELLOW)}
398            'f'=> {Ok(Self::BRIGHT_WHITE)}
399            _ => {Err(ParcingError::UnknownColor(value))}
400        }
401    }
402}
403
404impl Into<char> for Color{
405    fn into(self) -> char{
406        match self{
407            Self::BLACK             => {'0'}
408            Self::BLUE              => {'1'}
409            Self::GREEN             => {'2'}
410            Self::CYAN              => {'3'}
411            Self::RED               => {'4'}
412            Self::MAGENTA           => {'5'}
413            Self::YELLOW            => {'6'}
414            Self::WHITE             => {'7'}
415            Self::GRAY              => {'8'}
416            Self::BRIGHT_BLUE       => {'9'}
417            Self::BRIGHT_GREEN      => {'a'}
418            Self::BRIGHT_CYAN       => {'b'}
419            Self::BRIGHT_RED        => {'c'}
420            Self::BRIGHT_MAGENTA    => {'d'}
421            Self::BRIGHT_YELLOW     => {'e'}
422            Self::BRIGHT_WHITE      => {'f'}
423        }
424    }
425}
426
427fn only_payload(v: Vec<&str>) -> Vec<&str>{
428    let mut ret: Vec<&str> = Vec::new();
429    for s in v {
430       if s != ""{
431        ret.push(s);
432       }
433    }
434    ret
435}
436
437
438impl TryFrom<String> for Header{
439    type Error = ParcingError;
440    fn try_from(s: String) -> Result<Self, Self::Error> {
441        let mut width: u16 = 0; let mut w_set = false;
442        let mut height: u16 = 0; let mut h_set = false;
443        let mut delay: u16 = DEFAULT_DELAY;
444        let mut loop_enable: bool = DEFAULT_LOOP;
445        let mut color_mod: ColorMod = DEFAULT_COLORS;
446        let mut utf8: bool = DEFAULT_UTF8;
447        let mut datacols: u16 = 0; let mut d_set = false;
448        let mut preview: u16 = DEFAULT_PREVIEW;
449        let mut audio: Option<String> = None;
450        let mut title: Option<String> = None;
451        let mut author: Option<String> = None;
452        let rows = s.split("\n").collect::<Vec<&str>>();
453        for row in rows{
454            let tokens = row.split(" ").collect::<Vec<&str>>();
455            if tokens.len() < 1 {continue;}
456            let tokens = only_payload(tokens);
457            if tokens.len() < 1 {continue;}
458            match tokens[0]{
459                "width" => {
460                    if tokens.len() < 2 {continue;}
461                    width = match tokens[1].parse::<u16>(){
462                        Ok(v) => {v}
463                        Err(_) => {continue;}
464                    };
465                    w_set = true;
466                }
467                "height" => {
468                    if tokens.len() < 2 {continue;}
469                    height = match tokens[1].parse::<u16>(){
470                        Ok(v) => {v}
471                        Err(_) => {continue;}
472                    };
473                    h_set = true;
474                }
475                "delay" => {
476                    if tokens.len() < 2 {continue;}
477                    delay = match tokens[1].parse::<u16>(){
478                        Ok(v) => {v}
479                        Err(_) => {continue;}
480                    };
481                }
482                "loop" => {
483                    if tokens.len() < 2 {continue;}
484                    loop_enable = match tokens[1] {
485                        "true" => {true}
486                        "false" => {false}
487                        _ => {continue;}
488                    };
489                }
490                "colors" => {
491                    if tokens.len() < 2 {continue;}
492                    color_mod = match ColorMod::try_from(tokens[1]){
493                        Ok(v) => {v}
494                        _ => {continue;}
495                    };
496                }
497                "utf8" => {
498                    utf8 = true;
499                }
500                "datacols" => {
501                    if tokens.len() < 2 {continue;}
502                    datacols = match tokens[1].parse::<u16>(){
503                        Ok(v) => {v}
504                        Err(_) => {continue;}
505                    };
506                    d_set = true;
507                }
508                "preview" => {
509                    if tokens.len() < 2 {continue;}
510                    preview = match tokens[1].parse::<u16>(){
511                        Ok(v) => {v}
512                        Err(_) => {continue;}
513                    };
514                }
515                "audio" => {
516                    if tokens.len() < 2 {continue;}
517                    audio = match tokens[1]{
518                        "" => {continue;}
519                        a => {Some(a.to_string())}
520                    };
521                }
522                "title" => {
523                    if tokens.len() < 2 {continue;}
524                    let mut s = "".to_string();
525                    for i in 1..tokens.len() {
526                        if i > 1 {s.push_str(" ")}
527                        s.push_str(tokens[i])
528                    }
529                    title = Some(s);
530                }
531                "author" => {
532                    if tokens.len() < 2 {continue;}
533                    let mut s = "".to_string();
534                    for i in 1..tokens.len() {
535                        if i > 1 {s.push_str(" ")}
536                        s.push_str(tokens[i])
537                    }
538                    author = Some(s);
539                }
540                _ => {}
541            }
542        }
543        if !w_set {return Err(ParcingError::InvalidWidth);}
544        if !h_set {return Err(ParcingError::InvalidHeight);}
545        if !d_set {
546            datacols = color_mod.to_datacols();
547        }
548        Ok(Self{width, height, delay, loop_enable, color_mod, utf8, datacols, preview, audio, title, author})
549    }
550}
551
552impl Into<String> for Header{
553    fn into(self) -> String{
554        let mut ret = "".to_string();
555        ret.push_str("width ");
556        ret.push_str(&self.width.to_string());
557        ret.push_str("\nheight ");
558        ret.push_str(&self.height.to_string());
559        if self.delay != DEFAULT_DELAY {
560            ret.push_str("\ndelay ");
561            ret.push_str(&self.delay.to_string());
562        }
563        if self.loop_enable != DEFAULT_LOOP {
564            ret.push_str("\nloop ");
565            ret.push_str(match self.loop_enable{
566                true => {"true"}
567                false => {"false"}
568            });
569        }
570        if self.color_mod != DEFAULT_COLORS {
571            ret.push_str("\ncolors ");
572            let s: String = self.color_mod.into();
573            ret.push_str(&s);
574        }
575        if self.utf8 {
576            ret.push_str("\nutf8");
577        }
578        if self.color_mod.to_datacols() != self.datacols{
579            ret.push_str("\ndatacols ");
580            ret.push_str(&self.datacols.to_string());
581        }
582        if self.preview != DEFAULT_PREVIEW {
583            ret.push_str("\npreview ");
584            ret.push_str(&self.preview.to_string());
585        }
586        if let Some(a) = self.audio {
587            ret.push_str("\naudio ");
588            ret.push_str(&a);
589        }
590        if let Some(a) = self.title {
591            ret.push_str("\ntitle ");
592            ret.push_str(&a);
593        }
594        if let Some(a) = self.author {
595            ret.push_str("\nauthor ");
596            ret.push_str(&a);
597        }
598        ret.push_str("\n\n");
599        ret
600    }
601}