use somni_parser::Location;
use crate::{
parse::{Arm, Node},
resolve::ResolvedModule,
};
pub const RENDER_FN: &str = "__tmpl_render";
pub const EMIT_FN: &str = "__tmpl_emit";
pub const EMIT_LIT_FN: &str = "__tmpl_emit_lit";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MapEntry {
pub gen_start: usize,
pub tmpl_start: usize,
pub len: usize,
}
#[derive(Clone, Debug)]
pub struct Transpiled {
pub source: String,
pub literals: Vec<Location>,
pub map: Vec<MapEntry>,
}
impl Transpiled {
pub fn map_location(&self, loc: Location) -> Option<Location> {
let start = self.map_offset(loc.start)?;
let end = self.map_offset(loc.end).unwrap_or(start);
Some(Location {
start,
end: end.max(start),
})
}
fn map_offset(&self, off: usize) -> Option<usize> {
for e in &self.map {
if off >= e.gen_start && off <= e.gen_start + e.len {
return Some(e.tmpl_start + (off - e.gen_start));
}
}
None
}
}
struct Writer<'a> {
template: &'a str,
modules: &'a [ResolvedModule],
out: String,
literals: Vec<Location>,
map: Vec<MapEntry>,
}
impl Writer<'_> {
fn push(&mut self, s: &str) {
self.out.push_str(s);
}
fn push_verbatim(&mut self, loc: Location) {
let text = loc.extract(self.template);
self.map.push(MapEntry {
gen_start: self.out.len(),
tmpl_start: loc.start,
len: text.len(),
});
self.out.push_str(text);
}
fn emit_literal(&mut self, span: Location) {
if span.start >= span.end {
return;
}
let index = self.literals.len();
self.literals.push(span);
self.push(&format!("{EMIT_LIT_FN}({index});\n"));
}
fn emit_expr(&mut self, expr: Location) {
self.push(&format!("{EMIT_FN}("));
self.push_verbatim(expr);
self.push(");\n");
}
fn emit_text_with_replace(&mut self, span: Location, literal: &str, with_expr: Location) {
let text = span.extract(self.template);
let mut start = 0;
while let Some(found) = text[start..].find(literal) {
let abs = start + found;
if found > 0 {
self.emit_literal(Location {
start: span.start + start,
end: span.start + abs,
});
}
self.emit_expr(with_expr);
start = abs + literal.len();
}
if start < text.len() {
self.emit_literal(Location {
start: span.start + start,
end: span.end,
});
} else if start == 0 {
self.emit_literal(span);
}
}
fn nodes(&mut self, nodes: &[Node], replace: Option<(&str, Location)>) {
for node in nodes {
self.node(node, replace);
}
}
fn node(&mut self, node: &Node, replace: Option<(&str, Location)>) {
match node {
Node::Text(span) => match replace {
Some((literal, with_expr)) => {
self.emit_text_with_replace(*span, literal, with_expr)
}
None => self.emit_literal(*span),
},
Node::Interp(expr) => self.emit_expr(*expr),
Node::If { arms, otherwise } => {
self.if_chain(arms, otherwise.as_deref(), replace)
}
Node::For {
var,
ty,
iterable,
body,
} => {
self.push("for ");
self.push_verbatim(*var);
if let Some(ty) = ty {
self.push(": ");
self.push_verbatim(*ty);
}
self.push(" in (");
self.push_verbatim(*iterable);
self.push(") {\n");
self.nodes(body, replace);
self.push("}\n");
}
Node::Replace {
literal,
with_expr,
body,
} => {
self.nodes(body, Some((literal, *with_expr)));
}
Node::Include { args, module_id, .. } => {
let id = module_id.expect("includes must be resolved before transpile");
let module = &self.modules[id];
self.push(&module.fn_name);
self.push("(");
for (i, arg) in args.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.push_verbatim(arg.value);
}
self.push(");\n");
}
}
}
fn if_chain(
&mut self,
arms: &[Arm],
otherwise: Option<&[Node]>,
replace: Option<(&str, Location)>,
) {
let (first, rest) = arms.split_first().expect("if always has >= 1 arm");
self.push("if (");
self.push_verbatim(first.cond);
self.push(") {\n");
self.nodes(&first.body, replace);
self.push("}");
if rest.is_empty() {
match otherwise {
Some(body) => {
self.push(" else {\n");
self.nodes(body, replace);
self.push("}\n");
}
None => self.push("\n"),
}
} else {
self.push(" else {\n");
self.if_chain(rest, otherwise, replace);
self.push("}\n");
}
}
fn emit_module(&mut self, module: &ResolvedModule) {
self.push(&format!("fn {}(", module.fn_name));
for (i, (name, ty)) in module.params.iter().enumerate() {
if i > 0 {
self.push(", ");
}
self.push(name);
self.push(": ");
self.push(ty);
}
self.push(") {\n");
self.nodes(&module.nodes, None);
self.push("}\n");
}
}
pub fn transpile(template: &str, nodes: &[Node], modules: &[ResolvedModule]) -> Transpiled {
let mut w = Writer {
template,
modules,
out: String::new(),
literals: Vec::new(),
map: Vec::new(),
};
for module in modules {
w.emit_module(module);
}
w.push(&format!("fn {RENDER_FN}() {{\n"));
w.nodes(nodes, None);
w.push("}\n");
Transpiled {
source: w.out,
literals: w.literals,
map: w.map,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse::parse, syntax::Syntax};
fn transpile_str(src: &str, syntax: &Syntax) -> Transpiled {
let nodes = parse(src, syntax, 0).unwrap();
transpile(src, &nodes, &[])
}
#[test]
fn text_becomes_emit_lit() {
let t = transpile_str("hello", &Syntax::brackets());
assert!(t.source.contains(&format!("{EMIT_LIT_FN}(0)")));
assert_eq!(t.literals.len(), 1);
assert_eq!(t.literals[0].extract("hello"), "hello");
}
#[test]
fn interp_becomes_emit_verbatim() {
let src = "{{ str(age) }}";
let t = transpile_str(src, &Syntax::brackets());
assert!(
t.source.contains(&format!("{EMIT_FN}(str(age))")),
"{}",
t.source
);
}
#[test]
fn if_else_if_chain_nests() {
let src = "{% if a %}A{% else if b %}B{% else %}C{% endif %}";
let t = transpile_str(src, &Syntax::brackets());
assert_eq!(t.source.matches("if (").count(), 2);
assert!(t.source.contains("else {"));
}
#[test]
fn for_loop_shape() {
let src = "#for x: int in xs\n{{ str(x) }}\n#endfor\n";
let t = transpile_str(src, &Syntax::lines());
assert!(t.source.contains("for x: int in (xs) {"), "{}", t.source);
}
#[test]
fn source_map_translates_expression_back() {
let src = "prefix {{ boom }} suffix";
let t = transpile_str(src, &Syntax::brackets());
let gen_idx = t.source.find("boom").unwrap();
let gen_loc = Location {
start: gen_idx,
end: gen_idx + "boom".len(),
};
let tmpl_loc = t.map_location(gen_loc).unwrap();
assert_eq!(tmpl_loc.extract(src), "boom");
}
#[test]
fn replace_becomes_prefix_expr_suffix() {
let src = "#replace \"N\" with str(n)\n[N]\n#endreplace\n";
let t = transpile_str(src, &Syntax::lines());
assert!(
t.source.contains(&format!("{EMIT_LIT_FN}(0)")),
"{}",
t.source
);
assert!(
t.source.contains(&format!("{EMIT_FN}(str(n))")),
"{}",
t.source
);
assert!(
t.source.contains(&format!("{EMIT_LIT_FN}(1)")),
"{}",
t.source
);
assert_eq!(t.literals[0].extract(src), "[");
assert_eq!(t.literals[1].extract(src), "]\n");
assert!(!t.source.contains("replace"), "{}", t.source);
}
}