1use std::collections::{BTreeMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::{env, fs};
4
5#[cfg(feature = "serde")]
6use serde::Deserialize;
7
8use crate::CompileError;
9use parser::node::Whitespace;
10use parser::Syntax;
11
12#[derive(Debug)]
13pub(crate) struct Config<'a> {
14 pub(crate) dirs: Vec<PathBuf>,
15 pub(crate) syntaxes: BTreeMap<String, Syntax<'a>>,
16 pub(crate) default_syntax: &'a str,
17 pub(crate) escapers: Vec<(HashSet<String>, String)>,
18 pub(crate) whitespace: WhitespaceHandling,
19}
20
21impl<'a> Config<'a> {
22 pub(crate) fn new(
23 s: &'a str,
24 template_whitespace: Option<&str>,
25 ) -> std::result::Result<Config<'a>, CompileError> {
26 let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
27 let default_dirs = vec![root.join("templates")];
28
29 let mut syntaxes = BTreeMap::new();
30 syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), Syntax::default());
31
32 let raw = if s.is_empty() {
33 RawConfig::default()
34 } else {
35 RawConfig::from_toml_str(s)?
36 };
37
38 let (dirs, default_syntax, mut whitespace) = match raw.general {
39 Some(General {
40 dirs,
41 default_syntax,
42 whitespace,
43 }) => (
44 dirs.map_or(default_dirs, |v| {
45 v.into_iter().map(|dir| root.join(dir)).collect()
46 }),
47 default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME),
48 whitespace,
49 ),
50 None => (
51 default_dirs,
52 DEFAULT_SYNTAX_NAME,
53 WhitespaceHandling::default(),
54 ),
55 };
56 if let Some(template_whitespace) = template_whitespace {
57 whitespace = match template_whitespace {
58 "suppress" => WhitespaceHandling::Suppress,
59 "minimize" => WhitespaceHandling::Minimize,
60 "preserve" => WhitespaceHandling::Preserve,
61 s => return Err(format!("invalid value for `whitespace`: \"{s}\"").into()),
62 };
63 }
64
65 if let Some(raw_syntaxes) = raw.syntax {
66 for raw_s in raw_syntaxes {
67 let name = raw_s.name;
68
69 if syntaxes
70 .insert(name.to_string(), raw_s.try_into()?)
71 .is_some()
72 {
73 return Err(format!("syntax \"{name}\" is already defined").into());
74 }
75 }
76 }
77
78 if !syntaxes.contains_key(default_syntax) {
79 return Err(format!("default syntax \"{default_syntax}\" not found").into());
80 }
81
82 let mut escapers = Vec::new();
83 if let Some(configured) = raw.escaper {
84 for escaper in configured {
85 escapers.push((
86 escaper
87 .extensions
88 .iter()
89 .map(|ext| (*ext).to_string())
90 .collect(),
91 escaper.path.to_string(),
92 ));
93 }
94 }
95 for (extensions, path) in DEFAULT_ESCAPERS {
96 escapers.push((str_set(extensions), (*path).to_string()));
97 }
98
99 Ok(Config {
100 dirs,
101 syntaxes,
102 default_syntax,
103 escapers,
104 whitespace,
105 })
106 }
107
108 pub(crate) fn find_template(
109 &self,
110 path: &str,
111 start_at: Option<&Path>,
112 ) -> std::result::Result<PathBuf, CompileError> {
113 if let Some(root) = start_at {
114 let relative = root.with_file_name(path);
115 if relative.exists() {
116 return Ok(relative);
117 }
118 }
119
120 for dir in &self.dirs {
121 let rooted = dir.join(path);
122 if rooted.exists() {
123 return Ok(rooted);
124 }
125 }
126
127 Err(format!(
128 "template {:?} not found in directories {:?}",
129 path, self.dirs
130 )
131 .into())
132 }
133}
134
135impl<'a> TryInto<Syntax<'a>> for RawSyntax<'a> {
136 type Error = CompileError;
137
138 fn try_into(self) -> Result<Syntax<'a>, Self::Error> {
139 let default = Syntax::default();
140 let syntax = Syntax {
141 block_start: self.block_start.unwrap_or(default.block_start),
142 block_end: self.block_end.unwrap_or(default.block_end),
143 expr_start: self.expr_start.unwrap_or(default.expr_start),
144 expr_end: self.expr_end.unwrap_or(default.expr_end),
145 comment_start: self.comment_start.unwrap_or(default.comment_start),
146 comment_end: self.comment_end.unwrap_or(default.comment_end),
147 };
148
149 for s in [
150 syntax.block_start,
151 syntax.block_end,
152 syntax.expr_start,
153 syntax.expr_end,
154 syntax.comment_start,
155 syntax.comment_end,
156 ] {
157 if s.len() < 2 {
158 return Err(
159 format!("delimiters must be at least two characters long: {s:?}").into(),
160 );
161 } else if s.chars().any(|c| c.is_whitespace()) {
162 return Err(format!("delimiters may not contain white spaces: {s:?}").into());
163 }
164 }
165
166 for (s1, s2) in [
167 (syntax.block_start, syntax.expr_start),
168 (syntax.block_start, syntax.comment_start),
169 (syntax.expr_start, syntax.comment_start),
170 ] {
171 if s1.starts_with(s2) || s2.starts_with(s1) {
172 return Err(format!(
173 "a delimiter may not be the prefix of another delimiter: {s1:?} vs {s2:?}",
174 )
175 .into());
176 }
177 }
178
179 Ok(syntax)
180 }
181}
182
183#[cfg_attr(feature = "serde", derive(Deserialize))]
184#[derive(Default)]
185struct RawConfig<'a> {
186 #[cfg_attr(feature = "serde", serde(borrow))]
187 general: Option<General<'a>>,
188 syntax: Option<Vec<RawSyntax<'a>>>,
189 escaper: Option<Vec<RawEscaper<'a>>>,
190}
191
192impl RawConfig<'_> {
193 #[cfg(feature = "config")]
194 fn from_toml_str(s: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
195 basic_toml::from_str(s)
196 .map_err(|e: Error| format!("invalid TOML in {CONFIG_FILE_NAME}: {e}").into())
197 }
198
199 #[cfg(not(feature = "config"))]
200 fn from_toml_str(_: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
201 Err("TOML support not available".into())
202 }
203}
204
205#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
206#[cfg_attr(feature = "serde", derive(Deserialize))]
207#[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "lowercase"))]
208pub(crate) enum WhitespaceHandling {
209 /// The default behaviour. It will leave the whitespace characters "as is".
210 #[default]
211 Preserve,
212 /// It'll remove all the whitespace characters before and after the jinja block.
213 Suppress,
214 /// It'll remove all the whitespace characters except one before and after the jinja blocks.
215 /// If there is a newline character, the preserved character in the trimmed characters, it will
216 /// the one preserved.
217 Minimize,
218}
219
220impl From<WhitespaceHandling> for Whitespace {
221 fn from(ws: WhitespaceHandling) -> Self {
222 match ws {
223 WhitespaceHandling::Suppress => Whitespace::Suppress,
224 WhitespaceHandling::Preserve => Whitespace::Preserve,
225 WhitespaceHandling::Minimize => Whitespace::Minimize,
226 }
227 }
228}
229
230#[cfg_attr(feature = "serde", derive(Deserialize))]
231struct General<'a> {
232 #[cfg_attr(feature = "serde", serde(borrow))]
233 dirs: Option<Vec<&'a str>>,
234 default_syntax: Option<&'a str>,
235 #[cfg_attr(feature = "serde", serde(default))]
236 whitespace: WhitespaceHandling,
237}
238
239#[cfg_attr(feature = "serde", derive(Deserialize))]
240struct RawSyntax<'a> {
241 name: &'a str,
242 block_start: Option<&'a str>,
243 block_end: Option<&'a str>,
244 expr_start: Option<&'a str>,
245 expr_end: Option<&'a str>,
246 comment_start: Option<&'a str>,
247 comment_end: Option<&'a str>,
248}
249
250#[cfg_attr(feature = "serde", derive(Deserialize))]
251struct RawEscaper<'a> {
252 path: &'a str,
253 extensions: Vec<&'a str>,
254}
255
256pub(crate) fn read_config_file(
257 config_path: Option<&str>,
258) -> std::result::Result<String, CompileError> {
259 let root: PathBuf = PathBuf::from(env::var(key:"CARGO_MANIFEST_DIR").unwrap());
260 let filename: PathBuf = match config_path {
261 Some(config_path: &str) => root.join(config_path),
262 None => root.join(path:CONFIG_FILE_NAME),
263 };
264
265 if filename.exists() {
266 fs::read_to_string(&filename)
267 .map_err(|_| format!("unable to read {:?}", filename.to_str().unwrap()).into())
268 } else if config_path.is_some() {
269 Err(format!("`{}` does not exist", root.display()).into())
270 } else {
271 Ok("".to_string())
272 }
273}
274
275fn str_set<T>(vals: &[T]) -> HashSet<String>
276where
277 T: ToString,
278{
279 vals.iter().map(|s: &T| s.to_string()).collect()
280}
281
282#[allow(clippy::match_wild_err_arm)]
283pub(crate) fn get_template_source(tpl_path: &Path) -> std::result::Result<String, CompileError> {
284 match fs::read_to_string(tpl_path) {
285 Err(_) => Err(formatString!(
286 "unable to open template file '{}'",
287 tpl_path.to_str().unwrap()
288 )
289 .into()),
290 Ok(mut source: String) => {
291 if source.ends_with('\n') {
292 let _ = source.pop();
293 }
294 Ok(source)
295 }
296 }
297}
298
299static CONFIG_FILE_NAME: &str = "askama.toml";
300static DEFAULT_SYNTAX_NAME: &str = "default";
301static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[
302 (&["html", "htm", "svg", "xml"], "::askama::Html"),
303 (&["md", "none", "txt", "yml", ""], "::askama::Text"),
304 (&["j2", "jinja", "jinja2"], "::askama::Html"),
305];
306
307#[cfg(test)]
308mod tests {
309 use std::env;
310 use std::path::{Path, PathBuf};
311
312 use super::*;
313
314 #[test]
315 fn get_source() {
316 let path = Config::new("", None)
317 .and_then(|config| config.find_template("b.html", None))
318 .unwrap();
319 assert_eq!(get_template_source(&path).unwrap(), "bar");
320 }
321
322 #[test]
323 fn test_default_config() {
324 let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
325 root.push("templates");
326 let config = Config::new("", None).unwrap();
327 assert_eq!(config.dirs, vec![root]);
328 }
329
330 #[cfg(feature = "config")]
331 #[test]
332 fn test_config_dirs() {
333 let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
334 root.push("tpl");
335 let config = Config::new("[general]\ndirs = [\"tpl\"]", None).unwrap();
336 assert_eq!(config.dirs, vec![root]);
337 }
338
339 fn assert_eq_rooted(actual: &Path, expected: &str) {
340 let mut root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
341 root.push("templates");
342 let mut inner = PathBuf::new();
343 inner.push(expected);
344 assert_eq!(actual.strip_prefix(root).unwrap(), inner);
345 }
346
347 #[test]
348 fn find_absolute() {
349 let config = Config::new("", None).unwrap();
350 let root = config.find_template("a.html", None).unwrap();
351 let path = config.find_template("sub/b.html", Some(&root)).unwrap();
352 assert_eq_rooted(&path, "sub/b.html");
353 }
354
355 #[test]
356 #[should_panic]
357 fn find_relative_nonexistent() {
358 let config = Config::new("", None).unwrap();
359 let root = config.find_template("a.html", None).unwrap();
360 config.find_template("c.html", Some(&root)).unwrap();
361 }
362
363 #[test]
364 fn find_relative() {
365 let config = Config::new("", None).unwrap();
366 let root = config.find_template("sub/b.html", None).unwrap();
367 let path = config.find_template("c.html", Some(&root)).unwrap();
368 assert_eq_rooted(&path, "sub/c.html");
369 }
370
371 #[test]
372 fn find_relative_sub() {
373 let config = Config::new("", None).unwrap();
374 let root = config.find_template("sub/b.html", None).unwrap();
375 let path = config.find_template("sub1/d.html", Some(&root)).unwrap();
376 assert_eq_rooted(&path, "sub/sub1/d.html");
377 }
378
379 #[cfg(feature = "config")]
380 #[test]
381 fn add_syntax() {
382 let raw_config = r#"
383 [general]
384 default_syntax = "foo"
385
386 [[syntax]]
387 name = "foo"
388 block_start = "{<"
389
390 [[syntax]]
391 name = "bar"
392 expr_start = "{!"
393 "#;
394
395 let default_syntax = Syntax::default();
396 let config = Config::new(raw_config, None).unwrap();
397 assert_eq!(config.default_syntax, "foo");
398
399 let foo = config.syntaxes.get("foo").unwrap();
400 assert_eq!(foo.block_start, "{<");
401 assert_eq!(foo.block_end, default_syntax.block_end);
402 assert_eq!(foo.expr_start, default_syntax.expr_start);
403 assert_eq!(foo.expr_end, default_syntax.expr_end);
404 assert_eq!(foo.comment_start, default_syntax.comment_start);
405 assert_eq!(foo.comment_end, default_syntax.comment_end);
406
407 let bar = config.syntaxes.get("bar").unwrap();
408 assert_eq!(bar.block_start, default_syntax.block_start);
409 assert_eq!(bar.block_end, default_syntax.block_end);
410 assert_eq!(bar.expr_start, "{!");
411 assert_eq!(bar.expr_end, default_syntax.expr_end);
412 assert_eq!(bar.comment_start, default_syntax.comment_start);
413 assert_eq!(bar.comment_end, default_syntax.comment_end);
414 }
415
416 #[cfg(feature = "config")]
417 #[test]
418 fn add_syntax_two() {
419 let raw_config = r#"
420 syntax = [{ name = "foo", block_start = "{<" },
421 { name = "bar", expr_start = "{!" } ]
422
423 [general]
424 default_syntax = "foo"
425 "#;
426
427 let default_syntax = Syntax::default();
428 let config = Config::new(raw_config, None).unwrap();
429 assert_eq!(config.default_syntax, "foo");
430
431 let foo = config.syntaxes.get("foo").unwrap();
432 assert_eq!(foo.block_start, "{<");
433 assert_eq!(foo.block_end, default_syntax.block_end);
434 assert_eq!(foo.expr_start, default_syntax.expr_start);
435 assert_eq!(foo.expr_end, default_syntax.expr_end);
436 assert_eq!(foo.comment_start, default_syntax.comment_start);
437 assert_eq!(foo.comment_end, default_syntax.comment_end);
438
439 let bar = config.syntaxes.get("bar").unwrap();
440 assert_eq!(bar.block_start, default_syntax.block_start);
441 assert_eq!(bar.block_end, default_syntax.block_end);
442 assert_eq!(bar.expr_start, "{!");
443 assert_eq!(bar.expr_end, default_syntax.expr_end);
444 assert_eq!(bar.comment_start, default_syntax.comment_start);
445 assert_eq!(bar.comment_end, default_syntax.comment_end);
446 }
447
448 #[cfg(feature = "config")]
449 #[test]
450 fn longer_delimiters() {
451 let raw_config = r#"
452 [[syntax]]
453 name = "emoji"
454 block_start = "👉🙂👉"
455 block_end = "👈🙃👈"
456 expr_start = "🤜🤜"
457 expr_end = "🤛🤛"
458 comment_start = "👎_(ツ)_👎"
459 comment_end = "👍:D👍"
460
461 [general]
462 default_syntax = "emoji"
463 "#;
464
465 let config = Config::new(raw_config, None).unwrap();
466 assert_eq!(config.default_syntax, "emoji");
467
468 let foo = config.syntaxes.get("emoji").unwrap();
469 assert_eq!(foo.block_start, "👉🙂👉");
470 assert_eq!(foo.block_end, "👈🙃👈");
471 assert_eq!(foo.expr_start, "🤜🤜");
472 assert_eq!(foo.expr_end, "🤛🤛");
473 assert_eq!(foo.comment_start, "👎_(ツ)_👎");
474 assert_eq!(foo.comment_end, "👍:D👍");
475 }
476
477 #[cfg(feature = "config")]
478 #[test]
479 fn illegal_delimiters() {
480 let raw_config = r#"
481 [[syntax]]
482 name = "too_short"
483 block_start = "<"
484 "#;
485 let config = Config::new(raw_config, None);
486 assert_eq!(
487 config.unwrap_err().msg,
488 r#"delimiters must be at least two characters long: "<""#,
489 );
490
491 let raw_config = r#"
492 [[syntax]]
493 name = "contains_ws"
494 block_start = " {{ "
495 "#;
496 let config = Config::new(raw_config, None);
497 assert_eq!(
498 config.unwrap_err().msg,
499 r#"delimiters may not contain white spaces: " {{ ""#,
500 );
501
502 let raw_config = r#"
503 [[syntax]]
504 name = "is_prefix"
505 block_start = "{{"
506 expr_start = "{{$"
507 comment_start = "{{#"
508 "#;
509 let config = Config::new(raw_config, None);
510 assert_eq!(
511 config.unwrap_err().msg,
512 r#"a delimiter may not be the prefix of another delimiter: "{{" vs "{{$""#,
513 );
514 }
515
516 #[cfg(feature = "toml")]
517 #[should_panic]
518 #[test]
519 fn use_default_at_syntax_name() {
520 let raw_config = r#"
521 syntax = [{ name = "default" }]
522 "#;
523
524 let _config = Config::new(raw_config, None).unwrap();
525 }
526
527 #[cfg(feature = "toml")]
528 #[should_panic]
529 #[test]
530 fn duplicated_syntax_name_on_list() {
531 let raw_config = r#"
532 syntax = [{ name = "foo", block_start = "~<" },
533 { name = "foo", block_start = "%%" } ]
534 "#;
535
536 let _config = Config::new(raw_config, None).unwrap();
537 }
538
539 #[cfg(feature = "toml")]
540 #[should_panic]
541 #[test]
542 fn is_not_exist_default_syntax() {
543 let raw_config = r#"
544 [general]
545 default_syntax = "foo"
546 "#;
547
548 let _config = Config::new(raw_config, None).unwrap();
549 }
550
551 #[cfg(feature = "config")]
552 #[test]
553 fn escape_modes() {
554 let config = Config::new(
555 r#"
556 [[escaper]]
557 path = "::askama::Js"
558 extensions = ["js"]
559 "#,
560 None,
561 )
562 .unwrap();
563 assert_eq!(
564 config.escapers,
565 vec![
566 (str_set(&["js"]), "::askama::Js".into()),
567 (
568 str_set(&["html", "htm", "svg", "xml"]),
569 "::askama::Html".into()
570 ),
571 (
572 str_set(&["md", "none", "txt", "yml", ""]),
573 "::askama::Text".into()
574 ),
575 (str_set(&["j2", "jinja", "jinja2"]), "::askama::Html".into()),
576 ]
577 );
578 }
579
580 #[cfg(feature = "config")]
581 #[test]
582 fn test_whitespace_parsing() {
583 let config = Config::new(
584 r#"
585 [general]
586 whitespace = "suppress"
587 "#,
588 None,
589 )
590 .unwrap();
591 assert_eq!(config.whitespace, WhitespaceHandling::Suppress);
592
593 let config = Config::new(r#""#, None).unwrap();
594 assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
595
596 let config = Config::new(
597 r#"
598 [general]
599 whitespace = "preserve"
600 "#,
601 None,
602 )
603 .unwrap();
604 assert_eq!(config.whitespace, WhitespaceHandling::Preserve);
605
606 let config = Config::new(
607 r#"
608 [general]
609 whitespace = "minimize"
610 "#,
611 None,
612 )
613 .unwrap();
614 assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
615 }
616
617 #[cfg(feature = "toml")]
618 #[test]
619 fn test_whitespace_in_template() {
620 // Checking that template arguments have precedence over general configuration.
621 // So in here, in the template arguments, there is `whitespace = "minimize"` so
622 // the `WhitespaceHandling` should be `Minimize` as well.
623 let config = Config::new(
624 r#"
625 [general]
626 whitespace = "suppress"
627 "#,
628 Some(&"minimize".to_owned()),
629 )
630 .unwrap();
631 assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
632
633 let config = Config::new(r#""#, Some(&"minimize".to_owned())).unwrap();
634 assert_eq!(config.whitespace, WhitespaceHandling::Minimize);
635 }
636
637 #[test]
638 fn test_config_whitespace_error() {
639 let config = Config::new(r#""#, Some("trim"));
640 if let Err(err) = config {
641 assert_eq!(err.msg, "invalid value for `whitespace`: \"trim\"");
642 } else {
643 panic!("Config::new should have return an error");
644 }
645 }
646}
647