summaryrefslogtreecommitdiff
path: root/crimtag/src/lib.rs
diff options
context:
space:
mode:
authoreichlan <eichlan@jotunn.office.penny-arcade.com>2026-06-23 15:31:51 -0700
committereichlan <eichlan@jotunn.office.penny-arcade.com>2026-06-23 15:31:51 -0700
commit062048ef6e3e060bcf841ea2ec92e026f09d8da0 (patch)
tree22f7dad062b16f25fcfa1a28af13ac2937348ef0 /crimtag/src/lib.rs
parent7c299c86bbac76ec2ac199d68eb4dda093a64e3a (diff)
downloadcrimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.gz
crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.bz2
crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.xz
crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.zip
Roorganized into workspace and packages.
This is to accomidate a new proc_macro package, which can't have anything else in it.
Diffstat (limited to 'crimtag/src/lib.rs')
-rw-r--r--crimtag/src/lib.rs544
1 files changed, 544 insertions, 0 deletions
diff --git a/crimtag/src/lib.rs b/crimtag/src/lib.rs
new file mode 100644
index 0000000..e8f9e9f
--- /dev/null
+++ b/crimtag/src/lib.rs
@@ -0,0 +1,544 @@
1use std::path::{PathBuf,Path};
2use std::collections::HashMap;
3use core::error::Error;
4use std::time::SystemTime;
5
6mod lexer;
7mod parser;
8mod position;
9mod error;
10pub mod context;
11
12pub use error::{CrimError,CrimResult};
13pub use position::*;
14pub use context::{Context,Value,MappedStructure,StatefulContext,MappedValue,MappedList,MappedHash};
15
16#[derive(Debug)]
17pub struct Crimtag {
18 views: HashMap<String, View>,
19 sources: Vec<ViewSource>,
20}
21
22#[derive(PartialEq,Eq,Debug,Clone)]
23pub enum ViewSource {
24 File {
25 path: PathBuf,
26 loaded: SystemTime,
27 },
28 Static,
29 External {
30 key: String,
31 },
32}
33
34#[derive(Debug)]
35struct View {
36 name: String,
37 source: usize,
38 outputs: Vec<Output>,
39// theme: Option<String>
40 layout: Option<String>,
41}
42
43impl View {
44 fn process(&self, input: &dyn for<'a> MappedStructure<'a>) -> CrimResult<Context> {
45 let mut out_vars = Context::new();
46 for output in &self.outputs {
47 let mut buf = String::new();
48 let sc = StatefulContext::root( input );
49 exec( &sc, &output.code, &mut buf )?;
50 out_vars.insert( output.name.clone(), Value::String(buf) );
51 }
52 Ok(out_vars)
53 }
54}
55
56#[derive(Debug)]
57struct Output {
58 name: String,
59 code: Vec<Token>,
60}
61
62type Properties = HashMap<String, String>;
63
64#[derive(Debug)]
65enum Token {
66 Loop(Identifier, Vec<Token>),
67 Show(Identifier,Properties),
68 If(Identifier, Vec<Token>, Vec<Token>),
69 Text(String),
70}
71
72#[derive(Debug,Clone,PartialEq)]
73pub enum IdentifierValue {
74 Root,
75 Name(String),
76 Index(usize),
77}
78
79pub type Identifier = Vec<IdentifierValue>;
80
81impl Crimtag {
82 pub fn new() -> Self {
83 Self {
84 views: HashMap::new(),
85 sources: vec![ViewSource::Static],
86 }
87 }
88
89 fn id_source(&mut self, src: &ViewSource ) -> usize {
90 for idx in 0..self.sources.len() {
91 if self.sources[idx] == *src {
92 return idx;
93 }
94 }
95
96 // Nothing found, add a new one
97 let idx = self.sources.len();
98 self.sources.push( src.clone() );
99 idx
100 }
101
102 pub fn load_file(&mut self, path: &Path) -> Result<(),Box::<dyn Error>> {
103 let time = if let Ok(meta) = path.metadata() {
104 if let Ok(t) = meta.modified() {
105 t
106 } else if let Ok(t) = meta.created() {
107 t
108 } else {
109 SystemTime::now()
110 }
111 } else {
112 SystemTime::now()
113 };
114 let source_id = self.id_source(
115 &ViewSource::File {
116 path: path.to_path_buf(),
117 loaded: time,
118 }
119 );
120 let p = parser::Parser::new();
121 self.register_views(
122 p.parse(
123 &String::from_utf8( std::fs::read( path )? )?,
124 source_id
125 )?,
126 )?;
127 Ok(())
128 }
129
130 pub fn load_static(&mut self, data: &str) -> CrimResult<()> {
131 let source_id = self.id_source( &ViewSource::Static );
132 let p = parser::Parser::new();
133 self.register_views( p.parse( &data, source_id )? )
134 }
135
136 pub fn load_external(&mut self, key: &str, data: &str) -> CrimResult<()> {
137 let source_id = self.id_source(
138 &ViewSource::External{ key: key.to_string()}
139 );
140
141 let p = parser::Parser::new();
142 self.register_views(
143 p.parse( &data, source_id )?,
144 )
145 }
146
147 fn register_views(&mut self, views: Vec<View>) -> CrimResult<()> {
148 for view in views {
149 self.views.insert( view.name.clone(), view );
150 }
151 Ok(())
152 }
153
154 pub fn get_view_source(&self, view: &str) -> Option<ViewSource> {
155 if let Some(view) = self.views.get(view) {
156 if view.source < self.sources.len() {
157 Some(self.sources[view.source].clone())
158 } else {
159 None
160 }
161 } else {
162 None
163 }
164 }
165
166 pub fn render_partial(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult<Context> {
167 if let Some(view) = self.views.get(view) {
168 return view.process( input )
169 } else {
170 return Err(CrimError::other("No such view found".into()));
171 }
172 }
173
174 pub fn render(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult<String> {
175 if let Some(view) = self.views.get( view ) {
176 //println!("::Token tree::\n{:?}", view );
177 let mut output = view.process( input )?;
178 if let Some(l) = &view.layout {
179 self.render( l, &output )
180 } else {
181 if let Some(content) = output.remove("content") &&
182 let Value::String(s) = content {
183
184 Ok(s)
185 } else {
186 Err(CrimError::other("No content found in root layout.".into()))
187 }
188 }
189
190 } else {
191 Err(CrimError::other("No such view found".into()))
192 }
193 }
194}
195
196fn exec<'a>(input: &StatefulContext<'a>, tokens: &Vec<Token>, buf: &mut String) -> CrimResult<()> {
197 for token in tokens {
198 match token {
199 Token::Loop(ident,code) => {
200 //println!("!!! Loop over: {:?} {:?}", ident, input.get_value( &ident ));
201 if let Some(value) = input.get_value( &ident ) &&
202 let MappedValue::List(list) = value {
203 let output =
204 &list.for_each(&|rec: &MappedValue, buf: &mut String| {
205 if let MappedValue::Struct(d) = rec {
206 exec( &input.local(*d), code, buf )?;
207 }
208 Ok(())
209 })?;
210 //println!("!!! Output from loop: {}", output );
211 buf.push_str( &output );
212 }
213 }
214 Token::If(ident,code,other) => {
215 if let Some(value) = input.get_value( &ident ) &&
216 let MappedValue::Bool(b) = value && b {
217 exec(input, code, buf)?
218 } else {
219 exec(input, other, buf)?
220 }
221 }
222 Token::Show(ident,p) => {
223 let format = if let Some(s) = p.get("format") {
224 s
225 } else {
226 &"".to_string()
227 };
228 if let Some(value) = input.get_value( &ident ) {
229 match value {
230 MappedValue::Struct(_) => {
231 buf.push_str("Struct");
232 }
233 MappedValue::List(_) => {
234 buf.push_str("List");
235 }
236 MappedValue::Bool(b) => {
237 if b {
238 buf.push_str("true");
239 } else {
240 buf.push_str("false");
241 }
242 }
243 MappedValue::Str(s) => {
244 buf.push_str(s);
245 }
246 MappedValue::String(s) => {
247 buf.push_str(s.as_str());
248 }
249 MappedValue::Int8(i) => {
250 buf.push_str(&i.to_string());
251 }
252 MappedValue::Int16(i) => {
253 buf.push_str(&i.to_string());
254 }
255 MappedValue::Int32(i) => {
256 buf.push_str(&i.to_string());
257 }
258 MappedValue::Int64(i) => {
259 buf.push_str(&i.to_string());
260 }
261 MappedValue::UInt8(i) => {
262 buf.push_str(&i.to_string());
263 }
264 MappedValue::UInt16(i) => {
265 buf.push_str(&i.to_string());
266 }
267 MappedValue::UInt32(i) => {
268 buf.push_str(&i.to_string());
269 }
270 MappedValue::UInt64(i) => {
271 buf.push_str(&i.to_string());
272 }
273 MappedValue::Float32(f) => {
274 if format == "" {
275 buf.push_str(&f.to_string());
276 } else {
277 buf.push_str(&f.to_string());
278 }
279 }
280 MappedValue::Float64(f) => {
281 if format == "" {
282 buf.push_str(&f.to_string());
283 } else {
284 buf.push_str(&f.to_string());
285 }
286 }
287 }
288 }
289 }
290 Token::Text(s) => {
291 buf.push_str( s );
292 }
293 //_ => {}
294 }
295 }
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 use crate::lexer::*;
304
305 #[test]
306 fn lexing() {
307 let data = r#"Leading comment: [|view "basic" something="yeup"|>Hello there <|view|] Trailing text"#;
308 let ll = lexer::Lexer::new( &data );
309 for sym in ll {
310 if let SymbolType::Error{what} = sym.symbol() {
311 println!("Error {}:{}: {:?}", sym.start().line, sym.start().column, what );
312 break;
313 } else {
314 println!("Symbol: {:?}", sym );
315 }
316 }
317 }
318
319 #[test]
320 fn parsing() {
321 let mut ct = Crimtag::new();
322 if let Err(e) = ct.load_static(r#"Here is a sample
323[|view "index" theme="standard"|><html><body>Hi</body></html><|view|]
324That was fun! now a placeholder: [|view "placeholder"|] and now one with an implicit [|view "simple"|>Body -> [|show content|] <- and end<|view|] aoeu
325
326[|view "person" layout="simple"|>Welcome [|show person.last_name|], [|show person.first_name|]! It's lovely to see you again.<|view|]
327
328Now with explicit outputs:
329[|view "complex"|>
330 [|output "content"|>
331 Here's a [|show name|].
332 <|output|]
333 [|output "sidebar"|>
334 What's up world?
335 <|output|]
336 [|output "footer"|>
337 Same, I guess!
338 <|output|]
339<|view|]"#) {
340 println!("Error: {:?}", e );
341 } else {
342 println!("It finished");
343 if let Ok(v) = ct.render(
344 "person",
345 &Context::from([
346 ("hi".to_string(),Value::String("hi".to_string())),
347 ("person".to_string(),Value::Dictionary(HashMap::from([
348 ("first_name".to_string(), Value::String("Bob".to_string())),
349 ("last_name".to_string(), Value::String("Smith".to_string())),
350 ]))),
351 ])
352 ) {
353 println!("View result: {:?}", v );
354 } else {
355 println!("Error?");
356 }
357 }
358 }
359
360 #[test]
361 fn loops() -> Result<(),CrimError> {
362 let mut ct = Crimtag::new();
363 ct.load_static(r#"Looping code:
364[|view "index"|>We will enumerate people here:[|loop people|>
365 - [|show last_name|], [|show first_name|][|if show_title |>: [|show title|] <|else|> **title redacted** <|if|] ::>[|loop tags|> [|show tag|]<|loop|] <:: <|loop|]
366<|view|]
367"#)?;
368
369 let ctx = Context::from([
370 ("people".to_string(), vec![
371 [
372 ("first_name".into(), "Joe".into()),
373 ("last_name".into(), "Smith".into()),
374 ("show_title".into(), true.into()),
375 ("title".into(), "CEO".into()),
376 ("tags".into(), vec![
377 Context::from([("tag".into(), "jerk".into()),]).into(),
378 Context::from([("tag".into(), "ugly".into()),]).into(),
379 ].into()),
380 ].into(),
381 [
382 ("first_name".into(), "Chris".into()),
383 ("last_name".into(), "Perkens".into()),
384 ("show_title".into(), false.into()),
385 ("title".into(), "Baconeer".into()),
386 ("tags".into(), vec![
387 Context::from([("tag".into(), "jerk".into()),]).into(),
388 Context::from([("tag".into(), "ugly".into()),]).into(),
389 ].into()),
390 ].into(),
391 [
392 ("first_name".into(), "Will".into()),
393 ("last_name".into(), "Power".into()),
394 ("show_title".into(), false.into()),
395 ("title".into(), "CFO".into()),
396 ("tags".into(), vec![
397 Context::from([("tag".into(), "jerk".into()),]).into(),
398 Context::from([("tag".into(), "ugly".into()),]).into(),
399 ].into()),
400 ].into(),
401 [
402 ("first_name".into(), "Justin".into()),
403 ("last_name".into(), "Time".into()),
404 ("show_title".into(), true.into()),
405 ("title".into(), "CIO".into()),
406 ("tags".into(), vec![
407 Context::from([("tag".into(), "jerk".into()),]).into(),
408 Context::from([("tag".into(), "ugly".into()),]).into(),
409 ].into()),
410 ].into(),
411 ].into()),
412 ]);
413
414 println!("View result: {}", ct.render("index", &ctx)? );
415
416 Ok(())
417 }
418
419 #[test]
420 fn conditional() -> Result<(),CrimError> {
421 let mut ct = Crimtag::new();
422 ct.load_static(r#"Hi there
423[|view "index"|>
424 Color: [|if is_red|> red <|elif is_blue |> blue <|else|> green <|if|]
425<|view|]"#)?;
426
427 let ctx = Context::from([
428 ("is_red".into(), false.into()),
429 ("is_blue".into(), false.into()),
430 ("color".into(), "purple".into()),
431 ]);
432
433 println!("View result: {}", ct.render("index", &ctx)? );
434
435 Ok(())
436 }
437
438 struct Item {
439 pub id: i32,
440 pub title: String,
441 pub body: String,
442 }
443
444 struct Person {
445 pub id: i32,
446 pub username: String,
447 pub name: String,
448 }
449
450 struct Page {
451 pub user: Person,
452 pub total_items: i32,
453 pub total_pages: i32,
454 pub cur_page: i32,
455 pub items: Vec<Item>,
456 }
457
458 impl Page {
459 fn new() -> Page {
460 Page {
461 user: Person {
462 id: 443,
463 username: "eichlan".into(),
464 name: "Mike".into(),
465 },
466 total_items: 4000,
467 total_pages: 400,
468 cur_page: 35,
469 items: vec![
470 Item{
471 id: 123,
472 title: "hi".into(),
473 body: "body".into(),
474 },
475 Item{
476 id: 124,
477 title: "bye".into(),
478 body: "wooo".into(),
479 },
480 Item{
481 id: 125,
482 title: "ciao".into(),
483 body: "whatever".into(),
484 }
485 ],
486 }
487 }
488 }
489
490 impl<'a> MappedStructure<'a> for Page {
491 fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
492 match id {
493 "user" => Some(MappedValue::Struct(&self.user)),
494 "total_items" => Some(MappedValue::Int32(self.total_items)),
495 "total_pages" => Some(MappedValue::Int32(self.total_pages)),
496 "cur_page" => Some(MappedValue::Int32(self.cur_page)),
497 "items" => Some(MappedValue::<'a>::List(&self.items)),
498 _ => None,
499 }
500 }
501 }
502
503 impl<'a> MappedStructure<'a> for Person {
504 fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
505 match id {
506 "id" => Some(MappedValue::Int32(self.id)),
507 "username" => Some(MappedValue::String(&self.username)),
508 "name" => Some(MappedValue::String(&self.name)),
509 _ => None,
510 }
511 }
512 }
513
514 impl<'a> MappedStructure<'a> for Item {
515 fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> {
516 match id {
517 "id" => Some(MappedValue::Int32(self.id)),
518 "title" => Some(MappedValue::String(&self.title)),
519 "body" => Some(MappedValue::String(&self.body)),
520 _ => None,
521 }
522 }
523 }
524
525 #[test]
526 fn custom() -> Result<(), CrimError> {
527 let page = Page::new();
528
529 println!("{:?}",
530 page.get_value(&"total_pages")
531 );
532
533 if let Some(MappedValue::List(l)) = page.get_value(&"items") {
534 l.for_each(&|x: &MappedValue, buf: &mut String| {
535 if let MappedValue::Struct(s) = x {
536 println!(" - {:?}", s.get_value(&"title"));
537 buf.push_str(&format!("{:?}",s.get_value(&"title")));
538 }
539 Ok(())
540 }).expect("loop");
541 }
542 Ok(())
543 }
544}