1#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
2#![deny(elided_lifetimes_in_paths)]
3#![deny(unreachable_pub)]
4
5mod config;
6mod generator;
7mod heritage;
8mod html;
9mod input;
10mod integration;
11#[cfg(test)]
12mod tests;
13
14use std::borrow::{Borrow, Cow};
15use std::collections::hash_map::{Entry, HashMap};
16use std::fmt;
17use std::hash::{BuildHasher, Hash};
18use std::path::Path;
19use std::sync::Mutex;
20
21use parser::{Parsed, ascii_str, strip_common};
22#[cfg(not(feature = "__standalone"))]
23use proc_macro::TokenStream as TokenStream12;
24#[cfg(feature = "__standalone")]
25use proc_macro2::TokenStream as TokenStream12;
26use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
27use quote::{quote, quote_spanned};
28use rustc_hash::FxBuildHasher;
29
30use crate::config::{Config, read_config_file};
31use crate::generator::{TmplKind, template_to_string};
32use crate::heritage::{Context, Heritage};
33use crate::input::{AnyTemplateArgs, Print, TemplateArgs, TemplateInput};
34use crate::integration::{Buffer, build_template_enum};
35
36#[allow(clippy::useless_conversion)] #[cfg_attr(
200 not(feature = "__standalone"),
201 proc_macro_derive(Template, attributes(template))
202)]
203#[must_use]
204pub fn derive_template(input: TokenStream12) -> TokenStream12 {
205 let ast = match syn::parse2(input.into()) {
206 Ok(ast) => ast,
207 Err(err) => {
208 let msgs = err.into_iter().map(|err| err.to_string());
209 let ts = quote! {
210 const _: () = {
211 extern crate core;
212 #(core::compile_error!(#msgs);)*
213 };
214 };
215 return ts.into();
216 }
217 };
218
219 let mut buf = Buffer::new();
220 let mut args = AnyTemplateArgs::new(&ast);
221 let crate_name = args
222 .as_mut()
223 .map(|a| a.take_crate_name())
224 .unwrap_or_default();
225
226 let result = args.and_then(|args| build_template(&mut buf, &ast, args));
227 let ts = if let Err(CompileError { msg, span }) = result {
228 let mut ts = quote_spanned! {
229 span.unwrap_or(ast.ident.span()) =>
230 askama::helpers::core::compile_error!(#msg);
231 };
232 buf.clear();
233 if build_skeleton(&mut buf, &ast).is_ok() {
234 let source: TokenStream = buf.into_string().parse().unwrap();
235 ts.extend(source);
236 }
237 ts
238 } else {
239 buf.into_string().parse().unwrap()
240 };
241
242 let ts = TokenTree::Group(Group::new(Delimiter::None, ts));
243 let ts = if let Some(crate_name) = crate_name {
244 quote! {
245 const _: () = {
246 use #crate_name as askama;
247 #ts
248 };
249 }
250 } else {
251 quote! {
252 const _: () = {
253 extern crate askama;
254 #ts
255 };
256 }
257 };
258 ts.into()
259}
260
261fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result<usize, CompileError> {
262 let template_args = TemplateArgs::fallback();
263 let config = Config::new("", None, None, None, None)?;
264 let input = TemplateInput::new(ast, None, config, &template_args)?;
265 let mut contexts = HashMap::default();
266 let parsed = parser::Parsed::default();
267 contexts.insert(&input.path, Context::empty(&parsed));
268 template_to_string(buf, &input, &contexts, None, TmplKind::Struct)
269}
270
271pub(crate) fn build_template(
279 buf: &mut Buffer,
280 ast: &syn::DeriveInput,
281 args: AnyTemplateArgs,
282) -> Result<usize, CompileError> {
283 let err_span;
284 let mut result = match args {
285 AnyTemplateArgs::Struct(item) => {
286 err_span = item.source.1.or(item.template_span);
287 build_template_item(buf, ast, None, &item, TmplKind::Struct)
288 }
289 AnyTemplateArgs::Enum {
290 enum_args,
291 vars_args,
292 has_default_impl,
293 } => {
294 err_span = enum_args
295 .as_ref()
296 .and_then(|v| v.source.as_ref())
297 .map(|s| s.span())
298 .or_else(|| enum_args.as_ref().map(|v| v.template.span()));
299 build_template_enum(buf, ast, enum_args, vars_args, has_default_impl)
300 }
301 };
302 if let Err(err) = &mut result {
303 if err.span.is_none() {
304 err.span = err_span;
305 }
306 }
307 result
308}
309
310fn build_template_item(
311 buf: &mut Buffer,
312 ast: &syn::DeriveInput,
313 enum_ast: Option<&syn::DeriveInput>,
314 template_args: &TemplateArgs,
315 tmpl_kind: TmplKind<'_>,
316) -> Result<usize, CompileError> {
317 let config_path = template_args.config_path();
318 let (s, full_config_path) = read_config_file(config_path, template_args.config_span)?;
319 let config = Config::new(
320 &s,
321 config_path,
322 template_args.whitespace,
323 template_args.config_span,
324 full_config_path,
325 )?;
326 let input = TemplateInput::new(ast, enum_ast, config, template_args)?;
327
328 let mut templates = HashMap::default();
329 input.find_used_templates(&mut templates)?;
330
331 let mut contexts = HashMap::default();
332 for (path, parsed) in &templates {
333 contexts.insert(path, Context::new(input.config, path, parsed)?);
334 }
335
336 let ctx = &contexts[&input.path];
337 let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() {
338 Some(Heritage::new(ctx, &contexts))
339 } else {
340 None
341 };
342
343 if let Some((block_name, block_span)) = input.block {
344 let has_block = match &heritage {
345 Some(heritage) => heritage.blocks.contains_key(block_name),
346 None => ctx.blocks.contains_key(block_name),
347 };
348 if !has_block {
349 return Err(CompileError::no_file_info(
350 format_args!("cannot find block `{block_name}`"),
351 Some(block_span),
352 ));
353 }
354 }
355
356 if input.print == Print::Ast || input.print == Print::All {
357 eprintln!("{:?}", templates[&input.path].nodes());
358 }
359
360 let mark = buf.get_mark();
361 let size_hint = template_to_string(buf, &input, &contexts, heritage.as_ref(), tmpl_kind)?;
362 if input.print == Print::Code || input.print == Print::All {
363 eprintln!("{}", buf.marked_text(mark));
364 }
365 Ok(size_hint)
366}
367
368#[derive(Debug, Clone)]
369struct CompileError {
370 msg: String,
371 span: Option<Span>,
372}
373
374impl CompileError {
375 fn new<S: fmt::Display>(msg: S, file_info: Option<FileInfo<'_>>) -> Self {
376 Self::new_with_span(msg, file_info, None)
377 }
378
379 fn new_with_span<S: fmt::Display>(
380 msg: S,
381 file_info: Option<FileInfo<'_>>,
382 span: Option<Span>,
383 ) -> Self {
384 let msg = match file_info {
385 Some(file_info) => format!("{msg}{file_info}"),
386 None => msg.to_string(),
387 };
388 Self { msg, span }
389 }
390
391 fn no_file_info<S: ToString>(msg: S, span: Option<Span>) -> Self {
392 Self {
393 msg: msg.to_string(),
394 span,
395 }
396 }
397}
398
399impl std::error::Error for CompileError {}
400
401impl fmt::Display for CompileError {
402 #[inline]
403 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
404 fmt.write_str(&self.msg)
405 }
406}
407
408#[derive(Debug, Clone, Copy)]
409struct FileInfo<'a> {
410 path: &'a Path,
411 source: Option<&'a str>,
412 node_source: Option<&'a str>,
413}
414
415impl<'a> FileInfo<'a> {
416 fn new(path: &'a Path, source: Option<&'a str>, node_source: Option<&'a str>) -> Self {
417 Self {
418 path,
419 source,
420 node_source,
421 }
422 }
423
424 fn of(node: parser::Span<'a>, path: &'a Path, parsed: &'a Parsed) -> Self {
425 let source = parsed.source();
426 Self {
427 path,
428 source: Some(source),
429 node_source: node.as_suffix_of(source),
430 }
431 }
432}
433
434impl fmt::Display for FileInfo<'_> {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 if let (Some(source), Some(node_source)) = (self.source, self.node_source) {
437 let (error_info, file_path) = generate_error_info(source, node_source, self.path);
438 write!(
439 f,
440 "\n --> {file_path}:{row}:{column}\n{source_after}",
441 row = error_info.row,
442 column = error_info.column,
443 source_after = error_info.source_after,
444 )
445 } else {
446 write!(
447 f,
448 "\n --> {}",
449 match std::env::current_dir() {
450 Ok(cwd) => fmt_left!(move "{}", strip_common(&cwd, self.path)),
451 Err(_) => fmt_right!("{}", self.path.display()),
452 }
453 )
454 }
455 }
456}
457
458struct ErrorInfo {
459 row: usize,
460 column: usize,
461 source_after: String,
462}
463
464fn generate_row_and_column(src: &str, input: &str) -> ErrorInfo {
465 const MAX_LINE_LEN: usize = 80;
466
467 let offset = src.len() - input.len();
468 let (source_before, source_after) = src.split_at(offset);
469
470 let source_after = match source_after
471 .char_indices()
472 .enumerate()
473 .take(MAX_LINE_LEN + 1)
474 .last()
475 {
476 Some((MAX_LINE_LEN, (i, _))) => format!("{:?}...", &source_after[..i]),
477 _ => format!("{source_after:?}"),
478 };
479
480 let (row, last_line) = source_before.lines().enumerate().last().unwrap_or_default();
481 let column = last_line.chars().count();
482 ErrorInfo {
483 row: row + 1,
484 column,
485 source_after,
486 }
487}
488
489fn generate_error_info(src: &str, input: &str, file_path: &Path) -> (ErrorInfo, String) {
491 let file_path = match std::env::current_dir() {
492 Ok(cwd) => strip_common(&cwd, file_path),
493 Err(_) => file_path.display().to_string(),
494 };
495 let error_info = generate_row_and_column(src, input);
496 (error_info, file_path)
497}
498
499struct MsgValidEscapers<'a>(&'a [(Vec<Cow<'a, str>>, Cow<'a, str>)]);
500
501impl fmt::Display for MsgValidEscapers<'_> {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 let mut exts = self
504 .0
505 .iter()
506 .flat_map(|(exts, _)| exts)
507 .map(|x| format!("{x:?}"))
508 .collect::<Vec<_>>();
509 exts.sort();
510 write!(f, "The available extensions are: {}", exts.join(", "))
511 }
512}
513
514#[derive(Debug)]
515struct OnceMap<K, V>([Mutex<HashMap<K, V, FxBuildHasher>>; 8]);
516
517impl<K, V> Default for OnceMap<K, V> {
518 fn default() -> Self {
519 Self(Default::default())
520 }
521}
522
523impl<K: Hash + Eq, V> OnceMap<K, V> {
524 fn get_or_try_insert<T, Q, E>(
527 &self,
528 key: &Q,
529 make_key_value: impl FnOnce(&Q) -> Result<(K, V), E>,
530 to_value: impl FnOnce(&V) -> T,
531 ) -> Result<T, E>
532 where
533 K: Borrow<Q>,
534 Q: Hash + Eq,
535 {
536 let shard_idx = (FxBuildHasher.hash_one(key) % self.0.len() as u64) as usize;
537 let mut shard = self.0[shard_idx].lock().unwrap();
538 Ok(to_value(if let Some(v) = shard.get(key) {
539 v
540 } else {
541 let (k, v) = make_key_value(key)?;
542 match shard.entry(k) {
543 Entry::Vacant(entry) => entry.insert(v),
544 Entry::Occupied(_) => unreachable!("key in map when it should not have been"),
545 }
546 }))
547 }
548}
549
550enum EitherFormat<L, R>
551where
552 L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
553 R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
554{
555 Left(L),
556 Right(R),
557}
558
559impl<L, R> fmt::Display for EitherFormat<L, R>
560where
561 L: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
562 R: for<'a, 'b> Fn(&'a mut fmt::Formatter<'b>) -> fmt::Result,
563{
564 #[inline]
565 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566 match self {
567 Self::Left(v) => v(f),
568 Self::Right(v) => v(f),
569 }
570 }
571}
572
573macro_rules! fmt_left {
574 (move $fmt:literal $($tt:tt)*) => {
575 $crate::EitherFormat::Left(move |f: &mut std::fmt::Formatter<'_>| {
576 write!(f, $fmt $($tt)*)
577 })
578 };
579 ($fmt:literal $($tt:tt)*) => {
580 $crate::EitherFormat::Left(|f: &mut std::fmt::Formatter<'_>| {
581 write!(f, $fmt $($tt)*)
582 })
583 };
584}
585
586macro_rules! fmt_right {
587 (move $fmt:literal $($tt:tt)*) => {
588 $crate::EitherFormat::Right(move |f: &mut std::fmt::Formatter<'_>| {
589 write!(f, $fmt $($tt)*)
590 })
591 };
592 ($fmt:literal $($tt:tt)*) => {
593 $crate::EitherFormat::Right(|f: &mut std::fmt::Formatter<'_>| {
594 write!(f, $fmt $($tt)*)
595 })
596 };
597}
598
599pub(crate) use {fmt_left, fmt_right};
600
601const BUILTIN_FILTERS: &[&str] = &[
604 "capitalize",
605 "center",
606 "indent",
607 "lower",
608 "lowercase",
609 "title",
610 "trim",
611 "truncate",
612 "upper",
613 "uppercase",
614 "wordcount",
615];
616
617const BUILTIN_FILTERS_NEED_ALLOC: &[&str] = &["center", "truncate"];