Skip to main content

md_tmpl/
consts.rs

1//! Shared string constants for the template engine.
2//!
3//! Every string that appears as a magic literal in more than one module
4//! or defines core syntax/grammar is defined here once.
5
6// -- Built-in function names --------------------------------------------------
7
8/// Name of the loop-index function: `idx(binding)`.
9pub(crate) const FN_IDX: &str = "idx";
10
11/// Name of the length function: `len(expr)`.
12pub(crate) const FN_LEN: &str = "len";
13
14/// Name of the explicit kind/variant-name function: `kind(expr)`.
15pub(crate) const FN_KIND: &str = "kind";
16/// Name of the enum variants list function: `kinds(expr)`.
17pub(crate) const FN_KINDS: &str = "kinds";
18
19/// Name of the option-presence function: `has(expr)`.
20pub(crate) const FN_HAS: &str = "has";
21
22/// All built-in function names.
23///
24/// Used by static analysis to avoid treating function names as variable
25/// references (e.g. `idx` in `idx(item)` is not a variable).
26pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_KINDS, FN_HAS];
27
28// -- Filter names -------------------------------------------------------------
29
30/// Name of the `upper` filter.
31pub(crate) const FILTER_UPPER: &str = "upper";
32/// Name of the `lower` filter.
33pub(crate) const FILTER_LOWER: &str = "lower";
34/// Name of the `trim` filter.
35pub(crate) const FILTER_TRIM: &str = "trim";
36/// Name of the `fixed` filter.
37pub(crate) const FILTER_FIXED: &str = "fixed";
38/// Name of the `join` filter.
39pub(crate) const FILTER_JOIN: &str = "join";
40/// Name of the `limit` filter.
41pub(crate) const FILTER_LIMIT: &str = "limit";
42/// Name of the `add` filter.
43pub(crate) const FILTER_ADD: &str = "add";
44/// Name of the `sub` filter.
45pub(crate) const FILTER_SUB: &str = "sub";
46
47// -- Enum tag key -------------------------------------------------------------
48
49/// Struct key used for internally-tagged enum variants:
50/// `{"__kind__": "VariantName", ...}`.
51///
52/// Uses a dunder prefix to avoid collisions with user-defined field names.
53pub const ENUM_TAG_KEY: &str = "__kind__";
54/// Struct key used for enum variant lists: `__variants__`.
55pub const ENUM_VARIANTS_KEY: &str = "__variants__";
56
57// -- Expression syntax chars --------------------------------------------------
58
59/// Opening parenthesis for function calls: `idx(item)`, `len(items)`.
60pub const PAREN_OPEN: char = '(';
61/// Closing parenthesis for function calls.
62pub const PAREN_CLOSE: char = ')';
63/// Dot separator for dotted path expressions: `item.label`.
64pub const PATH_SEP: char = '.';
65/// Pipe separator for filter chains: `{{ name | upper }}`.
66pub const PIPE: char = '|';
67/// Opening angle bracket for embed literals: `<file.txt>`.
68pub const ANGLE_OPEN: char = '<';
69/// Closing angle bracket for delimiters: `>`.
70pub const ANGLE_CLOSE: char = '>';
71/// Opening square bracket for delimiters: `[`.
72pub const BRACKET_OPEN: char = '[';
73/// Closing square bracket for delimiters: `]`.
74pub const BRACKET_CLOSE: char = ']';
75/// Opening brace character: `{`.
76pub const BRACE_OPEN: char = '{';
77/// Closing brace character: `}`.
78pub const BRACE_CLOSE: char = '}';
79/// Comma separator: `,`.
80pub const COMMA: char = ',';
81/// Colon separator: `:`.
82pub const COLON: char = ':';
83/// Equals separator: `=`.
84pub const EQUALS: char = '=';
85/// Slash separator: `/`.
86pub const SLASH: char = '/';
87/// Absolute path prefix: `/`.
88pub const PATH_PREFIX_SLASH: &str = "/";
89/// Relative current directory prefix: `./`.
90pub const PATH_PREFIX_CUR: &str = "./";
91/// Relative parent directory prefix: `../`.
92pub const PATH_PREFIX_PARENT: &str = "../";
93/// Windows relative current directory prefix: `.\`.
94pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
95/// Windows relative parent directory prefix: `..\`.
96pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
97/// Backslash separator: `\`.
98pub const BACKSLASH: char = '\\';
99/// Newline character: `\n`.
100pub const CHAR_NEWLINE: char = '\n';
101/// Carriage return character: `\r`.
102pub const CHAR_CR: char = '\r';
103/// Space character: `' '`.
104pub const CHAR_SPACE: char = ' ';
105/// Tab character: `\t`.
106pub const CHAR_TAB: char = '\t';
107
108/// Newline string literal: `"\n"`.
109pub const STR_NEWLINE: &str = "\n";
110/// Double newline string literal: `"\n\n"`.
111pub const STR_DOUBLE_NEWLINE: &str = "\n\n";
112/// Carriage return + newline string literal: `"\r\n"`.
113pub const STR_CRLF: &str = "\r\n";
114
115/// Check if a resolved path starts with `/`, `./`, `../`, `.\`, or `..\`.
116#[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/// Check if a path starts with a valid import or include prefix (`/`, `./`, `../`, `.\`, `..\`, or an expression `{{`).
126#[must_use]
127pub fn is_valid_include_path(path: &str) -> bool {
128    is_valid_resolved_path(path) || path.starts_with(EXPR_START)
129}
130
131/// Template extension: `.tmpl.md`.
132pub const EXT_TMPL_MD: &str = ".tmpl.md";
133/// Template extension: `.tmpl`.
134pub const EXT_TMPL: &str = ".tmpl";
135/// Markdown extension: `.md`.
136pub const EXT_MD: &str = ".md";
137/// Block list item separator: ` - `.
138pub const LIST_BLOCK_SEP: &str = " - ";
139/// Block list item prefix: `- `.
140pub const LIST_ITEM_PREFIX: &str = "- ";
141/// Double-quote character for string literal delimiters.
142pub const QUOTE_DOUBLE: char = '"';
143/// Single-quote character for string literal delimiters.
144pub 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
157// -- Template tag delimiters -------------------------------------------------
158
159/// Delimiter indicating the start of an expression: `{{`.
160pub(crate) const EXPR_START: &str = "{{";
161/// Delimiter indicating the end of an expression: `}}`.
162pub(crate) const EXPR_END: &str = "}}";
163
164/// Delimiter indicating the start of a statement: `{%`.
165pub(crate) const STMT_START: &str = "{%";
166/// Delimiter indicating the end of a statement: `%}`.
167pub(crate) const STMT_END: &str = "%}";
168
169/// Delimiter indicating the start of a comment: `{#`.
170pub(crate) const COMMENT_START: &str = "{#";
171/// Delimiter indicating the end of a comment: `#}`.
172pub(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
182/// Whitespace control trim marker: `-`.
183pub(crate) const TRIM_MARKER: char = '-';
184
185// -- Grammar keywords and tags ------------------------------------------------
186
187/// Spaced for loop tag prefix: `for `.
188pub(crate) const TAG_FOR_PREFIX: &str = "for ";
189/// Spaced in keyword for loops: ` in `.
190pub(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
199/// Logical AND operator: `&&`.
200pub(crate) const OP_AND: &str = "&&";
201/// Logical OR operator: `||`.
202pub(crate) const OP_OR: &str = "||";
203/// Logical NOT operator: `!`.
204pub(crate) const OP_NOT: char = '!';
205
206/// Spaced if statement tag prefix: `if `.
207pub(crate) const TAG_IF_PREFIX: &str = "if ";
208/// Spaced elif statement tag prefix: `elif `.
209pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
210/// Else keyword: `else`.
211pub(crate) const KW_ELSE: &str = "else";
212
213/// Raw literal block keyword: `raw`.
214pub(crate) const KW_RAW: &str = "raw";
215/// Raw custom delimiter assignment prefix: `raw=`.
216pub(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
226/// Include keyword: `include`.
227pub(crate) const KW_INCLUDE: &str = "include";
228/// Include statement prefix: `include `.
229pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
230/// Include `with` override statement prefix: `with `.
231pub(crate) const TAG_WITH_PREFIX: &str = "with ";
232/// Spaced include `with` override: ` with `.
233pub(crate) const TAG_WITH_SPACED: &str = " with ";
234
235/// Inline template tag name: `tmpl `.
236pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
237
238/// Match statement tag prefix: `match `.
239pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
240/// Case arm tag prefix: `case `.
241pub(crate) const TAG_CASE_PREFIX: &str = "case ";
242/// Spaced case keyword: ` case`.
243pub(crate) const TAG_CASE_SPACED: &str = " case";
244/// Spaced case keyword for match-as-condition: ` case `.
245pub(crate) const KW_CASE_SPACED: &str = " case ";
246/// Variant separator in match case arms: `|`.
247pub(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
252// -- Closing block tags -------------------------------------------------------
253
254/// Closing tag for `if` statement block: `/if`.
255pub(crate) const CLOSE_IF: &str = "/if";
256/// Closing tag for `for` statement block: `/for`.
257pub(crate) const CLOSE_FOR: &str = "/for";
258/// Closing tag for `raw` statement block: `/raw`.
259pub(crate) const CLOSE_RAW: &str = "/raw";
260/// Closing tag for inline template definition: `/tmpl`.
261pub(crate) const CLOSE_TMPL: &str = "/tmpl";
262/// Closing tag for match block: `/match`.
263pub(crate) const CLOSE_MATCH: &str = "/match";
264
265// -- Markdown Blockquote delimiters -------------------------------------------
266
267/// Blockquote character used to prefix template directives: `>`.
268pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
269/// Spaced blockquote prefix: `> `.
270pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
271/// Compact statement blockquote start: `>{`.
272pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
273/// Spaced statement blockquote start: `> {%`.
274pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
275
276// -- Frontmatter YAML delimiters & keys ---------------------------------------
277
278/// YAML frontmatter block delimiter: `---`.
279pub(crate) const FM_DELIMITER: &str = "---";
280/// YAML frontmatter block delimiter ending line: `\n---`.
281pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
282
283/// Frontmatter key for template name: `name:`.
284pub(crate) const FM_NAME_PREFIX: &str = "name:";
285/// Frontmatter key for template description: `description:`.
286pub(crate) const FM_DESC_PREFIX: &str = "description:";
287/// Frontmatter key for template parameters: `params:`.
288pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
289/// Frontmatter key to allow unused declared parameters: `allow_unused:`.
290pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
291/// Frontmatter key for local type aliases: `types:`.
292pub(crate) const FM_TYPES_PREFIX: &str = "types:";
293/// Frontmatter key for cross-template imports: `imports:`.
294pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
295/// Frontmatter key for global constants: `consts:`.
296pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
297
298// -- Type annotations ---------------------------------------------------------
299
300/// Type name for strings: `str`.
301pub(crate) const TYPE_STR: &str = "str";
302/// Type name for booleans: `bool`.
303pub(crate) const TYPE_BOOL: &str = "bool";
304/// Type name for integers: `int`.
305pub(crate) const TYPE_INT: &str = "int";
306/// Type name for floating point numbers: `float`.
307pub(crate) const TYPE_FLOAT: &str = "float";
308/// Type name for lists: `list`.
309pub(crate) const TYPE_LIST: &str = "list";
310/// Type name for structs: `struct`.
311pub(crate) const TYPE_STRUCT: &str = "struct";
312/// Type name for enums: `enum`.
313pub(crate) const TYPE_ENUM: &str = "enum";
314/// Type name for templates: `tmpl`.
315pub(crate) const TYPE_TMPL: &str = "tmpl";
316/// Type name for none/null: `none`.
317pub(crate) const TYPE_NONE: &str = "none";
318
319/// Type prefix for lists with parentheses: `list(`.
320pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
321/// Type prefix for structs with parentheses: `struct(`.
322pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
323/// Type prefix for structs with angle brackets: `struct<`.
324pub(crate) const TYPE_STRUCT_ANGLE_PREFIX: &str = "struct<";
325/// Type prefix for structs with square brackets: `struct[`.
326pub(crate) const TYPE_STRUCT_BRACKET_PREFIX: &str = "struct[";
327/// Type prefix for structs with trailing space: `struct `.
328pub(crate) const TYPE_STRUCT_SPACE_PREFIX: &str = "struct ";
329/// Type prefix for enums with parentheses: `enum(`.
330pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
331/// Type prefix for templates with parentheses: `tmpl(`.
332pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
333/// Type name for options: `option`.
334pub(crate) const TYPE_OPTION: &str = "option";
335/// Type prefix for options with parentheses: `option(`.
336pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
337
338/// Variant name for the `Some` variant of `option(T)`.
339pub const OPTION_SOME: &str = "Some";
340/// Variant name for the `None` variant of `option(T)`.
341pub const OPTION_NONE: &str = "None";
342/// Field name for the inner value of `option(T)`'s `Some` variant.
343pub const OPTION_VAL_FIELD: &str = "val";
344/// Wildcard/default pattern in match arms: `_`.
345pub const MATCH_DEFAULT: &str = "_";
346
347// -- Variable prefixes --------------------------------------------------------
348
349/// Prefix for constant references: `consts.`.
350pub(crate) const PREFIX_CONSTS_DOT: &str = "consts.";
351/// Prefix for runtime options: `opts.`.
352pub(crate) const PREFIX_OPTS_DOT: &str = "opts.";
353/// Prefix for runtime options (alias): `options.`.
354pub(crate) const PREFIX_OPTIONS_DOT: &str = "options.";
355/// Prefix for parameter references: `params.`.
356pub(crate) const PREFIX_PARAMS_DOT: &str = "params.";
357
358// -- Literals -----------------------------------------------------------------
359
360/// Boolean true literal: `true`.
361pub(crate) const LIT_TRUE: &str = "true";
362/// Boolean false literal: `false`.
363pub(crate) const LIT_FALSE: &str = "false";
364
365/// Try to strip balanced quotes from a string literal token.
366///
367/// Returns `Some(inner)` if `token` is a valid quoted string literal
368/// (`"..."` or `'...'`), otherwise `None`.
369#[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
380// -- Error messages -----------------------------------------------------------
381
382/// Error when frontmatter block is missing.
383pub(crate) const ERR_MISSING_FM: &str =
384    "missing mandatory YAML frontmatter block (starts with ---)";
385/// Error when frontmatter block is unclosed.
386pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
387/// Prefix for undeclared variable references error.
388pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
389
390/// Error when a param is named after a reserved keyword.
391pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
392/// Error when two params have the same name.
393pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
394/// Error when a `types:` entry has a duplicate name.
395pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
396/// Error when a `types:` entry shadows a built-in type name.
397pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
398/// Error when a type alias and param name collide in `PascalCase`.
399pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
400    "type alias name conflicts with parameter name (PascalCase collision)";
401/// Error for circular import chains.
402#[cfg(feature = "std")]
403pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
404/// Error when a type alias name shadows an import alias (stem).
405pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
406/// Error when a param's `PascalCase` name shadows an import alias.
407pub(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";
410/// Error when a constant name is duplicated.
411pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
412/// Error when a param and a const share the same name.
413pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
414/// Error when a for-loop binding shadows a declared name.
415pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
416/// Error when a `{% %}` tag starts a line without a blockquote `>` prefix.
417pub(crate) const ERR_BARE_STMT_TAG: &str =
418    "statement tag at line start must be blockquote-prefixed with '> '";
419/// Error when compound type uses angle or square brackets instead of parentheses.
420pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
421    "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
422
423/// Built-in type names and keywords that cannot be used as user-defined names.
424pub(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    // -- strip_string_literal -------------------------------------------------
442
443    #[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        // Inner quotes of the opposite kind are preserved.
494        assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
495        assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
496    }
497
498    // -- RESERVED_NAMES -------------------------------------------------------
499
500    #[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    // -- BUILTIN_FUNCTIONS ----------------------------------------------------
518
519    #[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    // -- Delimiter constants --------------------------------------------------
534
535    #[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}