crml_core/
lib.rs

1pub mod selector;
2use selector::{Selector, SelectorState};
3
4/// A trait to render template structs.
5pub trait Template {
6    fn render(self) -> String;
7}
8
9/// The type of a given [`Token`].
10#[derive(Debug, PartialEq, Eq)]
11pub enum TokenType {
12    /// A comment in the code. Completely ignored.
13    ///
14    /// Starts with `/`.
15    Comment,
16    /// A direct string of Rust code:
17    ///
18    /// ```text
19    /// - let a = 1
20    /// ```
21    ///
22    /// Begins with `-`.
23    RustString,
24    /// A direct string of Rust code which is pushed to the output HTML:
25    ///
26    /// ```text
27    /// = (a + b).to_string()
28    ///
29    /// - fn get_new_string() {
30    /// -     String::new()
31    /// - }
32    ///
33    /// = get_new_string()
34    /// ```
35    ///
36    /// Begins with `=`.
37    PushedRustString,
38    /// A CSS selector which will be transformed into an HTML element:
39    ///
40    /// ```text
41    /// %element.class#id[attr=val]
42    /// ```
43    ///
44    /// Begins with `%`. If a single quote (`'`) comes after the selector,
45    /// everything else on the line will be treated as the `innerHTML`, and the
46    /// element will be closed as well.
47    Selector,
48    /// Raw HTML data:
49    ///
50    /// ```text
51    /// @<!DOCTYPE html>
52    /// ```
53    ///
54    /// Begins with `@`.
55    Html,
56    /// Raw text:
57    ///
58    /// ```text
59    /// anything not matched into the previous types
60    /// ```
61    Raw,
62}
63
64/// A *token* is a representation of fully parsed data.
65#[derive(Debug)]
66pub struct Token {
67    /// The type of the token.
68    pub r#type: TokenType,
69    /// The raw CRML string of the token.
70    pub raw: String,
71    /// The HTML string of the token.
72    pub html: String,
73    /// The indent level of the token.
74    pub indent: i32,
75    /// The line number the token is found on.
76    pub line: i32,
77    /// The selector of the token. Only applies to [`TokenType::Selector`].
78    pub selector: Option<SelectorState>,
79}
80
81impl Token {
82    /// Create a [`Token`] given its `indent` and `line` value.
83    pub fn from_indent_ln(indent: i32, line: i32) -> Self {
84        Self {
85            r#type: TokenType::Raw,
86            raw: "\n".to_string(),
87            html: "\n".to_string(),
88            indent,
89            line,
90            selector: None,
91        }
92    }
93
94    /// Create a [`Token`] from a given [`String`] value,
95    pub fn from_string(value: String, indent: i32, line: i32) -> Option<Self> {
96        let mut chars = value.chars();
97
98        match match chars.next() {
99            Some(c) => c,
100            None => {
101                return Some(Self::from_indent_ln(indent, line));
102            }
103        } {
104            '/' => {
105                // comment; ignore
106                if let Some(char) = chars.next() {
107                    if char == '>' {
108                        // raw html element closing, NOT COMMENT!
109                        return Some(Self {
110                            r#type: TokenType::Raw,
111                            raw: value.clone(),
112                            html: value,
113                            indent,
114                            line,
115                            selector: None,
116                        });
117                    }
118                }
119
120                return Some(Self::from_indent_ln(indent, line));
121            }
122            '-' => {
123                // starting with an opening sign; rust data
124                // not much real parsing to do here
125                let mut raw = String::new();
126
127                while let Some(char) = chars.next() {
128                    raw.push(char);
129                }
130
131                return Some(Self {
132                    r#type: TokenType::RustString,
133                    raw,
134                    html: String::new(),
135                    indent,
136                    line,
137                    selector: None,
138                });
139            }
140            '=' => {
141                // starting with an opening sign; rust data
142                // not much real parsing to do here
143                let mut raw = String::new();
144
145                while let Some(char) = chars.next() {
146                    raw.push(char);
147                }
148
149                return Some(Self {
150                    r#type: TokenType::PushedRustString,
151                    raw,
152                    html: String::new(),
153                    indent,
154                    line,
155                    selector: None,
156                });
157            }
158            '%' => {
159                // starting with a beginning sign; selector
160                let mut raw = String::new();
161                let mut data = String::new();
162                let mut inline: bool = false;
163                let mut whitespace_sensitive: bool = false;
164
165                while let Some(char) = chars.next() {
166                    // check for inline char (single quote)
167                    if char == '\'' {
168                        inline = true;
169                        break;
170                    } else if char == '~' {
171                        whitespace_sensitive = true;
172                        continue;
173                    }
174
175                    // push char
176                    raw.push(char);
177                }
178
179                if inline {
180                    while let Some(char) = chars.next() {
181                        data.push(char);
182                    }
183                }
184
185                let selector = Selector::new(raw.clone()).parse();
186                return Some(Self {
187                    r#type: TokenType::Selector,
188                    raw: format!("{raw}{data}"),
189                    html: if inline {
190                        // inline element
191                        format!("{}{data}</{}>", selector.clone().render(), selector.tag)
192                    } else {
193                        selector.clone().render()
194                    },
195                    indent: if whitespace_sensitive { -1 } else { indent },
196                    line,
197                    selector: Some(selector),
198                });
199            }
200            '@' => {
201                // begins with @; raw html
202                let mut raw = String::new();
203
204                while let Some(char) = chars.next() {
205                    raw.push(char);
206                }
207
208                return Some(Self {
209                    r#type: TokenType::Html,
210                    raw: raw.clone(),
211                    html: raw,
212                    indent,
213                    line,
214                    selector: None,
215                });
216            }
217            _ => {
218                // no recognizable starting character; raw data
219                // let sanitizer = Builder::new();
220                return Some(Self {
221                    r#type: TokenType::Raw,
222                    raw: value.clone(),
223                    // html: sanitizer.clean(&value).to_string(),
224                    html: value,
225                    indent,
226                    line,
227                    selector: None,
228                });
229            }
230        }
231    }
232}
233
234/// Iterable version of [`Parser`]. Created through [`Parser::parse`].
235pub struct TokenStream(Parser);
236
237impl Iterator for TokenStream {
238    type Item = Token;
239
240    fn next(&mut self) -> Option<Self::Item> {
241        self.0.next()
242    }
243}
244
245/// The current state of the given [`Parser`].
246pub struct ParserState {
247    /// The current line the parser is on.
248    ///
249    /// We parse line by line to enforce whitespace. This means we just need to
250    /// track what line we are currently on.
251    pub line_number: i32,
252}
253
254impl Default for ParserState {
255    fn default() -> Self {
256        Self { line_number: -1 }
257    }
258}
259
260/// General character-by-character parser for CRML.
261pub struct Parser(Vec<String>, ParserState);
262
263impl Parser {
264    /// Create a new [`Parser`]
265    pub fn new(input: String) -> Self {
266        let mut lines = Vec::new();
267
268        for line in input.split("\n") {
269            lines.push(line.to_owned())
270        }
271
272        // ...
273        Self(lines, ParserState::default())
274    }
275
276    /// Begin parsing the `input`
277    pub fn parse(self) -> TokenStream {
278        TokenStream(self)
279    }
280
281    /// Parse the next line in the given `input`
282    pub fn next(&mut self) -> Option<Token> {
283        // get line
284        self.1.line_number += 1;
285        let line = match self.0.get(self.1.line_number as usize) {
286            Some(l) => l,
287            None => return None,
288        };
289
290        if line.is_empty() {
291            return Some(Token::from_indent_ln(0, self.1.line_number));
292        }
293
294        // get indent
295        let mut indent: i32 = 0;
296        let mut chars = line.chars();
297
298        while let Some(char) = chars.next() {
299            if (char != ' ') & (char != '\t') {
300                break;
301            }
302
303            indent += 1;
304        }
305
306        // parse token
307        Token::from_string(line.trim().to_owned(), indent, self.1.line_number)
308    }
309}