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;
10#[cfg(test)]
11mod tests;
12
13use std::borrow::{Borrow, Cow};
14use std::collections::hash_map::{Entry, HashMap};
15use std::fmt;
16use std::hash::{BuildHasher, Hash};
17use std::path::Path;
18use std::sync::Mutex;
19
20use config::{Config, read_config_file};
21use generator::{Generator, MapChain};
22use heritage::{Context, Heritage};
23use input::{Print, TemplateArgs, TemplateInput};
24use parser::{Parsed, WithSpan, strip_common};
25#[cfg(not(feature = "__standalone"))]
26use proc_macro::TokenStream as TokenStream12;
27#[cfg(feature = "__standalone")]
28use proc_macro2::TokenStream as TokenStream12;
29use proc_macro2::{Span, TokenStream};
30use quote::quote_spanned;
31use rustc_hash::FxBuildHasher;
32
33/// The `Template` derive macro and its `template()` attribute.
34///
35/// Rinja works by generating one or more trait implementations for any
36/// `struct` type decorated with the `#[derive(Template)]` attribute. The
37/// code generation process takes some options that can be specified through
38/// the `template()` attribute.
39///
40/// ## Attributes
41///
42/// The following sub-attributes are currently recognized:
43///
44/// ### path
45///
46/// E.g. `path = "foo.html"`
47///
48/// Sets the path to the template file.
49/// The path is interpreted as relative to the configured template directories
50/// (by default, this is a `templates` directory next to your `Cargo.toml`).
51/// The file name extension is used to infer an escape mode (see below). In
52/// web framework integrations, the path's extension may also be used to
53/// infer the content type of the resulting response.
54/// Cannot be used together with `source`.
55///
56/// ### source
57///
58/// E.g. `source = "{{ foo }}"`
59///
60/// Directly sets the template source.
61/// This can be useful for test cases or short templates. The generated path
62/// is undefined, which generally makes it impossible to refer to this
63/// template from other templates. If `source` is specified, `ext` must also
64/// be specified (see below). Cannot be used together with `path`.
65/// `ext` (e.g. `ext = "txt"`): lets you specify the content type as a file
66/// extension. This is used to infer an escape mode (see below), and some
67/// web framework integrations use it to determine the content type.
68/// Cannot be used together with `path`.
69///
70/// ### `in_doc`
71///
72/// E.g. `in_doc = true`
73///
74/// As an alternative to supplying the code template code in an external file (as `path` argument),
75/// or as a string (as `source` argument), you can also enable the `"code-in-doc"` feature.
76/// With this feature, you can specify the template code directly in the documentation
77/// of the template `struct`.
78///
79/// Instead of `path = "…"` or `source = "…"`, specify `in_doc = true` in the `#[template]`
80/// attribute, and in the struct's documentation add a `rinja` code block:
81///
82/// ```rust,ignore
83/// /// ```rinja
84/// /// <div>{{ lines|linebreaksbr }}</div>
85/// /// ```
86/// #[derive(Template)]
87/// #[template(ext = "html", in_doc = true)]
88/// struct Example<'a> {
89/// lines: &'a str,
90/// }
91/// ```
92///
93/// ### print
94///
95/// E.g. `print = "code"`
96///
97/// Enable debugging by printing nothing (`none`), the parsed syntax tree (`ast`),
98/// the generated code (`code`) or `all` for both.
99/// The requested data will be printed to stdout at compile time.
100///
101/// ### escape
102///
103/// E.g. `escape = "none"`
104///
105/// Override the template's extension used for the purpose of determining the escaper for
106/// this template. See the section on configuring custom escapers for more information.
107///
108/// ### syntax
109///
110/// E.g. `syntax = "foo"`
111///
112/// Set the syntax name for a parser defined in the configuration file.
113/// The default syntax, `"default"`, is the one provided by Rinja.
114#[allow(clippy::useless_conversion)] // To be compatible with both `TokenStream`s
115#[cfg_attr(
116 not(feature = "__standalone"),
117 proc_macro_derive(Template, attributes(template))
118)]
119#[must_use]
120pub fn derive_template(input: TokenStream12) -> TokenStream12 {
121 let ast: DeriveInput = match syn::parse2(tokens:input.into()) {
122 Ok(ast: DeriveInput) => ast,
123 Err(err: Error) => {
124 let msgs: impl Iterator = err.into_iter().map(|err: Error| err.to_string());
125 return compile_error(msgs, Span::call_site()).into();
126 }
127 };
128 match build_template(&ast) {
129 Ok(source: String) => source.parse().unwrap(),
130 Err(CompileError {
131 msg: String,
132 span: Option,
133 rendered: _rendered: bool,
134 }) => {
135 let mut ts: TokenStream = compile_error(msgs:std::iter::once(msg), span.unwrap_or(default:ast.ident.span()));
136 if let Ok(source: String) = build_skeleton(&ast) {
137 let source: TokenStream = source.parse().unwrap();
138 ts.extend(iter:source);
139 }
140 ts.into()
141 }
142 }
143}
144
145fn compile_error(msgs: impl Iterator<Item = String>, span: Span) -> TokenStream {
146 let crate_: Ident = syn::Ident::new(CRATE, Span::call_site());
147 quote_spanned! {
148 span =>
149 const _: () = {
150 extern crate #crate_ as rinja;
151 #(rinja::helpers::core::compile_error!(#msgs);)*
152 };
153 }
154}
155
156fn build_skeleton(ast: &syn::DeriveInput) -> Result<String, CompileError> {
157 let template_args: TemplateArgs = TemplateArgs::fallback();
158 let config: &'static Config = Config::new(source:"", config_path:None, template_whitespace:None, config_span:None)?;
159 let input: TemplateInput<'_> = TemplateInput::new(ast, config, &template_args)?;
160 let mut contexts: HashMap<&Arc, Context<'_>, …> = HashMap::default();
161 let parsed: Parsed = parser::Parsed::default();
162 contexts.insert(&input.path, v:Context::empty(&parsed));
163 Generator::new(
164 &input,
165 &contexts,
166 None,
167 MapChain::default(),
168 input.block.is_some(),
169 0,
170 )
171 .build(&contexts[&input.path])
172}
173
174/// Takes a `syn::DeriveInput` and generates source code for it
175///
176/// Reads the metadata from the `template()` attribute to get the template
177/// metadata, then fetches the source from the filesystem. The source is
178/// parsed, and the parse tree is fed to the code generator. Will print
179/// the parse tree and/or generated source according to the `print` key's
180/// value as passed to the `template()` attribute.
181pub(crate) fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileError> {
182 let template_args: TemplateArgs = TemplateArgs::new(ast)?;
183 let mut result: Result = build_template_inner(ast, &template_args);
184 if let Err(err: &mut CompileError) = &mut result {
185 if err.span.is_none() {
186 err.span = template_args
187 .source
188 .as_ref()
189 .and_then(|(_, span)| *span)
190 .or(optb:template_args.template_span);
191 }
192 }
193 result
194}
195
196fn build_template_inner(
197 ast: &syn::DeriveInput,
198 template_args: &TemplateArgs,
199) -> Result<String, CompileError> {
200 let config_path = template_args.config_path();
201 let s = read_config_file(config_path, template_args.config_span)?;
202 let config = Config::new(
203 &s,
204 config_path,
205 template_args.whitespace.as_deref(),
206 template_args.config_span,
207 )?;
208 let input = TemplateInput::new(ast, config, template_args)?;
209
210 let mut templates = HashMap::default();
211 input.find_used_templates(&mut templates)?;
212
213 let mut contexts = HashMap::default();
214 for (path, parsed) in &templates {
215 contexts.insert(path, Context::new(input.config, path, parsed)?);
216 }
217
218 let ctx = &contexts[&input.path];
219 let heritage = if !ctx.blocks.is_empty() || ctx.extends.is_some() {
220 let heritage = Heritage::new(ctx, &contexts);
221
222 if let Some(block_name) = input.block {
223 if !heritage.blocks.contains_key(&block_name) {
224 return Err(CompileError::no_file_info(
225 format!("cannot find block {block_name}"),
226 None,
227 ));
228 }
229 }
230
231 Some(heritage)
232 } else {
233 None
234 };
235
236 if input.print == Print::Ast || input.print == Print::All {
237 eprintln!("{:?}", templates[&input.path].nodes());
238 }
239
240 let code = Generator::new(
241 &input,
242 &contexts,
243 heritage.as_ref(),
244 MapChain::default(),
245 input.block.is_some(),
246 0,
247 )
248 .build(&contexts[&input.path])?;
249 if input.print == Print::Code || input.print == Print::All {
250 eprintln!("{code}");
251 }
252 Ok(code)
253}
254
255#[derive(Debug, Clone)]
256struct CompileError {
257 msg: String,
258 span: Option<Span>,
259 rendered: bool,
260}
261
262impl CompileError {
263 fn new<S: fmt::Display>(msg: S, file_info: Option<FileInfo<'_>>) -> Self {
264 Self::new_with_span(msg, file_info, None)
265 }
266
267 fn new_with_span<S: fmt::Display>(
268 msg: S,
269 file_info: Option<FileInfo<'_>>,
270 span: Option<Span>,
271 ) -> Self {
272 let msg = match file_info {
273 Some(file_info) => format!("{msg}{file_info}"),
274 None => msg.to_string(),
275 };
276 Self {
277 msg,
278 span,
279 rendered: false,
280 }
281 }
282
283 fn no_file_info<S: fmt::Display>(msg: S, span: Option<Span>) -> Self {
284 Self {
285 msg: msg.to_string(),
286 span,
287 rendered: false,
288 }
289 }
290}
291
292impl std::error::Error for CompileError {}
293
294impl fmt::Display for CompileError {
295 #[inline]
296 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
297 fmt.write_str(&self.msg)
298 }
299}
300
301#[derive(Debug, Clone, Copy)]
302struct FileInfo<'a> {
303 path: &'a Path,
304 source: Option<&'a str>,
305 node_source: Option<&'a str>,
306}
307
308impl<'a> FileInfo<'a> {
309 fn new(path: &'a Path, source: Option<&'a str>, node_source: Option<&'a str>) -> Self {
310 Self {
311 path,
312 source,
313 node_source,
314 }
315 }
316
317 fn of<T>(node: &WithSpan<'a, T>, path: &'a Path, parsed: &'a Parsed) -> Self {
318 Self {
319 path,
320 source: Some(parsed.source()),
321 node_source: Some(node.span()),
322 }
323 }
324}
325
326impl fmt::Display for FileInfo<'_> {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 if let (Some(source: &str), Some(node_source: &str)) = (self.source, self.node_source) {
329 let (error_info: ErrorInfo, file_path: String) = generate_error_info(src:source, input:node_source, self.path);
330 write!(
331 f,
332 "\n --> {file_path}:{row}:{column}\n{source_after}",
333 row = error_info.row,
334 column = error_info.column,
335 source_after = &error_info.source_after,
336 )
337 } else {
338 let file_path: String = match std::env::current_dir() {
339 Ok(cwd: PathBuf) => strip_common(&cwd, self.path),
340 Err(_) => self.path.display().to_string(),
341 };
342 write!(f, "\n --> {file_path}")
343 }
344 }
345}
346
347struct ErrorInfo {
348 row: usize,
349 column: usize,
350 source_after: String,
351}
352
353fn generate_row_and_column(src: &str, input: &str) -> ErrorInfo {
354 let offset: usize = src.len() - input.len();
355 let (source_before: &str, source_after: &str) = src.split_at(mid:offset);
356
357 let source_after: String = match source_after.char_indices().enumerate().take(41).last() {
358 Some((80, (i: usize, _))) => format!("{:?}...", &source_after[..i]),
359 _ => format!("{source_after:?}"),
360 };
361
362 let (row: usize, last_line: &str) = source_before.lines().enumerate().last().unwrap_or_default();
363 let column: usize = last_line.chars().count();
364 ErrorInfo {
365 row: row + 1,
366 column,
367 source_after,
368 }
369}
370
371/// Return the error related information and its display file path.
372fn generate_error_info(src: &str, input: &str, file_path: &Path) -> (ErrorInfo, String) {
373 let file_path: String = match std::env::current_dir() {
374 Ok(cwd: PathBuf) => strip_common(&cwd, file_path),
375 Err(_) => file_path.display().to_string(),
376 };
377 let error_info: ErrorInfo = generate_row_and_column(src, input);
378 (error_info, file_path)
379}
380
381struct MsgValidEscapers<'a>(&'a [(Vec<Cow<'a, str>>, Cow<'a, str>)]);
382
383impl fmt::Display for MsgValidEscapers<'_> {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 let mut exts: Vec = self
386 .0
387 .iter()
388 .flat_map(|(exts: &Vec>, _)| exts)
389 .map(|x: &Cow<'_, str>| format!("{x:?}"))
390 .collect::<Vec<_>>();
391 exts.sort();
392 write!(f, "The available extensions are: {}", exts.join(", "))
393 }
394}
395
396#[derive(Debug)]
397struct OnceMap<K, V>([Mutex<HashMap<K, V, FxBuildHasher>>; 8]);
398
399impl<K, V> Default for OnceMap<K, V> {
400 fn default() -> Self {
401 Self(Default::default())
402 }
403}
404
405impl<K: Hash + Eq, V> OnceMap<K, V> {
406 // The API of this function was copied, and adapted from the `once_map` crate
407 // <https://crates.io/crates/once_map/0.4.18>.
408 fn get_or_try_insert<T, Q, E>(
409 &self,
410 key: &Q,
411 make_key_value: impl FnOnce(&Q) -> Result<(K, V), E>,
412 to_value: impl FnOnce(&V) -> T,
413 ) -> Result<T, E>
414 where
415 K: Borrow<Q>,
416 Q: Hash + Eq,
417 {
418 let shard_idx = (FxBuildHasher.hash_one(key) % self.0.len() as u64) as usize;
419 let mut shard = self.0[shard_idx].lock().unwrap();
420 Ok(to_value(if let Some(v) = shard.get(key) {
421 v
422 } else {
423 let (k, v) = make_key_value(key)?;
424 match shard.entry(k) {
425 Entry::Vacant(entry) => entry.insert(v),
426 Entry::Occupied(_) => unreachable!("key in map when it should not have been"),
427 }
428 }))
429 }
430}
431
432// This is used by the code generator to decide whether a named filter is part of
433// Rinja or should refer to a local `filters` module.
434const BUILT_IN_FILTERS: &[&str] = &[
435 "capitalize",
436 "center",
437 "indent",
438 "lower",
439 "lowercase",
440 "title",
441 "trim",
442 "truncate",
443 "upper",
444 "urlencode",
445 "wordcount",
446];
447
448const CRATE: &str = if cfg!(feature = "with-actix-web") {
449 "rinja_actix"
450} else if cfg!(feature = "with-axum") {
451 "rinja_axum"
452} else if cfg!(feature = "with-rocket") {
453 "rinja_rocket"
454} else if cfg!(feature = "with-warp") {
455 "rinja_warp"
456} else {
457 "rinja"
458};
459