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