1pub(crate) const FN_IDX: &str = "idx";
10
11pub(crate) const FN_LEN: &str = "len";
13
14pub(crate) const FN_KIND: &str = "kind";
16pub(crate) const FN_KIND_PREFIX: &str = "kind(";
18pub(crate) const FN_KINDS: &str = "kinds";
20
21pub(crate) const FN_HAS: &str = "has";
23
24pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_KINDS, FN_HAS];
29
30pub(crate) const FILTER_UPPER: &str = "upper";
34pub(crate) const FILTER_LOWER: &str = "lower";
36pub(crate) const FILTER_TRIM: &str = "trim";
38pub(crate) const FILTER_FIXED: &str = "fixed";
40pub(crate) const FILTER_JOIN: &str = "join";
42pub(crate) const FILTER_LIMIT: &str = "limit";
44pub(crate) const FILTER_ADD: &str = "add";
46pub(crate) const FILTER_SUB: &str = "sub";
48
49pub const ENUM_TAG_KEY: &str = "__kind__";
56pub const ENUM_VARIANTS_KEY: &str = "__variants__";
58
59pub const PAREN_OPEN: char = '(';
63pub const PAREN_CLOSE: char = ')';
65pub const PATH_SEP: char = '.';
67pub const PIPE: char = '|';
69pub const ANGLE_OPEN: char = '<';
71pub const ANGLE_CLOSE: char = '>';
73pub const BRACKET_OPEN: char = '[';
75pub const BRACKET_CLOSE: char = ']';
77pub const BRACE_OPEN: char = '{';
79pub const BRACE_CLOSE: char = '}';
81pub const COMMA: char = ',';
83pub const COLON: char = ':';
85pub const EQUALS: char = '=';
87pub const SLASH: char = '/';
89pub const PATH_PREFIX_SLASH: &str = "/";
91pub const PATH_PREFIX_CUR: &str = "./";
93pub const PATH_PREFIX_PARENT: &str = "../";
95pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
97pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
99pub const BACKSLASH: char = '\\';
101pub const CHAR_NEWLINE: char = '\n';
103pub const CHAR_CR: char = '\r';
105pub const CHAR_SPACE: char = ' ';
107pub const CHAR_TAB: char = '\t';
109
110pub const STR_NEWLINE: &str = "\n";
112pub const STR_DOUBLE_NEWLINE: &str = "\n\n";
114pub const STR_CRLF: &str = "\r\n";
116
117#[must_use]
119pub fn is_valid_resolved_path(path: &str) -> bool {
120 path.starts_with(PATH_PREFIX_SLASH)
121 || path.starts_with(PATH_PREFIX_CUR)
122 || path.starts_with(PATH_PREFIX_PARENT)
123 || path.starts_with(PATH_PREFIX_CUR_WIN)
124 || path.starts_with(PATH_PREFIX_PARENT_WIN)
125}
126
127#[must_use]
129pub fn is_valid_include_path(path: &str) -> bool {
130 is_valid_resolved_path(path) || path.starts_with(EXPR_START)
131}
132
133pub const EXT_TMPL_MD: &str = ".tmpl.md";
135pub const EXT_TMPL: &str = ".tmpl";
137pub const EXT_MD: &str = ".md";
139pub const LIST_BLOCK_SEP: &str = " - ";
141pub const LIST_ITEM_PREFIX: &str = "- ";
143pub const QUOTE_DOUBLE: char = '"';
145pub const QUOTE_SINGLE: char = '\'';
147
148pub const PAREN_OPEN_BYTE: u8 = b'(';
149pub const PAREN_CLOSE_BYTE: u8 = b')';
150pub const ANGLE_OPEN_BYTE: u8 = b'<';
151pub const ANGLE_CLOSE_BYTE: u8 = b'>';
152pub const BRACKET_OPEN_BYTE: u8 = b'[';
153pub const BRACKET_CLOSE_BYTE: u8 = b']';
154pub const BRACE_OPEN_BYTE: u8 = b'{';
155pub const BRACE_CLOSE_BYTE: u8 = b'}';
156pub const COLON_BYTE: u8 = b':';
157pub const EQUALS_BYTE: u8 = b'=';
158
159pub(crate) const EXPR_START: &str = "{{";
163pub(crate) const EXPR_END: &str = "}}";
165
166pub(crate) const STMT_START: &str = "{%";
168pub(crate) const STMT_END: &str = "%}";
170
171pub(crate) const COMMENT_START: &str = "{#";
173pub(crate) const COMMENT_END: &str = "#}";
175
176pub(crate) const TRIM_MARKER: char = '-';
178
179pub(crate) const TAG_FOR_PREFIX: &str = "for ";
183pub(crate) const KW_IN_SPACED: &str = " in ";
185
186pub(crate) const OP_EQ: &str = " == ";
187pub(crate) const OP_NE: &str = " != ";
188pub(crate) const OP_LE: &str = " <= ";
189pub(crate) const OP_GE: &str = " >= ";
190pub(crate) const OP_LT: &str = " < ";
191pub(crate) const OP_GT: &str = " > ";
192
193pub(crate) const OP_AND: &str = "&&";
195pub(crate) const OP_OR: &str = "||";
197pub(crate) const OP_NOT: char = '!';
199
200pub(crate) const TAG_IF_PREFIX: &str = "if ";
202pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
204pub(crate) const KW_ELSE: &str = "else";
206
207pub(crate) const KW_RAW: &str = "raw";
209pub(crate) const KW_RAW_ASSIGN: &str = "raw=";
211
212pub(crate) const KW_INCLUDE: &str = "include";
214pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
216pub(crate) const TAG_WITH_PREFIX: &str = "with ";
218pub(crate) const TAG_WITH_SPACED: &str = " with ";
220
221pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
223
224pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
226pub(crate) const TAG_CASE_PREFIX: &str = "case ";
228pub(crate) const TAG_CASE_SPACED: &str = " case";
230pub(crate) const KW_CASE_SPACED: &str = " case ";
232pub(crate) const VARIANT_SEP: char = '|';
234pub(crate) const KW_PANIC: &str = "panic";
235pub(crate) const TAG_PANIC_PREFIX: &str = "panic ";
236pub(crate) const TAG_PANIC_PAREN: &str = "panic(";
237
238pub(crate) const CLOSE_IF: &str = "/if";
242pub(crate) const CLOSE_FOR: &str = "/for";
244pub(crate) const CLOSE_RAW: &str = "/raw";
246pub(crate) const CLOSE_TMPL: &str = "/tmpl";
248pub(crate) const CLOSE_MATCH: &str = "/match";
250
251pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
255pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
257pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
259pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
261
262pub(crate) const FM_DELIMITER: &str = "---";
266pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
268
269pub(crate) const FM_NAME_PREFIX: &str = "name:";
271pub(crate) const FM_DESC_PREFIX: &str = "description:";
273pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
275pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
277pub(crate) const FM_TYPES_PREFIX: &str = "types:";
279pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
281pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
283pub(crate) const FM_ENV_PREFIX: &str = "env:";
285pub(crate) const FM_COMMENT_PREFIX: char = '#';
291
292pub(crate) const TYPE_STR: &str = "str";
296pub(crate) const TYPE_BOOL: &str = "bool";
298pub(crate) const TYPE_INT: &str = "int";
300pub(crate) const TYPE_FLOAT: &str = "float";
302pub(crate) const TYPE_LIST: &str = "list";
304pub(crate) const TYPE_STRUCT: &str = "struct";
306pub(crate) const TYPE_ENUM: &str = "enum";
308pub(crate) const TYPE_TMPL: &str = "tmpl";
310pub(crate) const TYPE_NONE: &str = "none";
312
313pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
315pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
317pub(crate) const TYPE_STRUCT_ANGLE_PREFIX: &str = "struct<";
319pub(crate) const TYPE_STRUCT_BRACKET_PREFIX: &str = "struct[";
321pub(crate) const TYPE_STRUCT_SPACE_PREFIX: &str = "struct ";
323pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
325pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
327pub(crate) const TYPE_OPTION: &str = "option";
329pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
331
332pub const OPTION_SOME: &str = "Some";
334pub const OPTION_NONE: &str = "None";
336pub const OPTION_VAL_FIELD: &str = "val";
338pub const MATCH_DEFAULT: &str = "_";
340
341pub(crate) const PREFIX_CONSTS_DOT: &str = "consts.";
345pub(crate) const PREFIX_OPTS_DOT: &str = "opts.";
347pub(crate) const PREFIX_OPTIONS_DOT: &str = "options.";
349pub(crate) const PREFIX_PARAMS_DOT: &str = "params.";
351
352pub(crate) const LIT_TRUE: &str = "true";
356pub(crate) const LIT_FALSE: &str = "false";
358
359#[must_use]
364pub fn strip_string_literal(token: &str) -> Option<&str> {
365 if token.len() >= 2
366 && ((token.starts_with(QUOTE_DOUBLE) && token.ends_with(QUOTE_DOUBLE))
367 || (token.starts_with(QUOTE_SINGLE) && token.ends_with(QUOTE_SINGLE)))
368 {
369 return Some(&token[1..token.len() - 1]);
370 }
371 None
372}
373
374#[must_use]
386pub fn unescape_string_literal(inner: &str) -> alloc::string::String {
387 use alloc::string::{String, ToString};
388 if !inner.contains(BACKSLASH) {
390 return inner.to_string();
391 }
392 let mut out = String::with_capacity(inner.len());
393 let mut chars = inner.chars();
394 while let Some(c) = chars.next() {
395 if c == BACKSLASH {
396 match chars.next() {
397 Some(QUOTE_DOUBLE) => out.push(QUOTE_DOUBLE),
398 Some(QUOTE_SINGLE) => out.push(QUOTE_SINGLE),
399 Some(BACKSLASH) | None => out.push(BACKSLASH),
401 Some(other) => {
403 out.push(BACKSLASH);
404 out.push(other);
405 }
406 }
407 } else {
408 out.push(c);
409 }
410 }
411 out
412}
413
414pub(crate) const ERR_MISSING_FM: &str =
418 "missing mandatory YAML frontmatter block (starts with ---)";
419pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
421pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
423
424pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
426pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
428pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
430pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
432pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
434 "type alias name conflicts with parameter name (PascalCase collision)";
435#[cfg(feature = "std")]
437pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
438pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
440pub(crate) const ERR_PARAM_SHADOWS_IMPORT: &str =
442 "parameter name (PascalCase) shadows import alias";
443pub(crate) const ERR_UNUSED_TYPE_ALIAS: &str = "unused type alias";
444pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
446pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
448pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
450pub(crate) const ERR_BARE_STMT_TAG: &str =
452 "statement tag at line start must be blockquote-prefixed with '> '";
453pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
455 "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
456
457pub(crate) const RESERVED_NAMES: &[&str] = &[
468 TYPE_LIST,
470 TYPE_STRUCT,
471 TYPE_ENUM,
472 TYPE_TMPL,
473 TYPE_OPTION,
474 TYPE_STR,
475 TYPE_INT,
476 TYPE_FLOAT,
477 TYPE_BOOL,
478 "params",
480 LIT_TRUE,
482 LIT_FALSE,
483 OPTION_SOME,
484 OPTION_NONE,
485 MATCH_DEFAULT,
486 ENUM_TAG_KEY,
488 ENUM_VARIANTS_KEY,
489 "__self",
493 "__Self",
494 "__super",
495 "__crate",
496];
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
505 fn strip_double_quoted_string() {
506 assert_eq!(strip_string_literal("\"hello\""), Some("hello"));
507 }
508
509 #[test]
510 fn strip_single_quoted_string() {
511 assert_eq!(strip_string_literal("'world'"), Some("world"));
512 }
513
514 #[test]
515 fn strip_empty_double_quoted_string() {
516 assert_eq!(strip_string_literal("\"\""), Some(""));
517 }
518
519 #[test]
520 fn strip_empty_single_quoted_string() {
521 assert_eq!(strip_string_literal("''"), Some(""));
522 }
523
524 #[test]
525 fn strip_unquoted_string_returns_none() {
526 assert_eq!(strip_string_literal("hello"), None);
527 }
528
529 #[test]
530 fn strip_mismatched_quotes_returns_none() {
531 assert_eq!(strip_string_literal("\"hello'"), None);
532 assert_eq!(strip_string_literal("'hello\""), None);
533 }
534
535 #[test]
536 fn strip_single_quote_char_returns_none() {
537 assert_eq!(strip_string_literal("\""), None);
538 assert_eq!(strip_string_literal("'"), None);
539 }
540
541 #[test]
542 fn strip_empty_input_returns_none() {
543 assert_eq!(strip_string_literal(""), None);
544 }
545
546 #[test]
547 fn strip_quoted_string_with_spaces() {
548 assert_eq!(strip_string_literal("\"hello world\""), Some("hello world"));
549 assert_eq!(strip_string_literal("'hello world'"), Some("hello world"));
550 }
551
552 #[test]
553 fn strip_quoted_string_with_inner_quotes() {
554 assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
556 assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
557 }
558
559 #[test]
562 fn reserved_names_contains_type_names() {
563 for name in &[
564 "list", "struct", "enum", "tmpl", "option", "str", "int", "float", "bool",
565 ] {
566 assert!(
567 RESERVED_NAMES.contains(name),
568 "{name} should be in RESERVED_NAMES"
569 );
570 }
571 }
572
573 #[test]
574 fn reserved_names_contains_params() {
575 assert!(RESERVED_NAMES.contains(&"params"));
576 }
577
578 #[test]
579 fn reserved_names_contains_pattern_keywords() {
580 for name in &["true", "false", "Some", "None", "_"] {
581 assert!(
582 RESERVED_NAMES.contains(name),
583 "{name} should be in RESERVED_NAMES"
584 );
585 }
586 }
587
588 #[test]
589 fn reserved_names_contains_internal_keys() {
590 for name in &[ENUM_TAG_KEY, ENUM_VARIANTS_KEY] {
591 assert!(
592 RESERVED_NAMES.contains(name),
593 "{name} should be in RESERVED_NAMES"
594 );
595 }
596 }
597
598 #[test]
601 fn builtin_functions_contains_expected_entries() {
602 assert!(BUILTIN_FUNCTIONS.contains(&"idx"));
603 assert!(BUILTIN_FUNCTIONS.contains(&"len"));
604 assert!(BUILTIN_FUNCTIONS.contains(&"kind"));
605 assert!(BUILTIN_FUNCTIONS.contains(&"kinds"));
606 assert!(BUILTIN_FUNCTIONS.contains(&"has"));
607 }
608
609 #[test]
610 fn builtin_functions_length() {
611 assert_eq!(BUILTIN_FUNCTIONS.len(), 5);
612 }
613
614 #[test]
617 fn expr_delimiters() {
618 assert_eq!(EXPR_START, "{{");
619 assert_eq!(EXPR_END, "}}");
620 }
621
622 #[test]
623 fn stmt_delimiters() {
624 assert_eq!(STMT_START, "{%");
625 assert_eq!(STMT_END, "%}");
626 }
627
628 #[test]
629 fn comment_delimiters() {
630 assert_eq!(COMMENT_START, "{#");
631 assert_eq!(COMMENT_END, "#}");
632 }
633
634 #[test]
635 fn closing_block_tags() {
636 assert_eq!(CLOSE_IF, "/if");
637 assert_eq!(CLOSE_FOR, "/for");
638 assert_eq!(CLOSE_RAW, "/raw");
639 assert_eq!(CLOSE_TMPL, "/tmpl");
640 assert_eq!(CLOSE_MATCH, "/match");
641 }
642
643 #[test]
644 fn frontmatter_delimiter() {
645 assert_eq!(FM_DELIMITER, "---");
646 }
647
648 #[test]
649 fn enum_tag_key_value() {
650 assert_eq!(ENUM_TAG_KEY, "__kind__");
651 }
652
653 #[test]
654 fn syntax_chars() {
655 assert_eq!(PAREN_OPEN, '(');
656 assert_eq!(PAREN_CLOSE, ')');
657 assert_eq!(PATH_SEP, '.');
658 assert_eq!(PIPE, '|');
659 assert_eq!(QUOTE_DOUBLE, '"');
660 assert_eq!(QUOTE_SINGLE, '\'');
661 }
662
663 #[test]
664 fn test_is_valid_include_path() {
665 assert!(is_valid_include_path("./file.tmpl.md"));
666 assert!(is_valid_include_path("../file.tmpl.md"));
667 assert!(is_valid_include_path(".\\file.tmpl.md"));
668 assert!(is_valid_include_path("..\\file.tmpl.md"));
669 assert!(is_valid_include_path("/file.tmpl.md"));
670 assert!(is_valid_include_path("{{ consts.DIR }}/file.tmpl.md"));
671 assert!(!is_valid_include_path("file.tmpl.md"));
672 assert!(!is_valid_include_path("sub/file.tmpl.md"));
673
674 assert!(is_valid_resolved_path("./file.tmpl.md"));
675 assert!(!is_valid_resolved_path("{{ consts.DIR }}/file.tmpl.md"));
676 }
677}