use super::error::TemplateParseError;
use super::parser::parse_outline;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutlineBlock {
pub kind: OutlineBlockKind,
pub start: usize,
pub end: usize,
pub bindings: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutlineBlockKind {
If,
Elif,
Else,
For,
Section,
Raw,
Comment,
}
pub fn parse(src: &str) -> Result<Vec<OutlineBlock>, TemplateParseError> {
parse_outline(src).map_err(TemplateParseError::from)
}
#[cfg(test)]
mod tests {
use super::{parse, OutlineBlockKind};
fn blocks(src: &str) -> Vec<(OutlineBlockKind, &str)> {
parse(src)
.expect("template parses")
.into_iter()
.map(|block| (block.kind, &src[block.start..block.end]))
.collect()
}
#[test]
fn every_branch_of_a_chain_closes_at_the_shared_end() {
let src = "{{ if a }}A{{ elif b }}B{{ else }}C{{ end }}";
assert_eq!(
blocks(src),
vec![
(OutlineBlockKind::If, src),
(OutlineBlockKind::Elif, "{{ elif b }}B{{ else }}C{{ end }}"),
(OutlineBlockKind::Else, "{{ else }}C{{ end }}"),
]
);
}
#[test]
fn a_loop_covers_its_empty_fallback() {
let src = "{{ for x in xs }}{{ x }}{{ else }}none{{ end }}";
assert_eq!(
blocks(src),
vec![
(OutlineBlockKind::For, src),
(OutlineBlockKind::Else, "{{ else }}none{{ end }}"),
]
);
}
#[test]
fn sections_raw_blocks_and_comments_are_all_located() {
let src = "{{# note #}}{{ section \"task\" }}{{ raw }}{{x}}{{ endraw }}{{ endsection }}";
assert_eq!(
blocks(src),
vec![
(OutlineBlockKind::Comment, "{{# note #}}"),
(
OutlineBlockKind::Section,
"{{ section \"task\" }}{{ raw }}{{x}}{{ endraw }}{{ endsection }}"
),
(OutlineBlockKind::Raw, "{{ raw }}{{x}}{{ endraw }}"),
]
);
}
#[test]
fn nested_blocks_come_back_in_source_order() {
let src = "{{ for x in xs }}{{ if x }}y{{ end }}{{ end }}";
assert_eq!(
blocks(src),
vec![
(OutlineBlockKind::For, src),
(OutlineBlockKind::If, "{{ if x }}y{{ end }}"),
]
);
}
#[test]
fn loops_report_the_names_they_bind() {
let bindings = |src: &str| {
parse(src)
.expect("parses")
.into_iter()
.filter(|block| block.kind == OutlineBlockKind::For)
.map(|block| block.bindings)
.collect::<Vec<_>>()
};
assert_eq!(
bindings("{{ for item in items }}x{{ end }}"),
vec![vec!["item".to_string()]]
);
assert_eq!(
bindings("{{ for key, value in dict }}x{{ end }}"),
vec![vec!["key".to_string(), "value".to_string()]]
);
assert_eq!(
bindings("{{ for x in xs }}a{{ else }}b{{ end }}"),
vec![vec!["x".to_string()]]
);
}
#[test]
fn only_loops_bind_names() {
for block in
parse("{{ if a }}x{{ end }}{{ section \"task\" }}y{{ endsection }}").expect("parses")
{
assert!(
block.bindings.is_empty(),
"{:?} should bind nothing",
block.kind
);
}
}
#[test]
fn a_template_without_blocks_has_an_empty_outline() {
assert_eq!(blocks("Hello {{ name }}, welcome."), Vec::new());
}
#[test]
fn an_unclosed_block_reports_where_it_opened() {
let error = parse("intro\n{{ if a }}\nbody\n").expect_err("unclosed `{{ if }}`");
assert_eq!((error.line, error.col), (2, 1));
assert!(
error.message.contains("missing matching `{{ end }}`"),
"unexpected message: {}",
error.message
);
}
#[test]
fn trim_markers_do_not_shift_a_block_off_its_directive() {
let src = "{{- if a -}}A{{- end -}}";
assert_eq!(blocks(src), vec![(OutlineBlockKind::If, src)]);
}
}