sato/
lib.rs

1/*!
2an s-expression based html templating system.
3
4# sato template language examples
5## basic template example
6```sato
7(html
8 (head
9  (title "basic example")))
10```
11
12## tag attributes
13```sato
14(html
15 (head (@ (some thing))
16  (title "basic example")))
17```
18
19## variables
20variables in sato are prefixed with a `$`.
21```sato
22(html
23 (head
24  (title $some_variable)))
25```
26
27## conditionals
28```sato
29(html
30 (head
31  (title (if (is-set $some_variable)
32             (div "$some_variable")
33             (div "variable is not set")))))
34```
35
36## iteration over arrays
37```sato
38(html
39 (body
40  (for i in $some_array
41       (div "element: " $i))))
42```
43
44## iteration over maps
45```sato
46(html
47 (body
48  (for k v in $some_map
49       (div $k ": " $v))))
50```
51
52## switch/case
53```sato
54(html
55 (body
56  (switch $blah
57          (case asdf
58            qwer)
59          (case zxcv
60            (div what else))
61          (case hjkl
62            nm))))
63```
64
65
66# basic library example
67```rust
68use sato::renderer::Renderer;
69use sato::context::RenderContext;
70use sato::template::Template;
71
72let renderer = Renderer::builder()
73    .build();
74let expr = r#"(html (head (title "basic example")))"#;
75let template = Template::from_str(expr).unwrap();
76let html = renderer.render(&template, &RenderContext::default()).unwrap();
77
78assert_eq!(html, "<!doctype html5><html><head><title>basic example</title></head></html>")
79```
80
81# using variables
82```rust
83use sato::renderer::Renderer;
84use sato::context::RenderContext;
85use sato::template::Template;
86
87let renderer = Renderer::builder()
88    .build();
89let expr = r#"(html (body (if (eq $asdf qwer) (for i in $array (div $i)))))"#;
90let template = Template::from_str(expr).unwrap();
91let context = RenderContext::builder()
92    .insert("asdf", "qwer")
93    .insert("array", vec!["zxc", "xcv", "cvb"])
94    .build();
95let html = renderer.render(&template, &context).unwrap();
96
97assert_eq!(html, "<!doctype html5><html><body><div>zxc</div><div>xcv</div><div>cvb</div></body></html>")
98```
99
100# custom handler functions
101```rust
102use sato::renderer::{Attributes, Renderer, RenderError};
103use sato::context::RenderContext;
104use sato::template::{Template, TemplateExprNode};
105
106let post_expr = r##"(div (h2 $title) (span "posted by " $author) $content (br) (div (for tag in $tags (span "#" $tag))))"##;
107let blogpost_template = Template::from_str(post_expr).unwrap();
108
109let renderer = Renderer::builder()
110    .function("blogpost", Box::new(move |attrs, expr, renderer, context| {
111        let title = attrs.get("title").unwrap();
112        let author = attrs.get("author").unwrap();
113
114        let mut new_context = context.clone();
115        new_context.insert("title", title);
116        new_context.insert("author", author);
117        new_context.insert("content", renderer.evaluate_multiple(expr, &new_context)?);
118
119        Ok(renderer.render(&blogpost_template, &new_context).unwrap().into())
120    }))
121    .build();
122let expr = r#"(html (body (blogpost (@ (title faketitle) (author me)) (div "my content here"))))"#;
123let template = Template::from_str(expr).unwrap();
124let context = RenderContext::builder()
125    .insert("tags", vec!["zxc", "xcv", "cvb"])
126    .build();
127let html = renderer.render(&template, &context).unwrap();
128
129assert_eq!(html, "<!doctype html5><html><body><div><h2>faketitle</h2><span>posted by me</span><div>my content here</div><br /><div><span>#zxc</span><span>#xcv</span><span>#cvb</span></div></div></body></html>")
130```
131
132
133# builtin functions
134## if
135`(if [condition] [true code block] [false code block])`
136if condition evaluates to true then execute the true block, if false then execute false block.
137
138## get
139`(get [array] [index])`
140
141`(get [map] [key])`
142
143gets an element from an array or map
144
145## is-set
146`(is-set [variable])`
147
148takes a single argument and returns true or false depending if the variable is set.
149
150## switch/case
151`(switch [variable] (case [value] [code block]) (case [value] [code block]) ...)`
152
153## for
154`(for [item] in [array] [code block])`
155
156`(for [key] [value] in [map] [code block])`
157
158`(for [item] in (range [min] [max] [step?]) [code block])`
159
160`(for (enumerate [index] [item]) in [array] [code block])`
161
162executes code block for each element in the iterable.
163
164## eq/gt/lt/gte/lte/ne
165`(eq [item] [item])`
166
167standard comparison operators, returns true or false
168
169## +, -, *, /, %
170standard math operators
171
172`(+ [item] [item])`
173
174*/
175
176
177mod builtins;
178pub mod context;
179pub mod renderer;
180pub mod template;
181
182pub use crate::renderer::{Renderer, RenderValue, Attribute, Attributes, RenderError};
183pub use crate::template::{Template, TemplateExprNode};
184pub use crate::context::{RenderContext, ContextValue};
185
186
187#[cfg(test)]
188mod tests {
189    use crate::context::{RenderContext, ContextValue};
190    use crate::renderer::{Renderer, RenderValue};
191    use crate::template::{Template, TemplateExprNode};
192
193    #[test]
194    fn test_no_builtins() {
195        let renderer = Renderer::builder()
196            .build();
197        let expr = r#"(head (title "test title"))"#;
198        let template = Template::from_str(expr).unwrap();
199        let html = renderer.render(&template, &RenderContext::default()).unwrap();
200
201        assert_eq!(html, "<head><title>test title</title></head>")
202    }
203
204    #[test]
205    fn test_basic_render() {
206        let renderer = Renderer::builder()
207            .build();
208        let expr = r#"(html (head (title "test title")))"#;
209        let template = Template::from_str(expr).unwrap();
210        let html = renderer.render(&template, &RenderContext::default()).unwrap();
211
212        assert_eq!(html, "<!doctype html5><html><head><title>test title</title></head></html>")
213    }
214
215    #[test]
216    fn test_attributes() {
217        let renderer = Renderer::builder()
218            .build();
219        let expr = r#"(html (head (@ (asdf qwer) (zxc asd))(title "test title")))"#;
220        let template = Template::from_str(expr).unwrap();
221        let html = renderer.render(&template, &RenderContext::default()).unwrap();
222        assert_eq!(html, r#"<!doctype html5><html><head asdf="qwer" zxc="asd"><title>test title</title></head></html>"#)
223    }
224
225    #[test]
226    fn test_attributes_on_empty_tag() {
227        let renderer = Renderer::builder()
228            .build();
229        let expr = r#"(html (body (@ (asdf qwer))))"#;
230        let template = Template::from_str(expr).unwrap();
231        let html = renderer.render(&template, &RenderContext::default()).unwrap();
232        assert_eq!(html, r#"<!doctype html5><html><body asdf="qwer" /></html>"#)
233    }
234
235    #[test]
236    fn test_basic_substitution() {
237        let renderer = Renderer::builder()
238            .build();
239        let expr = r#"(html (head (title $title)))"#;
240        let template = Template::from_str(expr).unwrap();
241        let context = RenderContext::builder()
242            .insert("title", "some sort of title")
243            .build();
244        let html = renderer.render(&template, &context).unwrap();
245        assert_eq!(html, r#"<!doctype html5><html><head><title>some sort of title</title></head></html>"#)
246    }
247
248    #[test]
249    fn test_vec_substitution() {
250        let renderer = Renderer::builder()
251            .build();
252        let expr = r#"(html (div $vec))"#;
253        let template = Template::from_str(expr).unwrap();
254        let context = RenderContext::builder()
255            .insert("asdf", "qwer")
256            .insert("vec", vec!["this", "that", "$asdf"])
257            .build();
258        let html = renderer.render(&template, &context).unwrap();
259        assert_eq!(html, r#"<!doctype html5><html><div>thisthatqwer</div></html>"#)
260    }
261
262    #[test]
263    fn test_if_is_set() {
264        let renderer = Renderer::builder()
265            .build();
266        let expr = r#"(html (head (if (is-set $title) (title $title) (title "not set"))))"#;
267        let template = Template::from_str(expr).unwrap();
268        let context = RenderContext::builder()
269            .insert("title", "some sort of title")
270            .build();
271        let html = renderer.render(&template, &context).unwrap();
272        assert_eq!(html, r#"<!doctype html5><html><head><title>some sort of title</title></head></html>"#)
273    }
274
275    #[test]
276    fn test_if_not_is_set() {
277        let renderer = Renderer::builder()
278            .build();
279        let expr = r#"(html (head (if (is-set $title) (title $title) (title "not set"))))"#;
280        let template = Template::from_str(expr).unwrap();
281        let context = RenderContext::builder()
282            .insert("title2", "some sort of title")
283            .build();
284        let html = renderer.render(&template, &context).unwrap();
285        assert_eq!(html, r#"<!doctype html5><html><head><title>not set</title></head></html>"#)
286    }
287
288    #[test]
289    fn test_array_iteration() {
290        let renderer = Renderer::builder()
291            .build();
292        let expr = r#"(html (body (for i in $asdf (div "iter " $i))))"#;
293        let template = Template::from_str(expr).unwrap();
294        let context = RenderContext::builder()
295            .insert("asdf", vec!["qaz", "wsx", "edc"])
296            .build();
297        let html = renderer.render(&template, &context).unwrap();
298        assert_eq!(html, r#"<!doctype html5><html><body><div>iter qaz</div><div>iter wsx</div><div>iter edc</div></body></html>"#)
299    }
300
301    #[test]
302    fn test_range_iteration() {
303        let renderer = Renderer::builder()
304            .build();
305        let expr = r#"(html (body (for i in (range 0 3) (div "iter " $i))))"#;
306        let template = Template::from_str(expr).unwrap();
307        let html = renderer.render(&template, &RenderContext::default()).unwrap();
308        assert_eq!(html, r#"<!doctype html5><html><body><div>iter 0</div><div>iter 1</div><div>iter 2</div></body></html>"#)
309    }
310
311    #[test]
312    fn test_array_index_iteration() {
313        let renderer = Renderer::builder()
314            .build();
315        let expr = r#"(html (body (for (enumerate k i) in $asdf (div $k ": iter " $i))))"#;
316        let template = Template::from_str(expr).unwrap();
317        let context = RenderContext::builder()
318            .insert("asdf", vec!["qaz", "wsx", "edc"])
319            .build();
320        let html = renderer.render(&template, &context).unwrap();
321        assert_eq!(html, r#"<!doctype html5><html><body><div>0: iter qaz</div><div>1: iter wsx</div><div>2: iter edc</div></body></html>"#)
322    }
323
324    #[test]
325    fn test_object_iteration() {
326        let renderer = Renderer::builder()
327            .build();
328        let expr = r#"(html (body (for k v in $asdf (div "key " $k ", value " $v))))"#;
329        let template = Template::from_str(expr).unwrap();
330        let internal_obj = RenderContext::builder()
331            .insert("as", "df")
332            .insert("qw", "er")
333            .build();
334        let context = RenderContext::builder()
335            .insert("asdf", internal_obj)
336            .build();
337        let html = renderer.render(&template, &context).unwrap();
338        assert_eq!(html, r#"<!doctype html5><html><body><div>key as, value df</div><div>key qw, value er</div></body></html>"#)
339    }
340
341    #[test]
342    fn test_object_access() {
343        let renderer = Renderer::builder()
344            .build();
345        let expr = r#"(html (body $asdf.as))"#;
346        let template = Template::from_str(expr).unwrap();
347
348        let internal_obj = RenderContext::builder()
349            .insert("as", "df")
350            .build();
351        let context = RenderContext::builder()
352            .insert("asdf", internal_obj)
353            .build();
354        let html = renderer.render(&template, &context).unwrap();
355        assert_eq!(html, r#"<!doctype html5><html><body>df</body></html>"#)
356    }
357
358    #[test]
359    fn test_deep_object_access() {
360        let renderer = Renderer::builder()
361            .build();
362        let expr = r#"(html (body $nested_object.as.df.qw.er))"#;
363        let template = Template::from_str(expr).unwrap();
364
365        let nested3_obj = RenderContext::builder()
366            .insert("er", "look at this nested thing")
367            .build();
368        let nested2_obj = RenderContext::builder()
369            .insert("qw", nested3_obj)
370            .build();
371        let nested1_obj = RenderContext::builder()
372            .insert("df", nested2_obj)
373            .build();
374        let nested0_obj = RenderContext::builder()
375            .insert("as", nested1_obj)
376            .build();
377
378        let context = RenderContext::builder()
379            .insert("nested_object", nested0_obj)
380            .build();
381        let html = renderer.render(&template, &context).unwrap();
382        assert_eq!(html, r#"<!doctype html5><html><body>look at this nested thing</body></html>"#)
383    }
384
385    #[test]
386    fn test_variable_in_attributes() {
387        let renderer = Renderer::builder()
388            .build();
389        let expr = r#"(html (head (@ (asdf $qwer) (zxc asd))(title "test title")))"#;
390        let template = Template::from_str(expr).unwrap();
391        let context = RenderContext::builder()
392            .insert("qwer", "zxcv")
393            .build();
394        let html = renderer.render(&template, &context).unwrap();
395        assert_eq!(html, r#"<!doctype html5><html><head asdf="zxcv" zxc="asd"><title>test title</title></head></html>"#)
396    }
397
398    #[test]
399    fn test_object_variable_in_attributes() {
400        let renderer = Renderer::builder()
401            .build();
402        let expr = r#"(html (for qw in $as (div (@ (class $qw.er)) $qw.zx)))"#;
403        let template = Template::from_str(expr).unwrap();
404
405        let obj1 = RenderContext::builder()
406            .insert("er", "cv")
407            .insert("zx", "hj")
408            .build();
409        let obj2 = RenderContext::builder()
410            .insert("er", "df")
411            .insert("zx", "nm")
412            .build();
413
414        let context = RenderContext::builder()
415            .insert("as", vec![obj1, obj2])
416            .build();
417        let html = renderer.render(&template, &context).unwrap();
418        assert_eq!(html, r#"<!doctype html5><html><div class="cv">hj</div><div class="df">nm</div></html>"#)
419    }
420
421    #[test]
422    fn test_case() {
423        let renderer = Renderer::builder()
424            .build();
425        let expr = r#"(html (div (switch $blah (case asdf qwer) (case zxcv (div what else)) (case hjkl nm))))"#;
426        let template = Template::from_str(expr).unwrap();
427        let context = RenderContext::builder()
428            .insert("blah", "zxcv")
429            .build();
430        let html = renderer.render(&template, &context).unwrap();
431        assert_eq!(html, r#"<!doctype html5><html><div><div>whatelse</div></div></html>"#)
432    }
433
434    #[test]
435    fn test_custom_closure() {
436        let renderer = Renderer::builder()
437            .function("blah", Box::new(|_, _, _, _| {
438                Ok("hello there".into())
439            }))
440            .build();
441        let expr = r#"(html (div (blah something or other)))"#;
442        let template = Template::from_str(expr).unwrap();
443        let html = renderer.render(&template, &RenderContext::default()).unwrap();
444        assert_eq!(html, r#"<!doctype html5><html><div>hello there</div></html>"#)
445    }
446
447    #[test]
448    fn test_custom_function() {
449        fn blah(_: crate::renderer::Attributes, _: &[TemplateExprNode], _: &Renderer, _: &RenderContext) -> Result<RenderValue, crate::renderer::RenderError> {
450            Ok("hello there".into())
451        }
452
453        let renderer = Renderer::builder()
454            .function("blarg", Box::new(blah))
455            .build();
456        let expr = r#"(html (div (blarg something or other)))"#;
457        let template = Template::from_str(expr).unwrap();
458        let html = renderer.render(&template, &RenderContext::default()).unwrap();
459        assert_eq!(html, r#"<!doctype html5><html><div>hello there</div></html>"#)
460    }
461
462    #[test]
463    fn test_using_attrs_in_closure() {
464        let renderer = Renderer::builder()
465            .function("blah", Box::new(|attrs, _, _, _| {
466                let mut output: Vec<String> = Vec::new();
467
468                for attr in &attrs {
469                    output.push("[".into());
470                    output.push(attr.0.clone());
471                    output.push(" = ".into());
472                    output.push(attr.1.clone());
473                    output.push("] ".into());
474                }
475
476                Ok(output.into())
477            }))
478            .build();
479        let expr = r#"(html (div (blah (@ (this is) (the attr)) something or other)))"#;
480        let template = Template::from_str(expr).unwrap();
481        let html = renderer.render(&template, &RenderContext::default()).unwrap();
482        assert_eq!(html, r#"<!doctype html5><html><div>[this = is] [the = attr] </div></html>"#)
483    }
484
485    #[test]
486    fn test_more_html_in_closure() {
487        let renderer = Renderer::builder()
488            .function("blah", Box::new(|_, expr, renderer, context| {
489                let mut output: Vec<RenderValue> = Vec::new();
490                output.push("<blah>".into());
491                output.push(renderer.evaluate_multiple(expr, context)?.into());
492                output.push("</blah>".into());
493                Ok(output.into())
494            }))
495            .build();
496        let expr = r#"(html (div (blah (span hello))))"#;
497        let template = Template::from_str(expr).unwrap();
498        let html = renderer.render(&template, &RenderContext::default()).unwrap();
499        assert_eq!(html, r#"<!doctype html5><html><div><blah><span>hello</span></blah></div></html>"#)
500    }
501
502    #[test]
503    fn test_renderer_in_closure() {
504        let subexpr = r#"(sub str)"#;
505        let subtemplate = Template::from_str(subexpr).unwrap();
506
507        let renderer = Renderer::builder()
508            .function("blah", Box::new(move |_, _, renderer, _| {
509                let mut output: Vec<String> = Vec::new();
510                output.push("<blah>".into());
511                let suboutput = renderer.render(&subtemplate, &RenderContext::default())?;
512                output.push(suboutput);
513                output.push("</blah>".into());
514                Ok(output.into())
515            }))
516            .build();
517        let expr = r#"(html (div (blah (span hello))))"#;
518        let template = Template::from_str(expr).unwrap();
519        let html = renderer.render(&template, &RenderContext::default()).unwrap();
520        assert_eq!(html, r#"<!doctype html5><html><div><blah><sub>str</sub></blah></div></html>"#)
521    }
522
523    #[test]
524    fn test_context_in_closure() {
525        let renderer = Renderer::builder()
526            .function("blah", Box::new(move |_, _, _, context| {
527                let s = match context.get("blah").unwrap() {
528                    ContextValue::String(s) => s,
529                    _ => panic!("not a str")
530                }.clone();
531
532                let mut output: Vec<String> = Vec::new();
533                output.push("<blah>".into());
534                output.push(s);
535                output.push("</blah>".into());
536                Ok(output.into())
537            }))
538            .build();
539        let expr = r#"(html (div (blah (span hello))))"#;
540        let template = Template::from_str(expr).unwrap();
541        let context = RenderContext::builder()
542            .insert("blah", "zxcv")
543            .build();
544        let html = renderer.render(&template, &context).unwrap();
545        assert_eq!(html, r#"<!doctype html5><html><div><blah>zxcv</blah></div></html>"#)
546    }
547
548    #[test]
549    fn test_closure_with_everything() {
550        let subexpr = r#"(sub $content)"#;
551        let subtemplate = Template::from_str(subexpr).unwrap();
552
553        let renderer = Renderer::builder()
554            .function("blah", Box::new(move |attr, expr, renderer, context| {
555                let mut output: Vec<RenderValue> = Vec::new();
556                output.push("<blah>".into());
557
558                let mut subcontext = RenderContext::default();
559                subcontext.insert("content", attr.get("something").unwrap().clone());
560                let suboutput = renderer.render(&subtemplate, &subcontext)?;
561                output.push(suboutput.into());
562                output.push(renderer.evaluate_multiple(expr, context)?.into());
563                output.push(match context.get("blah").unwrap() {
564                    ContextValue::String(s) => s,
565                    _ => panic!("not a str")
566                }.clone().into());
567
568                output.push("</blah>".into());
569                Ok(output.into())
570            }))
571            .build();
572        let expr = r#"(html (div (blah (@ (something extra)) (span hello))))"#;
573        let template = Template::from_str(expr).unwrap();
574        let context = RenderContext::builder()
575            .insert("blah", "zxcv")
576            .build();
577        let html = renderer.render(&template, &context).unwrap();
578        assert_eq!(html, r#"<!doctype html5><html><div><blah><sub>extra</sub><span>hello</span>zxcv</blah></div></html>"#)
579    }
580
581    #[test]
582    fn test_math_op_mod() {
583        let renderer = Renderer::builder()
584            .build();
585        let expr = r#"(html (body (for i in (range 0 5) (if (eq (% $i 2) 0) (div $i)))))"#;
586        let template = Template::from_str(expr).unwrap();
587        let context = RenderContext::builder()
588            .build();
589        let html = renderer.render(&template, &context).unwrap();
590        assert_eq!(html, r#"<!doctype html5><html><body><div>0</div><div>2</div><div>4</div></body></html>"#)
591    }
592
593    #[test]
594    fn test_variable_range_iteration() {
595        let renderer = Renderer::builder()
596            .build();
597        let expr = r#"(html (body (for i in (range $a $b) (div "iter " $i))))"#;
598        let template = Template::from_str(expr).unwrap();
599        let context = RenderContext::builder()
600            .insert("a", 4)
601            .insert("b", 7)
602            .build();
603        let html = renderer.render(&template, &context).unwrap();
604        assert_eq!(html, r#"<!doctype html5><html><body><div>iter 4</div><div>iter 5</div><div>iter 6</div></body></html>"#)
605    }
606
607    #[test]
608    fn test_math_ops() {
609        let renderer = Renderer::builder()
610            .build();
611        let expr = r#"(html (body (+ (/ (* $b (- $c (+ 2 3))) $c) (+ $b $c))))"#;
612        let template = Template::from_str(expr).unwrap();
613        let context = RenderContext::builder()
614            .insert("a", 4)
615            .insert("b", 7)
616            .insert("c", 12)
617            .build();
618        let html = renderer.render(&template, &context).unwrap();
619        assert_eq!(html, r#"<!doctype html5><html><body>23</body></html>"#)
620    }
621
622    #[test]
623    fn test_math_ops_in_attributes() {
624        let renderer = Renderer::builder()
625            .build();
626        let expr = r#"(html (body (@ (blah (+ 2 3)) (asdf (* $a $b)))))"#;
627        let template = Template::from_str(expr).unwrap();
628        let context = RenderContext::builder()
629            .insert("a", 4)
630            .insert("b", 7)
631            .build();
632        let html = renderer.render(&template, &context).unwrap();
633        assert_eq!(html, r#"<!doctype html5><html><body blah="5" asdf="28" /></html>"#)
634    }
635
636    #[test]
637    fn test_math_ops_in_for_range() {
638        let renderer = Renderer::builder()
639            .build();
640        let expr = r#"(html (body (for i in (range (- $b $a) (+ $b 1)) (div "iter " $i))))"#;
641        let template = Template::from_str(expr).unwrap();
642        let context = RenderContext::builder()
643            .insert("a", 4)
644            .insert("b", 7)
645            .build();
646        let html = renderer.render(&template, &context).unwrap();
647        assert_eq!(html, r#"<!doctype html5><html><body><div>iter 3</div><div>iter 4</div><div>iter 5</div><div>iter 6</div><div>iter 7</div></body></html>"#)
648    }
649
650    #[test]
651    fn test_get_from_array() {
652        let renderer = Renderer::builder()
653            .build();
654        let expr = r#"(html (body (get $a 0) (get $a 2)))"#;
655        let template = Template::from_str(expr).unwrap();
656        let context = RenderContext::builder()
657            .insert("a", vec!["asd", "qwe", "zxc"])
658            .build();
659        let html = renderer.render(&template, &context).unwrap();
660        assert_eq!(html, r#"<!doctype html5><html><body>asdzxc</body></html>"#)
661    }
662
663    #[test]
664    fn test_get_from_object() {
665        let renderer = Renderer::builder()
666            .build();
667        let expr = r#"(html (body (get $asdf as) (get $asdf zx)))"#;
668        let template = Template::from_str(expr).unwrap();
669        let internal_obj = RenderContext::builder()
670            .insert("as", "df")
671            .insert("qw", "er")
672            .insert("zx", "cv")
673            .build();
674        let context = RenderContext::builder()
675            .insert("asdf", internal_obj)
676            .build();
677        let html = renderer.render(&template, &context).unwrap();
678        assert_eq!(html, r#"<!doctype html5><html><body>dfcv</body></html>"#)
679    }
680
681    #[test]
682    fn test_nested_gets_from_array() {
683        let renderer = Renderer::builder()
684            .build();
685        let expr = r#"(html (body (get (get $a 2) 0)))"#;
686        let template = Template::from_str(expr).unwrap();
687
688        let subvec: ContextValue = vec![ContextValue::from("blah"), "123".into(), "this".into()].into();
689        let v: ContextValue = vec!["asd".into(), "qwe".into(), subvec, "zxc".into()].into();
690        let context = RenderContext::builder()
691            .insert("a", v)
692            .build();
693        let html = renderer.render(&template, &context).unwrap();
694        assert_eq!(html, r#"<!doctype html5><html><body>blah</body></html>"#)
695    }
696
697    #[test]
698    fn test_multi_type_vec() {
699        let renderer = Renderer::builder()
700            .build();
701        let expr = r#"(html (body $a))"#;
702        let template = Template::from_str(expr).unwrap();
703
704        let v: ContextValue = vec!["blah".into(), ContextValue::from(123), "this".into(), ContextValue::from(true)].into();
705        let context = RenderContext::builder()
706            .insert("a", v)
707            .build();
708        let html = renderer.render(&template, &context).unwrap();
709        assert_eq!(html, r#"<!doctype html5><html><body>blah123thistrue</body></html>"#)
710    }
711
712    #[test]
713    fn test_multielement_attributes() {
714        let renderer = Renderer::builder()
715            .build();
716        let expr = r#"(html (body (@ (asdf this that) (some thing $a))))"#;
717        let template = Template::from_str(expr).unwrap();
718        let context = RenderContext::builder()
719            .insert("a", "blah")
720            .build();
721        let html = renderer.render(&template, &context).unwrap();
722        assert_eq!(html, r#"<!doctype html5><html><body asdf="thisthat" some="thingblah" /></html>"#)
723    }
724
725    #[test]
726    fn test_var_in_both_attribute_positions() {
727        let renderer = Renderer::builder()
728            .build();
729        let expr = r#"(html (body (@ ($b asdf $a))))"#;
730        let template = Template::from_str(expr).unwrap();
731        let context = RenderContext::builder()
732            .insert("a", "blah")
733            .insert("b", "this")
734            .build();
735        let html = renderer.render(&template, &context).unwrap();
736        assert_eq!(html, r#"<!doctype html5><html><body this="asdfblah" /></html>"#)
737    }
738
739    #[test]
740    fn test_template_as_context_variable() {
741        let renderer = Renderer::builder()
742            .build();
743        let main_expr = r#"(html (body $body))"#;
744        let main_template = Template::from_str(main_expr).unwrap();
745        let sub_expr = r#"(span hello)"#;
746        let sub_template = Template::from_str(sub_expr).unwrap();
747        let context = RenderContext::builder()
748            .insert("body", sub_template)
749            .build();
750        let html = renderer.render(&main_template, &context).unwrap();
751        assert_eq!(html, r#"<!doctype html5><html><body><span>hello</span></body></html>"#)
752    }
753
754    #[test]
755    fn test_nested_for() {
756        let renderer = Renderer::builder()
757            .build();
758        let main_expr = r#"(html
759                            (for item in $items
760                                 (div $item.item
761                                      [(for id in $item.sub
762                                            $id)])))"#;
763
764        let items = [1, 2]
765            .into_iter()
766            .map(|i| {
767                RenderContext::builder()
768                    .insert("item", i)
769                    .insert("sub", vec![i+1, i+2])
770                    .build()
771            })
772            .collect::<Vec<_>>();
773
774        let context = RenderContext::builder()
775            .insert("items", items)
776            .build();
777
778        let main_template = Template::from_str(main_expr).unwrap();
779        let html = renderer.render(&main_template, &context).unwrap();
780
781        assert_eq!(html, r#"<!doctype html5><html><div>1[23]</div><div>2[34]</div></html>"#)
782    }
783
784    #[test]
785    fn test_if_unset_var_defaults_to_false() {
786        let renderer = Renderer::builder()
787            .build();
788        let expr = r#"(html (if $as.df 1 2))"#;
789        let template = Template::from_str(expr).unwrap();
790
791        let subcontext = RenderContext::builder()
792            .build();
793
794        let context = RenderContext::builder()
795            .insert("as", subcontext)
796            .build();
797        let html = renderer.render(&template, &context).unwrap();
798        assert_eq!(html, r#"<!doctype html5><html>2</html>"#)
799    }
800}