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_KINDS: &str = "kinds";
18
19pub(crate) const FN_HAS: &str = "has";
21
22pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_KINDS, FN_HAS];
27
28pub(crate) const FILTER_UPPER: &str = "upper";
32pub(crate) const FILTER_LOWER: &str = "lower";
34pub(crate) const FILTER_TRIM: &str = "trim";
36pub(crate) const FILTER_FIXED: &str = "fixed";
38pub(crate) const FILTER_JOIN: &str = "join";
40pub(crate) const FILTER_LIMIT: &str = "limit";
42pub(crate) const FILTER_ADD: &str = "add";
44pub(crate) const FILTER_SUB: &str = "sub";
46
47pub const ENUM_TAG_KEY: &str = "__kind__";
54pub const ENUM_VARIANTS_KEY: &str = "__variants__";
56
57pub const PAREN_OPEN: char = '(';
61pub const PAREN_CLOSE: char = ')';
63pub const PATH_SEP: char = '.';
65pub const PIPE: char = '|';
67pub const ANGLE_OPEN: char = '<';
69pub const ANGLE_CLOSE: char = '>';
71pub const BRACKET_OPEN: char = '[';
73pub const BRACKET_CLOSE: char = ']';
75pub const BRACE_OPEN: char = '{';
77pub const BRACE_CLOSE: char = '}';
79pub const COMMA: char = ',';
81pub const COLON: char = ':';
83pub const EQUALS: char = '=';
85pub const SLASH: char = '/';
87pub const PATH_PREFIX_SLASH: &str = "/";
89pub const PATH_PREFIX_CUR: &str = "./";
91pub const PATH_PREFIX_PARENT: &str = "../";
93pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
95pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
97pub const BACKSLASH: char = '\\';
99pub const CHAR_NEWLINE: char = '\n';
101pub const CHAR_CR: char = '\r';
103pub const CHAR_SPACE: char = ' ';
105pub const CHAR_TAB: char = '\t';
107
108pub const STR_NEWLINE: &str = "\n";
110pub const STR_DOUBLE_NEWLINE: &str = "\n\n";
112pub const STR_CRLF: &str = "\r\n";
114
115#[must_use]
117pub fn is_valid_resolved_path(path: &str) -> bool {
118 path.starts_with(PATH_PREFIX_SLASH)
119 || path.starts_with(PATH_PREFIX_CUR)
120 || path.starts_with(PATH_PREFIX_PARENT)
121 || path.starts_with(PATH_PREFIX_CUR_WIN)
122 || path.starts_with(PATH_PREFIX_PARENT_WIN)
123}
124
125#[must_use]
127pub fn is_valid_include_path(path: &str) -> bool {
128 is_valid_resolved_path(path) || path.starts_with(EXPR_START)
129}
130
131pub const EXT_TMPL_MD: &str = ".tmpl.md";
133pub const EXT_TMPL: &str = ".tmpl";
135pub const EXT_MD: &str = ".md";
137pub const LIST_BLOCK_SEP: &str = " - ";
139pub const LIST_ITEM_PREFIX: &str = "- ";
141pub const QUOTE_DOUBLE: char = '"';
143pub const QUOTE_SINGLE: char = '\'';
145
146pub const PAREN_OPEN_BYTE: u8 = b'(';
147pub const PAREN_CLOSE_BYTE: u8 = b')';
148pub const ANGLE_OPEN_BYTE: u8 = b'<';
149pub const ANGLE_CLOSE_BYTE: u8 = b'>';
150pub const BRACKET_OPEN_BYTE: u8 = b'[';
151pub const BRACKET_CLOSE_BYTE: u8 = b']';
152pub const BRACE_OPEN_BYTE: u8 = b'{';
153pub const BRACE_CLOSE_BYTE: u8 = b'}';
154pub const COLON_BYTE: u8 = b':';
155pub const EQUALS_BYTE: u8 = b'=';
156
157pub(crate) const EXPR_START: &str = "{{";
161pub(crate) const EXPR_END: &str = "}}";
163
164pub(crate) const STMT_START: &str = "{%";
166pub(crate) const STMT_END: &str = "%}";
168
169pub(crate) const COMMENT_START: &str = "{#";
171pub(crate) const COMMENT_END: &str = "#}";
173#[allow(dead_code)]
174pub(crate) const COMMENT_COMPACT_OPEN: &str = ">{#";
175#[allow(dead_code)]
176pub(crate) const COMMENT_SPACED_OPEN: &str = "> {#";
177#[allow(dead_code)]
178pub(crate) const FRONTMATTER_DELIM: &str = "---";
179#[allow(dead_code)]
180pub(crate) const STMT_START_SHORT: &str = "{%";
181
182pub(crate) const TRIM_MARKER: char = '-';
184
185pub(crate) const TAG_FOR_PREFIX: &str = "for ";
189pub(crate) const KW_IN_SPACED: &str = " in ";
191
192pub(crate) const OP_EQ: &str = " == ";
193pub(crate) const OP_NE: &str = " != ";
194pub(crate) const OP_LE: &str = " <= ";
195pub(crate) const OP_GE: &str = " >= ";
196pub(crate) const OP_LT: &str = " < ";
197pub(crate) const OP_GT: &str = " > ";
198
199pub(crate) const OP_AND: &str = "&&";
201pub(crate) const OP_OR: &str = "||";
203pub(crate) const OP_NOT: char = '!';
205
206pub(crate) const TAG_IF_PREFIX: &str = "if ";
208pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
210pub(crate) const KW_ELSE: &str = "else";
212
213pub(crate) const KW_RAW: &str = "raw";
215pub(crate) const KW_RAW_ASSIGN: &str = "raw=";
217#[allow(dead_code)]
218pub(crate) const KW_RAW_SPACED: &str = "raw ";
219#[allow(dead_code)]
220pub(crate) const KW_RAW_ASSIGN_SPACED: &str = "raw = ";
221#[allow(dead_code)]
222pub(crate) const CLOSE_RAW_TRIM: &str = "-/raw";
223#[allow(dead_code)]
224pub(crate) const KW_RAW_CLOSE_SPACED: &str = "raw%}";
225
226pub(crate) const KW_INCLUDE: &str = "include";
228pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
230pub(crate) const TAG_WITH_PREFIX: &str = "with ";
232pub(crate) const TAG_WITH_SPACED: &str = " with ";
234
235pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
237
238pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
240pub(crate) const TAG_CASE_PREFIX: &str = "case ";
242pub(crate) const TAG_CASE_SPACED: &str = " case";
244pub(crate) const KW_CASE_SPACED: &str = " case ";
246pub(crate) const VARIANT_SEP: char = '|';
248pub(crate) const KW_PANIC: &str = "panic";
249pub(crate) const TAG_PANIC_PREFIX: &str = "panic ";
250pub(crate) const TAG_PANIC_PAREN: &str = "panic(";
251
252pub(crate) const CLOSE_IF: &str = "/if";
256pub(crate) const CLOSE_FOR: &str = "/for";
258pub(crate) const CLOSE_RAW: &str = "/raw";
260pub(crate) const CLOSE_TMPL: &str = "/tmpl";
262pub(crate) const CLOSE_MATCH: &str = "/match";
264
265pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
269pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
271pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
273pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
275
276pub(crate) const FM_DELIMITER: &str = "---";
280pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
282
283pub(crate) const FM_NAME_PREFIX: &str = "name:";
285pub(crate) const FM_DESC_PREFIX: &str = "description:";
287pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
289pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
291pub(crate) const FM_TYPES_PREFIX: &str = "types:";
293pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
295pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
297
298pub(crate) const TYPE_STR: &str = "str";
302pub(crate) const TYPE_BOOL: &str = "bool";
304pub(crate) const TYPE_INT: &str = "int";
306pub(crate) const TYPE_FLOAT: &str = "float";
308pub(crate) const TYPE_LIST: &str = "list";
310pub(crate) const TYPE_STRUCT: &str = "struct";
312pub(crate) const TYPE_ENUM: &str = "enum";
314pub(crate) const TYPE_TMPL: &str = "tmpl";
316pub(crate) const TYPE_NONE: &str = "none";
318
319pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
321pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
323pub(crate) const TYPE_STRUCT_ANGLE_PREFIX: &str = "struct<";
325pub(crate) const TYPE_STRUCT_BRACKET_PREFIX: &str = "struct[";
327pub(crate) const TYPE_STRUCT_SPACE_PREFIX: &str = "struct ";
329pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
331pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
333pub(crate) const TYPE_OPTION: &str = "option";
335pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
337
338pub const OPTION_SOME: &str = "Some";
340pub const OPTION_NONE: &str = "None";
342pub const OPTION_VAL_FIELD: &str = "val";
344pub const MATCH_DEFAULT: &str = "_";
346
347pub(crate) const PREFIX_CONSTS_DOT: &str = "consts.";
351pub(crate) const PREFIX_OPTS_DOT: &str = "opts.";
353pub(crate) const PREFIX_OPTIONS_DOT: &str = "options.";
355pub(crate) const PREFIX_PARAMS_DOT: &str = "params.";
357
358pub(crate) const LIT_TRUE: &str = "true";
362pub(crate) const LIT_FALSE: &str = "false";
364
365#[must_use]
370pub fn strip_string_literal(token: &str) -> Option<&str> {
371 if token.len() >= 2
372 && ((token.starts_with(QUOTE_DOUBLE) && token.ends_with(QUOTE_DOUBLE))
373 || (token.starts_with(QUOTE_SINGLE) && token.ends_with(QUOTE_SINGLE)))
374 {
375 return Some(&token[1..token.len() - 1]);
376 }
377 None
378}
379
380pub(crate) const ERR_MISSING_FM: &str =
384 "missing mandatory YAML frontmatter block (starts with ---)";
385pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
387pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
389
390pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
392pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
394pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
396pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
398pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
400 "type alias name conflicts with parameter name (PascalCase collision)";
401#[cfg(feature = "std")]
403pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
404pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
406pub(crate) const ERR_PARAM_SHADOWS_IMPORT: &str =
408 "parameter name (PascalCase) shadows import alias";
409pub(crate) const ERR_UNUSED_TYPE_ALIAS: &str = "unused type alias";
410pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
412pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
414pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
416pub(crate) const ERR_BARE_STMT_TAG: &str =
418 "statement tag at line start must be blockquote-prefixed with '> '";
419pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
421 "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
422
423pub(crate) const RESERVED_NAMES: &[&str] = &[
425 TYPE_LIST,
426 TYPE_STRUCT,
427 TYPE_ENUM,
428 TYPE_TMPL,
429 TYPE_OPTION,
430 "params",
431 TYPE_STR,
432 TYPE_INT,
433 TYPE_FLOAT,
434 TYPE_BOOL,
435];
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
444 fn strip_double_quoted_string() {
445 assert_eq!(strip_string_literal("\"hello\""), Some("hello"));
446 }
447
448 #[test]
449 fn strip_single_quoted_string() {
450 assert_eq!(strip_string_literal("'world'"), Some("world"));
451 }
452
453 #[test]
454 fn strip_empty_double_quoted_string() {
455 assert_eq!(strip_string_literal("\"\""), Some(""));
456 }
457
458 #[test]
459 fn strip_empty_single_quoted_string() {
460 assert_eq!(strip_string_literal("''"), Some(""));
461 }
462
463 #[test]
464 fn strip_unquoted_string_returns_none() {
465 assert_eq!(strip_string_literal("hello"), None);
466 }
467
468 #[test]
469 fn strip_mismatched_quotes_returns_none() {
470 assert_eq!(strip_string_literal("\"hello'"), None);
471 assert_eq!(strip_string_literal("'hello\""), None);
472 }
473
474 #[test]
475 fn strip_single_quote_char_returns_none() {
476 assert_eq!(strip_string_literal("\""), None);
477 assert_eq!(strip_string_literal("'"), None);
478 }
479
480 #[test]
481 fn strip_empty_input_returns_none() {
482 assert_eq!(strip_string_literal(""), None);
483 }
484
485 #[test]
486 fn strip_quoted_string_with_spaces() {
487 assert_eq!(strip_string_literal("\"hello world\""), Some("hello world"));
488 assert_eq!(strip_string_literal("'hello world'"), Some("hello world"));
489 }
490
491 #[test]
492 fn strip_quoted_string_with_inner_quotes() {
493 assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
495 assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
496 }
497
498 #[test]
501 fn reserved_names_contains_type_names() {
502 for name in &[
503 "list", "struct", "enum", "tmpl", "option", "str", "int", "float", "bool",
504 ] {
505 assert!(
506 RESERVED_NAMES.contains(name),
507 "{name} should be in RESERVED_NAMES"
508 );
509 }
510 }
511
512 #[test]
513 fn reserved_names_contains_params() {
514 assert!(RESERVED_NAMES.contains(&"params"));
515 }
516
517 #[test]
520 fn builtin_functions_contains_expected_entries() {
521 assert!(BUILTIN_FUNCTIONS.contains(&"idx"));
522 assert!(BUILTIN_FUNCTIONS.contains(&"len"));
523 assert!(BUILTIN_FUNCTIONS.contains(&"kind"));
524 assert!(BUILTIN_FUNCTIONS.contains(&"kinds"));
525 assert!(BUILTIN_FUNCTIONS.contains(&"has"));
526 }
527
528 #[test]
529 fn builtin_functions_length() {
530 assert_eq!(BUILTIN_FUNCTIONS.len(), 5);
531 }
532
533 #[test]
536 fn expr_delimiters() {
537 assert_eq!(EXPR_START, "{{");
538 assert_eq!(EXPR_END, "}}");
539 }
540
541 #[test]
542 fn stmt_delimiters() {
543 assert_eq!(STMT_START, "{%");
544 assert_eq!(STMT_END, "%}");
545 }
546
547 #[test]
548 fn comment_delimiters() {
549 assert_eq!(COMMENT_START, "{#");
550 assert_eq!(COMMENT_END, "#}");
551 }
552
553 #[test]
554 fn closing_block_tags() {
555 assert_eq!(CLOSE_IF, "/if");
556 assert_eq!(CLOSE_FOR, "/for");
557 assert_eq!(CLOSE_RAW, "/raw");
558 assert_eq!(CLOSE_TMPL, "/tmpl");
559 assert_eq!(CLOSE_MATCH, "/match");
560 }
561
562 #[test]
563 fn frontmatter_delimiter() {
564 assert_eq!(FM_DELIMITER, "---");
565 }
566
567 #[test]
568 fn enum_tag_key_value() {
569 assert_eq!(ENUM_TAG_KEY, "__kind__");
570 }
571
572 #[test]
573 fn syntax_chars() {
574 assert_eq!(PAREN_OPEN, '(');
575 assert_eq!(PAREN_CLOSE, ')');
576 assert_eq!(PATH_SEP, '.');
577 assert_eq!(PIPE, '|');
578 assert_eq!(QUOTE_DOUBLE, '"');
579 assert_eq!(QUOTE_SINGLE, '\'');
580 }
581
582 #[test]
583 fn test_is_valid_include_path() {
584 assert!(is_valid_include_path("./file.tmpl.md"));
585 assert!(is_valid_include_path("../file.tmpl.md"));
586 assert!(is_valid_include_path(".\\file.tmpl.md"));
587 assert!(is_valid_include_path("..\\file.tmpl.md"));
588 assert!(is_valid_include_path("/file.tmpl.md"));
589 assert!(is_valid_include_path("{{ consts.DIR }}/file.tmpl.md"));
590 assert!(!is_valid_include_path("file.tmpl.md"));
591 assert!(!is_valid_include_path("sub/file.tmpl.md"));
592
593 assert!(is_valid_resolved_path("./file.tmpl.md"));
594 assert!(!is_valid_resolved_path("{{ consts.DIR }}/file.tmpl.md"));
595 }
596}