Skip to main content

md_tmpl_core/
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/// Prefix for detecting `kind(...)` calls in expressions: `"kind("`.
17pub(crate) const FN_KIND_PREFIX: &str = "kind(";
18/// Name of the enum variants list function: `kinds(expr)`.
19pub(crate) const FN_KINDS: &str = "kinds";
20
21/// Name of the option-presence function: `has(expr)`.
22pub(crate) const FN_HAS: &str = "has";
23
24/// All built-in function names.
25///
26/// Used by static analysis to avoid treating function names as variable
27/// references (e.g. `idx` in `idx(item)` is not a variable).
28pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_KINDS, FN_HAS];
29
30// -- Filter names -------------------------------------------------------------
31
32/// Name of the `upper` filter.
33pub(crate) const FILTER_UPPER: &str = "upper";
34/// Name of the `lower` filter.
35pub(crate) const FILTER_LOWER: &str = "lower";
36/// Name of the `trim` filter.
37pub(crate) const FILTER_TRIM: &str = "trim";
38/// Name of the `fixed` filter.
39pub(crate) const FILTER_FIXED: &str = "fixed";
40/// Name of the `join` filter.
41pub(crate) const FILTER_JOIN: &str = "join";
42/// Name of the `limit` filter.
43pub(crate) const FILTER_LIMIT: &str = "limit";
44/// Name of the `add` filter.
45pub(crate) const FILTER_ADD: &str = "add";
46/// Name of the `sub` filter.
47pub(crate) const FILTER_SUB: &str = "sub";
48
49// -- Enum tag key -------------------------------------------------------------
50
51/// Struct key used for internally-tagged enum variants:
52/// `{"__kind__": "VariantName", ...}`.
53///
54/// Uses a dunder prefix to avoid collisions with user-defined field names.
55pub const ENUM_TAG_KEY: &str = "__kind__";
56/// Struct key used for enum variant lists: `__variants__`.
57pub const ENUM_VARIANTS_KEY: &str = "__variants__";
58
59// -- Expression syntax chars --------------------------------------------------
60
61/// Opening parenthesis for function calls: `idx(item)`, `len(items)`.
62pub const PAREN_OPEN: char = '(';
63/// Closing parenthesis for function calls.
64pub const PAREN_CLOSE: char = ')';
65/// Dot separator for dotted path expressions: `item.label`.
66pub const PATH_SEP: char = '.';
67/// Pipe separator for filter chains: `{{ name | upper }}`.
68pub const PIPE: char = '|';
69/// Opening angle bracket for embed literals: `<file.txt>`.
70pub const ANGLE_OPEN: char = '<';
71/// Closing angle bracket for delimiters: `>`.
72pub const ANGLE_CLOSE: char = '>';
73/// Opening square bracket for delimiters: `[`.
74pub const BRACKET_OPEN: char = '[';
75/// Closing square bracket for delimiters: `]`.
76pub const BRACKET_CLOSE: char = ']';
77/// Opening brace character: `{`.
78pub const BRACE_OPEN: char = '{';
79/// Closing brace character: `}`.
80pub const BRACE_CLOSE: char = '}';
81/// Comma separator: `,`.
82pub const COMMA: char = ',';
83/// Colon separator: `:`.
84pub const COLON: char = ':';
85/// Equals separator: `=`.
86pub const EQUALS: char = '=';
87/// Slash separator: `/`.
88pub const SLASH: char = '/';
89/// Absolute path prefix: `/`.
90pub const PATH_PREFIX_SLASH: &str = "/";
91/// Relative current directory prefix: `./`.
92pub const PATH_PREFIX_CUR: &str = "./";
93/// Relative parent directory prefix: `../`.
94pub const PATH_PREFIX_PARENT: &str = "../";
95/// Windows relative current directory prefix: `.\`.
96pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
97/// Windows relative parent directory prefix: `..\`.
98pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
99/// Backslash separator: `\`.
100pub const BACKSLASH: char = '\\';
101/// Newline character: `\n`.
102pub const CHAR_NEWLINE: char = '\n';
103/// Carriage return character: `\r`.
104pub const CHAR_CR: char = '\r';
105/// Space character: `' '`.
106pub const CHAR_SPACE: char = ' ';
107/// Tab character: `\t`.
108pub const CHAR_TAB: char = '\t';
109
110/// Newline string literal: `"\n"`.
111pub const STR_NEWLINE: &str = "\n";
112/// Double newline string literal: `"\n\n"`.
113pub const STR_DOUBLE_NEWLINE: &str = "\n\n";
114/// Carriage return + newline string literal: `"\r\n"`.
115pub const STR_CRLF: &str = "\r\n";
116
117/// Check if a resolved path starts with `/`, `./`, `../`, `.\`, or `..\`.
118#[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/// Check if a path starts with a valid import or include prefix (`/`, `./`, `../`, `.\`, `..\`, or an expression `{{`).
128#[must_use]
129pub fn is_valid_include_path(path: &str) -> bool {
130    is_valid_resolved_path(path) || path.starts_with(EXPR_START)
131}
132
133/// Template extension: `.tmpl.md`.
134pub const EXT_TMPL_MD: &str = ".tmpl.md";
135/// Template extension: `.tmpl`.
136pub const EXT_TMPL: &str = ".tmpl";
137/// Markdown extension: `.md`.
138pub const EXT_MD: &str = ".md";
139/// Block list item separator: ` - `.
140pub const LIST_BLOCK_SEP: &str = " - ";
141/// Block list item prefix: `- `.
142pub const LIST_ITEM_PREFIX: &str = "- ";
143/// Double-quote character for string literal delimiters.
144pub const QUOTE_DOUBLE: char = '"';
145/// Single-quote character for string literal delimiters.
146pub 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
159// -- Template tag delimiters -------------------------------------------------
160
161/// Delimiter indicating the start of an expression: `{{`.
162pub(crate) const EXPR_START: &str = "{{";
163/// Delimiter indicating the end of an expression: `}}`.
164pub(crate) const EXPR_END: &str = "}}";
165
166/// Delimiter indicating the start of a statement: `{%`.
167pub(crate) const STMT_START: &str = "{%";
168/// Delimiter indicating the end of a statement: `%}`.
169pub(crate) const STMT_END: &str = "%}";
170
171/// Delimiter indicating the start of a comment: `{#`.
172pub(crate) const COMMENT_START: &str = "{#";
173/// Delimiter indicating the end of a comment: `#}`.
174pub(crate) const COMMENT_END: &str = "#}";
175
176/// Whitespace control trim marker: `-`.
177pub(crate) const TRIM_MARKER: char = '-';
178
179// -- Grammar keywords and tags ------------------------------------------------
180
181/// Spaced for loop tag prefix: `for `.
182pub(crate) const TAG_FOR_PREFIX: &str = "for ";
183/// Spaced in keyword for loops: ` in `.
184pub(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
193/// Logical AND operator: `&&`.
194pub(crate) const OP_AND: &str = "&&";
195/// Logical OR operator: `||`.
196pub(crate) const OP_OR: &str = "||";
197/// Logical NOT operator: `!`.
198pub(crate) const OP_NOT: char = '!';
199
200/// Spaced if statement tag prefix: `if `.
201pub(crate) const TAG_IF_PREFIX: &str = "if ";
202/// Spaced elif statement tag prefix: `elif `.
203pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
204/// Else keyword: `else`.
205pub(crate) const KW_ELSE: &str = "else";
206
207/// Raw literal block keyword: `raw`.
208pub(crate) const KW_RAW: &str = "raw";
209/// Raw custom delimiter assignment prefix: `raw=`.
210pub(crate) const KW_RAW_ASSIGN: &str = "raw=";
211
212/// Include keyword: `include`.
213pub(crate) const KW_INCLUDE: &str = "include";
214/// Include statement prefix: `include `.
215pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
216/// Include `with` override statement prefix: `with `.
217pub(crate) const TAG_WITH_PREFIX: &str = "with ";
218/// Spaced include `with` override: ` with `.
219pub(crate) const TAG_WITH_SPACED: &str = " with ";
220
221/// Inline template tag name: `tmpl `.
222pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
223
224/// Match statement tag prefix: `match `.
225pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
226/// Case arm tag prefix: `case `.
227pub(crate) const TAG_CASE_PREFIX: &str = "case ";
228/// Spaced case keyword: ` case`.
229pub(crate) const TAG_CASE_SPACED: &str = " case";
230/// Spaced case keyword for match-as-condition: ` case `.
231pub(crate) const KW_CASE_SPACED: &str = " case ";
232/// Variant separator in match case arms: `|`.
233pub(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
238// -- Closing block tags -------------------------------------------------------
239
240/// Closing tag for `if` statement block: `/if`.
241pub(crate) const CLOSE_IF: &str = "/if";
242/// Closing tag for `for` statement block: `/for`.
243pub(crate) const CLOSE_FOR: &str = "/for";
244/// Closing tag for `raw` statement block: `/raw`.
245pub(crate) const CLOSE_RAW: &str = "/raw";
246/// Closing tag for inline template definition: `/tmpl`.
247pub(crate) const CLOSE_TMPL: &str = "/tmpl";
248/// Closing tag for match block: `/match`.
249pub(crate) const CLOSE_MATCH: &str = "/match";
250
251// -- Markdown Blockquote delimiters -------------------------------------------
252
253/// Blockquote character used to prefix template directives: `>`.
254pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
255/// Spaced blockquote prefix: `> `.
256pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
257/// Compact statement blockquote start: `>{`.
258pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
259/// Spaced statement blockquote start: `> {%`.
260pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
261
262// -- Frontmatter YAML delimiters & keys ---------------------------------------
263
264/// YAML frontmatter block delimiter: `---`.
265pub(crate) const FM_DELIMITER: &str = "---";
266/// YAML frontmatter block delimiter ending line: `\n---`.
267pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
268
269/// Frontmatter key for template name: `name:`.
270pub(crate) const FM_NAME_PREFIX: &str = "name:";
271/// Frontmatter key for template description: `description:`.
272pub(crate) const FM_DESC_PREFIX: &str = "description:";
273/// Frontmatter key for template parameters: `params:`.
274pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
275/// Frontmatter key to allow unused declared parameters: `allow_unused:`.
276pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
277/// Frontmatter key for local type aliases: `types:`.
278pub(crate) const FM_TYPES_PREFIX: &str = "types:";
279/// Frontmatter key for cross-template imports: `imports:`.
280pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
281/// Frontmatter key for global constants: `consts:`.
282pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
283/// Frontmatter key for compile-time environment variables: `env:`.
284pub(crate) const FM_ENV_PREFIX: &str = "env:";
285/// Frontmatter full-line comment prefix: `#`.
286///
287/// A line whose first non-whitespace character is `#` is treated as a
288/// documentation comment inside the frontmatter block. It is ignored during
289/// parsing and, crucially, does not terminate an in-progress block list.
290pub(crate) const FM_COMMENT_PREFIX: char = '#';
291
292// -- Type annotations ---------------------------------------------------------
293
294/// Type name for strings: `str`.
295pub(crate) const TYPE_STR: &str = "str";
296/// Type name for booleans: `bool`.
297pub(crate) const TYPE_BOOL: &str = "bool";
298/// Type name for integers: `int`.
299pub(crate) const TYPE_INT: &str = "int";
300/// Type name for floating point numbers: `float`.
301pub(crate) const TYPE_FLOAT: &str = "float";
302/// Type name for lists: `list`.
303pub(crate) const TYPE_LIST: &str = "list";
304/// Type name for structs: `struct`.
305pub(crate) const TYPE_STRUCT: &str = "struct";
306/// Type name for enums: `enum`.
307pub(crate) const TYPE_ENUM: &str = "enum";
308/// Type name for templates: `tmpl`.
309pub(crate) const TYPE_TMPL: &str = "tmpl";
310/// Type name for none/null: `none`.
311pub(crate) const TYPE_NONE: &str = "none";
312
313/// Type prefix for lists with parentheses: `list(`.
314pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
315/// Type prefix for structs with parentheses: `struct(`.
316pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
317/// Type prefix for structs with angle brackets: `struct<`.
318pub(crate) const TYPE_STRUCT_ANGLE_PREFIX: &str = "struct<";
319/// Type prefix for structs with square brackets: `struct[`.
320pub(crate) const TYPE_STRUCT_BRACKET_PREFIX: &str = "struct[";
321/// Type prefix for structs with trailing space: `struct `.
322pub(crate) const TYPE_STRUCT_SPACE_PREFIX: &str = "struct ";
323/// Type prefix for enums with parentheses: `enum(`.
324pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
325/// Type prefix for templates with parentheses: `tmpl(`.
326pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
327/// Type name for options: `option`.
328pub(crate) const TYPE_OPTION: &str = "option";
329/// Type prefix for options with parentheses: `option(`.
330pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
331
332/// Variant name for the `Some` variant of `option(T)`.
333pub const OPTION_SOME: &str = "Some";
334/// Variant name for the `None` variant of `option(T)`.
335pub const OPTION_NONE: &str = "None";
336/// Field name for the inner value of `option(T)`'s `Some` variant.
337pub const OPTION_VAL_FIELD: &str = "val";
338/// Wildcard/default pattern in match arms: `_`.
339pub const MATCH_DEFAULT: &str = "_";
340
341// -- Variable prefixes --------------------------------------------------------
342
343/// Prefix for constant references: `consts.`.
344pub(crate) const PREFIX_CONSTS_DOT: &str = "consts.";
345/// Prefix for runtime options: `opts.`.
346pub(crate) const PREFIX_OPTS_DOT: &str = "opts.";
347/// Prefix for runtime options (alias): `options.`.
348pub(crate) const PREFIX_OPTIONS_DOT: &str = "options.";
349/// Prefix for parameter references: `params.`.
350pub(crate) const PREFIX_PARAMS_DOT: &str = "params.";
351
352// -- Literals -----------------------------------------------------------------
353
354/// Boolean true literal: `true`.
355pub(crate) const LIT_TRUE: &str = "true";
356/// Boolean false literal: `false`.
357pub(crate) const LIT_FALSE: &str = "false";
358
359/// Try to strip balanced quotes from a string literal token.
360///
361/// Returns `Some(inner)` if `token` is a valid quoted string literal
362/// (`"..."` or `'...'`), otherwise `None`.
363#[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/// Unescape the inner content of a string literal (surrounding quotes already
375/// stripped) using md-tmpl's uniform escape rules.
376///
377/// Recognized escapes:
378/// - `\\` → `\`
379/// - `\"` → `"`
380/// - `\'` → `'`
381///
382/// Any other backslash sequence `\X` is preserved VERBATIM (both the backslash
383/// and `X` are kept), so pre-existing strings containing literal backslashes
384/// (e.g. `\n`, `c:\path`) are unaffected. A trailing lone backslash is kept.
385#[must_use]
386pub fn unescape_string_literal(inner: &str) -> alloc::string::String {
387    use alloc::string::{String, ToString};
388    // Fast path: no backslash means nothing to unescape.
389    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                // A `\\` escape, or a trailing lone backslash — keep one backslash.
400                Some(BACKSLASH) | None => out.push(BACKSLASH),
401                // Unknown escape — keep the backslash and the following char.
402                Some(other) => {
403                    out.push(BACKSLASH);
404                    out.push(other);
405                }
406            }
407        } else {
408            out.push(c);
409        }
410    }
411    out
412}
413
414// -- Error messages -----------------------------------------------------------
415
416/// Error when frontmatter block is missing.
417pub(crate) const ERR_MISSING_FM: &str =
418    "missing mandatory YAML frontmatter block (starts with ---)";
419/// Error when frontmatter block is unclosed.
420pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
421/// Prefix for undeclared variable references error.
422pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
423
424/// Error when a param is named after a reserved keyword.
425pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
426/// Error when two params have the same name.
427pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
428/// Error when a `types:` entry has a duplicate name.
429pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
430/// Error when a `types:` entry shadows a built-in type name.
431pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
432/// Error when a type alias and param name collide in `PascalCase`.
433pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
434    "type alias name conflicts with parameter name (PascalCase collision)";
435/// Error for circular import chains.
436#[cfg(feature = "std")]
437pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
438/// Error when a type alias name shadows an import alias (stem).
439pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
440/// Error when a param's `PascalCase` name shadows an import alias.
441pub(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";
444/// Error when a constant name is duplicated.
445pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
446/// Error when a param and a const share the same name.
447pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
448/// Error when a for-loop binding shadows a declared name.
449pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
450/// Error when a `{% %}` tag starts a line without a blockquote `>` prefix.
451pub(crate) const ERR_BARE_STMT_TAG: &str =
452    "statement tag at line start must be blockquote-prefixed with '> '";
453/// Error when compound type uses angle or square brackets instead of parentheses.
454pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
455    "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
456
457/// Built-in type names, pattern-syntax keywords, and internal keys that cannot
458/// be used as user-defined names (params, consts, type aliases, import stems).
459///
460/// - Type names: `str`, `bool`, `int`, `float`, `list`, `struct`, `enum`, `tmpl`, `option`
461/// - Namespace: `params`
462/// - Pattern keywords: `true`, `false`, `Some`, `None`, `_` — used in `{% case %}` arms
463/// - Internal keys: `__kind__`, `__variants__` — used for enum variant tagging
464/// - Codegen collision guards: `__self`, `__Self`, `__super`, `__crate` — Rust
465///   codegen renames `self` → `__self` etc. because these cannot be raw
466///   identifiers; the mangled names are reserved to prevent collisions.
467pub(crate) const RESERVED_NAMES: &[&str] = &[
468    // Type names
469    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    // Namespace
479    "params",
480    // Pattern-syntax keywords (match/case arm labels)
481    LIT_TRUE,
482    LIT_FALSE,
483    OPTION_SOME,
484    OPTION_NONE,
485    MATCH_DEFAULT,
486    // Internal enum keys
487    ENUM_TAG_KEY,
488    ENUM_VARIANTS_KEY,
489    // Codegen collision guards — Rust codegen renames `self` → `__self` etc.
490    // because these cannot be raw identifiers.  Reserve the mangled names so
491    // that a user-defined `__self` param doesn't collide with the rename.
492    "__self",
493    "__Self",
494    "__super",
495    "__crate",
496];
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    // -- strip_string_literal -------------------------------------------------
503
504    #[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        // Inner quotes of the opposite kind are preserved.
555        assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
556        assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
557    }
558
559    // -- RESERVED_NAMES -------------------------------------------------------
560
561    #[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    // -- BUILTIN_FUNCTIONS ----------------------------------------------------
599
600    #[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    // -- Delimiter constants --------------------------------------------------
615
616    #[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}