stilts_lang/
lib.rs

1//! Stilts Lang is the parser for the stilts language
2//! Parse a full template with [`parse_template`] the
3//! output is is an ast whose definition is in the [`types`] module
4
5pub use error::Error;
6pub use located::Located;
7pub use state::Delims;
8use state::State;
9use types::Root;
10
11pub(crate) type Input<'i> = winnow::Stateful<Located<'i>, State>;
12
13mod error;
14mod located;
15mod parse;
16mod state;
17pub mod types;
18
19/// Parse a template
20///
21/// The template lives as long as the input, if you want you can convert it into
22/// an owned version using [`Root::into_owned`]. This is practically guaranteed to
23/// do a bunch of allocations, but it really shouldn't take that long
24pub fn parse_template(input: &str, delims: Delims) -> Result<Root<'_>, Error<'_>> {
25    use winnow::Parser;
26    let input = winnow::Stateful {
27        input: Located::new(input),
28        state: State::default(),
29    };
30    parse::root(&delims)
31        .parse(input)
32        .map_err(|err| err.into_inner())
33}
34
35#[cfg(test)]
36mod test {
37    use crate::{parse_template, types::{Expr, IfBranch, Item, ItemBlock, ItemFor, ItemIf, ItemMacro, ItemMatch, MatchArm, Root}, Delims};
38    use pretty_assertions::assert_eq;
39    use syn::{parse::Parser as _, punctuated::Punctuated};
40
41    const TEMPLATE: &str = r###"
42{% extends "base.html" %}
43
44{% fn my_func(s: &str) -> String {
45    let mut out = "OOF".to_string();
46    out.push_str(s);
47    out
48} %}
49
50{% macro my_mac(time: std::time::Duration) %}
51    INSIDE MY MAC
52{% end %}
53
54{% block head %}
55    {% a %}
56    {% super() %}
57    overwrites
58{% end %}
59
60{% block header %}
61    {% for i in 0..10 %}
62        {% match i %}
63            {% when 2 if i != 0 %}
64                {% i.json() %}
65            {% when 3 | 4 %}
66                {% a %}
67            {% when _ %}
68        {% end %}
69    {% end %}
70    {% if true %}
71        {% a %}
72    {% else %}
73        {% a %}
74    {% end %}
75{% end %}
76
77{% block main %}
78    {% "Hello Word" %}
79    {% include "other.html" %}
80    {% a %}
81{% end %}
82
83{% block footer %}
84    {% call my_mac(std::time::Duration::from_secs(50)) %}
85    {% my_func(s) %}
86{% end %}
87"###;
88
89    #[test]
90    pub fn parse_example_template() {
91        let res = parse_template(TEMPLATE, Delims::default()).unwrap();
92        let expects = Root {
93            content: vec![
94                Item::Expr(Expr::Extends("base.html".into())),
95                Item::Content("\n\n".into()),
96                Item::Expr(Expr::Stmt(syn::parse_str(r#"fn my_func(s: &str) -> String {
97                    let mut out = "OOF".to_string();
98                    out.push_str(s);
99                    out
100                }"#).unwrap())),
101                Item::Content("\n\n".into()),
102                Item::Macro(ItemMacro {
103                    name: syn::parse_str("my_mac").unwrap(),
104                    args: Punctuated::parse_terminated.parse_str("time: std::time::Duration").unwrap(),
105                    content: vec![Item::Content("\n    INSIDE MY MAC\n".into())],
106                }),
107                Item::Content("\n\n".into()),
108                Item::Block(ItemBlock {
109                    name: "head".into(),
110                    content: vec![
111                        Item::Content("\n    ".into()),
112                        Item::Expr(Expr::Expr(syn::parse_str("a").unwrap())),
113                        Item::Content("\n    ".into()),
114                        Item::Expr(Expr::SuperCall),
115                        Item::Content("\n    overwrites\n".into()),
116                    ]
117                }),
118                Item::Content("\n\n".into()),
119                Item::Block(ItemBlock {
120                    name: "header".into(),
121                    content: vec![
122                        Item::Content("\n    ".into()),
123                        Item::For(ItemFor {
124                            label: None,
125                            pat: syn::Pat::parse_single.parse_str("i").unwrap(),
126                            expr: syn::parse_str("0..10").unwrap(),
127                            content: vec![
128                                Item::Content("\n        ".into()),
129                                Item::Match(ItemMatch {
130                                    expr: syn::parse_str("i").unwrap(),
131                                    arms: vec![
132                                        MatchArm {
133                                            pat: syn::Pat::parse_multi.parse_str("2").unwrap(),
134                                            guard: Some(syn::parse_str("i != 0").unwrap()),
135                                            content: vec![
136                                                Item::Content("\n                ".into()),
137                                                Item::Expr(Expr::Expr(syn::parse_str("i.json()").unwrap())),
138                                                Item::Content("\n            ".into()),
139                                            ],
140                                        },
141                                        MatchArm {
142                                            pat: syn::Pat::parse_multi.parse_str("3 | 4").unwrap(),
143                                            guard: None,
144                                            content: vec![
145                                                Item::Content("\n                ".into()),
146                                                Item::Expr(Expr::Expr(syn::parse_str("a").unwrap())),
147                                                Item::Content("\n            ".into()),
148                                            ],
149                                        },
150                                        MatchArm {
151                                            pat: syn::Pat::parse_multi.parse_str("_").unwrap(),
152                                            guard: None,
153                                            content: vec![Item::Content("\n        ".into())],
154                                        }
155                                    ]
156                                }),
157                                Item::Content("\n    ".into()),
158                            ],
159                        }),
160                        Item::Content("\n    ".into()),
161                        Item::If(ItemIf {
162                            cond: syn::parse_str("true").unwrap(),
163                            content: vec![
164                                Item::Content("\n        ".into()),
165                                Item::Expr(Expr::Expr(syn::parse_str("a").unwrap())),
166                                Item::Content("\n    ".into()),
167                            ],
168                            branch: IfBranch::Else {
169                                content: vec![
170                                    Item::Content("\n        ".into()),
171                                    Item::Expr(Expr::Expr(syn::parse_str("a").unwrap())),
172                                    Item::Content("\n    ".into()),
173                                ],
174                            },
175                        }),
176                        Item::Content("\n".into()),
177                    ]
178                }),
179                Item::Content("\n\n".into()),
180                Item::Block(ItemBlock {
181                    name: "main".into(),
182                    content: vec![
183                        Item::Content("\n    ".into()),
184                        Item::Expr(Expr::Expr(syn::parse_str(r#""Hello Word""#).unwrap())),
185                        Item::Content("\n    ".into()),
186                        Item::Expr(Expr::Include {
187                            reference: "other.html".into(),
188                            args: Punctuated::parse_terminated.parse_str("").unwrap()
189                        }),
190                        Item::Content("\n    ".into()),
191                        Item::Expr(Expr::Expr(syn::parse_str("a").unwrap())),
192                        Item::Content("\n".into())
193                    ],
194                }),
195                Item::Content("\n\n".into()),
196                Item::Block(ItemBlock {
197                    name: "footer".into(),
198                    content: vec![
199                        Item::Content("\n    ".into()),
200                        Item::Expr(Expr::MacroCall {
201                            name: syn::parse_str("my_mac").unwrap(),
202                            args: Punctuated::parse_terminated.parse_str("std::time::Duration::from_secs(50)").unwrap(),
203                        }),
204                        Item::Content("\n    ".into()),
205                        Item::Expr(Expr::Expr(syn::parse_str("my_func(s)").unwrap())),
206                        Item::Content("\n".into()),
207                    ],
208                }),
209                Item::Content("\n".into()),
210            ]
211        };
212        assert_eq!(res, expects);
213    }
214}