| 1 | /** |
| 2 | * Macro Execution Flags - modifiers that change how macros are resolved at runtime. |
| 3 | * |
| 4 | * Flags are special symbols placed between the opening braces `{{` and the macro identifier. |
| 5 | * Example: `{{!user}}` - the `!` is an "immediate resolve" flag. |
| 6 | * |
| 7 | * Multiple flags can be combined: `{{!?myMacro}}` or `{{ ! ? myMacro }}` |
| 8 | */ |
| 9 | |
| 10 | /** |
| 11 | * @typedef {Object} MacroFlags |
| 12 | * @property {boolean} immediate - Whether the immediate (`!`) flag is set. |
| 13 | * @property {boolean} delayed - Whether the delayed (`?`) flag is set. |
| 14 | * @property {boolean} reevaluate - Whether the re-evaluate (`~`) flag is set. |
| 15 | * @property {boolean} filter - Whether the filter (`>`) flag is set. |
| 16 | * @property {boolean} closingBlock - Whether the closing block (`/`) flag is set. |
| 17 | * @property {boolean} preserveWhitespace - Whether the preserve whitespace (`#`) flag is set. |
| 18 | * @property {string[]} raw - The raw flag symbols in order of appearance. |
| 19 | */ |
| 20 | |
| 21 | /** |
| 22 | * Enum of all recognized macro execution flags. |
| 23 | * |
| 24 | * @readonly |
| 25 | * @enum {string} |
| 26 | */ |
| 27 | export const MacroFlagType = Object.freeze({ |
| 28 | /** |
| 29 | * Immediate resolve flag (`!`). |
| 30 | * This macro will be resolved first (in order of appearance) before "normal" macros. |
| 31 | * @status TBD - Not implemented in v1 |
| 32 | */ |
| 33 | IMMEDIATE: '!', |
| 34 | |
| 35 | /** |
| 36 | * Delayed resolve flag (`?`). |
| 37 | * This macro will be resolved last (in order of appearance) after "normal" macros. |
| 38 | * @status TBD - Not implemented in v1 |
| 39 | */ |
| 40 | DELAYED: '?', |
| 41 | |
| 42 | /** |
| 43 | * Re-evaluate flag (`~`). |
| 44 | * Marks a macro for potential re-evaluation. |
| 45 | * @status TBD - Not implemented in v1 |
| 46 | */ |
| 47 | REEVALUATE: '~', |
| 48 | |
| 49 | /** |
| 50 | * Filter/pipe flag (`>`). |
| 51 | * Indicates that this macro should resolve `|` characters as output filters. |
| 52 | * @status Parsed - Filter feature not yet implemented |
| 53 | */ |
| 54 | FILTER: '>', |
| 55 | |
| 56 | /** |
| 57 | * Closing block flag (`/`). |
| 58 | * Marks this macro as the closing block of a scoped macro with the same identifier. |
| 59 | * A closing block macro does not support arguments itself. |
| 60 | * Example: `{{setvar::myvar}}long text{{/setvar}}` |
| 61 | * @status Implemented - Content between opening and closing tags becomes the last unnamed argument |
| 62 | */ |
| 63 | CLOSING_BLOCK: '/', |
| 64 | |
| 65 | /** |
| 66 | * Preserve whitespace flag (`#`). |
| 67 | * Prevents automatic trimming of scoped content. |
| 68 | * By default, scoped macro content is trimmed. Use this flag to preserve leading/trailing whitespace. |
| 69 | * Also provides backwards compatibility with legacy handlebars-style syntax like `{{#if ...}}`. |
| 70 | * Example: `{{#setvar::myvar}} content with spaces {{/setvar}}` |
| 71 | * @status Implemented - Prevents auto-trim on scoped content |
| 72 | */ |
| 73 | PRESERVE_WHITESPACE: '#', |
| 74 | |
| 75 | // Note: Variable shorthand (. and $) are NOT flags - they are special prefixes |
| 76 | // that trigger the variable expression parsing branch. See MacroLexer.js Var tokens. |
| 77 | }); |
| 78 | |
| 79 | /** |
| 80 | * @typedef {Object} MacroFlagDefinition |
| 81 | * @property {MacroFlagType} type - The flag type enum value (also the symbol). |
| 82 | * @property {string} name - Human-readable name for the flag. |
| 83 | * @property {string} description - Description of what the flag does. |
| 84 | * @property {boolean} implemented - Whether this flag's behavior is implemented. |
| 85 | * @property {boolean} affectsParser - Whether this flag changes parsing behavior (e.g., filter flag). |
| 86 | */ |
| 87 | |
| 88 | /** |
| 89 | * Definitions for all macro flags with metadata. |
| 90 | * |
| 91 | * @type {Map<string, MacroFlagDefinition>} |
| 92 | */ |
| 93 | export const MacroFlagDefinitions = new Map([ |
| 94 | [MacroFlagType.IMMEDIATE, { |
| 95 | type: MacroFlagType.IMMEDIATE, |
| 96 | name: 'Immediate', |
| 97 | description: 'Resolve this macro before other macros in the same text.', |
| 98 | implemented: false, |
| 99 | affectsParser: false, |
| 100 | }], |
| 101 | [MacroFlagType.DELAYED, { |
| 102 | type: MacroFlagType.DELAYED, |
| 103 | name: 'Delayed', |
| 104 | description: 'Resolve this macro after other macros in the same text.', |
| 105 | implemented: false, |
| 106 | affectsParser: false, |
| 107 | }], |
| 108 | [MacroFlagType.REEVALUATE, { |
| 109 | type: MacroFlagType.REEVALUATE, |
| 110 | name: 'Re-evaluate', |
| 111 | description: 'Mark this macro for re-evaluation.', |
| 112 | implemented: false, |
| 113 | affectsParser: false, |
| 114 | }], |
| 115 | [MacroFlagType.FILTER, { |
| 116 | type: MacroFlagType.FILTER, |
| 117 | name: 'Filter', |
| 118 | description: 'Enable pipe-based output filters for this macro.', |
| 119 | implemented: false, |
| 120 | affectsParser: true, // Changes how `|` is parsed |
| 121 | }], |
| 122 | [MacroFlagType.CLOSING_BLOCK, { |
| 123 | type: MacroFlagType.CLOSING_BLOCK, |
| 124 | name: 'Closing Block', |
| 125 | description: 'Marks this as a closing block for a scoped macro.', |
| 126 | implemented: true, |
| 127 | affectsParser: false, |
| 128 | }], |
| 129 | [MacroFlagType.PRESERVE_WHITESPACE, { |
| 130 | type: MacroFlagType.PRESERVE_WHITESPACE, |
| 131 | name: 'Preserve Whitespace', |
| 132 | description: 'Prevent automatic trimming of scoped content (legacy # syntax).', |
| 133 | implemented: true, |
| 134 | affectsParser: false, |
| 135 | }], |
| 136 | ]); |
| 137 | |
| 138 | /** |
| 139 | * Set of all valid flag symbols for quick lookup. |
| 140 | * |
| 141 | * @type {Set<string>} |
| 142 | */ |
| 143 | export const ValidFlagSymbols = new Set(Object.values(MacroFlagType)); |
| 144 | |
| 145 | /** |
| 146 | * Creates a default MacroFlags object with all flags set to false. |
| 147 | * |
| 148 | * @returns {MacroFlags} |
| 149 | */ |
| 150 | export function createEmptyFlags() { |
| 151 | return { |
| 152 | immediate: false, |
| 153 | delayed: false, |
| 154 | reevaluate: false, |
| 155 | filter: false, |
| 156 | closingBlock: false, |
| 157 | preserveWhitespace: false, |
| 158 | raw: [], |
| 159 | }; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Parses an array of flag symbols into a MacroFlags object. |
| 164 | * |
| 165 | * @param {string[]} flagSymbols - Array of flag symbol strings (e.g., ['!', '?']). |
| 166 | * @returns {MacroFlags} |
| 167 | */ |
| 168 | export function parseFlags(flagSymbols) { |
| 169 | const flags = createEmptyFlags(); |
| 170 | |
| 171 | for (const symbol of flagSymbols) { |
| 172 | switch (symbol) { |
| 173 | case MacroFlagType.IMMEDIATE: |
| 174 | flags.immediate = true; |
| 175 | break; |
| 176 | case MacroFlagType.DELAYED: |
| 177 | flags.delayed = true; |
| 178 | break; |
| 179 | case MacroFlagType.REEVALUATE: |
| 180 | flags.reevaluate = true; |
| 181 | break; |
| 182 | case MacroFlagType.FILTER: |
| 183 | flags.filter = true; |
| 184 | break; |
| 185 | case MacroFlagType.CLOSING_BLOCK: |
| 186 | flags.closingBlock = true; |
| 187 | break; |
| 188 | case MacroFlagType.PRESERVE_WHITESPACE: |
| 189 | flags.preserveWhitespace = true; |
| 190 | break; |
| 191 | default: |
| 192 | console.warn(`Can't parse unknown macro flag: ${symbol}`); |
| 193 | } |
| 194 | flags.raw.push(symbol); |
| 195 | } |
| 196 | |
| 197 | return flags; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Checks if a MacroFlags object has any flags set. |
| 202 | * |
| 203 | * @param {MacroFlags} flags - The flags object to check. |
| 204 | * @returns {boolean} True if at least one flag is set. |
| 205 | */ |
| 206 | export function hasAnyFlag(flags) { |
| 207 | return flags.raw.length > 0; |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Gets the flag definition for a given symbol. |
| 212 | * |
| 213 | * @param {string} symbol - The flag symbol (e.g., '!'). |
| 214 | * @returns {MacroFlagDefinition|undefined} |
| 215 | */ |
| 216 | export function getFlagDefinition(symbol) { |
| 217 | return MacroFlagDefinitions.get(symbol); |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Checks if a given symbol is a valid macro flag. |
| 222 | * |
| 223 | * @param {string} symbol - The symbol to check. |
| 224 | * @returns {boolean} |
| 225 | */ |
| 226 | export function isValidFlag(symbol) { |
| 227 | return ValidFlagSymbols.has(symbol); |
| 228 | } |