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
17/// Name of the option-presence function: `has(expr)`.
18pub(crate) const FN_HAS: &str = "has";
19
20/// All built-in function names.
21///
22/// Used by static analysis to avoid treating function names as variable
23/// references (e.g. `idx` in `idx(item)` is not a variable).
24pub(crate) const BUILTIN_FUNCTIONS: &[&str] = &[FN_IDX, FN_LEN, FN_KIND, FN_HAS];
25
26// -- Filter names -------------------------------------------------------------
27
28/// Name of the `upper` filter.
29pub(crate) const FILTER_UPPER: &str = "upper";
30/// Name of the `lower` filter.
31pub(crate) const FILTER_LOWER: &str = "lower";
32/// Name of the `trim` filter.
33pub(crate) const FILTER_TRIM: &str = "trim";
34/// Name of the `fixed` filter.
35pub(crate) const FILTER_FIXED: &str = "fixed";
36/// Name of the `join` filter.
37pub(crate) const FILTER_JOIN: &str = "join";
38/// Name of the `limit` filter.
39pub(crate) const FILTER_LIMIT: &str = "limit";
40/// Name of the `add` filter.
41pub(crate) const FILTER_ADD: &str = "add";
42/// Name of the `sub` filter.
43pub(crate) const FILTER_SUB: &str = "sub";
44
45// -- Enum tag key -------------------------------------------------------------
46
47/// Struct key used for internally-tagged enum variants:
48/// `{"__kind__": "VariantName", ...}`.
49///
50/// Uses a dunder prefix to avoid collisions with user-defined field names.
51pub const ENUM_TAG_KEY: &str = "__kind__";
52
53/// Pseudo-field suffix for legacy `.length` access.
54pub(crate) const PSEUDO_FIELD_LENGTH: &str = ".length";
55
56// -- Expression syntax chars --------------------------------------------------
57
58/// Opening parenthesis for function calls: `idx(item)`, `len(items)`.
59pub const PAREN_OPEN: char = '(';
60/// Closing parenthesis for function calls.
61pub const PAREN_CLOSE: char = ')';
62/// Dot separator for dotted path expressions: `item.label`.
63pub const PATH_SEP: char = '.';
64/// Pipe separator for filter chains: `{{ name | upper }}`.
65pub const PIPE: char = '|';
66/// Opening angle bracket for embed literals: `<file.txt>`.
67pub const ANGLE_OPEN: char = '<';
68/// Closing angle bracket for delimiters: `>`.
69pub const ANGLE_CLOSE: char = '>';
70/// Opening square bracket for delimiters: `[`.
71pub const BRACKET_OPEN: char = '[';
72/// Closing square bracket for delimiters: `]`.
73pub const BRACKET_CLOSE: char = ']';
74/// Opening brace character: `{`.
75pub const BRACE_OPEN: char = '{';
76/// Closing brace character: `}`.
77pub const BRACE_CLOSE: char = '}';
78/// Comma separator: `,`.
79pub const COMMA: char = ',';
80/// Colon separator: `:`.
81pub const COLON: char = ':';
82/// Equals separator: `=`.
83pub const EQUALS: char = '=';
84/// Slash separator: `/`.
85pub const SLASH: char = '/';
86/// Absolute path prefix: `/`.
87pub const PATH_PREFIX_SLASH: &str = "/";
88/// Relative current directory prefix: `./`.
89pub const PATH_PREFIX_CUR: &str = "./";
90/// Relative parent directory prefix: `../`.
91pub const PATH_PREFIX_PARENT: &str = "../";
92/// Windows relative current directory prefix: `.\`.
93pub const PATH_PREFIX_CUR_WIN: &str = ".\\";
94/// Windows relative parent directory prefix: `..\`.
95pub const PATH_PREFIX_PARENT_WIN: &str = "..\\";
96/// Backslash separator: `\`.
97pub const BACKSLASH: char = '\\';
98
99/// Check if a path starts with a valid import or include prefix (`/`, `./`, `../`, `.\`, or `..\`).
100#[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
109/// Template extension: `.tmpl.md`.
110pub const EXT_TMPL_MD: &str = ".tmpl.md";
111/// Template extension: `.tmpl`.
112pub const EXT_TMPL: &str = ".tmpl";
113/// Markdown extension: `.md`.
114pub const EXT_MD: &str = ".md";
115/// Block list item separator: ` - `.
116pub const LIST_BLOCK_SEP: &str = " - ";
117/// Block list item prefix: `- `.
118pub const LIST_ITEM_PREFIX: &str = "- ";
119/// Double-quote character for string literal delimiters.
120pub const QUOTE_DOUBLE: char = '"';
121/// Single-quote character for string literal delimiters.
122pub 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
135// -- Template tag delimiters -------------------------------------------------
136
137/// Delimiter indicating the start of an expression: `{{`.
138pub(crate) const EXPR_START: &str = "{{";
139/// Delimiter indicating the end of an expression: `}}`.
140pub(crate) const EXPR_END: &str = "}}";
141
142/// Delimiter indicating the start of a statement: `{%`.
143pub(crate) const STMT_START: &str = "{%";
144/// Delimiter indicating the end of a statement: `%}`.
145pub(crate) const STMT_END: &str = "%}";
146
147/// Delimiter indicating the start of a comment: `{#`.
148pub(crate) const COMMENT_START: &str = "{#";
149/// Delimiter indicating the end of a comment: `#}`.
150pub(crate) const COMMENT_END: &str = "#}";
151
152/// Whitespace control trim marker: `-`.
153pub(crate) const TRIM_MARKER: char = '-';
154
155// -- Grammar keywords and tags ------------------------------------------------
156
157/// Spaced for loop tag prefix: `for `.
158pub(crate) const TAG_FOR_PREFIX: &str = "for ";
159/// Spaced in keyword for loops: ` in `.
160pub(crate) const KW_IN_SPACED: &str = " in ";
161
162/// Spaced if statement tag prefix: `if `.
163pub(crate) const TAG_IF_PREFIX: &str = "if ";
164/// Spaced elif statement tag prefix: `elif `.
165pub(crate) const TAG_ELIF_PREFIX: &str = "elif ";
166/// Else keyword: `else`.
167pub(crate) const KW_ELSE: &str = "else";
168
169/// Raw literal block keyword: `raw`.
170pub(crate) const KW_RAW: &str = "raw";
171/// Raw custom delimiter assignment prefix: `raw=`.
172pub(crate) const KW_RAW_ASSIGN: &str = "raw=";
173
174/// Include statement prefix: `include `.
175pub(crate) const TAG_INCLUDE_PREFIX: &str = "include ";
176/// Include `with` override statement prefix: `with `.
177pub(crate) const TAG_WITH_PREFIX: &str = "with ";
178/// Spaced include `with` override: ` with `.
179pub(crate) const TAG_WITH_SPACED: &str = " with ";
180
181/// Inline template tag name: `tmpl `.
182pub(crate) const TAG_TMPL_PREFIX: &str = "tmpl ";
183
184/// Match statement tag prefix: `match `.
185pub(crate) const TAG_MATCH_PREFIX: &str = "match ";
186/// Case arm tag prefix: `case `.
187pub(crate) const TAG_CASE_PREFIX: &str = "case ";
188
189// -- Closing block tags -------------------------------------------------------
190
191/// Closing tag for `if` statement block: `/if`.
192pub(crate) const CLOSE_IF: &str = "/if";
193/// Closing tag for `for` statement block: `/for`.
194pub(crate) const CLOSE_FOR: &str = "/for";
195/// Closing tag for `raw` statement block: `/raw`.
196pub(crate) const CLOSE_RAW: &str = "/raw";
197/// Closing tag for inline template definition: `/tmpl`.
198pub(crate) const CLOSE_TMPL: &str = "/tmpl";
199/// Closing tag for match block: `/match`.
200pub(crate) const CLOSE_MATCH: &str = "/match";
201
202// -- Markdown Blockquote delimiters -------------------------------------------
203
204/// Blockquote character used to prefix template directives: `>`.
205pub(crate) const BLOCKQUOTE_PREFIX: char = '>';
206/// Spaced blockquote prefix: `> `.
207pub(crate) const BLOCKQUOTE_PREFIX_SPACED: &str = "> ";
208/// Compact statement blockquote start: `>{`.
209pub(crate) const BLOCKQUOTE_COMPACT_OPEN: &str = ">{";
210/// Spaced statement blockquote start: `> {%`.
211pub(crate) const BLOCKQUOTE_SPACED_OPEN: &str = "> {%";
212
213// -- Frontmatter YAML delimiters & keys ---------------------------------------
214
215/// YAML frontmatter block delimiter: `---`.
216pub(crate) const FM_DELIMITER: &str = "---";
217/// YAML frontmatter block delimiter ending line: `\n---`.
218pub(crate) const FM_DELIMITER_NEWLINE: &str = "\n---";
219
220/// Frontmatter key for template name: `name:`.
221pub(crate) const FM_NAME_PREFIX: &str = "name:";
222/// Frontmatter key for template description: `description:`.
223pub(crate) const FM_DESC_PREFIX: &str = "description:";
224/// Frontmatter key for template parameters: `params:`.
225pub(crate) const FM_PARAMS_PREFIX: &str = "params:";
226/// Frontmatter key to allow unused declared parameters: `allow_unused:`.
227pub(crate) const FM_ALLOW_UNUSED_PREFIX: &str = "allow_unused:";
228/// Frontmatter key for local type aliases: `types:`.
229pub(crate) const FM_TYPES_PREFIX: &str = "types:";
230/// Frontmatter key for cross-template imports: `imports:`.
231pub(crate) const FM_IMPORTS_PREFIX: &str = "imports:";
232/// Frontmatter key for global constants: `consts:`.
233pub(crate) const FM_CONSTS_PREFIX: &str = "consts:";
234
235// -- Type annotations ---------------------------------------------------------
236
237/// Type name for strings: `str`.
238pub(crate) const TYPE_STR: &str = "str";
239/// Type name for booleans: `bool`.
240pub(crate) const TYPE_BOOL: &str = "bool";
241/// Type name for integers: `int`.
242pub(crate) const TYPE_INT: &str = "int";
243/// Type name for floating point numbers: `float`.
244pub(crate) const TYPE_FLOAT: &str = "float";
245/// Type name for lists: `list`.
246pub(crate) const TYPE_LIST: &str = "list";
247/// Type name for structs: `struct`.
248pub(crate) const TYPE_STRUCT: &str = "struct";
249/// Type name for enums: `enum`.
250pub(crate) const TYPE_ENUM: &str = "enum";
251/// Type name for templates: `tmpl`.
252pub(crate) const TYPE_TMPL: &str = "tmpl";
253
254/// Type prefix for lists with parentheses: `list(`.
255pub(crate) const TYPE_LIST_PREFIX: &str = "list(";
256/// Type prefix for structs with parentheses: `struct(`.
257pub(crate) const TYPE_STRUCT_PREFIX: &str = "struct(";
258/// Type prefix for enums with parentheses: `enum(`.
259pub(crate) const TYPE_ENUM_PREFIX: &str = "enum(";
260/// Type prefix for templates with parentheses: `tmpl(`.
261pub(crate) const TYPE_TMPL_PREFIX: &str = "tmpl(";
262/// Type name for options: `option`.
263pub(crate) const TYPE_OPTION: &str = "option";
264/// Type prefix for options with parentheses: `option(`.
265pub(crate) const TYPE_OPTION_PREFIX: &str = "option(";
266
267/// Variant name for the `Some` variant of `option(T)`.
268pub const OPTION_SOME: &str = "Some";
269/// Variant name for the `None` variant of `option(T)`.
270pub const OPTION_NONE: &str = "None";
271/// Field name for the inner value of `option(T)`'s `Some` variant.
272pub const OPTION_VAL_FIELD: &str = "val";
273
274// -- Literals -----------------------------------------------------------------
275
276/// Boolean true literal: `true`.
277pub(crate) const LIT_TRUE: &str = "true";
278/// Boolean false literal: `false`.
279pub(crate) const LIT_FALSE: &str = "false";
280
281/// Try to strip balanced quotes from a string literal token.
282///
283/// Returns `Some(inner)` if `token` is a valid quoted string literal
284/// (`"..."` or `'...'`), otherwise `None`.
285#[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
296// -- Error messages -----------------------------------------------------------
297
298/// Error when frontmatter block is missing.
299pub(crate) const ERR_MISSING_FM: &str =
300    "missing mandatory YAML frontmatter block (starts with ---)";
301/// Error when frontmatter block is unclosed.
302pub(crate) const ERR_UNCLOSED_FM: &str = "unclosed YAML frontmatter block";
303/// Prefix for undeclared variable references error.
304pub(crate) const ERR_UNDECLARED_PREFIX: &str = "undeclared variable(s) referenced in body: ";
305
306/// Error when a param is named after a reserved keyword.
307pub(crate) const ERR_RESERVED_KEYWORD: &str = "reserved keyword used as name";
308/// Error when two params have the same name.
309pub(crate) const ERR_DUPLICATE_PARAM: &str = "duplicate parameter name";
310/// Error when a `types:` entry has a duplicate name.
311pub(crate) const ERR_DUPLICATE_TYPE_ALIAS: &str = "duplicate type alias";
312/// Error when a `types:` entry shadows a built-in type name.
313pub(crate) const ERR_BUILTIN_SHADOW: &str = "type alias shadows built-in type name";
314/// Error when a type alias and param name collide in `PascalCase`.
315pub(crate) const ERR_TYPE_PARAM_CONFLICT: &str =
316    "type alias name conflicts with parameter name (PascalCase collision)";
317/// Error for circular import chains.
318#[cfg(feature = "std")]
319pub(crate) const ERR_CIRCULAR_IMPORT: &str = "circular import detected";
320/// Error when a type alias name shadows an import alias (stem).
321pub(crate) const ERR_TYPE_SHADOWS_IMPORT: &str = "type alias shadows import alias";
322/// Error when a param's `PascalCase` name shadows an import alias.
323pub(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";
326/// Error when a constant name is duplicated.
327pub(crate) const ERR_DUPLICATE_CONST: &str = "duplicate constant name";
328/// Error when a param and a const share the same name.
329pub(crate) const ERR_PARAM_CONST_CONFLICT: &str = "parameter name conflicts with constant name";
330/// Error when a for-loop binding shadows a declared name.
331pub(crate) const ERR_FOR_BINDING_SHADOWS: &str = "for loop binding shadows";
332/// Error when a `{% %}` tag starts a line without a blockquote `>` prefix.
333pub(crate) const ERR_BARE_STMT_TAG: &str =
334    "statement tag at line start must be blockquote-prefixed with '> '";
335/// Error when compound type uses angle or square brackets instead of parentheses.
336pub(crate) const ERR_COMPOUND_BRACKETS_PROHIBITED: &str =
337    "must use parentheses (...); angle brackets <...> and square brackets [...] are prohibited";
338
339/// Built-in type names and keywords that cannot be used as user-defined names.
340pub(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    // -- strip_string_literal -------------------------------------------------
358
359    #[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        // Inner quotes of the opposite kind are preserved.
410        assert_eq!(strip_string_literal("\"it's\""), Some("it's"));
411        assert_eq!(strip_string_literal("'say \"hi\"'"), Some("say \"hi\""));
412    }
413
414    // -- RESERVED_NAMES -------------------------------------------------------
415
416    #[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    // -- BUILTIN_FUNCTIONS ----------------------------------------------------
434
435    #[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    // -- Delimiter constants --------------------------------------------------
449
450    #[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}