harn_vm/stdlib/template/
outline.rs1use super::error::TemplateParseError;
15use super::parser::parse_outline;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct OutlineBlock {
27 pub kind: OutlineBlockKind,
28 pub start: usize,
29 pub end: usize,
30 pub bindings: Vec<String>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum OutlineBlockKind {
38 If,
40 Elif,
42 Else,
45 For,
47 Section,
49 Raw,
51 Comment,
53}
54
55pub fn parse(src: &str) -> Result<Vec<OutlineBlock>, TemplateParseError> {
61 parse_outline(src).map_err(TemplateParseError::from)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::{parse, OutlineBlockKind};
67
68 fn blocks(src: &str) -> Vec<(OutlineBlockKind, &str)> {
71 parse(src)
72 .expect("template parses")
73 .into_iter()
74 .map(|block| (block.kind, &src[block.start..block.end]))
75 .collect()
76 }
77
78 #[test]
79 fn every_branch_of_a_chain_closes_at_the_shared_end() {
80 let src = "{{ if a }}A{{ elif b }}B{{ else }}C{{ end }}";
81 assert_eq!(
82 blocks(src),
83 vec![
84 (OutlineBlockKind::If, src),
85 (OutlineBlockKind::Elif, "{{ elif b }}B{{ else }}C{{ end }}"),
86 (OutlineBlockKind::Else, "{{ else }}C{{ end }}"),
87 ]
88 );
89 }
90
91 #[test]
92 fn a_loop_covers_its_empty_fallback() {
93 let src = "{{ for x in xs }}{{ x }}{{ else }}none{{ end }}";
94 assert_eq!(
95 blocks(src),
96 vec![
97 (OutlineBlockKind::For, src),
98 (OutlineBlockKind::Else, "{{ else }}none{{ end }}"),
99 ]
100 );
101 }
102
103 #[test]
104 fn sections_raw_blocks_and_comments_are_all_located() {
105 let src = "{{# note #}}{{ section \"task\" }}{{ raw }}{{x}}{{ endraw }}{{ endsection }}";
106 assert_eq!(
107 blocks(src),
108 vec![
109 (OutlineBlockKind::Comment, "{{# note #}}"),
110 (
111 OutlineBlockKind::Section,
112 "{{ section \"task\" }}{{ raw }}{{x}}{{ endraw }}{{ endsection }}"
113 ),
114 (OutlineBlockKind::Raw, "{{ raw }}{{x}}{{ endraw }}"),
115 ]
116 );
117 }
118
119 #[test]
120 fn nested_blocks_come_back_in_source_order() {
121 let src = "{{ for x in xs }}{{ if x }}y{{ end }}{{ end }}";
122 assert_eq!(
123 blocks(src),
124 vec![
125 (OutlineBlockKind::For, src),
126 (OutlineBlockKind::If, "{{ if x }}y{{ end }}"),
127 ]
128 );
129 }
130
131 #[test]
132 fn loops_report_the_names_they_bind() {
133 let bindings = |src: &str| {
134 parse(src)
135 .expect("parses")
136 .into_iter()
137 .filter(|block| block.kind == OutlineBlockKind::For)
138 .map(|block| block.bindings)
139 .collect::<Vec<_>>()
140 };
141 assert_eq!(
142 bindings("{{ for item in items }}x{{ end }}"),
143 vec![vec!["item".to_string()]]
144 );
145 assert_eq!(
146 bindings("{{ for key, value in dict }}x{{ end }}"),
147 vec![vec!["key".to_string(), "value".to_string()]]
148 );
149 assert_eq!(
151 bindings("{{ for x in xs }}a{{ else }}b{{ end }}"),
152 vec![vec!["x".to_string()]]
153 );
154 }
155
156 #[test]
157 fn only_loops_bind_names() {
158 for block in
159 parse("{{ if a }}x{{ end }}{{ section \"task\" }}y{{ endsection }}").expect("parses")
160 {
161 assert!(
162 block.bindings.is_empty(),
163 "{:?} should bind nothing",
164 block.kind
165 );
166 }
167 }
168
169 #[test]
170 fn a_template_without_blocks_has_an_empty_outline() {
171 assert_eq!(blocks("Hello {{ name }}, welcome."), Vec::new());
172 }
173
174 #[test]
175 fn an_unclosed_block_reports_where_it_opened() {
176 let error = parse("intro\n{{ if a }}\nbody\n").expect_err("unclosed `{{ if }}`");
177 assert_eq!((error.line, error.col), (2, 1));
178 assert!(
179 error.message.contains("missing matching `{{ end }}`"),
180 "unexpected message: {}",
181 error.message
182 );
183 }
184
185 #[test]
186 fn trim_markers_do_not_shift_a_block_off_its_directive() {
187 let src = "{{- if a -}}A{{- end -}}";
188 assert_eq!(blocks(src), vec![(OutlineBlockKind::If, src)]);
189 }
190}