/src/askama/askama_derive/src/config.rs
Line | Count | Source |
1 | | use std::borrow::{Borrow, Cow}; |
2 | | use std::collections::hash_map::Entry; |
3 | | use std::mem::ManuallyDrop; |
4 | | use std::ops::Deref; |
5 | | use std::path::{Path, PathBuf, absolute}; |
6 | | use std::sync::{Arc, OnceLock}; |
7 | | use std::{env, fs}; |
8 | | |
9 | | use parser::node::Whitespace; |
10 | | use parser::{ParseError, Parsed, Syntax, SyntaxBuilder}; |
11 | | use proc_macro2::Span; |
12 | | #[cfg(feature = "config")] |
13 | | use serde_derive::Deserialize; |
14 | | |
15 | | use crate::{CompileError, FileInfo, HashMap, OnceMap}; |
16 | | |
17 | | #[derive(Debug)] |
18 | | pub(crate) struct Config { |
19 | | pub(crate) dirs: Vec<PathBuf>, |
20 | | pub(crate) syntaxes: HashMap<String, SyntaxAndCache>, |
21 | | pub(crate) default_syntax: &'static str, |
22 | | pub(crate) escapers: Vec<(Vec<Cow<'static, str>>, Cow<'static, str>)>, |
23 | | pub(crate) whitespace: Whitespace, |
24 | | pub(crate) full_config_path: Option<PathBuf>, |
25 | | // `Config` is self referential and `_key` owns it data, so it must come last |
26 | | _key: OwnedConfigKey, |
27 | | } |
28 | | |
29 | | impl Drop for Config { |
30 | | #[track_caller] |
31 | 0 | fn drop(&mut self) { |
32 | 0 | panic!(); |
33 | | } |
34 | | } |
35 | | |
36 | | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] |
37 | | struct OwnedConfigKey(&'static ConfigKey<'static>); |
38 | | |
39 | | #[derive(Debug, PartialEq, Eq, Hash)] |
40 | | struct ConfigKey<'a> { |
41 | | root: Cow<'a, Path>, |
42 | | source: Cow<'a, str>, |
43 | | config_path: Option<Cow<'a, str>>, |
44 | | template_whitespace: Option<Whitespace>, |
45 | | } |
46 | | |
47 | | impl ToOwned for ConfigKey<'_> { |
48 | | type Owned = OwnedConfigKey; |
49 | | |
50 | 1 | fn to_owned(&self) -> Self::Owned { |
51 | 1 | let owned_key = ConfigKey { |
52 | 1 | root: Cow::Owned(self.root.as_ref().to_owned()), |
53 | 1 | source: Cow::Owned(self.source.as_ref().to_owned()), |
54 | 1 | config_path: self |
55 | 1 | .config_path |
56 | 1 | .as_ref() |
57 | 1 | .map(|s| Cow::Owned(s.as_ref().to_owned())), |
58 | 1 | template_whitespace: self.template_whitespace, |
59 | | }; |
60 | 1 | OwnedConfigKey(Box::leak(Box::new(owned_key))) |
61 | 1 | } |
62 | | } |
63 | | |
64 | | impl<'a> Borrow<ConfigKey<'a>> for OwnedConfigKey { |
65 | | #[inline] |
66 | 17.8k | fn borrow(&self) -> &ConfigKey<'a> { |
67 | 17.8k | self.0 |
68 | 17.8k | } |
69 | | } |
70 | | |
71 | | impl Config { |
72 | 17.8k | pub(crate) fn new( |
73 | 17.8k | source: &str, |
74 | 17.8k | config_path: Option<&str>, |
75 | 17.8k | template_whitespace: Option<Whitespace>, |
76 | 17.8k | config_span: Option<Span>, |
77 | 17.8k | full_config_path: Option<PathBuf>, |
78 | 17.8k | ) -> Result<&'static Config, CompileError> { |
79 | | static CACHE: ManuallyDrop<OnceLock<OnceMap<OwnedConfigKey, &'static Config>>> = |
80 | | ManuallyDrop::new(OnceLock::new()); |
81 | 17.8k | CACHE.get_or_init(OnceMap::default).get_or_try_insert( |
82 | 17.8k | &ConfigKey { |
83 | 17.8k | root: Cow::Owned(manifest_root()), |
84 | 17.8k | source: source.into(), |
85 | 17.8k | config_path: config_path.map(Cow::Borrowed), |
86 | 17.8k | template_whitespace, |
87 | 17.8k | }, |
88 | 1 | |key| { |
89 | 1 | let config = Config::new_uncached(key.to_owned(), config_span, full_config_path)?; |
90 | 1 | let config = &*Box::leak(Box::new(config)); |
91 | 1 | Ok((config._key, config)) |
92 | 1 | }, |
93 | | |config| *config, |
94 | | ) |
95 | 17.8k | } |
96 | | |
97 | 1 | fn new_uncached( |
98 | 1 | key: OwnedConfigKey, |
99 | 1 | config_span: Option<Span>, |
100 | 1 | full_config_path: Option<PathBuf>, |
101 | 1 | ) -> Result<Config, CompileError> { |
102 | 1 | let s = key.0.source.as_ref(); |
103 | 1 | let config_path = key.0.config_path.as_deref(); |
104 | 1 | let root = key.0.root.as_ref(); |
105 | | |
106 | 1 | let default_dirs = vec![root.join("templates")]; |
107 | | |
108 | 1 | let mut syntaxes = HashMap::default(); |
109 | 1 | syntaxes.insert(DEFAULT_SYNTAX_NAME.to_string(), SyntaxAndCache::default()); |
110 | | |
111 | 1 | let raw = if s.is_empty() { |
112 | 1 | RawConfig::default() |
113 | | } else { |
114 | 0 | RawConfig::from_toml_str(s)? |
115 | | }; |
116 | | |
117 | 1 | let (dirs, default_syntax, whitespace) = match raw.general { |
118 | | Some(General { |
119 | 0 | dirs, |
120 | 0 | default_syntax, |
121 | 0 | whitespace, |
122 | | }) => ( |
123 | 0 | dirs.map_or(default_dirs, |v| { |
124 | 0 | v.into_iter() |
125 | 0 | .flat_map(|dir| get_config_dirs(root, dir)) |
126 | 0 | .collect() |
127 | 0 | }), |
128 | 0 | default_syntax.unwrap_or(DEFAULT_SYNTAX_NAME), |
129 | 0 | whitespace, |
130 | | ), |
131 | 1 | None => (default_dirs, DEFAULT_SYNTAX_NAME, Whitespace::default()), |
132 | | }; |
133 | 1 | let file_info = config_path.map(|path| FileInfo::new(Path::new(path), None, None)); |
134 | 1 | let whitespace = key.0.template_whitespace.unwrap_or(whitespace); |
135 | | |
136 | 1 | if let Some(raw_syntaxes) = raw.syntax { |
137 | 0 | for raw_s in raw_syntaxes { |
138 | 0 | let name = raw_s.name; |
139 | 0 | match syntaxes.entry(name.to_string()) { |
140 | 0 | Entry::Vacant(entry) => { |
141 | 0 | entry.insert(raw_s.to_syntax().map(SyntaxAndCache::new).map_err( |
142 | 0 | |err| CompileError::new_with_span_stable(err, file_info, config_span), |
143 | 0 | )?); |
144 | | } |
145 | | Entry::Occupied(_) => { |
146 | 0 | return Err(CompileError::new( |
147 | 0 | format_args!("syntax {name:?} is already defined"), |
148 | 0 | file_info, |
149 | 0 | )); |
150 | | } |
151 | | } |
152 | | } |
153 | 1 | } |
154 | | |
155 | 1 | if !syntaxes.contains_key(default_syntax) { |
156 | 0 | return Err(CompileError::new( |
157 | 0 | format_args!("default syntax \"{default_syntax}\" not found"), |
158 | 0 | file_info, |
159 | 0 | )); |
160 | 1 | } |
161 | | |
162 | 1 | let mut escapers = Vec::new(); |
163 | 1 | if let Some(configured) = raw.escaper { |
164 | 0 | for escaper in configured { |
165 | 0 | escapers.push((str_set(&escaper.extensions), escaper.path.into())); |
166 | 0 | } |
167 | 1 | } |
168 | 3 | for (extensions, name) in DEFAULT_ESCAPERS { |
169 | 2 | escapers.push(( |
170 | 2 | str_set(extensions), |
171 | 2 | format!("askama::filters::{name}").into(), |
172 | 2 | )); |
173 | 2 | } |
174 | | |
175 | 1 | Ok(Config { |
176 | 1 | dirs, |
177 | 1 | syntaxes, |
178 | 1 | default_syntax, |
179 | 1 | escapers, |
180 | 1 | whitespace, |
181 | 1 | full_config_path, |
182 | 1 | _key: key, |
183 | 1 | }) |
184 | 1 | } |
185 | | |
186 | 0 | pub(crate) fn find_template( |
187 | 0 | &self, |
188 | 0 | path: &str, |
189 | 0 | start_at: Option<&Path>, |
190 | 0 | file_info: Option<FileInfo<'_>>, |
191 | 0 | span: Option<proc_macro2::Span>, |
192 | 0 | ) -> Result<Arc<Path>, CompileError> { |
193 | 0 | let path = 'find_path: { |
194 | 0 | if let Some(root) = start_at { |
195 | 0 | let relative = root.with_file_name(path); |
196 | 0 | if relative.exists() { |
197 | 0 | break 'find_path relative; |
198 | 0 | } |
199 | 0 | } |
200 | 0 | for dir in &self.dirs { |
201 | 0 | let rooted = dir.join(path); |
202 | 0 | if rooted.exists() { |
203 | 0 | break 'find_path rooted; |
204 | 0 | } |
205 | | } |
206 | 0 | return Err(CompileError::new_with_span( |
207 | 0 | format_args!( |
208 | 0 | "template {:?} not found in directories {:?}", |
209 | 0 | path, self.dirs, |
210 | 0 | ), |
211 | 0 | file_info, |
212 | 0 | span, |
213 | 0 | )); |
214 | | }; |
215 | 0 | match absolute(&path) { |
216 | 0 | Ok(path) => Ok(path.into()), |
217 | 0 | Err(err) => Err(CompileError::new_with_span( |
218 | 0 | format_args!("could not get absolute path for {path:?}: {err}"), |
219 | 0 | file_info, |
220 | 0 | span, |
221 | 0 | )), |
222 | | } |
223 | 0 | } |
224 | | } |
225 | | |
226 | | #[cfg(not(feature = "config"))] |
227 | 0 | fn get_config_dirs(_root: &Path, _dir: &str) -> impl Iterator<Item = PathBuf> { |
228 | 0 | std::iter::empty() |
229 | 0 | } |
230 | | |
231 | | #[cfg(feature = "config")] |
232 | | fn get_config_dirs(root: &Path, dir: &str) -> impl Iterator<Item = PathBuf> { |
233 | | let path = root.join(dir); |
234 | | if dir.contains('*') |
235 | | && let Some(path) = path.to_str() |
236 | | && let Ok(matches) = glob::glob(path) |
237 | | { |
238 | | PathOrPaths::Paths(matches) |
239 | | } else { |
240 | | PathOrPaths::Path(Some(path)) |
241 | | } |
242 | | } |
243 | | |
244 | | #[cfg(feature = "config")] |
245 | | enum PathOrPaths { |
246 | | Paths(glob::Paths), |
247 | | Path(Option<PathBuf>), |
248 | | } |
249 | | |
250 | | #[cfg(feature = "config")] |
251 | | impl Iterator for PathOrPaths { |
252 | | type Item = PathBuf; |
253 | | |
254 | | fn next(&mut self) -> Option<Self::Item> { |
255 | | match self { |
256 | | Self::Paths(paths) => loop { |
257 | | if let Ok(path) = paths.next()? |
258 | | && path.is_dir() |
259 | | { |
260 | | return Some(path); |
261 | | } |
262 | | }, |
263 | | Self::Path(path) => path.take(), |
264 | | } |
265 | | } |
266 | | } |
267 | | |
268 | | #[derive(Debug, Default)] |
269 | | pub(crate) struct SyntaxAndCache { |
270 | | syntax: Syntax<'static>, |
271 | | cache: OnceMap<OwnedSyntaxAndCacheKey, Arc<Parsed>>, |
272 | | } |
273 | | |
274 | | impl Deref for SyntaxAndCache { |
275 | | type Target = Syntax<'static>; |
276 | | |
277 | | #[inline] |
278 | | fn deref(&self) -> &Self::Target { |
279 | | &self.syntax |
280 | | } |
281 | | } |
282 | | |
283 | | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
284 | | struct OwnedSyntaxAndCacheKey(SyntaxAndCacheKey<'static>); |
285 | | |
286 | | impl Deref for OwnedSyntaxAndCacheKey { |
287 | | type Target = SyntaxAndCacheKey<'static>; |
288 | | |
289 | | #[inline] |
290 | 2.37k | fn deref(&self) -> &Self::Target { |
291 | 2.37k | &self.0 |
292 | 2.37k | } |
293 | | } |
294 | | |
295 | | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
296 | | struct SyntaxAndCacheKey<'a> { |
297 | | source: Cow<'a, Arc<str>>, |
298 | | source_path: Option<Cow<'a, Arc<Path>>>, |
299 | | } |
300 | | |
301 | | impl<'a> Borrow<SyntaxAndCacheKey<'a>> for OwnedSyntaxAndCacheKey { |
302 | 17.1k | fn borrow(&self) -> &SyntaxAndCacheKey<'a> { |
303 | 17.1k | &self.0 |
304 | 17.1k | } |
305 | | } |
306 | | |
307 | | impl SyntaxAndCache { |
308 | 0 | fn new(syntax: Syntax<'static>) -> Self { |
309 | 0 | Self { |
310 | 0 | syntax, |
311 | 0 | cache: OnceMap::default(), |
312 | 0 | } |
313 | 0 | } |
314 | | |
315 | 17.2k | pub(crate) fn parse( |
316 | 17.2k | &self, |
317 | 17.2k | source: Arc<str>, |
318 | 17.2k | source_path: Option<Arc<Path>>, |
319 | 17.2k | ) -> Result<Arc<Parsed>, ParseError> { |
320 | 17.2k | self.cache.get_or_try_insert( |
321 | 17.2k | &SyntaxAndCacheKey { |
322 | 17.2k | source: Cow::Owned(source), |
323 | 17.2k | source_path: source_path.map(Cow::Owned), |
324 | 17.2k | }, |
325 | 1.18k | |key| { |
326 | 1.18k | let key = OwnedSyntaxAndCacheKey(SyntaxAndCacheKey { |
327 | 1.18k | source: Cow::Owned(Arc::clone(key.source.as_ref())), |
328 | 1.18k | source_path: key |
329 | 1.18k | .source_path |
330 | 1.18k | .as_deref() |
331 | 1.18k | .map(|v| Cow::Owned(Arc::clone(v))), |
332 | | }); |
333 | 1.18k | let parsed = Parsed::new( |
334 | 1.18k | Arc::clone(key.source.as_ref()), |
335 | 1.18k | key.source_path.as_deref().map(Arc::clone), |
336 | 1.18k | &self.syntax, |
337 | 462 | )?; |
338 | 726 | Ok((key, Arc::new(parsed))) |
339 | 1.18k | }, |
340 | | Arc::clone, |
341 | | ) |
342 | 17.2k | } |
343 | | } |
344 | | |
345 | | #[cfg_attr(feature = "config", derive(Deserialize))] |
346 | | #[derive(Default)] |
347 | | struct RawConfig<'a> { |
348 | | #[cfg_attr(feature = "config", serde(borrow))] |
349 | | general: Option<General<'a>>, |
350 | | syntax: Option<Vec<SyntaxBuilder<'a>>>, |
351 | | escaper: Option<Vec<RawEscaper<'a>>>, |
352 | | } |
353 | | |
354 | | impl RawConfig<'_> { |
355 | | #[cfg(feature = "config")] |
356 | | fn from_toml_str(s: &str) -> Result<RawConfig<'_>, CompileError> { |
357 | | toml::from_str(s).map_err(|e| { |
358 | | CompileError::no_file_info(format!("invalid TOML in {CONFIG_FILE_NAME}: {e}"), None) |
359 | | }) |
360 | | } |
361 | | |
362 | | #[cfg(not(feature = "config"))] |
363 | 0 | fn from_toml_str(_: &str) -> Result<RawConfig<'_>, CompileError> { |
364 | 0 | Err(CompileError::no_file_info( |
365 | 0 | "TOML support not available", |
366 | 0 | None, |
367 | 0 | )) |
368 | 0 | } |
369 | | } |
370 | | |
371 | | #[cfg_attr(feature = "config", derive(Deserialize))] |
372 | | struct General<'a> { |
373 | | #[cfg_attr(feature = "config", serde(borrow))] |
374 | | dirs: Option<Vec<&'a str>>, |
375 | | default_syntax: Option<&'a str>, |
376 | | #[cfg_attr(feature = "config", serde(default))] |
377 | | whitespace: Whitespace, |
378 | | } |
379 | | |
380 | | #[cfg_attr(feature = "config", derive(Deserialize))] |
381 | | struct RawEscaper<'a> { |
382 | | path: &'a str, |
383 | | extensions: Vec<&'a str>, |
384 | | } |
385 | | |
386 | 17.2k | pub(crate) fn read_config_file( |
387 | 17.2k | config_path: Option<&str>, |
388 | 17.2k | span: Option<Span>, |
389 | 17.2k | ) -> Result<(String, Option<PathBuf>), CompileError> { |
390 | 17.2k | let root = manifest_root(); |
391 | 17.2k | let filename = match config_path { |
392 | 0 | Some(config_path) => root.join(config_path), |
393 | 17.2k | None => root.join(CONFIG_FILE_NAME), |
394 | | }; |
395 | | |
396 | 17.2k | if filename.exists() { |
397 | 0 | let content = fs::read_to_string(&filename).map_err(|err| { |
398 | 0 | CompileError::no_file_info( |
399 | 0 | format_args!("unable to read {}: {err}", filename.display()), |
400 | 0 | span, |
401 | | ) |
402 | 0 | })?; |
403 | 0 | Ok((content, filename.canonicalize().ok())) |
404 | 17.2k | } else if config_path.is_some() { |
405 | 0 | Err(CompileError::no_file_info( |
406 | 0 | format_args!("`{}` does not exist", filename.display()), |
407 | 0 | span, |
408 | 0 | )) |
409 | | } else { |
410 | 17.2k | Ok((String::new(), None)) |
411 | | } |
412 | 17.2k | } |
413 | | |
414 | 35.1k | fn manifest_root() -> PathBuf { |
415 | 35.1k | env::var_os("CARGO_MANIFEST_DIR").map_or_else(|| PathBuf::from("."), PathBuf::from) |
416 | 35.1k | } |
417 | | |
418 | 2 | fn str_set(vals: &[&'static str]) -> Vec<Cow<'static, str>> { |
419 | 14 | vals.iter().map(|s| Cow::Borrowed(*s)).collect() |
420 | 2 | } |
421 | | |
422 | | static CONFIG_FILE_NAME: &str = "askama.toml"; |
423 | | static DEFAULT_SYNTAX_NAME: &str = "default"; |
424 | | static DEFAULT_ESCAPERS: &[(&[&str], &str)] = &[ |
425 | | ( |
426 | | &[ |
427 | | "askama", "html", "htm", "j2", "jinja", "jinja2", "rinja", "svg", "xml", |
428 | | ], |
429 | | "Html", |
430 | | ), |
431 | | (&["md", "none", "txt", "yml", ""], "Text"), |
432 | | ]; |
433 | | |
434 | | #[cfg(test)] |
435 | | mod tests { |
436 | | use std::path::{Path, PathBuf, absolute}; |
437 | | |
438 | | use super::*; |
439 | | |
440 | | #[test] |
441 | | fn test_default_config() { |
442 | | let mut root = manifest_root(); |
443 | | root.push("templates"); |
444 | | let config = Config::new("", None, None, None, None).unwrap(); |
445 | | assert_eq!(config.dirs, vec![root]); |
446 | | } |
447 | | |
448 | | #[cfg(feature = "config")] |
449 | | #[test] |
450 | | fn test_config_dirs() { |
451 | | let mut root = manifest_root(); |
452 | | root.push("tpl"); |
453 | | let config = Config::new("[general]\ndirs = [\"tpl\"]", None, None, None, None).unwrap(); |
454 | | assert_eq!(config.dirs, vec![root]); |
455 | | } |
456 | | |
457 | | #[cfg(feature = "config")] |
458 | | #[test] |
459 | | fn test_config_dirs_glob() { |
460 | | let root = manifest_root(); |
461 | | let config = Config::new( |
462 | | "[general]\ndirs = [\"templates/*\"]", |
463 | | None, |
464 | | None, |
465 | | None, |
466 | | None, |
467 | | ) |
468 | | .unwrap(); |
469 | | // We ensure that it includes only top level folders. |
470 | | assert_eq!(config.dirs, vec![root.join("templates/sub")]); |
471 | | |
472 | | let config = Config::new( |
473 | | "[general]\ndirs = [\"templates/**\"]", |
474 | | None, |
475 | | None, |
476 | | None, |
477 | | None, |
478 | | ) |
479 | | .unwrap(); |
480 | | // We ensure that it includes top level and sub-folders. |
481 | | assert_eq!( |
482 | | config.dirs, |
483 | | vec![root.join("templates/sub"), root.join("templates/sub/sub1")] |
484 | | ); |
485 | | } |
486 | | |
487 | | fn assert_eq_rooted(actual: &Path, expected: &str) { |
488 | | let mut root = absolute(manifest_root()).unwrap(); |
489 | | root.push("templates"); |
490 | | let mut inner = PathBuf::new(); |
491 | | inner.push(expected); |
492 | | assert_eq!(actual.strip_prefix(root).unwrap(), inner); |
493 | | } |
494 | | |
495 | | #[test] |
496 | | fn find_absolute() { |
497 | | let config = Config::new("", None, None, None, None).unwrap(); |
498 | | let root = config.find_template("a.html", None, None, None).unwrap(); |
499 | | let path = config |
500 | | .find_template("sub/b.html", Some(&root), None, None) |
501 | | .unwrap(); |
502 | | assert_eq_rooted(&path, "sub/b.html"); |
503 | | } |
504 | | |
505 | | #[test] |
506 | | #[should_panic] |
507 | | fn find_relative_nonexistent() { |
508 | | let config = Config::new("", None, None, None, None).unwrap(); |
509 | | let root = config.find_template("a.html", None, None, None).unwrap(); |
510 | | config |
511 | | .find_template("c.html", Some(&root), None, None) |
512 | | .unwrap(); |
513 | | } |
514 | | |
515 | | #[test] |
516 | | fn find_relative() { |
517 | | let config = Config::new("", None, None, None, None).unwrap(); |
518 | | let root = config |
519 | | .find_template("sub/b.html", None, None, None) |
520 | | .unwrap(); |
521 | | let path = config |
522 | | .find_template("c.html", Some(&root), None, None) |
523 | | .unwrap(); |
524 | | assert_eq_rooted(&path, "sub/c.html"); |
525 | | } |
526 | | |
527 | | #[test] |
528 | | fn find_relative_sub() { |
529 | | let config = Config::new("", None, None, None, None).unwrap(); |
530 | | let root = config |
531 | | .find_template("sub/b.html", None, None, None) |
532 | | .unwrap(); |
533 | | let path = config |
534 | | .find_template("sub1/d.html", Some(&root), None, None) |
535 | | .unwrap(); |
536 | | assert_eq_rooted(&path, "sub/sub1/d.html"); |
537 | | } |
538 | | |
539 | | #[cfg(feature = "config")] |
540 | | #[test] |
541 | | fn add_syntax() { |
542 | | let raw_config = r#" |
543 | | [general] |
544 | | default_syntax = "foo" |
545 | | |
546 | | [[syntax]] |
547 | | name = "foo" |
548 | | block_start = "{<" |
549 | | |
550 | | [[syntax]] |
551 | | name = "bar" |
552 | | expr_start = "{!" |
553 | | "#; |
554 | | |
555 | | let default_syntax = Syntax::default(); |
556 | | let config = Config::new(raw_config, None, None, None, None).unwrap(); |
557 | | assert_eq!(config.default_syntax, "foo"); |
558 | | |
559 | | let foo = config.syntaxes.get("foo").unwrap(); |
560 | | assert_eq!(foo.block_start, "{<"); |
561 | | assert_eq!(foo.block_end, default_syntax.block_end); |
562 | | assert_eq!(foo.expr_start, default_syntax.expr_start); |
563 | | assert_eq!(foo.expr_end, default_syntax.expr_end); |
564 | | assert_eq!(foo.comment_start, default_syntax.comment_start); |
565 | | assert_eq!(foo.comment_end, default_syntax.comment_end); |
566 | | |
567 | | let bar = config.syntaxes.get("bar").unwrap(); |
568 | | assert_eq!(bar.block_start, default_syntax.block_start); |
569 | | assert_eq!(bar.block_end, default_syntax.block_end); |
570 | | assert_eq!(bar.expr_start, "{!"); |
571 | | assert_eq!(bar.expr_end, default_syntax.expr_end); |
572 | | assert_eq!(bar.comment_start, default_syntax.comment_start); |
573 | | assert_eq!(bar.comment_end, default_syntax.comment_end); |
574 | | } |
575 | | |
576 | | #[cfg(feature = "config")] |
577 | | #[test] |
578 | | fn add_syntax_two() { |
579 | | let raw_config = r#" |
580 | | syntax = [{ name = "foo", block_start = "{<" }, |
581 | | { name = "bar", expr_start = "{!" } ] |
582 | | |
583 | | [general] |
584 | | default_syntax = "foo" |
585 | | "#; |
586 | | |
587 | | let default_syntax = Syntax::default(); |
588 | | let config = Config::new(raw_config, None, None, None, None).unwrap(); |
589 | | assert_eq!(config.default_syntax, "foo"); |
590 | | |
591 | | let foo = config.syntaxes.get("foo").unwrap(); |
592 | | assert_eq!(foo.block_start, "{<"); |
593 | | assert_eq!(foo.block_end, default_syntax.block_end); |
594 | | assert_eq!(foo.expr_start, default_syntax.expr_start); |
595 | | assert_eq!(foo.expr_end, default_syntax.expr_end); |
596 | | assert_eq!(foo.comment_start, default_syntax.comment_start); |
597 | | assert_eq!(foo.comment_end, default_syntax.comment_end); |
598 | | |
599 | | let bar = config.syntaxes.get("bar").unwrap(); |
600 | | assert_eq!(bar.block_start, default_syntax.block_start); |
601 | | assert_eq!(bar.block_end, default_syntax.block_end); |
602 | | assert_eq!(bar.expr_start, "{!"); |
603 | | assert_eq!(bar.expr_end, default_syntax.expr_end); |
604 | | assert_eq!(bar.comment_start, default_syntax.comment_start); |
605 | | assert_eq!(bar.comment_end, default_syntax.comment_end); |
606 | | } |
607 | | |
608 | | #[cfg(feature = "config")] |
609 | | #[test] |
610 | | fn longer_delimiters() { |
611 | | let raw_config = r#" |
612 | | [[syntax]] |
613 | | name = "emoji" |
614 | | block_start = "👉🙂👉" |
615 | | block_end = "👈🙃👈" |
616 | | expr_start = "🤜🤜" |
617 | | expr_end = "🤛🤛" |
618 | | comment_start = "👎_(ツ)_👎" |
619 | | comment_end = "👍:D👍" |
620 | | |
621 | | [general] |
622 | | default_syntax = "emoji" |
623 | | "#; |
624 | | |
625 | | let config = Config::new(raw_config, None, None, None, None).unwrap(); |
626 | | assert_eq!(config.default_syntax, "emoji"); |
627 | | |
628 | | let foo = config.syntaxes.get("emoji").unwrap(); |
629 | | assert_eq!(foo.block_start, "👉🙂👉"); |
630 | | assert_eq!(foo.block_end, "👈🙃👈"); |
631 | | assert_eq!(foo.expr_start, "🤜🤜"); |
632 | | assert_eq!(foo.expr_end, "🤛🤛"); |
633 | | assert_eq!(foo.comment_start, "👎_(ツ)_👎"); |
634 | | assert_eq!(foo.comment_end, "👍:D👍"); |
635 | | } |
636 | | |
637 | | #[cfg(feature = "config")] |
638 | | #[test] |
639 | | fn illegal_delimiters() { |
640 | | #[track_caller] |
641 | | fn expect_err<T, E>(result: Result<T, E>) -> E { |
642 | | match result { |
643 | | Ok(_) => panic!("should have failed"), |
644 | | Err(err) => err, |
645 | | } |
646 | | } |
647 | | |
648 | | let raw_config = r#" |
649 | | [[syntax]] |
650 | | name = "too_short" |
651 | | block_start = "<" |
652 | | "#; |
653 | | let config = Config::new(raw_config, None, None, None, None); |
654 | | assert_eq!( |
655 | | expect_err(config).msg, |
656 | | r#"delimiters must be at least two characters long. The opening block delimiter ("<") is too short"#, |
657 | | ); |
658 | | |
659 | | let raw_config = r#" |
660 | | [[syntax]] |
661 | | name = "contains_ws" |
662 | | block_start = " {{ " |
663 | | "#; |
664 | | let config = Config::new(raw_config, None, None, None, None); |
665 | | assert_eq!( |
666 | | expect_err(config).msg, |
667 | | r#"delimiters may not contain white spaces. The opening block delimiter (" {{ ") contains white spaces"#, |
668 | | ); |
669 | | |
670 | | let raw_config = r#" |
671 | | [[syntax]] |
672 | | name = "is_prefix" |
673 | | block_start = "{{" |
674 | | expr_start = "{{$" |
675 | | comment_start = "{{#" |
676 | | "#; |
677 | | let config = Config::new(raw_config, None, None, None, None); |
678 | | assert_eq!( |
679 | | expect_err(config).msg, |
680 | | r#"an opening delimiter may not be the prefix of another delimiter. The block delimiter ("{{") clashes with the expression delimiter ("{{$")"#, |
681 | | ); |
682 | | } |
683 | | |
684 | | #[cfg(feature = "config")] |
685 | | #[should_panic] |
686 | | #[test] |
687 | | fn use_default_at_syntax_name() { |
688 | | let raw_config = r#" |
689 | | syntax = [{ name = "default" }] |
690 | | "#; |
691 | | |
692 | | let _config = Config::new(raw_config, None, None, None, None).unwrap(); |
693 | | } |
694 | | |
695 | | #[cfg(feature = "config")] |
696 | | #[should_panic] |
697 | | #[test] |
698 | | fn duplicated_syntax_name_on_list() { |
699 | | let raw_config = r#" |
700 | | syntax = [{ name = "foo", block_start = "~<" }, |
701 | | { name = "foo", block_start = "%%" } ] |
702 | | "#; |
703 | | |
704 | | let _config = Config::new(raw_config, None, None, None, None).unwrap(); |
705 | | } |
706 | | |
707 | | #[cfg(feature = "config")] |
708 | | #[should_panic] |
709 | | #[test] |
710 | | fn is_not_exist_default_syntax() { |
711 | | let raw_config = r#" |
712 | | [general] |
713 | | default_syntax = "foo" |
714 | | "#; |
715 | | |
716 | | let _config = Config::new(raw_config, None, None, None, None).unwrap(); |
717 | | } |
718 | | |
719 | | #[cfg(feature = "config")] |
720 | | #[test] |
721 | | fn escape_modes() { |
722 | | let config = Config::new( |
723 | | r#" |
724 | | [[escaper]] |
725 | | path = "::my_filters::Js" |
726 | | extensions = ["js"] |
727 | | "#, |
728 | | None, |
729 | | None, |
730 | | None, |
731 | | None, |
732 | | ) |
733 | | .unwrap(); |
734 | | assert_eq!( |
735 | | config.escapers, |
736 | | vec![ |
737 | | (str_set(&["js"]), "::my_filters::Js".into()), |
738 | | ( |
739 | | str_set(&[ |
740 | | "askama", "html", "htm", "j2", "jinja", "jinja2", "rinja", "svg", "xml" |
741 | | ]), |
742 | | "askama::filters::Html".into() |
743 | | ), |
744 | | ( |
745 | | str_set(&["md", "none", "txt", "yml", ""]), |
746 | | "askama::filters::Text".into() |
747 | | ), |
748 | | ] |
749 | | ); |
750 | | } |
751 | | |
752 | | #[cfg(feature = "config")] |
753 | | #[test] |
754 | | fn test_whitespace_parsing() { |
755 | | let config = Config::new( |
756 | | r#" |
757 | | [general] |
758 | | whitespace = "suppress" |
759 | | "#, |
760 | | None, |
761 | | None, |
762 | | None, |
763 | | None, |
764 | | ) |
765 | | .unwrap(); |
766 | | assert_eq!(config.whitespace, Whitespace::Suppress); |
767 | | |
768 | | let config = Config::new(r#""#, None, None, None, None).unwrap(); |
769 | | assert_eq!(config.whitespace, Whitespace::Preserve); |
770 | | |
771 | | let config = Config::new( |
772 | | r#" |
773 | | [general] |
774 | | whitespace = "preserve" |
775 | | "#, |
776 | | None, |
777 | | None, |
778 | | None, |
779 | | None, |
780 | | ) |
781 | | .unwrap(); |
782 | | assert_eq!(config.whitespace, Whitespace::Preserve); |
783 | | |
784 | | let config = Config::new( |
785 | | r#" |
786 | | [general] |
787 | | whitespace = "minimize" |
788 | | "#, |
789 | | None, |
790 | | None, |
791 | | None, |
792 | | None, |
793 | | ) |
794 | | .unwrap(); |
795 | | assert_eq!(config.whitespace, Whitespace::Minimize); |
796 | | } |
797 | | |
798 | | #[cfg(feature = "config")] |
799 | | #[test] |
800 | | fn test_whitespace_in_template() { |
801 | | // Checking that template arguments have precedence over general configuration. |
802 | | // So in here, in the template arguments, there is `whitespace = "minimize"` so |
803 | | // the `Whitespace` should be `Minimize` as well. |
804 | | let config = Config::new( |
805 | | r#" |
806 | | [general] |
807 | | whitespace = "suppress" |
808 | | "#, |
809 | | None, |
810 | | Some(Whitespace::Minimize), |
811 | | None, |
812 | | None, |
813 | | ) |
814 | | .unwrap(); |
815 | | assert_eq!(config.whitespace, Whitespace::Minimize); |
816 | | |
817 | | let config = Config::new(r#""#, None, Some(Whitespace::Minimize), None, None).unwrap(); |
818 | | assert_eq!(config.whitespace, Whitespace::Minimize); |
819 | | } |
820 | | } |