1 | use std::borrow::Cow; |
2 | use std::collections::hash_map::{Entry, HashMap}; |
3 | use std::fs::read_to_string; |
4 | use std::iter::FusedIterator; |
5 | use std::path::{Path, PathBuf}; |
6 | use std::sync::{Arc, OnceLock}; |
7 | |
8 | use mime::Mime; |
9 | use parser::{Node, Parsed}; |
10 | use proc_macro2::Span; |
11 | use rustc_hash::FxBuildHasher; |
12 | use syn::punctuated::Punctuated; |
13 | use syn::spanned::Spanned; |
14 | |
15 | use crate::config::{Config, SyntaxAndCache}; |
16 | use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap}; |
17 | |
18 | pub(crate) struct TemplateInput<'a> { |
19 | pub(crate) ast: &'a syn::DeriveInput, |
20 | pub(crate) config: &'a Config, |
21 | pub(crate) syntax: &'a SyntaxAndCache<'a>, |
22 | pub(crate) source: &'a Source, |
23 | pub(crate) source_span: Option<Span>, |
24 | pub(crate) block: Option<&'a str>, |
25 | pub(crate) print: Print, |
26 | pub(crate) escaper: &'a str, |
27 | pub(crate) ext: Option<&'a str>, |
28 | pub(crate) mime_type: String, |
29 | pub(crate) path: Arc<Path>, |
30 | pub(crate) fields: Vec<String>, |
31 | } |
32 | |
33 | impl TemplateInput<'_> { |
34 | /// Extract the template metadata from the `DeriveInput` structure. This |
35 | /// mostly recovers the data for the `TemplateInput` fields from the |
36 | /// `template()` attribute list fields. |
37 | pub(crate) fn new<'n>( |
38 | ast: &'n syn::DeriveInput, |
39 | config: &'n Config, |
40 | args: &'n TemplateArgs, |
41 | ) -> Result<TemplateInput<'n>, CompileError> { |
42 | let TemplateArgs { |
43 | source, |
44 | block, |
45 | print, |
46 | escaping, |
47 | ext, |
48 | ext_span, |
49 | syntax, |
50 | .. |
51 | } = args; |
52 | |
53 | // Validate the `source` and `ext` value together, since they are |
54 | // related. In case `source` was used instead of `path`, the value |
55 | // of `ext` is merged into a synthetic `path` value here. |
56 | let &(ref source, source_span) = source.as_ref().ok_or_else(|| { |
57 | CompileError::new( |
58 | #[cfg (not(feature = "code-in-doc" ))] |
59 | "specify one template argument `path` or `source`" , |
60 | #[cfg (feature = "code-in-doc" )] |
61 | "specify one template argument `path`, `source` or `in_doc`" , |
62 | None, |
63 | ) |
64 | })?; |
65 | let path = match (&source, &ext) { |
66 | (Source::Path(path), _) => config.find_template(path, None, None)?, |
67 | (&Source::Source(_), Some(ext)) => { |
68 | PathBuf::from(format!(" {}. {}" , ast.ident, ext)).into() |
69 | } |
70 | (&Source::Source(_), None) => { |
71 | return Err(CompileError::no_file_info( |
72 | #[cfg (not(feature = "code-in-doc" ))] |
73 | "must include `ext` attribute when using `source` attribute" , |
74 | #[cfg (feature = "code-in-doc" )] |
75 | "must include `ext` attribute when using `source` or `in_doc` attribute" , |
76 | None, |
77 | )); |
78 | } |
79 | }; |
80 | |
81 | // Validate syntax |
82 | let syntax = syntax.as_deref().map_or_else( |
83 | || Ok(config.syntaxes.get(config.default_syntax).unwrap()), |
84 | |s| { |
85 | config.syntaxes.get(s).ok_or_else(|| { |
86 | CompileError::no_file_info(format!("syntax ` {s}` is undefined" ), None) |
87 | }) |
88 | }, |
89 | )?; |
90 | |
91 | // Match extension against defined output formats |
92 | |
93 | let escaping = escaping |
94 | .as_deref() |
95 | .or_else(|| path.extension().and_then(|s| s.to_str())) |
96 | .unwrap_or_default(); |
97 | |
98 | let escaper = config |
99 | .escapers |
100 | .iter() |
101 | .find_map(|(extensions, path)| { |
102 | extensions |
103 | .contains(&Cow::Borrowed(escaping)) |
104 | .then_some(path.as_ref()) |
105 | }) |
106 | .ok_or_else(|| { |
107 | CompileError::no_file_info( |
108 | format!( |
109 | "no escaper defined for extension ' {escaping}'. {}" , |
110 | MsgValidEscapers(&config.escapers), |
111 | ), |
112 | *ext_span, |
113 | ) |
114 | })?; |
115 | |
116 | let mime_type = |
117 | extension_to_mime_type(ext_default_to_path(ext.as_deref(), &path).unwrap_or("txt" )) |
118 | .to_string(); |
119 | |
120 | let empty_punctuated = syn::punctuated::Punctuated::new(); |
121 | let fields = match ast.data { |
122 | syn::Data::Struct(ref struct_) => { |
123 | if let syn::Fields::Named(ref fields) = &struct_.fields { |
124 | &fields.named |
125 | } else { |
126 | &empty_punctuated |
127 | } |
128 | } |
129 | syn::Data::Union(ref union_) => &union_.fields.named, |
130 | syn::Data::Enum(_) => &empty_punctuated, |
131 | } |
132 | .iter() |
133 | .map(|f| match &f.ident { |
134 | Some(ident) => ident.to_string(), |
135 | None => unreachable!("we checked that we are using a struct" ), |
136 | }) |
137 | .collect::<Vec<_>>(); |
138 | |
139 | Ok(TemplateInput { |
140 | ast, |
141 | config, |
142 | syntax, |
143 | source, |
144 | source_span, |
145 | block: block.as_deref(), |
146 | print: *print, |
147 | escaper, |
148 | ext: ext.as_deref(), |
149 | mime_type, |
150 | path, |
151 | fields, |
152 | }) |
153 | } |
154 | |
155 | pub(crate) fn find_used_templates( |
156 | &self, |
157 | map: &mut HashMap<Arc<Path>, Arc<Parsed>, FxBuildHasher>, |
158 | ) -> Result<(), CompileError> { |
159 | let (source, source_path) = match &self.source { |
160 | Source::Source(s) => (s.clone(), None), |
161 | Source::Path(_) => ( |
162 | get_template_source(&self.path, None)?, |
163 | Some(Arc::clone(&self.path)), |
164 | ), |
165 | }; |
166 | |
167 | let mut dependency_graph = Vec::new(); |
168 | let mut check = vec![(Arc::clone(&self.path), source, source_path)]; |
169 | while let Some((path, source, source_path)) = check.pop() { |
170 | let parsed = match self.syntax.parse(Arc::clone(&source), source_path) { |
171 | Ok(parsed) => parsed, |
172 | Err(err) => { |
173 | let msg = err |
174 | .message |
175 | .unwrap_or_else(|| "failed to parse template source" .into()); |
176 | let file_path = err |
177 | .file_path |
178 | .as_deref() |
179 | .unwrap_or(Path::new("<source attribute>" )); |
180 | let file_info = |
181 | FileInfo::new(file_path, Some(&source), Some(&source[err.offset..])); |
182 | return Err(CompileError::new(msg, Some(file_info))); |
183 | } |
184 | }; |
185 | |
186 | let mut top = true; |
187 | let mut nested = vec![parsed.nodes()]; |
188 | while let Some(nodes) = nested.pop() { |
189 | for n in nodes { |
190 | let mut add_to_check = |new_path: Arc<Path>| -> Result<(), CompileError> { |
191 | if let Entry::Vacant(e) = map.entry(new_path) { |
192 | // Add a dummy entry to `map` in order to prevent adding `path` |
193 | // multiple times to `check`. |
194 | let new_path = e.key(); |
195 | let source = get_template_source( |
196 | new_path, |
197 | Some((&path, parsed.source(), n.span())), |
198 | )?; |
199 | check.push((new_path.clone(), source, Some(new_path.clone()))); |
200 | e.insert(Arc::default()); |
201 | } |
202 | Ok(()) |
203 | }; |
204 | |
205 | match n { |
206 | Node::Extends(extends) if top => { |
207 | let extends = self.config.find_template( |
208 | extends.path, |
209 | Some(&path), |
210 | Some(FileInfo::of(extends, &path, &parsed)), |
211 | )?; |
212 | let dependency_path = (path.clone(), extends.clone()); |
213 | if path == extends { |
214 | // We add the path into the graph to have a better looking error. |
215 | dependency_graph.push(dependency_path); |
216 | return cyclic_graph_error(&dependency_graph); |
217 | } else if dependency_graph.contains(&dependency_path) { |
218 | return cyclic_graph_error(&dependency_graph); |
219 | } |
220 | dependency_graph.push(dependency_path); |
221 | add_to_check(extends)?; |
222 | } |
223 | Node::Macro(m) if top => { |
224 | nested.push(&m.nodes); |
225 | } |
226 | Node::Import(import) if top => { |
227 | let import = self.config.find_template( |
228 | import.path, |
229 | Some(&path), |
230 | Some(FileInfo::of(import, &path, &parsed)), |
231 | )?; |
232 | add_to_check(import)?; |
233 | } |
234 | Node::FilterBlock(f) => { |
235 | nested.push(&f.nodes); |
236 | } |
237 | Node::Include(include) => { |
238 | let include = self.config.find_template( |
239 | include.path, |
240 | Some(&path), |
241 | Some(FileInfo::of(include, &path, &parsed)), |
242 | )?; |
243 | add_to_check(include)?; |
244 | } |
245 | Node::BlockDef(b) => { |
246 | nested.push(&b.nodes); |
247 | } |
248 | Node::If(i) => { |
249 | for cond in &i.branches { |
250 | nested.push(&cond.nodes); |
251 | } |
252 | } |
253 | Node::Loop(l) => { |
254 | nested.push(&l.body); |
255 | nested.push(&l.else_nodes); |
256 | } |
257 | Node::Match(m) => { |
258 | for arm in &m.arms { |
259 | nested.push(&arm.nodes); |
260 | } |
261 | } |
262 | Node::Lit(_) |
263 | | Node::Comment(_) |
264 | | Node::Expr(_, _) |
265 | | Node::Call(_) |
266 | | Node::Extends(_) |
267 | | Node::Let(_) |
268 | | Node::Import(_) |
269 | | Node::Macro(_) |
270 | | Node::Raw(_) |
271 | | Node::Continue(_) |
272 | | Node::Break(_) => {} |
273 | } |
274 | } |
275 | top = false; |
276 | } |
277 | map.insert(path, parsed); |
278 | } |
279 | Ok(()) |
280 | } |
281 | |
282 | #[inline ] |
283 | pub(crate) fn extension(&self) -> Option<&str> { |
284 | ext_default_to_path(self.ext, &self.path) |
285 | } |
286 | } |
287 | |
288 | #[derive (Debug, Default)] |
289 | pub(crate) struct TemplateArgs { |
290 | pub(crate) source: Option<(Source, Option<Span>)>, |
291 | block: Option<String>, |
292 | print: Print, |
293 | escaping: Option<String>, |
294 | ext: Option<String>, |
295 | ext_span: Option<Span>, |
296 | syntax: Option<String>, |
297 | config: Option<String>, |
298 | pub(crate) whitespace: Option<String>, |
299 | pub(crate) template_span: Option<Span>, |
300 | pub(crate) config_span: Option<Span>, |
301 | } |
302 | |
303 | impl TemplateArgs { |
304 | pub(crate) fn new(ast: &syn::DeriveInput) -> Result<Self, CompileError> { |
305 | // Check that an attribute called `template()` exists at least once and that it is |
306 | // the proper type (list). |
307 | |
308 | let mut templates_attrs = ast |
309 | .attrs |
310 | .iter() |
311 | .filter(|attr| attr.path().is_ident("template" )) |
312 | .peekable(); |
313 | let mut args = match templates_attrs.peek() { |
314 | Some(attr) => Self { |
315 | template_span: Some(attr.path().span()), |
316 | ..Self::default() |
317 | }, |
318 | None => { |
319 | return Err(CompileError::no_file_info( |
320 | "no attribute `template` found" , |
321 | None, |
322 | )); |
323 | } |
324 | }; |
325 | let attrs = templates_attrs |
326 | .map(|attr| { |
327 | type Attrs = Punctuated<syn::Meta, syn::Token![,]>; |
328 | match attr.parse_args_with(Attrs::parse_terminated) { |
329 | Ok(args) => Ok(args), |
330 | Err(e) => Err(CompileError::no_file_info( |
331 | format!("unable to parse template arguments: {e}" ), |
332 | Some(attr.path().span()), |
333 | )), |
334 | } |
335 | }) |
336 | .flat_map(ResultIter::from); |
337 | |
338 | // Loop over the meta attributes and find everything that we |
339 | // understand. Return a CompileError if something is not right. |
340 | // `source` contains an enum that can represent `path` or `source`. |
341 | for item in attrs { |
342 | let pair = match item? { |
343 | syn::Meta::NameValue(pair) => pair, |
344 | v => { |
345 | return Err(CompileError::no_file_info( |
346 | "unsupported attribute argument" , |
347 | Some(v.span()), |
348 | )); |
349 | } |
350 | }; |
351 | |
352 | let ident = match pair.path.get_ident() { |
353 | Some(ident) => ident, |
354 | None => unreachable!("not possible in syn::Meta::NameValue(…)" ), |
355 | }; |
356 | |
357 | let mut value_expr = &pair.value; |
358 | let value = loop { |
359 | match value_expr { |
360 | syn::Expr::Lit(lit) => break lit, |
361 | syn::Expr::Group(group) => value_expr = &group.expr, |
362 | v => { |
363 | return Err(CompileError::no_file_info( |
364 | format!("unsupported argument value type for ` {ident}`" ), |
365 | Some(v.span()), |
366 | )); |
367 | } |
368 | } |
369 | }; |
370 | |
371 | if ident == "path" { |
372 | source_or_path(ident, value, &mut args.source, Source::Path)?; |
373 | args.ext_span = Some(value.span()); |
374 | } else if ident == "source" { |
375 | source_or_path(ident, value, &mut args.source, |s| Source::Source(s.into()))?; |
376 | } else if ident == "in_doc" { |
377 | source_from_docs(ident, value, &mut args.source, ast)?; |
378 | } else if ident == "block" { |
379 | set_template_str_attr(ident, value, &mut args.block)?; |
380 | } else if ident == "print" { |
381 | if let syn::Lit::Str(s) = &value.lit { |
382 | args.print = match s.value().as_str() { |
383 | "all" => Print::All, |
384 | "ast" => Print::Ast, |
385 | "code" => Print::Code, |
386 | "none" => Print::None, |
387 | v => { |
388 | return Err(CompileError::no_file_info( |
389 | format!("invalid value for `print` option: {v}" ), |
390 | Some(s.span()), |
391 | )); |
392 | } |
393 | }; |
394 | } else { |
395 | return Err(CompileError::no_file_info( |
396 | "`print` value must be string literal" , |
397 | Some(value.lit.span()), |
398 | )); |
399 | } |
400 | } else if ident == "escape" { |
401 | set_template_str_attr(ident, value, &mut args.escaping)?; |
402 | } else if ident == "ext" { |
403 | set_template_str_attr(ident, value, &mut args.ext)?; |
404 | args.ext_span = Some(value.span()); |
405 | } else if ident == "syntax" { |
406 | set_template_str_attr(ident, value, &mut args.syntax)?; |
407 | } else if ident == "config" { |
408 | set_template_str_attr(ident, value, &mut args.config)?; |
409 | args.config_span = Some(value.span()); |
410 | } else if ident == "whitespace" { |
411 | set_template_str_attr(ident, value, &mut args.whitespace)?; |
412 | } else { |
413 | return Err(CompileError::no_file_info( |
414 | format!("unsupported attribute key ` {ident}` found" ), |
415 | Some(ident.span()), |
416 | )); |
417 | } |
418 | } |
419 | |
420 | Ok(args) |
421 | } |
422 | |
423 | pub(crate) fn fallback() -> Self { |
424 | Self { |
425 | source: Some((Source::Source("" .into()), None)), |
426 | ext: Some("txt" .to_string()), |
427 | ..Self::default() |
428 | } |
429 | } |
430 | |
431 | pub(crate) fn config_path(&self) -> Option<&str> { |
432 | self.config.as_deref() |
433 | } |
434 | } |
435 | |
436 | /// Try to find the source in the comment, in a `rinja` code block. |
437 | /// |
438 | /// This is only done if no path or source was given in the `#[template]` attribute. |
439 | fn source_from_docs( |
440 | name: &syn::Ident, |
441 | value: &syn::ExprLit, |
442 | dest: &mut Option<(Source, Option<Span>)>, |
443 | ast: &syn::DeriveInput, |
444 | ) -> Result<(), CompileError> { |
445 | match &value.lit { |
446 | syn::Lit::Bool(syn::LitBool { value, .. }) => { |
447 | if !value { |
448 | return Ok(()); |
449 | } |
450 | } |
451 | lit => { |
452 | return Err(CompileError::no_file_info( |
453 | "argument `in_doc` expects as boolean value" , |
454 | Some(lit.span()), |
455 | )); |
456 | } |
457 | }; |
458 | #[cfg (not(feature = "code-in-doc" ))] |
459 | { |
460 | let _ = (name, dest, ast); |
461 | Err(CompileError::no_file_info( |
462 | "enable feature `code-in-doc` to use `in_doc` argument" , |
463 | Some(name.span()), |
464 | )) |
465 | } |
466 | #[cfg (feature = "code-in-doc" )] |
467 | { |
468 | ensure_source_once(name, dest)?; |
469 | let (span, source) = collect_comment_blocks(name, ast)?; |
470 | let source = strip_common_ws_prefix(source); |
471 | let source = collect_rinja_code_blocks(name, ast, source)?; |
472 | *dest = Some((source, span)); |
473 | Ok(()) |
474 | } |
475 | } |
476 | |
477 | #[cfg (feature = "code-in-doc" )] |
478 | fn collect_comment_blocks( |
479 | name: &syn::Ident, |
480 | ast: &syn::DeriveInput, |
481 | ) -> Result<(Option<Span>, String), CompileError> { |
482 | let mut span: Option<Span> = None; |
483 | let mut assign_span = |kv: &syn::MetaNameValue| { |
484 | // FIXME: uncomment once <https://github.com/rust-lang/rust/issues/54725> is stable |
485 | // let new_span = kv.path.span(); |
486 | // span = Some(match span { |
487 | // Some(cur_span) => cur_span.join(new_span).unwrap_or(cur_span), |
488 | // None => new_span, |
489 | // }); |
490 | |
491 | if span.is_none() { |
492 | span = Some(kv.path.span()); |
493 | } |
494 | }; |
495 | |
496 | let mut source = String::new(); |
497 | for a in &ast.attrs { |
498 | // is a comment? |
499 | let syn::Meta::NameValue(kv) = &a.meta else { |
500 | continue; |
501 | }; |
502 | if !kv.path.is_ident("doc" ) { |
503 | continue; |
504 | } |
505 | |
506 | // is an understood comment, e.g. not `#[doc = inline_str(…)]` |
507 | let mut value = &kv.value; |
508 | let value = loop { |
509 | match value { |
510 | syn::Expr::Lit(lit) => break lit, |
511 | syn::Expr::Group(group) => value = &group.expr, |
512 | _ => continue, |
513 | } |
514 | }; |
515 | let syn::Lit::Str(value) = &value.lit else { |
516 | continue; |
517 | }; |
518 | |
519 | assign_span(kv); |
520 | source.push_str(value.value().as_str()); |
521 | source.push(' \n' ); |
522 | } |
523 | if source.is_empty() { |
524 | return Err(no_rinja_code_block(name, ast)); |
525 | } |
526 | |
527 | Ok((span, source)) |
528 | } |
529 | |
530 | #[cfg (feature = "code-in-doc" )] |
531 | fn no_rinja_code_block(name: &syn::Ident, ast: &syn::DeriveInput) -> CompileError { |
532 | let kind = match &ast.data { |
533 | syn::Data::Struct(_) => "struct" , |
534 | syn::Data::Enum(_) => "enum" , |
535 | syn::Data::Union(_) => "union" , |
536 | }; |
537 | CompileError::no_file_info( |
538 | format!( |
539 | "when using `in_doc = true`, the {kind}'s documentation needs a `rinja` code block" |
540 | ), |
541 | Some(name.span()), |
542 | ) |
543 | } |
544 | |
545 | #[cfg (feature = "code-in-doc" )] |
546 | fn strip_common_ws_prefix(source: String) -> String { |
547 | let mut common_prefix_iter = source |
548 | .lines() |
549 | .filter_map(|s| Some(&s[..s.find(|c: char| !c.is_ascii_whitespace())?])); |
550 | let mut common_prefix = common_prefix_iter.next().unwrap_or_default(); |
551 | for p in common_prefix_iter { |
552 | if common_prefix.is_empty() { |
553 | break; |
554 | } |
555 | let ((pos, _), _) = common_prefix |
556 | .char_indices() |
557 | .zip(p.char_indices()) |
558 | .take_while(|(l, r)| l == r) |
559 | .last() |
560 | .unwrap_or_default(); |
561 | common_prefix = &common_prefix[..pos]; |
562 | } |
563 | if common_prefix.is_empty() { |
564 | return source; |
565 | } |
566 | |
567 | source |
568 | .lines() |
569 | .flat_map(|s| [s.get(common_prefix.len()..).unwrap_or_default(), " \n" ]) |
570 | .collect() |
571 | } |
572 | |
573 | #[cfg (feature = "code-in-doc" )] |
574 | fn collect_rinja_code_blocks( |
575 | name: &syn::Ident, |
576 | ast: &syn::DeriveInput, |
577 | source: String, |
578 | ) -> Result<Source, CompileError> { |
579 | use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd}; |
580 | |
581 | let mut tmpl_source = String::new(); |
582 | let mut in_rinja_code = false; |
583 | let mut had_rinja_code = false; |
584 | for e in Parser::new(&source) { |
585 | match (in_rinja_code, e) { |
586 | (false, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(s)))) => { |
587 | if s.split("," ).any(|s| JINJA_EXTENSIONS.contains(&s)) { |
588 | in_rinja_code = true; |
589 | had_rinja_code = true; |
590 | } |
591 | } |
592 | (true, Event::End(TagEnd::CodeBlock)) => in_rinja_code = false, |
593 | (true, Event::Text(text)) => tmpl_source.push_str(&text), |
594 | _ => {} |
595 | } |
596 | } |
597 | if !had_rinja_code { |
598 | return Err(no_rinja_code_block(name, ast)); |
599 | } |
600 | |
601 | if tmpl_source.ends_with(' \n' ) { |
602 | tmpl_source.pop(); |
603 | } |
604 | Ok(Source::Source(tmpl_source.into())) |
605 | } |
606 | |
607 | struct ResultIter<I, E>(Result<I, Option<E>>); |
608 | |
609 | impl<I: IntoIterator, E> From<Result<I, E>> for ResultIter<I::IntoIter, E> { |
610 | fn from(value: Result<I, E>) -> Self { |
611 | Self(match value { |
612 | Ok(i: I) => Ok(i.into_iter()), |
613 | Err(e: E) => Err(Some(e)), |
614 | }) |
615 | } |
616 | } |
617 | |
618 | impl<I: Iterator, E> Iterator for ResultIter<I, E> { |
619 | type Item = Result<I::Item, E>; |
620 | |
621 | fn next(&mut self) -> Option<Self::Item> { |
622 | match &mut self.0 { |
623 | Ok(iter: &mut I) => Some(Ok(iter.next()?)), |
624 | Err(err: &mut Option) => Some(Err(err.take()?)), |
625 | } |
626 | } |
627 | } |
628 | |
629 | impl<I: FusedIterator, E> FusedIterator for ResultIter<I, E> {} |
630 | |
631 | fn source_or_path( |
632 | name: &syn::Ident, |
633 | value: &syn::ExprLit, |
634 | dest: &mut Option<(Source, Option<Span>)>, |
635 | ctor: fn(String) -> Source, |
636 | ) -> Result<(), CompileError> { |
637 | ensure_source_once(name, source:dest)?; |
638 | if let syn::Lit::Str(s: &LitStr) = &value.lit { |
639 | *dest = Some((ctor(s.value()), Some(value.span()))); |
640 | Ok(()) |
641 | } else { |
642 | Err(CompileError::no_file_info( |
643 | msg:format!("` {name}` value must be string literal" ), |
644 | span:Some(value.lit.span()), |
645 | )) |
646 | } |
647 | } |
648 | |
649 | fn ensure_source_once( |
650 | name: &syn::Ident, |
651 | source: &mut Option<(Source, Option<Span>)>, |
652 | ) -> Result<(), CompileError> { |
653 | if source.is_none() { |
654 | Ok(()) |
655 | } else { |
656 | Err(CompileError::no_file_info( |
657 | #[cfg (feature = "code-in-doc" )] |
658 | "must specify `source`, `path` or `is_doc` exactly once" , |
659 | #[cfg (not(feature = "code-in-doc" ))] |
660 | "must specify `source` or `path` exactly once" , |
661 | Some(name.span()), |
662 | )) |
663 | } |
664 | } |
665 | |
666 | fn set_template_str_attr( |
667 | name: &syn::Ident, |
668 | value: &syn::ExprLit, |
669 | dest: &mut Option<String>, |
670 | ) -> Result<(), CompileError> { |
671 | if dest.is_some() { |
672 | Err(CompileError::no_file_info( |
673 | msg:format!("attribute ` {name}` already set" ), |
674 | span:Some(name.span()), |
675 | )) |
676 | } else if let syn::Lit::Str(s: &LitStr) = &value.lit { |
677 | *dest = Some(s.value()); |
678 | Ok(()) |
679 | } else { |
680 | Err(CompileError::no_file_info( |
681 | msg:format!("` {name}` value must be string literal" ), |
682 | span:Some(value.lit.span()), |
683 | )) |
684 | } |
685 | } |
686 | |
687 | #[inline ] |
688 | fn ext_default_to_path<'a>(ext: Option<&'a str>, path: &'a Path) -> Option<&'a str> { |
689 | ext.or_else(|| extension(path)) |
690 | } |
691 | |
692 | fn extension(path: &Path) -> Option<&str> { |
693 | let ext: &str = path.extension()?.to_str()?; |
694 | if JINJA_EXTENSIONS.contains(&ext) { |
695 | // an extension was found: file stem cannot be absent |
696 | Path::new(path.file_stem().unwrap()) |
697 | .extension() |
698 | .and_then(|s| s.to_str()) |
699 | .or(optb:Some(ext)) |
700 | } else { |
701 | Some(ext) |
702 | } |
703 | } |
704 | |
705 | #[derive (Debug, Hash, PartialEq)] |
706 | pub(crate) enum Source { |
707 | Path(String), |
708 | Source(Arc<str>), |
709 | } |
710 | |
711 | #[derive (Clone, Copy, Debug, PartialEq, Hash)] |
712 | pub(crate) enum Print { |
713 | All, |
714 | Ast, |
715 | Code, |
716 | None, |
717 | } |
718 | |
719 | impl Default for Print { |
720 | fn default() -> Self { |
721 | Self::None |
722 | } |
723 | } |
724 | |
725 | pub(crate) fn extension_to_mime_type(ext: &str) -> Mime { |
726 | let basic_type: Mime = mime_guess::from_ext(ext).first_or_octet_stream(); |
727 | for (simple: &Mime, utf_8: &Mime) in &TEXT_TYPES { |
728 | if &basic_type == simple { |
729 | return utf_8.clone(); |
730 | } |
731 | } |
732 | basic_type |
733 | } |
734 | |
735 | const TEXT_TYPES: [(Mime, Mime); 7] = [ |
736 | (mime::TEXT_PLAIN, mime::TEXT_PLAIN_UTF_8), |
737 | (mime::TEXT_HTML, mime::TEXT_HTML_UTF_8), |
738 | (mime::TEXT_CSS, mime::TEXT_CSS_UTF_8), |
739 | (mime::TEXT_CSV, mime::TEXT_CSV_UTF_8), |
740 | ( |
741 | mime::TEXT_TAB_SEPARATED_VALUES, |
742 | mime::TEXT_TAB_SEPARATED_VALUES_UTF_8, |
743 | ), |
744 | ( |
745 | mime::APPLICATION_JAVASCRIPT, |
746 | mime::APPLICATION_JAVASCRIPT_UTF_8, |
747 | ), |
748 | (mime::IMAGE_SVG, mime::IMAGE_SVG), |
749 | ]; |
750 | |
751 | fn cyclic_graph_error(dependency_graph: &[(Arc<Path>, Arc<Path>)]) -> Result<(), CompileError> { |
752 | Err(CompileError::no_file_info( |
753 | msg:format!( |
754 | "cyclic dependency in graph {:#?}" , |
755 | dependency_graph |
756 | .iter() |
757 | .map(|e| format!(" {:#?} --> {:#?}" , e.0, e.1)) |
758 | .collect::<Vec<String>>() |
759 | ), |
760 | span:None, |
761 | )) |
762 | } |
763 | |
764 | pub(crate) fn get_template_source( |
765 | tpl_path: &Arc<Path>, |
766 | import_from: Option<(&Arc<Path>, &str, &str)>, |
767 | ) -> Result<Arc<str>, CompileError> { |
768 | static CACHE: OnceLock<OnceMap<Arc<Path>, Arc<str>>> = OnceLock::new(); |
769 | |
770 | CACHE.get_or_init(OnceMap::default).get_or_try_insert( |
771 | tpl_path, |
772 | |tpl_path| match read_to_string(tpl_path) { |
773 | Ok(mut source) => { |
774 | if source.ends_with(' \n' ) { |
775 | let _ = source.pop(); |
776 | } |
777 | Ok((Arc::clone(tpl_path), Arc::from(source))) |
778 | } |
779 | Err(err) => Err(CompileError::new( |
780 | format_args!( |
781 | "unable to open template file ' {}': {err}" , |
782 | tpl_path.to_str().unwrap(), |
783 | ), |
784 | import_from.map(|(node_file, file_source, node_source)| { |
785 | FileInfo::new(node_file, Some(file_source), Some(node_source)) |
786 | }), |
787 | )), |
788 | }, |
789 | Arc::clone, |
790 | ) |
791 | } |
792 | |
793 | const JINJA_EXTENSIONS: &[&str] = &["j2" , "jinja" , "jinja2" , "rinja" ]; |
794 | |
795 | #[cfg (test)] |
796 | mod tests { |
797 | use super::*; |
798 | |
799 | #[test ] |
800 | fn test_ext() { |
801 | assert_eq!(extension(Path::new("foo-bar.txt" )), Some("txt" )); |
802 | assert_eq!(extension(Path::new("foo-bar.html" )), Some("html" )); |
803 | assert_eq!(extension(Path::new("foo-bar.unknown" )), Some("unknown" )); |
804 | assert_eq!(extension(Path::new("foo-bar.svg" )), Some("svg" )); |
805 | |
806 | assert_eq!(extension(Path::new("foo/bar/baz.txt" )), Some("txt" )); |
807 | assert_eq!(extension(Path::new("foo/bar/baz.html" )), Some("html" )); |
808 | assert_eq!(extension(Path::new("foo/bar/baz.unknown" )), Some("unknown" )); |
809 | assert_eq!(extension(Path::new("foo/bar/baz.svg" )), Some("svg" )); |
810 | } |
811 | |
812 | #[test ] |
813 | fn test_double_ext() { |
814 | assert_eq!(extension(Path::new("foo-bar.html.txt" )), Some("txt" )); |
815 | assert_eq!(extension(Path::new("foo-bar.txt.html" )), Some("html" )); |
816 | assert_eq!(extension(Path::new("foo-bar.txt.unknown" )), Some("unknown" )); |
817 | |
818 | assert_eq!(extension(Path::new("foo/bar/baz.html.txt" )), Some("txt" )); |
819 | assert_eq!(extension(Path::new("foo/bar/baz.txt.html" )), Some("html" )); |
820 | assert_eq!( |
821 | extension(Path::new("foo/bar/baz.txt.unknown" )), |
822 | Some("unknown" ) |
823 | ); |
824 | } |
825 | |
826 | #[test ] |
827 | fn test_skip_jinja_ext() { |
828 | assert_eq!(extension(Path::new("foo-bar.html.j2" )), Some("html" )); |
829 | assert_eq!(extension(Path::new("foo-bar.html.jinja" )), Some("html" )); |
830 | assert_eq!(extension(Path::new("foo-bar.html.jinja2" )), Some("html" )); |
831 | |
832 | assert_eq!(extension(Path::new("foo/bar/baz.txt.j2" )), Some("txt" )); |
833 | assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja" )), Some("txt" )); |
834 | assert_eq!(extension(Path::new("foo/bar/baz.txt.jinja2" )), Some("txt" )); |
835 | } |
836 | |
837 | #[test ] |
838 | fn test_only_jinja_ext() { |
839 | assert_eq!(extension(Path::new("foo-bar.j2" )), Some("j2" )); |
840 | assert_eq!(extension(Path::new("foo-bar.jinja" )), Some("jinja" )); |
841 | assert_eq!(extension(Path::new("foo-bar.jinja2" )), Some("jinja2" )); |
842 | } |
843 | |
844 | #[test ] |
845 | fn get_source() { |
846 | let path = Config::new("" , None, None, None) |
847 | .and_then(|config| config.find_template("b.html" , None, None)) |
848 | .unwrap(); |
849 | assert_eq!(get_template_source(&path, None).unwrap(), "bar" .into()); |
850 | } |
851 | } |
852 | |