mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-29 22:11:17 +00:00
2338 lines
79 KiB
Rust
2338 lines
79 KiB
Rust
use std::borrow::Cow;
|
|
use std::collections::hash_map::{Entry, HashMap};
|
|
use std::fmt::{Arguments, Display, Write};
|
|
use std::ops::Deref;
|
|
use std::path::Path;
|
|
use std::rc::Rc;
|
|
use std::{cmp, hash, mem, str};
|
|
|
|
use crate::config::WhitespaceHandling;
|
|
use crate::heritage::{Context, Heritage};
|
|
use crate::input::{Source, TemplateInput};
|
|
use crate::{CompileError, CRATE};
|
|
|
|
use parser::node::{
|
|
Call, Comment, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Match, Whitespace, Ws,
|
|
};
|
|
use parser::{Expr, Filter, Node, Target, WithSpan};
|
|
use quote::quote;
|
|
|
|
pub(crate) struct Generator<'a> {
|
|
// The template input state: original struct AST and attributes
|
|
input: &'a TemplateInput<'a>,
|
|
// All contexts, keyed by the package-relative template path
|
|
contexts: &'a HashMap<&'a Rc<Path>, Context<'a>>,
|
|
// The heritage contains references to blocks and their ancestry
|
|
heritage: Option<&'a Heritage<'a>>,
|
|
// Variables accessible directly from the current scope (not redirected to context)
|
|
locals: MapChain<'a, Cow<'a, str>, LocalMeta>,
|
|
// Suffix whitespace from the previous literal. Will be flushed to the
|
|
// output buffer unless suppressed by whitespace suppression on the next
|
|
// non-literal.
|
|
next_ws: Option<&'a str>,
|
|
// Whitespace suppression from the previous non-literal. Will be used to
|
|
// determine whether to flush prefix whitespace from the next literal.
|
|
skip_ws: WhitespaceHandling,
|
|
// If currently in a block, this will contain the name of a potential parent block
|
|
super_block: Option<(&'a str, usize)>,
|
|
// Buffer for writable
|
|
buf_writable: WritableBuffer<'a>,
|
|
// Counter for write! hash named arguments
|
|
named: usize,
|
|
}
|
|
|
|
impl<'a> Generator<'a> {
|
|
pub(crate) fn new<'n>(
|
|
input: &'n TemplateInput<'_>,
|
|
contexts: &'n HashMap<&'n Rc<Path>, Context<'n>>,
|
|
heritage: Option<&'n Heritage<'_>>,
|
|
locals: MapChain<'n, Cow<'n, str>, LocalMeta>,
|
|
buf_writable_discard: bool,
|
|
) -> Generator<'n> {
|
|
Generator {
|
|
input,
|
|
contexts,
|
|
heritage,
|
|
locals,
|
|
next_ws: None,
|
|
skip_ws: WhitespaceHandling::Preserve,
|
|
super_block: None,
|
|
buf_writable: WritableBuffer {
|
|
discard: buf_writable_discard,
|
|
..Default::default()
|
|
},
|
|
named: 0,
|
|
}
|
|
}
|
|
|
|
// Takes a Context and generates the relevant implementations.
|
|
pub(crate) fn build(mut self, ctx: &Context<'a>) -> Result<String, CompileError> {
|
|
let mut buf = Buffer::new();
|
|
|
|
self.impl_template(ctx, &mut buf)?;
|
|
self.impl_display(&mut buf);
|
|
|
|
#[cfg(feature = "with-actix-web")]
|
|
self.impl_actix_web_responder(&mut buf);
|
|
#[cfg(feature = "with-axum")]
|
|
self.impl_axum_into_response(&mut buf);
|
|
#[cfg(feature = "with-rocket")]
|
|
self.impl_rocket_responder(&mut buf);
|
|
#[cfg(feature = "with-warp")]
|
|
self.impl_warp_reply(&mut buf);
|
|
|
|
Ok(buf.buf)
|
|
}
|
|
|
|
// Implement `Template` for the given context struct.
|
|
fn impl_template(&mut self, ctx: &Context<'a>, buf: &mut Buffer) -> Result<(), CompileError> {
|
|
self.write_header(buf, format_args!("{CRATE}::Template"), None);
|
|
buf.write("fn render_into(&self, writer: &mut (impl ::std::fmt::Write + ?Sized)) -> ");
|
|
buf.write(CRATE);
|
|
buf.writeln("::Result<()> {");
|
|
|
|
buf.discard = self.buf_writable.discard;
|
|
// Make sure the compiler understands that the generated code depends on the template files.
|
|
for path in self.contexts.keys() {
|
|
// Skip the fake path of templates defined in rust source.
|
|
let path_is_valid = match self.input.source {
|
|
Source::Path(_) => true,
|
|
Source::Source(_) => **path != self.input.path,
|
|
};
|
|
if path_is_valid {
|
|
let path = path.to_str().unwrap();
|
|
buf.writeln(
|
|
quote! {
|
|
const _: &[::core::primitive::u8] = ::core::include_bytes!(#path);
|
|
}
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
|
|
let size_hint = if let Some(heritage) = self.heritage {
|
|
self.handle(heritage.root, heritage.root.nodes, buf, AstLevel::Top)
|
|
} else {
|
|
self.handle(ctx, ctx.nodes, buf, AstLevel::Top)
|
|
}?;
|
|
buf.discard = false;
|
|
|
|
self.flush_ws(Ws(None, None));
|
|
buf.write(CRATE);
|
|
buf.writeln("::Result::Ok(())");
|
|
buf.writeln("}");
|
|
|
|
buf.writeln(format_args!(
|
|
"const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = {:?};",
|
|
self.input.extension(),
|
|
));
|
|
buf.writeln(format_args!(
|
|
"const SIZE_HINT: ::std::primitive::usize = {size_hint};",
|
|
));
|
|
buf.writeln(format_args!(
|
|
"const MIME_TYPE: &'static ::std::primitive::str = {:?};",
|
|
self.input.mime_type,
|
|
));
|
|
|
|
buf.writeln("}");
|
|
Ok(())
|
|
}
|
|
|
|
// Implement `Display` for the given context struct.
|
|
fn impl_display(&mut self, buf: &mut Buffer) {
|
|
self.write_header(buf, "::std::fmt::Display", None);
|
|
buf.writeln("#[inline]");
|
|
buf.writeln("fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {");
|
|
buf.write(CRATE);
|
|
buf.writeln("::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})");
|
|
buf.writeln("}");
|
|
buf.writeln("}");
|
|
}
|
|
|
|
// Implement Actix-web's `Responder`.
|
|
#[cfg(feature = "with-actix-web")]
|
|
fn impl_actix_web_responder(&mut self, buf: &mut Buffer) {
|
|
self.write_header(buf, "::rinja_actix::actix_web::Responder", None);
|
|
buf.writeln("type Body = ::rinja_actix::actix_web::body::BoxBody;");
|
|
buf.writeln("#[inline]");
|
|
buf.writeln(
|
|
"fn respond_to(self, _req: &::rinja_actix::actix_web::HttpRequest) \
|
|
-> ::rinja_actix::actix_web::HttpResponse<Self::Body> {",
|
|
);
|
|
buf.writeln("::rinja_actix::into_response(&self)");
|
|
buf.writeln("}");
|
|
buf.writeln("}");
|
|
}
|
|
|
|
// Implement Axum's `IntoResponse`.
|
|
#[cfg(feature = "with-axum")]
|
|
fn impl_axum_into_response(&mut self, buf: &mut Buffer) {
|
|
self.write_header(buf, "::rinja_axum::axum_core::response::IntoResponse", None);
|
|
buf.writeln("#[inline]");
|
|
buf.writeln(
|
|
"fn into_response(self)\
|
|
-> ::rinja_axum::axum_core::response::Response {",
|
|
);
|
|
buf.writeln("::rinja_axum::into_response(&self)");
|
|
buf.writeln("}");
|
|
buf.writeln("}");
|
|
}
|
|
|
|
// Implement Rocket's `Responder`.
|
|
#[cfg(feature = "with-rocket")]
|
|
fn impl_rocket_responder(&mut self, buf: &mut Buffer) {
|
|
let lifetime1 = syn::Lifetime::new("'rinja1", proc_macro2::Span::call_site());
|
|
let param1 = syn::GenericParam::Lifetime(syn::LifetimeParam::new(lifetime1));
|
|
|
|
self.write_header(
|
|
buf,
|
|
"::rinja_rocket::rocket::response::Responder<'rinja1, 'static>",
|
|
Some(vec![param1]),
|
|
);
|
|
buf.writeln("#[inline]");
|
|
buf.writeln(
|
|
"fn respond_to(self, _: &'rinja1 ::rinja_rocket::rocket::request::Request<'_>) \
|
|
-> ::rinja_rocket::rocket::response::Result<'static> {",
|
|
);
|
|
buf.writeln("::rinja_rocket::respond(&self)");
|
|
buf.writeln("}");
|
|
buf.writeln("}");
|
|
}
|
|
|
|
#[cfg(feature = "with-warp")]
|
|
fn impl_warp_reply(&mut self, buf: &mut Buffer) {
|
|
self.write_header(buf, "::rinja_warp::warp::reply::Reply", None);
|
|
buf.writeln("#[inline]");
|
|
buf.writeln("fn into_response(self) -> ::rinja_warp::warp::reply::Response {");
|
|
buf.writeln("::rinja_warp::into_response(&self)");
|
|
buf.writeln("}");
|
|
buf.writeln("}");
|
|
}
|
|
|
|
// Writes header for the `impl` for `TraitFromPathName` or `Template`
|
|
// for the given context struct.
|
|
fn write_header(
|
|
&mut self,
|
|
buf: &mut Buffer,
|
|
target: impl Display,
|
|
params: Option<Vec<syn::GenericParam>>,
|
|
) {
|
|
let mut generics;
|
|
let (impl_generics, orig_ty_generics, where_clause) = if let Some(params) = params {
|
|
generics = self.input.ast.generics.clone();
|
|
for param in params {
|
|
generics.params.push(param);
|
|
}
|
|
|
|
let (_, orig_ty_generics, _) = self.input.ast.generics.split_for_impl();
|
|
let (impl_generics, _, where_clause) = generics.split_for_impl();
|
|
(impl_generics, orig_ty_generics, where_clause)
|
|
} else {
|
|
self.input.ast.generics.split_for_impl()
|
|
};
|
|
|
|
buf.writeln(format_args!(
|
|
"{} {} for {}{} {{",
|
|
quote!(impl #impl_generics),
|
|
target,
|
|
self.input.ast.ident,
|
|
quote!(#orig_ty_generics #where_clause),
|
|
));
|
|
}
|
|
|
|
/* Helper methods for handling node types */
|
|
|
|
fn handle(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
nodes: &'a [Node<'_>],
|
|
buf: &mut Buffer,
|
|
level: AstLevel,
|
|
) -> Result<usize, CompileError> {
|
|
let mut size_hint = 0;
|
|
for n in nodes {
|
|
match *n {
|
|
Node::Lit(ref lit) => {
|
|
self.visit_lit(lit);
|
|
}
|
|
Node::Comment(ref comment) => {
|
|
self.write_comment(comment);
|
|
}
|
|
Node::Expr(ws, ref val) => {
|
|
self.write_expr(ws, val);
|
|
}
|
|
Node::Let(ref l) => {
|
|
self.write_let(ctx, buf, l)?;
|
|
}
|
|
Node::If(ref i) => {
|
|
size_hint += self.write_if(ctx, buf, i)?;
|
|
}
|
|
Node::Match(ref m) => {
|
|
size_hint += self.write_match(ctx, buf, m)?;
|
|
}
|
|
Node::Loop(ref loop_block) => {
|
|
size_hint += self.write_loop(ctx, buf, loop_block)?;
|
|
}
|
|
Node::BlockDef(ref b) => {
|
|
size_hint +=
|
|
self.write_block(ctx, buf, Some(b.name), Ws(b.ws1.0, b.ws2.1), b)?;
|
|
}
|
|
Node::Include(ref i) => {
|
|
size_hint += self.handle_include(ctx, buf, i)?;
|
|
}
|
|
Node::Call(ref call) => {
|
|
size_hint += self.write_call(ctx, buf, call)?;
|
|
}
|
|
Node::FilterBlock(ref filter) => {
|
|
size_hint += self.write_filter_block(ctx, buf, filter)?;
|
|
}
|
|
Node::Macro(ref m) => {
|
|
if level != AstLevel::Top {
|
|
return Err(
|
|
ctx.generate_error("macro blocks only allowed at the top level", m)
|
|
);
|
|
}
|
|
self.flush_ws(m.ws1);
|
|
self.prepare_ws(m.ws2);
|
|
}
|
|
Node::Raw(ref raw) => {
|
|
self.handle_ws(raw.ws1);
|
|
self.visit_lit(&raw.lit);
|
|
self.handle_ws(raw.ws2);
|
|
}
|
|
Node::Import(ref i) => {
|
|
if level != AstLevel::Top {
|
|
return Err(
|
|
ctx.generate_error("import blocks only allowed at the top level", i)
|
|
);
|
|
}
|
|
self.handle_ws(i.ws);
|
|
}
|
|
Node::Extends(ref e) => {
|
|
if level != AstLevel::Top {
|
|
return Err(
|
|
ctx.generate_error("extend blocks only allowed at the top level", e)
|
|
);
|
|
}
|
|
// No whitespace handling: child template top-level is not used,
|
|
// except for the blocks defined in it.
|
|
}
|
|
Node::Break(ref ws) => {
|
|
self.handle_ws(**ws);
|
|
self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("break;");
|
|
}
|
|
Node::Continue(ref ws) => {
|
|
self.handle_ws(**ws);
|
|
self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("continue;");
|
|
}
|
|
}
|
|
}
|
|
|
|
if AstLevel::Top == level {
|
|
// Handle any pending whitespace.
|
|
if self.next_ws.is_some() {
|
|
self.flush_ws(Ws(Some(self.skip_ws.into()), None));
|
|
}
|
|
|
|
size_hint += self.write_buf_writable(ctx, buf)?;
|
|
}
|
|
Ok(size_hint)
|
|
}
|
|
|
|
fn write_if(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
i: &'a If<'_>,
|
|
) -> Result<usize, CompileError> {
|
|
let mut flushed = 0;
|
|
let mut arm_sizes = Vec::new();
|
|
let mut has_else = false;
|
|
for (i, cond) in i.branches.iter().enumerate() {
|
|
self.handle_ws(cond.ws);
|
|
flushed += self.write_buf_writable(ctx, buf)?;
|
|
if i > 0 {
|
|
self.locals.pop();
|
|
}
|
|
|
|
self.locals.push();
|
|
let mut arm_size = 0;
|
|
if let Some(CondTest { target, expr }) = &cond.cond {
|
|
if i == 0 {
|
|
buf.write("if ");
|
|
} else {
|
|
buf.write("} else if ");
|
|
}
|
|
|
|
if let Some(target) = target {
|
|
let mut expr_buf = Buffer::new();
|
|
buf.write("let ");
|
|
// If this is a chain condition, then we need to declare the variable after the
|
|
// left expression has been handled but before the right expression is handled
|
|
// but this one should have access to the let-bound variable.
|
|
match &**expr {
|
|
Expr::BinOp(op, ref left, ref right) if *op == "||" || *op == "&&" => {
|
|
self.visit_expr(ctx, &mut expr_buf, left)?;
|
|
self.visit_target(buf, true, true, target);
|
|
expr_buf.write(format_args!(" {op} "));
|
|
self.visit_expr(ctx, &mut expr_buf, right)?;
|
|
}
|
|
_ => {
|
|
self.visit_expr(ctx, &mut expr_buf, expr)?;
|
|
self.visit_target(buf, true, true, target);
|
|
}
|
|
}
|
|
buf.write(" = &");
|
|
buf.write(expr_buf.buf);
|
|
} else {
|
|
// The following syntax `*(&(...) as &bool)` is used to
|
|
// trigger Rust's automatic dereferencing, to coerce
|
|
// e.g. `&&&&&bool` to `bool`. First `&(...) as &bool`
|
|
// coerces e.g. `&&&bool` to `&bool`. Then `*(&bool)`
|
|
// finally dereferences it to `bool`.
|
|
buf.write("*(&(");
|
|
buf.write(self.visit_expr_root(ctx, expr)?);
|
|
buf.write(") as &bool)");
|
|
}
|
|
} else {
|
|
buf.write("} else");
|
|
has_else = true;
|
|
}
|
|
|
|
buf.writeln(" {");
|
|
|
|
arm_size += self.handle(ctx, &cond.nodes, buf, AstLevel::Nested)?;
|
|
arm_sizes.push(arm_size);
|
|
}
|
|
self.handle_ws(i.ws);
|
|
flushed += self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("}");
|
|
|
|
self.locals.pop();
|
|
|
|
if !has_else {
|
|
arm_sizes.push(0);
|
|
}
|
|
Ok(flushed + median(&mut arm_sizes))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn write_match(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
m: &'a Match<'a>,
|
|
) -> Result<usize, CompileError> {
|
|
let Match {
|
|
ws1,
|
|
ref expr,
|
|
ref arms,
|
|
ws2,
|
|
} = *m;
|
|
|
|
self.flush_ws(ws1);
|
|
let flushed = self.write_buf_writable(ctx, buf)?;
|
|
let mut arm_sizes = Vec::new();
|
|
|
|
let expr_code = self.visit_expr_root(ctx, expr)?;
|
|
buf.writeln(format_args!("match &{expr_code} {{"));
|
|
|
|
let mut arm_size = 0;
|
|
for (i, arm) in arms.iter().enumerate() {
|
|
self.handle_ws(arm.ws);
|
|
|
|
if i > 0 {
|
|
arm_sizes.push(arm_size + self.write_buf_writable(ctx, buf)?);
|
|
|
|
buf.writeln("}");
|
|
self.locals.pop();
|
|
}
|
|
|
|
self.locals.push();
|
|
self.visit_target(buf, true, true, &arm.target);
|
|
buf.writeln(" => {");
|
|
|
|
arm_size = self.handle(ctx, &arm.nodes, buf, AstLevel::Nested)?;
|
|
}
|
|
|
|
self.handle_ws(ws2);
|
|
arm_sizes.push(arm_size + self.write_buf_writable(ctx, buf)?);
|
|
buf.writeln("}");
|
|
self.locals.pop();
|
|
|
|
buf.writeln("}");
|
|
|
|
Ok(flushed + median(&mut arm_sizes))
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn write_loop(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
loop_block: &'a WithSpan<'_, Loop<'_>>,
|
|
) -> Result<usize, CompileError> {
|
|
self.handle_ws(loop_block.ws1);
|
|
self.locals.push();
|
|
|
|
let expr_code = self.visit_expr_root(ctx, &loop_block.iter)?;
|
|
|
|
let has_else_nodes = !loop_block.else_nodes.is_empty();
|
|
|
|
let flushed = self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("{");
|
|
if has_else_nodes {
|
|
buf.writeln("let mut _did_loop = false;");
|
|
}
|
|
match &*loop_block.iter {
|
|
Expr::Range(_, _, _) => buf.writeln(format_args!("let _iter = {expr_code};")),
|
|
Expr::Array(..) => buf.writeln(format_args!("let _iter = {expr_code}.iter();")),
|
|
// If `iter` is a call then we assume it's something that returns
|
|
// an iterator. If not then the user can explicitly add the needed
|
|
// call without issues.
|
|
Expr::Call(..) | Expr::Index(..) => {
|
|
buf.writeln(format_args!("let _iter = ({expr_code}).into_iter();"))
|
|
}
|
|
// If accessing `self` then it most likely needs to be
|
|
// borrowed, to prevent an attempt of moving.
|
|
_ if expr_code.starts_with("self.") => {
|
|
buf.writeln(format_args!("let _iter = (&{expr_code}).into_iter();"))
|
|
}
|
|
// If accessing a field then it most likely needs to be
|
|
// borrowed, to prevent an attempt of moving.
|
|
Expr::Attr(..) => buf.writeln(format_args!("let _iter = (&{expr_code}).into_iter();")),
|
|
// Otherwise, we borrow `iter` assuming that it implements `IntoIterator`.
|
|
_ => buf.writeln(format_args!("let _iter = ({expr_code}).into_iter();")),
|
|
}
|
|
if let Some(cond) = &loop_block.cond {
|
|
self.locals.push();
|
|
buf.write("let _iter = _iter.filter(|");
|
|
self.visit_target(buf, true, true, &loop_block.var);
|
|
buf.write("| -> bool {");
|
|
self.visit_expr(ctx, buf, cond)?;
|
|
buf.writeln("});");
|
|
self.locals.pop();
|
|
}
|
|
|
|
self.locals.push();
|
|
buf.write("for (");
|
|
self.visit_target(buf, true, true, &loop_block.var);
|
|
buf.write(", _loop_item) in ");
|
|
buf.write(CRATE);
|
|
buf.writeln("::helpers::TemplateLoop::new(_iter) {");
|
|
|
|
if has_else_nodes {
|
|
buf.writeln("_did_loop = true;");
|
|
}
|
|
let mut size_hint1 = self.handle(ctx, &loop_block.body, buf, AstLevel::Nested)?;
|
|
self.handle_ws(loop_block.ws2);
|
|
size_hint1 += self.write_buf_writable(ctx, buf)?;
|
|
self.locals.pop();
|
|
buf.writeln("}");
|
|
|
|
let mut size_hint2;
|
|
if has_else_nodes {
|
|
buf.writeln("if !_did_loop {");
|
|
self.locals.push();
|
|
size_hint2 = self.handle(ctx, &loop_block.else_nodes, buf, AstLevel::Nested)?;
|
|
self.handle_ws(loop_block.ws3);
|
|
size_hint2 += self.write_buf_writable(ctx, buf)?;
|
|
self.locals.pop();
|
|
buf.writeln("}");
|
|
} else {
|
|
self.handle_ws(loop_block.ws3);
|
|
size_hint2 = self.write_buf_writable(ctx, buf)?;
|
|
}
|
|
|
|
buf.writeln("}");
|
|
|
|
Ok(flushed + ((size_hint1 * 3) + size_hint2) / 2)
|
|
}
|
|
|
|
fn write_call(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
call: &'a WithSpan<'_, Call<'_>>,
|
|
) -> Result<usize, CompileError> {
|
|
let Call {
|
|
ws,
|
|
scope,
|
|
name,
|
|
ref args,
|
|
} = **call;
|
|
if name == "super" {
|
|
return self.write_block(ctx, buf, None, ws, call);
|
|
}
|
|
|
|
let (def, own_ctx) = match scope {
|
|
Some(s) => {
|
|
let path = ctx.imports.get(s).ok_or_else(|| {
|
|
ctx.generate_error(&format!("no import found for scope {s:?}"), call)
|
|
})?;
|
|
let mctx = self.contexts.get(path).ok_or_else(|| {
|
|
ctx.generate_error(&format!("context for {path:?} not found"), call)
|
|
})?;
|
|
let def = mctx.macros.get(name).ok_or_else(|| {
|
|
ctx.generate_error(&format!("macro {name:?} not found in scope {s:?}"), call)
|
|
})?;
|
|
(def, mctx)
|
|
}
|
|
None => {
|
|
let def = ctx.macros.get(name).ok_or_else(|| {
|
|
ctx.generate_error(&format!("macro {name:?} not found"), call)
|
|
})?;
|
|
(def, ctx)
|
|
}
|
|
};
|
|
|
|
self.flush_ws(ws); // Cannot handle_ws() here: whitespace from macro definition comes first
|
|
self.locals.push();
|
|
self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("{");
|
|
self.prepare_ws(def.ws1);
|
|
|
|
let mut names = Buffer::new();
|
|
let mut values = Buffer::new();
|
|
let mut is_first_variable = true;
|
|
if args.len() != def.args.len() {
|
|
return Err(ctx.generate_error(
|
|
&format!(
|
|
"macro {name:?} expected {} argument{}, found {}",
|
|
def.args.len(),
|
|
if def.args.len() != 1 { "s" } else { "" },
|
|
args.len()
|
|
),
|
|
call,
|
|
));
|
|
}
|
|
let mut named_arguments = HashMap::new();
|
|
// Since named arguments can only be passed last, we only need to check if the last argument
|
|
// is a named one.
|
|
if let Some(Expr::NamedArgument(_, _)) = args.last().map(|expr| &**expr) {
|
|
// First we check that all named arguments actually exist in the called item.
|
|
for arg in args.iter().rev() {
|
|
let Expr::NamedArgument(arg_name, _) = &**arg else {
|
|
break;
|
|
};
|
|
if !def.args.iter().any(|arg| arg == arg_name) {
|
|
return Err(ctx.generate_error(
|
|
&format!("no argument named `{arg_name}` in macro {name:?}"),
|
|
call,
|
|
));
|
|
}
|
|
named_arguments.insert(Cow::Borrowed(arg_name), arg);
|
|
}
|
|
}
|
|
|
|
// Handling both named and unnamed arguments requires to be careful of the named arguments
|
|
// order. To do so, we iterate through the macro defined arguments and then check if we have
|
|
// a named argument with this name:
|
|
//
|
|
// * If there is one, we add it and move to the next argument.
|
|
// * If there isn't one, then we pick the next argument (we can do it without checking
|
|
// anything since named arguments are always last).
|
|
let mut allow_positional = true;
|
|
for (index, arg) in def.args.iter().enumerate() {
|
|
let expr = match named_arguments.get(&Cow::Borrowed(arg)) {
|
|
Some(expr) => {
|
|
allow_positional = false;
|
|
expr
|
|
}
|
|
None => {
|
|
if !allow_positional {
|
|
// If there is already at least one named argument, then it's not allowed
|
|
// to use unnamed ones at this point anymore.
|
|
return Err(ctx.generate_error(
|
|
&format!(
|
|
"cannot have unnamed argument (`{arg}`) after named argument in macro \
|
|
{name:?}"
|
|
),
|
|
call,
|
|
));
|
|
}
|
|
&args[index]
|
|
}
|
|
};
|
|
match &**expr {
|
|
// If `expr` is already a form of variable then
|
|
// don't reintroduce a new variable. This is
|
|
// to avoid moving non-copyable values.
|
|
Expr::Var(name) if *name != "self" => {
|
|
let var = self.locals.resolve_or_self(name);
|
|
self.locals
|
|
.insert(Cow::Borrowed(arg), LocalMeta::with_ref(var));
|
|
}
|
|
Expr::Attr(obj, attr) => {
|
|
let mut attr_buf = Buffer::new();
|
|
self.visit_attr(ctx, &mut attr_buf, obj, attr)?;
|
|
|
|
let var = self.locals.resolve(&attr_buf.buf).unwrap_or(attr_buf.buf);
|
|
self.locals
|
|
.insert(Cow::Borrowed(arg), LocalMeta::with_ref(var));
|
|
}
|
|
// Everything else still needs to become variables,
|
|
// to avoid having the same logic be executed
|
|
// multiple times, e.g. in the case of macro
|
|
// parameters being used multiple times.
|
|
_ => {
|
|
if is_first_variable {
|
|
is_first_variable = false
|
|
} else {
|
|
names.write(", ");
|
|
values.write(", ");
|
|
}
|
|
names.write(arg);
|
|
|
|
values.write("(");
|
|
if !is_copyable(expr) {
|
|
values.write("&");
|
|
}
|
|
values.write(self.visit_expr_root(ctx, expr)?);
|
|
values.write(")");
|
|
self.locals.insert_with_default(Cow::Borrowed(arg));
|
|
}
|
|
}
|
|
}
|
|
|
|
debug_assert_eq!(names.buf.is_empty(), values.buf.is_empty());
|
|
if !names.buf.is_empty() {
|
|
buf.writeln(format_args!("let ({}) = ({});", names.buf, values.buf));
|
|
}
|
|
|
|
let mut size_hint = self.handle(own_ctx, &def.nodes, buf, AstLevel::Nested)?;
|
|
|
|
self.flush_ws(def.ws2);
|
|
size_hint += self.write_buf_writable(ctx, buf)?;
|
|
buf.writeln("}");
|
|
self.locals.pop();
|
|
self.prepare_ws(ws);
|
|
Ok(size_hint)
|
|
}
|
|
|
|
fn write_filter_block(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
filter: &'a WithSpan<'_, FilterBlock<'_>>,
|
|
) -> Result<usize, CompileError> {
|
|
self.flush_ws(filter.ws1);
|
|
let mut var_name = String::new();
|
|
for id in 0.. {
|
|
var_name = format!("__filter_block{id}");
|
|
if self.locals.get(&Cow::Borrowed(&var_name)).is_none() {
|
|
// No variable with this name exists, we're in the clear!
|
|
break;
|
|
}
|
|
}
|
|
let current_buf = mem::take(&mut self.buf_writable.buf);
|
|
|
|
self.prepare_ws(filter.ws1);
|
|
let mut size_hint = self.handle(ctx, &filter.nodes, buf, AstLevel::Nested)?;
|
|
self.flush_ws(filter.ws2);
|
|
|
|
let WriteParts {
|
|
size_hint: write_size_hint,
|
|
buffers,
|
|
} = self.prepare_format(ctx)?;
|
|
size_hint += match buffers {
|
|
None => return Ok(0),
|
|
Some(WritePartsBuffers { format, expr: None }) => {
|
|
buf.writeln(format_args!("let {var_name} = {:#?};", &format.buf));
|
|
write_size_hint
|
|
}
|
|
Some(WritePartsBuffers {
|
|
format,
|
|
expr: Some(expr),
|
|
}) => {
|
|
buf.writeln(format_args!(
|
|
"let {var_name} = format!({:#?}, {});",
|
|
&format.buf,
|
|
expr.buf.trim(),
|
|
));
|
|
write_size_hint
|
|
}
|
|
};
|
|
|
|
self.buf_writable.buf = current_buf;
|
|
|
|
let mut filter_buf = Buffer::new();
|
|
let Filter {
|
|
name: filter_name,
|
|
arguments,
|
|
} = &filter.filters;
|
|
let mut arguments = arguments.clone();
|
|
|
|
insert_first_filter_argument(&mut arguments, var_name.clone());
|
|
|
|
let wrap = self.visit_filter(ctx, &mut filter_buf, filter_name, &arguments, filter)?;
|
|
|
|
self.buf_writable
|
|
.push(Writable::Generated(filter_buf.buf, wrap));
|
|
self.prepare_ws(filter.ws2);
|
|
|
|
// We don't forget to add the created variable into the list of variables in the scope.
|
|
self.locals
|
|
.insert(Cow::Owned(var_name), LocalMeta::initialized());
|
|
|
|
Ok(size_hint)
|
|
}
|
|
|
|
fn handle_include(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
i: &'a Include<'_>,
|
|
) -> Result<usize, CompileError> {
|
|
self.flush_ws(i.ws);
|
|
self.write_buf_writable(ctx, buf)?;
|
|
let path = self
|
|
.input
|
|
.config
|
|
.find_template(i.path, Some(&self.input.path))?;
|
|
|
|
// Make sure the compiler understands that the generated code depends on the template file.
|
|
{
|
|
let path = path.to_str().unwrap();
|
|
buf.writeln(
|
|
quote! {
|
|
const _: &[::core::primitive::u8] = ::core::include_bytes!(#path);
|
|
}
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
// We clone the context of the child in order to preserve their macros and imports.
|
|
// But also add all the imports and macros from this template that don't override the
|
|
// child's ones to preserve this template's context.
|
|
let child_ctx = &mut self.contexts[&path].clone();
|
|
for (name, mac) in &ctx.macros {
|
|
child_ctx.macros.entry(name).or_insert(mac);
|
|
}
|
|
for (name, import) in &ctx.imports {
|
|
child_ctx
|
|
.imports
|
|
.entry(name)
|
|
.or_insert_with(|| import.clone());
|
|
}
|
|
|
|
// Create a new generator for the child, and call it like in `impl_template` as if it were
|
|
// a full template, while preserving the context.
|
|
let heritage = if !child_ctx.blocks.is_empty() || child_ctx.extends.is_some() {
|
|
Some(Heritage::new(child_ctx, self.contexts))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let handle_ctx = match &heritage {
|
|
Some(heritage) => heritage.root,
|
|
None => child_ctx,
|
|
};
|
|
let locals = MapChain::with_parent(&self.locals);
|
|
let mut child = Self::new(
|
|
self.input,
|
|
self.contexts,
|
|
heritage.as_ref(),
|
|
locals,
|
|
self.buf_writable.discard,
|
|
);
|
|
let mut size_hint = child.handle(handle_ctx, handle_ctx.nodes, buf, AstLevel::Top)?;
|
|
size_hint += child.write_buf_writable(handle_ctx, buf)?;
|
|
self.prepare_ws(i.ws);
|
|
|
|
Ok(size_hint)
|
|
}
|
|
|
|
fn is_shadowing_variable<T>(
|
|
&self,
|
|
ctx: &Context<'_>,
|
|
var: &Target<'a>,
|
|
l: &WithSpan<'_, T>,
|
|
) -> Result<bool, CompileError> {
|
|
match var {
|
|
Target::Name(name) => {
|
|
let name = normalize_identifier(name);
|
|
match self.locals.get(&Cow::Borrowed(name)) {
|
|
// declares a new variable
|
|
None => Ok(false),
|
|
// an initialized variable gets shadowed
|
|
Some(meta) if meta.initialized => Ok(true),
|
|
// initializes a variable that was introduced in a LetDecl before
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
Target::Tuple(_, targets) => {
|
|
for target in targets {
|
|
match self.is_shadowing_variable(ctx, target, l) {
|
|
Ok(false) => continue,
|
|
outcome => return outcome,
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
Target::Struct(_, named_targets) => {
|
|
for (_, target) in named_targets {
|
|
match self.is_shadowing_variable(ctx, target, l) {
|
|
Ok(false) => continue,
|
|
outcome => return outcome,
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
_ => Err(ctx.generate_error(
|
|
"literals are not allowed on the left-hand side of an assignment",
|
|
l,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn write_let(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
l: &'a WithSpan<'_, Let<'_>>,
|
|
) -> Result<(), CompileError> {
|
|
self.handle_ws(l.ws);
|
|
|
|
let Some(val) = &l.val else {
|
|
self.write_buf_writable(ctx, buf)?;
|
|
buf.write("let ");
|
|
self.visit_target(buf, false, true, &l.var);
|
|
buf.writeln(";");
|
|
return Ok(());
|
|
};
|
|
|
|
let mut expr_buf = Buffer::new();
|
|
self.visit_expr(ctx, &mut expr_buf, val)?;
|
|
|
|
let shadowed = self.is_shadowing_variable(ctx, &l.var, l)?;
|
|
if shadowed {
|
|
// Need to flush the buffer if the variable is being shadowed,
|
|
// to ensure the old variable is used.
|
|
self.write_buf_writable(ctx, buf)?;
|
|
}
|
|
if shadowed
|
|
|| !matches!(l.var, Target::Name(_))
|
|
|| matches!(&l.var, Target::Name(name) if self.locals.get(&Cow::Borrowed(name)).is_none())
|
|
{
|
|
buf.write("let ");
|
|
}
|
|
|
|
self.visit_target(buf, true, true, &l.var);
|
|
let (before, after) = if !is_copyable(val) {
|
|
("&(", ")")
|
|
} else {
|
|
("", "")
|
|
};
|
|
buf.writeln(format_args!(" = {before}{}{after};", &expr_buf.buf));
|
|
Ok(())
|
|
}
|
|
|
|
// If `name` is `Some`, this is a call to a block definition, and we have to find
|
|
// the first block for that name from the ancestry chain. If name is `None`, this
|
|
// is from a `super()` call, and we can get the name from `self.super_block`.
|
|
fn write_block<T>(
|
|
&mut self,
|
|
ctx: &Context<'a>,
|
|
buf: &mut Buffer,
|
|
name: Option<&'a str>,
|
|
outer: Ws,
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<usize, CompileError> {
|
|
// Flush preceding whitespace according to the outer WS spec
|
|
self.flush_ws(outer);
|
|
|
|
let cur = match (name, self.super_block) {
|
|
// The top-level context contains a block definition
|
|
(Some(cur_name), None) => (cur_name, 0),
|
|
// A block definition contains a block definition of the same name
|
|
(Some(cur_name), Some((prev_name, _))) if cur_name == prev_name => {
|
|
return Err(ctx.generate_error(
|
|
&format!("cannot define recursive blocks ({cur_name})"),
|
|
node,
|
|
));
|
|
}
|
|
// A block definition contains a definition of another block
|
|
(Some(cur_name), Some((_, _))) => (cur_name, 0),
|
|
// `super()` was called inside a block
|
|
(None, Some((prev_name, gen))) => (prev_name, gen + 1),
|
|
// `super()` is called from outside a block
|
|
(None, None) => {
|
|
return Err(ctx.generate_error("cannot call 'super()' outside block", node))
|
|
}
|
|
};
|
|
|
|
self.write_buf_writable(ctx, buf)?;
|
|
|
|
let block_fragment_write = self.input.block == name && self.buf_writable.discard;
|
|
// Allow writing to the buffer if we're in the block fragment
|
|
if block_fragment_write {
|
|
self.buf_writable.discard = false;
|
|
}
|
|
let prev_buf_discard = mem::replace(&mut buf.discard, self.buf_writable.discard);
|
|
|
|
// Get the block definition from the heritage chain
|
|
let heritage = self
|
|
.heritage
|
|
.ok_or_else(|| ctx.generate_error("no block ancestors available", node))?;
|
|
let (child_ctx, def) = *heritage.blocks[cur.0].get(cur.1).ok_or_else(|| {
|
|
ctx.generate_error(
|
|
&match name {
|
|
None => format!("no super() block found for block '{}'", cur.0),
|
|
Some(name) => format!("no block found for name '{name}'"),
|
|
},
|
|
node,
|
|
)
|
|
})?;
|
|
|
|
// We clone the context of the child in order to preserve their macros and imports.
|
|
// But also add all the imports and macros from this template that don't override the
|
|
// child's ones to preserve this template's context.
|
|
let mut child_ctx = child_ctx.clone();
|
|
for (name, mac) in &ctx.macros {
|
|
child_ctx.macros.entry(name).or_insert(mac);
|
|
}
|
|
for (name, import) in &ctx.imports {
|
|
child_ctx
|
|
.imports
|
|
.entry(name)
|
|
.or_insert_with(|| import.clone());
|
|
}
|
|
|
|
let mut child = Self::new(
|
|
self.input,
|
|
self.contexts,
|
|
Some(heritage),
|
|
// Variables are NOT inherited from the parent scope.
|
|
MapChain::default(),
|
|
self.buf_writable.discard,
|
|
);
|
|
child.buf_writable = mem::take(&mut self.buf_writable);
|
|
|
|
// Handle inner whitespace suppression spec and process block nodes
|
|
child.prepare_ws(def.ws1);
|
|
|
|
child.super_block = Some(cur);
|
|
let size_hint = child.handle(&child_ctx, &def.nodes, buf, AstLevel::Block)?;
|
|
|
|
if !child.locals.is_current_empty() {
|
|
// Need to flush the buffer before popping the variable stack
|
|
child.write_buf_writable(ctx, buf)?;
|
|
}
|
|
|
|
child.flush_ws(def.ws2);
|
|
self.buf_writable = child.buf_writable;
|
|
|
|
// Restore original block context and set whitespace suppression for
|
|
// succeeding whitespace according to the outer WS spec
|
|
self.prepare_ws(outer);
|
|
|
|
// If we are rendering a specific block and the discard changed, it means that we're done
|
|
// with the block we want to render and that from this point, everything will be discarded.
|
|
//
|
|
// To get this block content rendered as well, we need to write to the buffer before then.
|
|
if buf.discard != prev_buf_discard {
|
|
self.write_buf_writable(ctx, buf)?;
|
|
}
|
|
// Restore the original buffer discarding state
|
|
if block_fragment_write {
|
|
self.buf_writable.discard = true;
|
|
}
|
|
buf.discard = prev_buf_discard;
|
|
|
|
Ok(size_hint)
|
|
}
|
|
|
|
fn write_expr(&mut self, ws: Ws, s: &'a WithSpan<'a, Expr<'a>>) {
|
|
self.handle_ws(ws);
|
|
self.buf_writable.push(Writable::Expr(s));
|
|
}
|
|
|
|
// Write expression buffer and empty
|
|
fn write_buf_writable(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
) -> Result<usize, CompileError> {
|
|
let WriteParts { size_hint, buffers } = self.prepare_format(ctx)?;
|
|
match buffers {
|
|
None => Ok(size_hint),
|
|
Some(WritePartsBuffers { format, expr: None }) => {
|
|
buf.writeln(format_args!("writer.write_str({:#?})?;", &format.buf));
|
|
Ok(size_hint)
|
|
}
|
|
Some(WritePartsBuffers {
|
|
format,
|
|
expr: Some(expr),
|
|
}) => {
|
|
buf.writeln("::std::write!(");
|
|
buf.writeln("writer,");
|
|
buf.writeln(format_args!("{:#?},", &format.buf));
|
|
buf.writeln(expr.buf.trim());
|
|
buf.writeln(")?;");
|
|
Ok(size_hint)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is the common code to generate an expression. It is used for filter blocks and for
|
|
/// expressions more generally. It stores the size it represents and the buffers. Take a look
|
|
/// at `WriteParts` for more details.
|
|
fn prepare_format(&mut self, ctx: &Context<'_>) -> Result<WriteParts, CompileError> {
|
|
if self.buf_writable.is_empty() {
|
|
return Ok(WriteParts {
|
|
size_hint: 0,
|
|
buffers: None,
|
|
});
|
|
}
|
|
|
|
if self
|
|
.buf_writable
|
|
.iter()
|
|
.all(|w| matches!(w, Writable::Lit(_)))
|
|
{
|
|
let mut buf_lit = Buffer::new();
|
|
for s in mem::take(&mut self.buf_writable.buf) {
|
|
if let Writable::Lit(s) = s {
|
|
buf_lit.write(s);
|
|
};
|
|
}
|
|
return Ok(WriteParts {
|
|
size_hint: buf_lit.buf.len(),
|
|
buffers: Some(WritePartsBuffers {
|
|
format: buf_lit,
|
|
expr: None,
|
|
}),
|
|
});
|
|
}
|
|
|
|
let mut expr_cache = HashMap::with_capacity(self.buf_writable.len());
|
|
|
|
let mut size_hint = 0;
|
|
let mut buf_format = Buffer::new();
|
|
let mut buf_expr = Buffer::new();
|
|
|
|
for s in mem::take(&mut self.buf_writable.buf) {
|
|
match s {
|
|
Writable::Lit(s) => {
|
|
buf_format.write(s.replace('{', "{{").replace('}', "}}"));
|
|
size_hint += s.len();
|
|
}
|
|
Writable::Expr(s) => {
|
|
let mut expr_buf = Buffer::new();
|
|
let wrapped = self.visit_expr(ctx, &mut expr_buf, s)?;
|
|
let cacheable = is_cacheable(s);
|
|
size_hint += self.named_expression(
|
|
&mut buf_expr,
|
|
&mut buf_format,
|
|
expr_buf.buf,
|
|
wrapped,
|
|
cacheable,
|
|
&mut expr_cache,
|
|
);
|
|
}
|
|
Writable::Generated(s, wrapped) => {
|
|
size_hint += self.named_expression(
|
|
&mut buf_expr,
|
|
&mut buf_format,
|
|
s,
|
|
wrapped,
|
|
false,
|
|
&mut expr_cache,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(WriteParts {
|
|
size_hint,
|
|
buffers: Some(WritePartsBuffers {
|
|
format: buf_format,
|
|
expr: Some(buf_expr),
|
|
}),
|
|
})
|
|
}
|
|
|
|
fn named_expression(
|
|
&mut self,
|
|
buf_expr: &mut Buffer,
|
|
buf_format: &mut Buffer,
|
|
expr: String,
|
|
wrapped: DisplayWrap,
|
|
cacheable: bool,
|
|
expr_cache: &mut HashMap<String, usize>,
|
|
) -> usize {
|
|
let expression = match wrapped {
|
|
DisplayWrap::Wrapped => expr,
|
|
DisplayWrap::Unwrapped => format!(
|
|
"{CRATE}::MarkupDisplay::new_unsafe(&({}), {})",
|
|
expr, self.input.escaper
|
|
),
|
|
};
|
|
let id = match expr_cache.entry(expression) {
|
|
Entry::Occupied(e) if cacheable => *e.get(),
|
|
entry => {
|
|
let id = self.named;
|
|
self.named += 1;
|
|
|
|
buf_expr.write(format_args!("expr{id} = &{},", entry.key()));
|
|
|
|
if let Entry::Vacant(e) = entry {
|
|
e.insert(id);
|
|
}
|
|
|
|
id
|
|
}
|
|
};
|
|
|
|
buf_format.write(format_args!("{{expr{id}}}"));
|
|
3
|
|
}
|
|
|
|
fn visit_lit(&mut self, lit: &'a Lit<'_>) {
|
|
assert!(self.next_ws.is_none());
|
|
let Lit { lws, val, rws } = *lit;
|
|
if !lws.is_empty() {
|
|
match self.skip_ws {
|
|
WhitespaceHandling::Suppress => {}
|
|
_ if val.is_empty() => {
|
|
assert!(rws.is_empty());
|
|
self.next_ws = Some(lws);
|
|
}
|
|
WhitespaceHandling::Preserve => self.buf_writable.push(Writable::Lit(lws)),
|
|
WhitespaceHandling::Minimize => {
|
|
self.buf_writable
|
|
.push(Writable::Lit(match lws.contains('\n') {
|
|
true => "\n",
|
|
false => " ",
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !val.is_empty() {
|
|
self.skip_ws = WhitespaceHandling::Preserve;
|
|
self.buf_writable.push(Writable::Lit(val));
|
|
}
|
|
|
|
if !rws.is_empty() {
|
|
self.next_ws = Some(rws);
|
|
}
|
|
}
|
|
|
|
fn write_comment(&mut self, comment: &'a WithSpan<'_, Comment<'_>>) {
|
|
self.handle_ws(comment.ws);
|
|
}
|
|
|
|
/* Visitor methods for expression types */
|
|
|
|
fn visit_expr_root(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
expr: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<String, CompileError> {
|
|
let mut buf = Buffer::new();
|
|
self.visit_expr(ctx, &mut buf, expr)?;
|
|
Ok(buf.buf)
|
|
}
|
|
|
|
fn visit_expr(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
expr: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
Ok(match **expr {
|
|
Expr::BoolLit(s) => self.visit_bool_lit(buf, s),
|
|
Expr::NumLit(s) => self.visit_num_lit(buf, s),
|
|
Expr::StrLit(s) => self.visit_str_lit(buf, s),
|
|
Expr::CharLit(s) => self.visit_char_lit(buf, s),
|
|
Expr::Var(s) => self.visit_var(buf, s),
|
|
Expr::Path(ref path) => self.visit_path(buf, path),
|
|
Expr::Array(ref elements) => self.visit_array(ctx, buf, elements)?,
|
|
Expr::Attr(ref obj, name) => self.visit_attr(ctx, buf, obj, name)?,
|
|
Expr::Index(ref obj, ref key) => self.visit_index(ctx, buf, obj, key)?,
|
|
Expr::Filter(Filter {
|
|
name,
|
|
ref arguments,
|
|
}) => self.visit_filter(ctx, buf, name, arguments, expr)?,
|
|
Expr::Unary(op, ref inner) => self.visit_unary(ctx, buf, op, inner)?,
|
|
Expr::BinOp(op, ref left, ref right) => self.visit_binop(ctx, buf, op, left, right)?,
|
|
Expr::Range(op, ref left, ref right) => {
|
|
self.visit_range(ctx, buf, op, left.as_deref(), right.as_deref())?
|
|
}
|
|
Expr::Group(ref inner) => self.visit_group(ctx, buf, inner)?,
|
|
Expr::Call(ref obj, ref args) => self.visit_call(ctx, buf, obj, args)?,
|
|
Expr::RustMacro(ref path, args) => self.visit_rust_macro(buf, path, args),
|
|
Expr::Try(ref expr) => self.visit_try(ctx, buf, expr)?,
|
|
Expr::Tuple(ref exprs) => self.visit_tuple(ctx, buf, exprs)?,
|
|
Expr::NamedArgument(_, ref expr) => self.visit_named_argument(ctx, buf, expr)?,
|
|
Expr::Generated(ref s) => self.visit_generated(buf, s),
|
|
})
|
|
}
|
|
|
|
fn visit_try(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
expr: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write("::core::result::Result::map_err(");
|
|
self.visit_expr(ctx, buf, expr)?;
|
|
buf.write(", |err| ");
|
|
buf.write(CRATE);
|
|
buf.write("::shared::Error::Custom(::core::convert::Into::into(err)))?");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_rust_macro(&mut self, buf: &mut Buffer, path: &[&str], args: &str) -> DisplayWrap {
|
|
self.visit_path(buf, path);
|
|
buf.write("!(");
|
|
buf.write(args);
|
|
buf.write(")");
|
|
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
name: &str,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
filter: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
match name {
|
|
"deref" => return self._visit_deref_filter(ctx, buf, args, filter),
|
|
"escape" | "e" => return self._visit_escape_filter(ctx, buf, args, filter),
|
|
"fmt" => return self._visit_fmt_filter(ctx, buf, args, filter),
|
|
"format" => return self._visit_format_filter(ctx, buf, args, filter),
|
|
"join" => return self._visit_join_filter(ctx, buf, args),
|
|
"json" | "tojson" => return self._visit_json_filter(ctx, buf, args, filter),
|
|
"ref" => return self._visit_ref_filter(ctx, buf, args, filter),
|
|
"safe" => return self._visit_safe_filter(ctx, buf, args, filter),
|
|
_ => {}
|
|
}
|
|
|
|
if crate::BUILT_IN_FILTERS.contains(&name) {
|
|
buf.write(format_args!("{CRATE}::filters::{name}("));
|
|
} else {
|
|
buf.write(format_args!("filters::{name}("));
|
|
}
|
|
self._visit_args(ctx, buf, args)?;
|
|
buf.write(")?");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn _visit_ref_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
let arg = match args {
|
|
[arg] => arg,
|
|
_ => return Err(ctx.generate_error("unexpected argument(s) in `as_ref` filter", node)),
|
|
};
|
|
buf.write("&");
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn _visit_deref_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
let arg = match args {
|
|
[arg] => arg,
|
|
_ => return Err(ctx.generate_error("unexpected argument(s) in `deref` filter", node)),
|
|
};
|
|
buf.write("*");
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn _visit_json_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if cfg!(not(feature = "serde_json")) {
|
|
return Err(ctx.generate_error(
|
|
"the `json` filter requires the `serde_json` feature to be enabled",
|
|
node,
|
|
));
|
|
}
|
|
|
|
let filter = match args.len() {
|
|
1 => "json",
|
|
2 => "json_pretty",
|
|
_ => return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node)),
|
|
};
|
|
|
|
buf.write(format_args!("{CRATE}::filters::{filter}("));
|
|
self._visit_args(ctx, buf, args)?;
|
|
buf.write(")?");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn _visit_safe_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if args.len() != 1 {
|
|
return Err(ctx.generate_error("unexpected argument(s) in `safe` filter", node));
|
|
}
|
|
buf.write(CRATE);
|
|
buf.write("::filters::safe(");
|
|
buf.write(self.input.escaper);
|
|
buf.write(", ");
|
|
self._visit_args(ctx, buf, args)?;
|
|
buf.write(")?");
|
|
Ok(DisplayWrap::Wrapped)
|
|
}
|
|
|
|
fn _visit_escape_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if args.len() > 2 {
|
|
return Err(ctx.generate_error("only two arguments allowed to escape filter", node));
|
|
}
|
|
let opt_escaper = match args.get(1).map(|expr| &**expr) {
|
|
Some(Expr::StrLit(name)) => Some(*name),
|
|
Some(_) => {
|
|
return Err(ctx.generate_error("invalid escaper type for escape filter", node))
|
|
}
|
|
None => None,
|
|
};
|
|
let escaper = match opt_escaper {
|
|
Some(name) => self
|
|
.input
|
|
.config
|
|
.escapers
|
|
.iter()
|
|
.find_map(|(escapers, escaper)| escapers.contains(name).then_some(escaper))
|
|
.ok_or_else(|| ctx.generate_error("invalid escaper for escape filter", node))?,
|
|
None => self.input.escaper,
|
|
};
|
|
buf.write(CRATE);
|
|
buf.write("::filters::escape(");
|
|
buf.write(escaper);
|
|
buf.write(", ");
|
|
self._visit_args(ctx, buf, &args[..1])?;
|
|
buf.write(")?");
|
|
Ok(DisplayWrap::Wrapped)
|
|
}
|
|
|
|
fn _visit_format_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if !args.is_empty() {
|
|
if let Expr::StrLit(fmt) = *args[0] {
|
|
buf.write("::std::format!(");
|
|
self.visit_str_lit(buf, fmt);
|
|
if args.len() > 1 {
|
|
buf.write(", ");
|
|
self._visit_args(ctx, buf, &args[1..])?;
|
|
}
|
|
buf.write(")");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
}
|
|
}
|
|
Err(ctx.generate_error(r#"use filter format like `"a={} b={}"|format(a, b)`"#, node))
|
|
}
|
|
|
|
fn _visit_fmt_filter<T>(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
node: &WithSpan<'_, T>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if let [_, arg2] = args {
|
|
if let Expr::StrLit(fmt) = **arg2 {
|
|
buf.write("::std::format!(");
|
|
self.visit_str_lit(buf, fmt);
|
|
buf.write(", ");
|
|
self._visit_args(ctx, buf, &args[..1])?;
|
|
buf.write(")");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
}
|
|
}
|
|
Err(ctx.generate_error(r#"use filter fmt like `value|fmt("{:?}")`"#, node))
|
|
}
|
|
|
|
// Force type coercion on first argument to `join` filter (see #39).
|
|
fn _visit_join_filter(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write(CRATE);
|
|
buf.write("::filters::join((&");
|
|
for (i, arg) in args.iter().enumerate() {
|
|
if i > 0 {
|
|
buf.write(", &");
|
|
}
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
if i == 0 {
|
|
buf.write(").into_iter()");
|
|
}
|
|
}
|
|
buf.write(")?");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn _visit_args(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
) -> Result<(), CompileError> {
|
|
if args.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
for (i, arg) in args.iter().enumerate() {
|
|
if i > 0 {
|
|
buf.write(", ");
|
|
}
|
|
|
|
let borrow = !is_copyable(arg);
|
|
if borrow {
|
|
buf.write("&(");
|
|
}
|
|
|
|
match **arg {
|
|
Expr::Call(ref left, _) if !matches!(***left, Expr::Path(_)) => {
|
|
buf.writeln("{");
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
buf.writeln("}");
|
|
}
|
|
_ => {
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
}
|
|
}
|
|
|
|
if borrow {
|
|
buf.write(")");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn visit_attr(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
obj: &WithSpan<'_, Expr<'_>>,
|
|
attr: &str,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if let Expr::Var(name) = **obj {
|
|
if name == "loop" {
|
|
if attr == "index" {
|
|
buf.write("(_loop_item.index + 1)");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
} else if attr == "index0" {
|
|
buf.write("_loop_item.index");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
} else if attr == "first" {
|
|
buf.write("_loop_item.first");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
} else if attr == "last" {
|
|
buf.write("_loop_item.last");
|
|
return Ok(DisplayWrap::Unwrapped);
|
|
} else {
|
|
return Err(ctx.generate_error("unknown loop variable", obj));
|
|
}
|
|
}
|
|
}
|
|
self.visit_expr(ctx, buf, obj)?;
|
|
buf.write(format_args!(".{}", normalize_identifier(attr)));
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_index(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
obj: &WithSpan<'_, Expr<'_>>,
|
|
key: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write("&");
|
|
self.visit_expr(ctx, buf, obj)?;
|
|
buf.write("[");
|
|
self.visit_expr(ctx, buf, key)?;
|
|
buf.write("]");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_call(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
left: &WithSpan<'_, Expr<'_>>,
|
|
args: &[WithSpan<'_, Expr<'_>>],
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
match &**left {
|
|
Expr::Attr(sub_left, method) if ***sub_left == Expr::Var("loop") => match *method {
|
|
"cycle" => match args {
|
|
[arg] => {
|
|
if matches!(**arg, Expr::Array(ref arr) if arr.is_empty()) {
|
|
return Err(
|
|
ctx.generate_error("loop.cycle(…) cannot use an empty array", arg)
|
|
);
|
|
}
|
|
buf.write("({");
|
|
buf.write("let _cycle = &(");
|
|
self.visit_expr(ctx, buf, arg)?;
|
|
buf.writeln(");");
|
|
buf.writeln("let _len = _cycle.len();");
|
|
buf.writeln("if _len == 0 {");
|
|
buf.writeln(format_args!(
|
|
"return ::core::result::Result::Err({CRATE}::Error::Fmt);"
|
|
));
|
|
buf.writeln("}");
|
|
buf.writeln("_cycle[_loop_item.index % _len]");
|
|
buf.writeln("})");
|
|
}
|
|
_ => {
|
|
return Err(
|
|
ctx.generate_error("loop.cycle(…) cannot use an empty array", left)
|
|
)
|
|
}
|
|
},
|
|
s => return Err(ctx.generate_error(&format!("unknown loop method: {s:?}"), left)),
|
|
},
|
|
sub_left => {
|
|
match sub_left {
|
|
Expr::Var(name) => match self.locals.resolve(name) {
|
|
Some(resolved) => buf.write(resolved),
|
|
None => buf.write(format_args!("self.{}", normalize_identifier(name))),
|
|
},
|
|
_ => {
|
|
self.visit_expr(ctx, buf, left)?;
|
|
}
|
|
}
|
|
buf.write("(");
|
|
self._visit_args(ctx, buf, args)?;
|
|
buf.write(")");
|
|
}
|
|
}
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_unary(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
op: &str,
|
|
inner: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write(op);
|
|
self.visit_expr(ctx, buf, inner)?;
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_range(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
op: &str,
|
|
left: Option<&WithSpan<'_, Expr<'_>>>,
|
|
right: Option<&WithSpan<'_, Expr<'_>>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
if let Some(left) = left {
|
|
self.visit_expr(ctx, buf, left)?;
|
|
}
|
|
buf.write(op);
|
|
if let Some(right) = right {
|
|
self.visit_expr(ctx, buf, right)?;
|
|
}
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_binop(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
op: &str,
|
|
left: &WithSpan<'_, Expr<'_>>,
|
|
right: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
self.visit_expr(ctx, buf, left)?;
|
|
buf.write(format_args!(" {op} "));
|
|
self.visit_expr(ctx, buf, right)?;
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_group(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
inner: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write("(");
|
|
self.visit_expr(ctx, buf, inner)?;
|
|
buf.write(")");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_tuple(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
exprs: &[WithSpan<'_, Expr<'_>>],
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write("(");
|
|
for (index, expr) in exprs.iter().enumerate() {
|
|
if index > 0 {
|
|
buf.write(" ");
|
|
}
|
|
self.visit_expr(ctx, buf, expr)?;
|
|
buf.write(",");
|
|
}
|
|
buf.write(")");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_named_argument(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
expr: &WithSpan<'_, Expr<'_>>,
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
self.visit_expr(ctx, buf, expr)?;
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_array(
|
|
&mut self,
|
|
ctx: &Context<'_>,
|
|
buf: &mut Buffer,
|
|
elements: &[WithSpan<'_, Expr<'_>>],
|
|
) -> Result<DisplayWrap, CompileError> {
|
|
buf.write("[");
|
|
for (i, el) in elements.iter().enumerate() {
|
|
if i > 0 {
|
|
buf.write(", ");
|
|
}
|
|
self.visit_expr(ctx, buf, el)?;
|
|
}
|
|
buf.write("]");
|
|
Ok(DisplayWrap::Unwrapped)
|
|
}
|
|
|
|
fn visit_path(&mut self, buf: &mut Buffer, path: &[&str]) -> DisplayWrap {
|
|
for (i, part) in path.iter().enumerate() {
|
|
if i > 0 {
|
|
buf.write("::");
|
|
}
|
|
buf.write(part);
|
|
}
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_var(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
if s == "self" {
|
|
buf.write(s);
|
|
return DisplayWrap::Unwrapped;
|
|
}
|
|
|
|
buf.write(normalize_identifier(&self.locals.resolve_or_self(s)));
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_generated(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
buf.write(s);
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_bool_lit(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
buf.write(s);
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_str_lit(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
buf.write(format_args!("\"{s}\""));
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_char_lit(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
buf.write(format_args!("'{s}'"));
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_num_lit(&mut self, buf: &mut Buffer, s: &str) -> DisplayWrap {
|
|
buf.write(s);
|
|
DisplayWrap::Unwrapped
|
|
}
|
|
|
|
fn visit_target(
|
|
&mut self,
|
|
buf: &mut Buffer,
|
|
initialized: bool,
|
|
first_level: bool,
|
|
target: &Target<'a>,
|
|
) {
|
|
match target {
|
|
Target::Placeholder(s) | Target::Rest(s) => {
|
|
buf.write(s);
|
|
}
|
|
Target::Name(name) => {
|
|
let name = normalize_identifier(name);
|
|
match initialized {
|
|
true => self
|
|
.locals
|
|
.insert(Cow::Borrowed(name), LocalMeta::initialized()),
|
|
false => self.locals.insert_with_default(Cow::Borrowed(name)),
|
|
}
|
|
buf.write(name);
|
|
}
|
|
Target::OrChain(targets) => match targets.first() {
|
|
None => buf.write("_"),
|
|
Some(first_target) => {
|
|
self.visit_target(buf, initialized, first_level, first_target);
|
|
for target in &targets[1..] {
|
|
buf.write(" | ");
|
|
self.visit_target(buf, initialized, first_level, target);
|
|
}
|
|
}
|
|
},
|
|
Target::Tuple(path, targets) => {
|
|
buf.write(SeparatedPath(path));
|
|
buf.write("(");
|
|
for target in targets {
|
|
self.visit_target(buf, initialized, false, target);
|
|
buf.write(",");
|
|
}
|
|
buf.write(")");
|
|
}
|
|
Target::Struct(path, targets) => {
|
|
buf.write(SeparatedPath(path));
|
|
buf.write(" { ");
|
|
for (name, target) in targets {
|
|
if let Target::Rest(s) = target {
|
|
buf.write(s);
|
|
continue;
|
|
}
|
|
|
|
buf.write(normalize_identifier(name));
|
|
buf.write(": ");
|
|
self.visit_target(buf, initialized, false, target);
|
|
buf.write(",");
|
|
}
|
|
buf.write(" }");
|
|
}
|
|
Target::Path(path) => {
|
|
self.visit_path(buf, path);
|
|
}
|
|
Target::StrLit(s) => {
|
|
if first_level {
|
|
buf.write("&");
|
|
}
|
|
self.visit_str_lit(buf, s);
|
|
}
|
|
Target::NumLit(s) => {
|
|
if first_level {
|
|
buf.write("&");
|
|
}
|
|
self.visit_num_lit(buf, s);
|
|
}
|
|
Target::CharLit(s) => {
|
|
if first_level {
|
|
buf.write("&");
|
|
}
|
|
self.visit_char_lit(buf, s);
|
|
}
|
|
Target::BoolLit(s) => {
|
|
if first_level {
|
|
buf.write("&");
|
|
}
|
|
buf.write(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Helper methods for dealing with whitespace nodes */
|
|
|
|
// Combines `flush_ws()` and `prepare_ws()` to handle both trailing whitespace from the
|
|
// preceding literal and leading whitespace from the succeeding literal.
|
|
fn handle_ws(&mut self, ws: Ws) {
|
|
self.flush_ws(ws);
|
|
self.prepare_ws(ws);
|
|
}
|
|
|
|
fn should_trim_ws(&self, ws: Option<Whitespace>) -> WhitespaceHandling {
|
|
match ws {
|
|
Some(Whitespace::Suppress) => WhitespaceHandling::Suppress,
|
|
Some(Whitespace::Preserve) => WhitespaceHandling::Preserve,
|
|
Some(Whitespace::Minimize) => WhitespaceHandling::Minimize,
|
|
None => self.input.config.whitespace,
|
|
}
|
|
}
|
|
|
|
// If the previous literal left some trailing whitespace in `next_ws` and the
|
|
// prefix whitespace suppressor from the given argument, flush that whitespace.
|
|
// In either case, `next_ws` is reset to `None` (no trailing whitespace).
|
|
fn flush_ws(&mut self, ws: Ws) {
|
|
if self.next_ws.is_none() {
|
|
return;
|
|
}
|
|
|
|
// If `whitespace` is set to `suppress`, we keep the whitespace characters only if there is
|
|
// a `+` character.
|
|
match self.should_trim_ws(ws.0) {
|
|
WhitespaceHandling::Preserve => {
|
|
let val = self.next_ws.unwrap();
|
|
if !val.is_empty() {
|
|
self.buf_writable.push(Writable::Lit(val));
|
|
}
|
|
}
|
|
WhitespaceHandling::Minimize => {
|
|
let val = self.next_ws.unwrap();
|
|
if !val.is_empty() {
|
|
self.buf_writable
|
|
.push(Writable::Lit(match val.contains('\n') {
|
|
true => "\n",
|
|
false => " ",
|
|
}));
|
|
}
|
|
}
|
|
WhitespaceHandling::Suppress => {}
|
|
}
|
|
self.next_ws = None;
|
|
}
|
|
|
|
// Sets `skip_ws` to match the suffix whitespace suppressor from the given
|
|
// argument, to determine whether to suppress leading whitespace from the
|
|
// next literal.
|
|
fn prepare_ws(&mut self, ws: Ws) {
|
|
self.skip_ws = self.should_trim_ws(ws.1);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Buffer {
|
|
// The buffer to generate the code into
|
|
buf: String,
|
|
discard: bool,
|
|
}
|
|
|
|
impl Buffer {
|
|
fn new() -> Self {
|
|
Self {
|
|
buf: String::new(),
|
|
discard: false,
|
|
}
|
|
}
|
|
|
|
fn writeln(&mut self, src: impl BufferFmt) {
|
|
if !self.discard {
|
|
src.append_to(&mut self.buf);
|
|
self.buf.push('\n');
|
|
}
|
|
}
|
|
|
|
fn write(&mut self, src: impl BufferFmt) {
|
|
if !self.discard {
|
|
src.append_to(&mut self.buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
trait BufferFmt {
|
|
fn append_to(&self, buf: &mut String);
|
|
}
|
|
|
|
impl<T: BufferFmt + ?Sized> BufferFmt for &T {
|
|
fn append_to(&self, buf: &mut String) {
|
|
T::append_to(self, buf)
|
|
}
|
|
}
|
|
|
|
impl BufferFmt for str {
|
|
fn append_to(&self, buf: &mut String) {
|
|
buf.push_str(self);
|
|
}
|
|
}
|
|
|
|
impl BufferFmt for String {
|
|
fn append_to(&self, buf: &mut String) {
|
|
buf.push_str(self);
|
|
}
|
|
}
|
|
|
|
impl BufferFmt for Arguments<'_> {
|
|
fn append_to(&self, buf: &mut String) {
|
|
buf.write_fmt(*self).unwrap();
|
|
}
|
|
}
|
|
|
|
struct SeparatedPath<I>(I);
|
|
|
|
impl<I: IntoIterator<Item = E> + Copy, E: BufferFmt> BufferFmt for SeparatedPath<I> {
|
|
fn append_to(&self, buf: &mut String) {
|
|
for (idx, item) in self.0.into_iter().enumerate() {
|
|
if idx > 0 {
|
|
buf.push_str("::");
|
|
}
|
|
item.append_to(buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct LocalMeta {
|
|
refs: Option<String>,
|
|
initialized: bool,
|
|
}
|
|
|
|
impl LocalMeta {
|
|
fn initialized() -> Self {
|
|
Self {
|
|
refs: None,
|
|
initialized: true,
|
|
}
|
|
}
|
|
|
|
fn with_ref(refs: String) -> Self {
|
|
Self {
|
|
refs: Some(refs),
|
|
initialized: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
// type SetChain<'a, T> = MapChain<'a, T, ()>;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct MapChain<'a, K, V>
|
|
where
|
|
K: cmp::Eq + hash::Hash,
|
|
{
|
|
parent: Option<&'a MapChain<'a, K, V>>,
|
|
scopes: Vec<HashMap<K, V>>,
|
|
}
|
|
|
|
impl<'a, K: 'a, V: 'a> MapChain<'a, K, V>
|
|
where
|
|
K: cmp::Eq + hash::Hash,
|
|
{
|
|
fn with_parent<'p>(parent: &'p MapChain<'_, K, V>) -> MapChain<'p, K, V> {
|
|
MapChain {
|
|
parent: Some(parent),
|
|
scopes: vec![HashMap::new()],
|
|
}
|
|
}
|
|
|
|
/// Iterates the scopes in reverse and returns `Some(LocalMeta)`
|
|
/// from the first scope where `key` exists.
|
|
fn get(&self, key: &K) -> Option<&V> {
|
|
let mut scopes = self.scopes.iter().rev();
|
|
scopes
|
|
.find_map(|set| set.get(key))
|
|
.or_else(|| self.parent.and_then(|set| set.get(key)))
|
|
}
|
|
|
|
fn is_current_empty(&self) -> bool {
|
|
self.scopes.last().unwrap().is_empty()
|
|
}
|
|
|
|
fn insert(&mut self, key: K, val: V) {
|
|
self.scopes.last_mut().unwrap().insert(key, val);
|
|
|
|
// Note that if `insert` returns `Some` then it implies
|
|
// an identifier is reused. For e.g. `{% macro f(a, a) %}`
|
|
// and `{% let (a, a) = ... %}` then this results in a
|
|
// generated template, which when compiled fails with the
|
|
// compile error "identifier `a` used more than once".
|
|
}
|
|
|
|
fn insert_with_default(&mut self, key: K)
|
|
where
|
|
V: Default,
|
|
{
|
|
self.insert(key, V::default());
|
|
}
|
|
|
|
fn push(&mut self) {
|
|
self.scopes.push(HashMap::new());
|
|
}
|
|
|
|
fn pop(&mut self) {
|
|
self.scopes.pop().unwrap();
|
|
assert!(!self.scopes.is_empty());
|
|
}
|
|
}
|
|
|
|
impl MapChain<'_, Cow<'_, str>, LocalMeta> {
|
|
fn resolve(&self, name: &str) -> Option<String> {
|
|
let name = normalize_identifier(name);
|
|
self.get(&Cow::Borrowed(name)).map(|meta| match &meta.refs {
|
|
Some(expr) => expr.clone(),
|
|
None => name.to_string(),
|
|
})
|
|
}
|
|
|
|
fn resolve_or_self(&self, name: &str) -> String {
|
|
let name = normalize_identifier(name);
|
|
self.resolve(name).unwrap_or_else(|| format!("self.{name}"))
|
|
}
|
|
}
|
|
|
|
impl<'a, K: Eq + hash::Hash, V> Default for MapChain<'a, K, V> {
|
|
fn default() -> Self {
|
|
Self {
|
|
parent: None,
|
|
scopes: vec![HashMap::new()],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if enough assumptions can be made,
|
|
/// to determine that `self` is copyable.
|
|
fn is_copyable(expr: &Expr<'_>) -> bool {
|
|
is_copyable_within_op(expr, false)
|
|
}
|
|
|
|
fn is_copyable_within_op(expr: &Expr<'_>, within_op: bool) -> bool {
|
|
match expr {
|
|
Expr::BoolLit(_) | Expr::NumLit(_) | Expr::StrLit(_) | Expr::CharLit(_) => true,
|
|
Expr::Unary(.., expr) => is_copyable_within_op(expr, true),
|
|
Expr::BinOp(_, lhs, rhs) => {
|
|
is_copyable_within_op(lhs, true) && is_copyable_within_op(rhs, true)
|
|
}
|
|
Expr::Range(..) => true,
|
|
// The result of a call likely doesn't need to be borrowed,
|
|
// as in that case the call is more likely to return a
|
|
// reference in the first place then.
|
|
Expr::Call(..) | Expr::Path(..) | Expr::Filter(..) => true,
|
|
// If the `expr` is within a `Unary` or `BinOp` then
|
|
// an assumption can be made that the operand is copy.
|
|
// If not, then the value is moved and adding `.clone()`
|
|
// will solve that issue. However, if the operand is
|
|
// implicitly borrowed, then it's likely not even possible
|
|
// to get the template to compile.
|
|
_ => within_op && is_attr_self(expr),
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if this is an `Attr` where the `obj` is `"self"`.
|
|
pub(crate) fn is_attr_self(expr: &Expr<'_>) -> bool {
|
|
match expr {
|
|
Expr::Attr(obj, _) if matches!(***obj, Expr::Var("self")) => true,
|
|
Expr::Attr(obj, _) if matches!(***obj, Expr::Attr(..)) => is_attr_self(obj),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the outcome of this expression may be used multiple times in the same
|
|
/// `write!()` call, without evaluating the expression again, i.e. the expression should be
|
|
/// side-effect free.
|
|
pub(crate) fn is_cacheable(expr: &WithSpan<'_, Expr<'_>>) -> bool {
|
|
match &**expr {
|
|
// Literals are the definition of pure:
|
|
Expr::BoolLit(_) => true,
|
|
Expr::NumLit(_) => true,
|
|
Expr::StrLit(_) => true,
|
|
Expr::CharLit(_) => true,
|
|
// fmt::Display should have no effects:
|
|
Expr::Var(_) => true,
|
|
Expr::Path(_) => true,
|
|
// Check recursively:
|
|
Expr::Array(args) => args.iter().all(is_cacheable),
|
|
Expr::Attr(lhs, _) => is_cacheable(lhs),
|
|
Expr::Index(lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs),
|
|
Expr::Filter(Filter { arguments, .. }) => arguments.iter().all(is_cacheable),
|
|
Expr::Unary(_, arg) => is_cacheable(arg),
|
|
Expr::BinOp(_, lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs),
|
|
Expr::Range(_, lhs, rhs) => {
|
|
lhs.as_ref().map_or(true, |v| is_cacheable(v))
|
|
&& rhs.as_ref().map_or(true, |v| is_cacheable(v))
|
|
}
|
|
Expr::Group(arg) => is_cacheable(arg),
|
|
Expr::Tuple(args) => args.iter().all(is_cacheable),
|
|
Expr::NamedArgument(_, expr) => is_cacheable(expr),
|
|
// We have too little information to tell if the expression is pure:
|
|
Expr::Call(_, _) => false,
|
|
Expr::RustMacro(_, _) => false,
|
|
Expr::Try(_) => false,
|
|
Expr::Generated(_) => true,
|
|
}
|
|
}
|
|
|
|
fn median(sizes: &mut [usize]) -> usize {
|
|
sizes.sort_unstable();
|
|
if sizes.len() % 2 == 1 {
|
|
sizes[sizes.len() / 2]
|
|
} else {
|
|
(sizes[sizes.len() / 2 - 1] + sizes[sizes.len() / 2]) / 2
|
|
}
|
|
}
|
|
|
|
/// In `FilterBlock`, we have a recursive `Expr::Filter` entry, where the more you go "down",
|
|
/// the sooner you are called in the Rust code. Example:
|
|
///
|
|
/// ```text
|
|
/// {% filter a|b|c %}bla{% endfilter %}
|
|
/// ```
|
|
///
|
|
/// Will be translated as:
|
|
///
|
|
/// ```text
|
|
/// FilterBlock {
|
|
/// filters: Filter {
|
|
/// name: "c",
|
|
/// arguments: vec![
|
|
/// Filter {
|
|
/// name: "b",
|
|
/// arguments: vec![
|
|
/// Filter {
|
|
/// name: "a",
|
|
/// arguments: vec![],
|
|
/// }.
|
|
/// ],
|
|
/// }
|
|
/// ],
|
|
/// },
|
|
/// // ...
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// So in here, we want to insert the variable containing the content of the filter block inside
|
|
/// the call to `"a"`. To do so, we recursively go through all `Filter` and finally insert our
|
|
/// variable as the first argument to the `"a"` call.
|
|
fn insert_first_filter_argument(args: &mut Vec<WithSpan<'_, Expr<'_>>>, var_name: String) {
|
|
if let Some(expr) = args.first_mut() {
|
|
if let Expr::Filter(Filter {
|
|
ref mut arguments, ..
|
|
}) = **expr
|
|
{
|
|
insert_first_filter_argument(arguments, var_name);
|
|
return;
|
|
}
|
|
}
|
|
args.insert(0, WithSpan::new(Expr::Generated(var_name), ""));
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum AstLevel {
|
|
Top,
|
|
Block,
|
|
Nested,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum DisplayWrap {
|
|
Wrapped,
|
|
Unwrapped,
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
struct WritableBuffer<'a> {
|
|
buf: Vec<Writable<'a>>,
|
|
discard: bool,
|
|
}
|
|
|
|
impl<'a> WritableBuffer<'a> {
|
|
fn push(&mut self, writable: Writable<'a>) {
|
|
if !self.discard {
|
|
self.buf.push(writable);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Deref for WritableBuffer<'a> {
|
|
type Target = [Writable<'a>];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.buf[..]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum Writable<'a> {
|
|
Lit(&'a str),
|
|
Expr(&'a WithSpan<'a, Expr<'a>>),
|
|
Generated(String, DisplayWrap),
|
|
}
|
|
|
|
struct WriteParts {
|
|
size_hint: usize,
|
|
buffers: Option<WritePartsBuffers>,
|
|
}
|
|
|
|
/// If "expr" is `None`, it means we can generate code like this:
|
|
///
|
|
/// ```ignore
|
|
/// let var = format;
|
|
/// ```
|
|
///
|
|
/// Otherwise we need to format "expr" using "format":
|
|
///
|
|
/// ```ignore
|
|
/// let var = format!(format, expr);
|
|
/// ```
|
|
#[derive(Debug)]
|
|
struct WritePartsBuffers {
|
|
format: Buffer,
|
|
expr: Option<Buffer>,
|
|
}
|
|
|
|
/// Identifiers to be replaced with raw identifiers, so as to avoid
|
|
/// collisions between template syntax and Rust's syntax. In particular
|
|
/// [Rust keywords](https://doc.rust-lang.org/reference/keywords.html)
|
|
/// should be replaced, since they're not reserved words in Rinja
|
|
/// syntax but have a high probability of causing problems in the
|
|
/// generated code.
|
|
///
|
|
/// This list excludes the Rust keywords *self*, *Self*, and *super*
|
|
/// because they are not allowed to be raw identifiers, and *loop*
|
|
/// because it's used something like a keyword in the template
|
|
/// language.
|
|
fn normalize_identifier(ident: &str) -> &str {
|
|
// This table works for as long as the replacement string is the original string
|
|
// prepended with "r#". The strings get right-padded to the same length with b'_'.
|
|
// While the code does not need it, please keep the list sorted when adding new
|
|
// keywords.
|
|
|
|
if ident.len() > parser::node::MAX_KW_LEN {
|
|
return ident;
|
|
}
|
|
let kws = parser::node::KWS[ident.len()];
|
|
|
|
let mut padded_ident = [b'_'; parser::node::MAX_KW_LEN];
|
|
padded_ident[..ident.len()].copy_from_slice(ident.as_bytes());
|
|
|
|
// Since the individual buckets are quite short, a linear search is faster than a binary search.
|
|
let replacement = match kws.iter().find(|probe| {
|
|
padded_ident == <[u8; parser::node::MAX_KW_LEN]>::try_from(&probe[2..]).unwrap()
|
|
}) {
|
|
Some(replacement) => replacement,
|
|
None => return ident,
|
|
};
|
|
|
|
// SAFETY: We know that the input byte slice is pure-ASCII.
|
|
unsafe { std::str::from_utf8_unchecked(&replacement[..ident.len() + 2]) }
|
|
}
|