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}