1pub(crate) const FN_IDX: &str = "idx";
10
11pub(crate) const FN_LEN: &str = "len";
13
14pub(crate) const FN_KIND: &str = "kind";
16
17pub(crate) const FN_HAS: &str = "has";
19
20pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_HAS];
25
26pub(crate) const FILTER_UPPER: &str = "upper";
30pub(crate) const FILTER_LOWER: &str = "lower";
32pub(crate) const FILTER_TRIM: &str = "trim";
34pub(crate) const FILTER_FIXED: &str = "fixed";
36pub(crate) const FILTER_JOIN: &str = "join";
38pub(crate) const FILTER_LIMIT: &str = "limit";
40pub(crate) const FILTER_ADD: &str = "add";
42pub(crate) const FILTER_SUB: &str = "sub";
44
45pub const ENUM_TAG_KEY: &str = "__kind__";
52
53pub(crate) const PSEUDO_FIELD_LENGTH: &str = ".length";
55
56pub const PAREN_OPEN: char = '(';
60pub const PAREN_CLOSE: char = ')';
62pub const PATH_SEP: char = '.';
64pub const PIPE: char = '|';
66pub const ANGLE_OPEN: char = '<';
68pub const ANGLE_CLOSE: char = '>';
70pub const BRACKET_OPEN: char = '[';
72pub const BRACKET_CLOSE: char = ']';
74pub const BRACE_OPEN: char = '{';
76pub const BRACE_CLOSE: char = '}';
78pub const COMMA: char = ',';
80pub const COLON: char = ':';
82pub const EQUALS: char = '=';
84pub const SLASH: char = '/';
86pub const PATH_PREFIX_SLASH: &str = "/";
88pub const PATH_PREFIX_CUR: &str = "./";
90pub const PATH_PREFIX_PARENT: &str = "../";
92pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
94pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
96pub const BACKSLASH: char = '\\';
98
99#[must_use]
101pub fn is_valid_include_path(path: &str) -> bool {
102 path.starts_with(PATH_PREFIX_SLASH)
103 || path.starts_with(PATH_PREFIX_CUR)
104 || path.starts_with(PATH_PREFIX_PARENT)
105 || path.starts_with(PATH_PREFIX_CUR_WIN)
106 || path.starts_with(PATH_PREFIX_PARENT_WIN)
107}
108
109pub const EXT_TMPL_MD: &str = ".tmpl.md";
111pub const EXT_TMPL: &str = ".tmpl";
113pub const EXT_MD: &str = ".md";
115pub const LIST_BLOCK_SEP: &str = " - ";
117pub const LIST_ITEM_PREFIX: &str = "- ";
119pub const QUOTE_DOUBLE: char = '"';
121pub const QUOTE_SINGLE: char = '\'';
123
124pub const PAREN_OPEN_BYTE: u8 = b'(';
125pub const PAREN_CLOSE_BYTE: u8 = b')';
126pub const ANGLE_OPEN_BYTE: u8 = b'<';
127pub const ANGLE_CLOSE_BYTE: u8 = b'>';
128pub const BRACKET_OPEN_BYTE: u8 = b'[';
129pub const BRACKET_CLOSE_BYTE: u8 = b']';
130pub const BRACE_OPEN_BYTE: u8 = b'{';
131pub const BRACE_CLOSE_BYTE: u8 = b'}';
132pub const COLON_BYTE: u8 = b':';
133pub const EQUALS_BYTE: u8 = b'=';
134
135pub(crate) const EXPR_START: &str = "{{";
139pub(crate) const EXPR_END: &str = "}}";
141
142pub(crate) const STMT_START: &str = "{%";
144pub(crate) const STMT_END: &str = "%}";
146
147pub(crate) const COMMENT_START: &str = "{#";
149pub(crate) const COMMENT_END: &str = "#}";
151
152pub(crate) const TRIM_MARKER: char = '-';
154
155pub(crate) const TAG_FOR_PREFIX: &str = "for ";
159pub(crate) const KW_IN_SPACED: &str = " in ";
161
162pub(crate) const TAG_IF_PREFIX: &str = "if ";
164pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
166pub(crate) const KW_ELSE: &str = "else";
168
169pub(crate) const KW_RAW: &str = "raw";
171pub(crate) const KW_RAW_ASSIGN: &str = "raw=";
173
174pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
176pub(crate) const TAG_WITH_PREFIX: &str = "with ";
178pub(crate) const TAG_WITH_SPACED: &str = " with ";
180
181pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
183
184pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
186pub(crate) const TAG_CASE_PREFIX: &str = "case ";
188
189pub(crate) const CLOSE_IF: &str = "/if";
193pub(crate) const CLOSE_FOR: &str = "/for";
195pub(crate) const CLOSE_RAW: &str = "/raw";
197pub(crate) const CLOSE_TMPL: &str = "/tmpl";
199pub(crate) const CLOSE_MATCH: &str = "/match";
201
202pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
206pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
208pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
210pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
212
213pub(crate) const FM_DELIMITER: &str = "---";
217pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
219
220pub(crate) const FM_NAME_PREFIX: &str = "name:";
222pub(crate) const FM_DESC_PREFIX: &str = "description:";
224pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
226pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
228pub(crate) const FM_TYPES_PREFIX: &str = "types:";
230pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
232pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
234
235pub(crate) const TYPE_STR: &str = "str";
239pub(crate) const TYPE_BOOL: &str = "bool";
241pub(crate) const TYPE_INT: &str = "int";
243pub(crate) const TYPE_FLOAT: &str = "float";
245pub(crate) const TYPE_LIST: &str = "list";
247pub(crate) const TYPE_STRUCT: &str = "struct";
249pub(crate) const TYPE_ENUM: &str = "enum";
251pub(crate) const TYPE_TMPL: &str = "tmpl";
253
254pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
256pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
258pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
260pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
262pub(crate) const TYPE_OPTION: &str = "option";
264pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
266
267pub const OPTION_SOME: &str = "Some";
269pub const OPTION_NONE: &str = "None";
271pub const OPTION_VAL_FIELD: &str = "val";
273
274pub(crate) const LIT_TRUE: &str = "true";
278pub(crate) const LIT_FALSE: &str = "false";
280
281#[must_use]
286pub fn strip_string_literal(token: &str) -> Option<&str> {
287 if token.len() >= 2
288 && ((token.starts_with(QUOTE_DOUBLE) && token.ends_with(QUOTE_DOUBLE))
289 || (token.starts_with(QUOTE_SINGLE) && token.ends_with(QUOTE_SINGLE)))
290 {
291 return Some(&token[1..token.len() - 1]);
292 }
293 None
294}
295
296pub(crate) const ERR_MISSING_FM: &str =
300 "missing mandatory YAML frontmatter block (starts with ---)";
301pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
303pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
305
306pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
308pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
310pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
312pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
314pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
316 "type alias name conflicts with parameter name (PascalCase collision)";
317#[cfg(feature = "std")]
319pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
320pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
322pub(crate) const ERR_PARAM_SHADOWS_IMPORT: &str =
324 "parameter name (PascalCase) shadows import alias";
325pub(crate) const ERR_UNUSED_TYPE_ALIAS: &str = "unused type alias";
326pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
328pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
330pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
332pub(crate) const ERR_BARE_STMT_TAG: &str =
334 "statement tag at line start must be blockquote-prefixed with '> '";
335pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
337 "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
338
339pub(crate) const RESERVED_NAMES: &[&str] = &[
341 TYPE_LIST,
342 TYPE_STRUCT,
343 TYPE_ENUM,
344 TYPE_TMPL,
345 TYPE_OPTION,
346 "params",
347 TYPE_STR,
348 TYPE_INT,
349 TYPE_FLOAT,
350 TYPE_BOOL,
351];
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
360 fn strip_double_quoted_string() {
361 assert_eq!(strip_string_literal("\"hello\""), Some("hello"));
362 }
363
364 #[test]
365 fn strip_single_quoted_string() {
366 assert_eq!(strip_string_literal("'world'"), Some("world"));
367 }
368
369 #[test]
370 fn strip_empty_double_quoted_string() {
371 assert_eq!(strip_string_literal("\"\""), Some(""));
372 }
373
374 #[test]
375 fn strip_empty_single_quoted_string() {
376 assert_eq!(strip_string_literal("''"), Some(""));
377 }
378
379 #[test]
380 fn strip_unquoted_string_returns_none() {
381 assert_eq!(strip_string_literal("hello"), None);
382 }
383
384 #[test]
385 fn strip_mismatched_quotes_returns_none() {
386 assert_eq!(strip_string_literal("\"hello'"), None);
387 assert_eq!(strip_string_literal("'hello\""), None);
388 }
389
390 #[test]
391 fn strip_single_quote_char_returns_none() {
392 assert_eq!(strip_string_literal("\""), None);
393 assert_eq!(strip_string_literal("'"), None);
394 }
395
396 #[test]
397 fn strip_empty_input_returns_none() {
398 assert_eq!(strip_string_literal(""), None);
399 }
400
401 #[test]
402 fn strip_quoted_string_with_spaces() {
403 assert_eq!(strip_string_literal("\"hello world\""), Some("hello world"));
404 assert_eq!(strip_string_literal("'hello world'"), Some("hello world"));
405 }
406
407 #[test]
408 fn strip_quoted_string_with_inner_quotes() {
409 assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
411 assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
412 }
413
414 #[test]
417 fn reserved_names_contains_type_names() {
418 for name in &[
419 "list", "struct", "enum", "tmpl", "option", "str", "int", "float", "bool",
420 ] {
421 assert!(
422 RESERVED_NAMES.contains(name),
423 "{name} should be in RESERVED_NAMES"
424 );
425 }
426 }
427
428 #[test]
429 fn reserved_names_contains_params() {
430 assert!(RESERVED_NAMES.contains(&"params"));
431 }
432
433 #[test]
436 fn builtin_functions_contains_expected_entries() {
437 assert!(BUILTIN_FUNCTIONS.contains(&"idx"));
438 assert!(BUILTIN_FUNCTIONS.contains(&"len"));
439 assert!(BUILTIN_FUNCTIONS.contains(&"kind"));
440 assert!(BUILTIN_FUNCTIONS.contains(&"has"));
441 }
442
443 #[test]
444 fn builtin_functions_length() {
445 assert_eq!(BUILTIN_FUNCTIONS.len(), 4);
446 }
447
448 #[test]
451 fn expr_delimiters() {
452 assert_eq!(EXPR_START, "{{");
453 assert_eq!(EXPR_END, "}}");
454 }
455
456 #[test]
457 fn stmt_delimiters() {
458 assert_eq!(STMT_START, "{%");
459 assert_eq!(STMT_END, "%}");
460 }
461
462 #[test]
463 fn comment_delimiters() {
464 assert_eq!(COMMENT_START, "{#");
465 assert_eq!(COMMENT_END, "#}");
466 }
467
468 #[test]
469 fn closing_block_tags() {
470 assert_eq!(CLOSE_IF, "/if");
471 assert_eq!(CLOSE_FOR, "/for");
472 assert_eq!(CLOSE_RAW, "/raw");
473 assert_eq!(CLOSE_TMPL, "/tmpl");
474 assert_eq!(CLOSE_MATCH, "/match");
475 }
476
477 #[test]
478 fn frontmatter_delimiter() {
479 assert_eq!(FM_DELIMITER, "---");
480 }
481
482 #[test]
483 fn enum_tag_key_value() {
484 assert_eq!(ENUM_TAG_KEY, "__kind__");
485 }
486
487 #[test]
488 fn syntax_chars() {
489 assert_eq!(PAREN_OPEN, '(');
490 assert_eq!(PAREN_CLOSE, ')');
491 assert_eq!(PATH_SEP, '.');
492 assert_eq!(PIPE, '|');
493 assert_eq!(QUOTE_DOUBLE, '"');
494 assert_eq!(QUOTE_SINGLE, '\'');
495 }
496
497 #[test]
498 fn test_is_valid_include_path() {
499 assert!(is_valid_include_path("./file.tmpl.md"));
500 assert!(is_valid_include_path("../file.tmpl.md"));
501 assert!(is_valid_include_path(".\\file.tmpl.md"));
502 assert!(is_valid_include_path("..\\file.tmpl.md"));
503 assert!(is_valid_include_path("/file.tmpl.md"));
504 assert!(!is_valid_include_path("file.tmpl.md"));
505 assert!(!is_valid_include_path("sub/file.tmpl.md"));
506 }
507}