@memberjunction/core-entities-server 5.22.0 → 5.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/custom/MJDuplicateRunEntityServer.server.d.ts +14 -0
  2. package/dist/custom/MJDuplicateRunEntityServer.server.d.ts.map +1 -1
  3. package/dist/custom/MJDuplicateRunEntityServer.server.js +95 -4
  4. package/dist/custom/MJDuplicateRunEntityServer.server.js.map +1 -1
  5. package/dist/custom/MJTemplateContentEntityServer.server.d.ts +0 -1
  6. package/dist/custom/MJTemplateContentEntityServer.server.d.ts.map +1 -1
  7. package/dist/custom/MJTemplateContentEntityServer.server.js +13 -68
  8. package/dist/custom/MJTemplateContentEntityServer.server.js.map +1 -1
  9. package/dist/custom/template-extraction/index.d.ts +5 -0
  10. package/dist/custom/template-extraction/index.d.ts.map +1 -0
  11. package/dist/custom/template-extraction/index.js +6 -0
  12. package/dist/custom/template-extraction/index.js.map +1 -0
  13. package/dist/custom/template-extraction/parser.d.ts +9 -0
  14. package/dist/custom/template-extraction/parser.d.ts.map +1 -0
  15. package/dist/custom/template-extraction/parser.js +685 -0
  16. package/dist/custom/template-extraction/parser.js.map +1 -0
  17. package/dist/custom/template-extraction/pipeline.d.ts +22 -0
  18. package/dist/custom/template-extraction/pipeline.d.ts.map +1 -0
  19. package/dist/custom/template-extraction/pipeline.js +181 -0
  20. package/dist/custom/template-extraction/pipeline.js.map +1 -0
  21. package/dist/custom/template-extraction/types.d.ts +101 -0
  22. package/dist/custom/template-extraction/types.d.ts.map +1 -0
  23. package/dist/custom/template-extraction/types.js +5 -0
  24. package/dist/custom/template-extraction/types.js.map +1 -0
  25. package/package.json +21 -20
@@ -0,0 +1,685 @@
1
+ // ═══════════════════════════════════════════════════
2
+ // Deterministic Nunjucks template parameter extraction via AST walking.
3
+ //
4
+ // Parses the template text with nunjucks.parser.parse(), walks the AST,
5
+ // and extracts all template parameters with inferred types, required-ness,
6
+ // default values, and property schemas.
7
+ // ═══════════════════════════════════════════════════
8
+ import nunjucks from 'nunjucks';
9
+ // The nunjucks parser API is publicly available at runtime but not declared in @types/nunjucks.
10
+ // Access it via the module's actual exports.
11
+ const nunjucksParser = nunjucks.parser;
12
+ /**
13
+ * Parse a Nunjucks template and deterministically extract all template parameters.
14
+ *
15
+ * @param templateText The raw Nunjucks template string
16
+ * @returns ParseResult with extracted parameters and any warnings
17
+ */
18
+ export function ParseTemplateParameters(templateText) {
19
+ const warnings = [];
20
+ // Handle null/empty input
21
+ if (!templateText || templateText.trim().length === 0) {
22
+ return { parameters: [], warnings: [] };
23
+ }
24
+ // Strip MJ-specific {@include ...} directives before parsing — Nunjucks doesn't understand them
25
+ const cleanedText = stripIncludeDirectives(templateText);
26
+ let ast;
27
+ try {
28
+ ast = nunjucksParser.parse(cleanedText);
29
+ }
30
+ catch (e) {
31
+ const msg = e instanceof Error ? e.message : String(e);
32
+ warnings.push(`Nunjucks parse error: ${msg}`);
33
+ return { parameters: [], warnings };
34
+ }
35
+ const walker = new ASTWalker();
36
+ walker.walk(ast);
37
+ const parameters = walker.buildParameters();
38
+ return { parameters, warnings };
39
+ }
40
+ /**
41
+ * Remove {@include ...} directives which are MJ-specific and would cause parse errors.
42
+ */
43
+ function stripIncludeDirectives(text) {
44
+ return text.replace(/\{@include\s+[^}]+\}/g, '');
45
+ }
46
+ // ═══════════════════════════════════════════════════
47
+ // AST Walker
48
+ // ═══════════════════════════════════════════════════
49
+ class ASTWalker {
50
+ constructor() {
51
+ this.params = new Map();
52
+ this.scopeStack = [{ locals: new Set() }];
53
+ /** Tracks how deep we are inside conditionals that guard a specific variable */
54
+ this.conditionalDepth = 0;
55
+ /** Set of parameter names that are currently being guarded by a conditional */
56
+ this.guardedParams = new Set();
57
+ // ─── Handler dispatch table ──────────────────────────────────────────────
58
+ this.handlers = {
59
+ Root: (n) => this.walkChildren(n),
60
+ NodeList: (n) => this.walkChildren(n),
61
+ Output: (n) => this.walkChildren(n),
62
+ TemplateData: () => { },
63
+ Symbol: (n) => this.handleSymbol(n),
64
+ LookupVal: (n) => this.handleLookupVal(n),
65
+ Filter: (n) => this.handleFilter(n),
66
+ For: (n) => this.handleFor(n),
67
+ If: (n) => this.handleIf(n),
68
+ Set: (n) => this.handleSet(n),
69
+ Macro: (n) => this.handleMacro(n),
70
+ And: (n) => this.handleBinaryOp(n),
71
+ Or: (n) => this.handleOr(n),
72
+ Not: (n) => this.handleUnaryOp(n),
73
+ Add: (n) => this.handleBinaryOp(n),
74
+ Sub: (n) => this.handleBinaryOp(n),
75
+ Mul: (n) => this.handleBinaryOp(n),
76
+ Div: (n) => this.handleBinaryOp(n),
77
+ Mod: (n) => this.handleBinaryOp(n),
78
+ Pow: (n) => this.handleBinaryOp(n),
79
+ Neg: (n) => this.handleUnaryOp(n),
80
+ Pos: (n) => this.handleUnaryOp(n),
81
+ FloorDiv: (n) => this.handleBinaryOp(n),
82
+ Concat: (n) => this.handleBinaryOp(n),
83
+ Compare: (n) => this.handleCompare(n),
84
+ In: (n) => this.handleBinaryOp(n),
85
+ Is: (n) => this.handleBinaryOp(n),
86
+ InlineIf: (n) => this.handleInlineIf(n),
87
+ Group: (n) => this.walkChildren(n),
88
+ Array: (n) => this.walkChildren(n),
89
+ Dict: (n) => this.walkChildren(n),
90
+ Pair: (n) => this.handlePair(n),
91
+ FunCall: (n) => this.handleFunCall(n),
92
+ Caller: (n) => this.walkBody(n),
93
+ Block: (n) => this.walkBody(n),
94
+ Extends: () => { },
95
+ Include: () => { },
96
+ Import: () => { },
97
+ FromImport: () => { },
98
+ CallExtension: (n) => this.handleCallExtension(n),
99
+ CallExtensionAsync: (n) => this.handleCallExtension(n),
100
+ Capture: (n) => this.walkBody(n),
101
+ Literal: () => { },
102
+ // KeywordArgs handled via FunCall args
103
+ KeywordArgs: (n) => this.walkChildren(n),
104
+ };
105
+ }
106
+ walk(node) {
107
+ if (!node)
108
+ return;
109
+ const handler = this.handlers[node.typename];
110
+ if (handler) {
111
+ handler.call(this, node);
112
+ }
113
+ else {
114
+ this.walkChildren(node);
115
+ }
116
+ }
117
+ /**
118
+ * Build final DeterministicParameter[] from accumulated data.
119
+ */
120
+ buildParameters() {
121
+ const result = [];
122
+ for (const acc of this.params.values()) {
123
+ const type = resolveType(acc.types);
124
+ result.push({
125
+ name: acc.name,
126
+ type,
127
+ isRequired: acc.usedUnconditionally,
128
+ defaultValue: acc.defaultValue,
129
+ isSystemVariable: acc.name.startsWith('_'),
130
+ appliedFilters: [...acc.filters],
131
+ properties: buildPropertyTree(acc.properties),
132
+ usages: acc.usages,
133
+ });
134
+ }
135
+ // Sort alphabetically for deterministic output
136
+ result.sort((a, b) => a.name.localeCompare(b.name));
137
+ return result;
138
+ }
139
+ // ─── Node handlers ───────────────────────────────────────────────────────
140
+ /**
141
+ * Handle a standalone Symbol reference: `{{ name }}`
142
+ */
143
+ handleSymbol(node) {
144
+ const name = node.value;
145
+ if (this.isLocal(name) || isBuiltinSymbol(name))
146
+ return;
147
+ this.recordUsage(name, 'Scalar', node, name);
148
+ }
149
+ /**
150
+ * Handle property access: `{{ user.name }}`, `{{ user.address.city }}`
151
+ * Walks up the LookupVal chain to find the root symbol.
152
+ */
153
+ handleLookupVal(node) {
154
+ const { rootName, path } = resolveLookupChain(node);
155
+ if (!rootName || this.isLocal(rootName) || isBuiltinSymbol(rootName))
156
+ return;
157
+ // The root is an Object type, and we record the property path
158
+ const fullPath = path.join('.');
159
+ this.recordUsage(rootName, 'Object', node, fullPath);
160
+ this.recordPropertyAccess(rootName, path.slice(1)); // skip the root name itself
161
+ }
162
+ /**
163
+ * Handle filter expressions: `{{ val | safe }}`, `{{ items | join(', ') }}`
164
+ * The filter name is NOT a parameter. The first arg of `args` is the expression being filtered.
165
+ */
166
+ handleFilter(node) {
167
+ const filterName = typeof node.name === 'string' ? node.name : node.name?.value;
168
+ // Check for `| default('value')` filter — extracts the default value
169
+ if (filterName === 'default' && node.args?.children && node.args.children.length >= 2) {
170
+ const expr = node.args.children[0];
171
+ const defaultNode = node.args.children[1];
172
+ const paramName = extractRootSymbol(expr);
173
+ if (paramName && !this.isLocal(paramName) && !isBuiltinSymbol(paramName)) {
174
+ const defaultVal = defaultNode.value;
175
+ if (defaultVal !== undefined) {
176
+ this.ensureParam(paramName).defaultValue = String(defaultVal);
177
+ }
178
+ }
179
+ }
180
+ // Record the filter name on the parameter (for metadata)
181
+ if (filterName && node.args?.children && node.args.children.length > 0) {
182
+ const expr = node.args.children[0];
183
+ const paramName = extractRootSymbol(expr);
184
+ if (paramName && !this.isLocal(paramName) && !isBuiltinSymbol(paramName)) {
185
+ this.ensureParam(paramName).filters.add(filterName);
186
+ }
187
+ }
188
+ // Walk the filtered expression (first arg), skip subsequent literal args
189
+ if (node.args?.children && node.args.children.length > 0) {
190
+ this.walk(node.args.children[0]);
191
+ }
192
+ }
193
+ /**
194
+ * Handle `{% for item in items %}` loops.
195
+ * `name` is the loop variable (local), `arr` is the iterable (parameter).
196
+ */
197
+ handleFor(node) {
198
+ // Walk the iterable expression — this is where the parameter reference lives
199
+ if (node.arr) {
200
+ // Before walking, mark the iterable as an Array type
201
+ const iterableName = extractRootSymbol(node.arr);
202
+ if (iterableName && !this.isLocal(iterableName) && !isBuiltinSymbol(iterableName)) {
203
+ // If the arr is a LookupVal (e.g., `entity.fields`), the root is Object, but the leaf is Array
204
+ if (node.arr.typename === 'LookupVal') {
205
+ this.handleLookupVal(node.arr);
206
+ // Mark the leaf property as Array type
207
+ const { path } = resolveLookupChain(node.arr);
208
+ if (path.length > 1) {
209
+ this.markPropertyAsArray(iterableName, path.slice(1));
210
+ }
211
+ }
212
+ else {
213
+ this.recordUsage(iterableName, 'Array', node.arr, iterableName);
214
+ }
215
+ }
216
+ else {
217
+ // If it's a complex expression, walk it normally
218
+ this.walk(node.arr);
219
+ }
220
+ }
221
+ // Push loop variable into scope
222
+ const loopVar = this.extractForLoopVar(node);
223
+ this.pushScope(loopVar ? [loopVar] : []);
224
+ // Walk the loop body with the loop var in scope
225
+ if (node.body)
226
+ this.walk(node.body);
227
+ if (node.else_)
228
+ this.walk(node.else_);
229
+ this.popScope();
230
+ }
231
+ /**
232
+ * Handle `{% if condition %}...{% elif %}...{% else %}...{% endif %}`.
233
+ * Tracks which parameters are being guarded by the conditional.
234
+ *
235
+ * A parameter referenced in the condition expression (e.g., `{% if foo %}`) is
236
+ * considered "guarded" — both the condition reference itself AND any usage in the
237
+ * body are marked conditional, because the entire block only runs when the param
238
+ * is truthy/present, implying it's optional.
239
+ */
240
+ handleIf(node) {
241
+ // Detect which params are being guarded by this condition
242
+ const guardedNames = extractReferencedParams(node.cond, this.currentLocals());
243
+ // Push conditional guard context BEFORE walking the condition,
244
+ // so the condition's own references are also marked conditional.
245
+ const previousGuarded = new Set(this.guardedParams);
246
+ for (const name of guardedNames) {
247
+ this.guardedParams.add(name);
248
+ }
249
+ this.conditionalDepth++;
250
+ // Walk the condition (params referenced here are recorded as conditional)
251
+ if (node.cond)
252
+ this.walk(node.cond);
253
+ // Walk the true branch
254
+ if (node.body)
255
+ this.walk(node.body);
256
+ // Restore guard context before walking elif/else
257
+ this.conditionalDepth--;
258
+ for (const name of guardedNames) {
259
+ if (!previousGuarded.has(name)) {
260
+ this.guardedParams.delete(name);
261
+ }
262
+ }
263
+ if (node.else_)
264
+ this.walk(node.else_);
265
+ }
266
+ /**
267
+ * Handle `{% set x = expr %}` — introduces a local variable.
268
+ *
269
+ * Nunjucks Set nodes store the value expression in the `.value` property
270
+ * (which is an AST node, not a primitive), and for capture form
271
+ * `{% set x %}...{% endset %}` the content is in `.body`.
272
+ */
273
+ handleSet(node) {
274
+ // Walk the value expression first (it may reference params).
275
+ // The Set node uses `.value` for the assigned expression — but our ASTNode
276
+ // interface types `.value` as `string | number | boolean`. At runtime the
277
+ // Set node's `.value` is actually another AST node (e.g., Add, Symbol, etc.).
278
+ const valueExpr = node.value;
279
+ if (valueExpr && typeof valueExpr === 'object' && 'typename' in valueExpr) {
280
+ this.walk(valueExpr);
281
+ }
282
+ // Also check body ({% set x %}...{% endset %} capture form)
283
+ if (node.body)
284
+ this.walk(node.body);
285
+ // Then add the variable to the current scope
286
+ if (node.targets) {
287
+ for (const target of node.targets) {
288
+ if (target.typename === 'Symbol' && typeof target.value === 'string') {
289
+ this.currentScope().locals.add(target.value);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Handle `{% macro name(arg1, arg2) %}...{% endmacro %}`.
296
+ * Macro args are local, not parameters.
297
+ */
298
+ handleMacro(node) {
299
+ const macroArgs = [];
300
+ if (node.args?.children) {
301
+ for (const arg of node.args.children) {
302
+ if (arg.typename === 'Symbol' && typeof arg.value === 'string') {
303
+ macroArgs.push(arg.value);
304
+ }
305
+ }
306
+ }
307
+ this.pushScope(macroArgs);
308
+ if (node.body)
309
+ this.walk(node.body);
310
+ this.popScope();
311
+ }
312
+ /**
313
+ * Handle binary operations: `{{ a + b }}`, `{{ a and b }}`, etc.
314
+ */
315
+ handleBinaryOp(node) {
316
+ if (node.left)
317
+ this.walk(node.left);
318
+ if (node.right)
319
+ this.walk(node.right);
320
+ }
321
+ /**
322
+ * Handle `{{ desc or 'fallback' }}` — the Or node.
323
+ * When the right side is a Literal, it's a default value.
324
+ */
325
+ handleOr(node) {
326
+ if (node.left)
327
+ this.walk(node.left);
328
+ // Check if right side is a literal (fallback/default pattern)
329
+ if (node.right?.typename === 'Literal' && node.left) {
330
+ const paramName = extractRootSymbol(node.left);
331
+ if (paramName && !this.isLocal(paramName) && !isBuiltinSymbol(paramName)) {
332
+ const acc = this.ensureParam(paramName);
333
+ if (acc.defaultValue === null && node.right.value !== undefined) {
334
+ acc.defaultValue = String(node.right.value);
335
+ }
336
+ }
337
+ }
338
+ if (node.right)
339
+ this.walk(node.right);
340
+ }
341
+ /**
342
+ * Handle unary operations: `{{ not x }}`, `{{ -x }}`
343
+ */
344
+ handleUnaryOp(node) {
345
+ if (node.target)
346
+ this.walk(node.target);
347
+ if (node.expr)
348
+ this.walk(node.expr);
349
+ }
350
+ /**
351
+ * Handle Compare nodes: `{{ a > 0 }}`, `{{ a == b }}`
352
+ */
353
+ handleCompare(node) {
354
+ if (node.expr)
355
+ this.walk(node.expr);
356
+ if (node.ops) {
357
+ for (const op of node.ops) {
358
+ if (op.expr)
359
+ this.walk(op.expr);
360
+ }
361
+ }
362
+ }
363
+ /**
364
+ * Handle inline ternary: `{{ 'yes' if active else 'no' }}`
365
+ */
366
+ handleInlineIf(node) {
367
+ if (node.cond)
368
+ this.walk(node.cond);
369
+ if (node.body)
370
+ this.walk(node.body);
371
+ if (node.else_)
372
+ this.walk(node.else_);
373
+ }
374
+ /**
375
+ * Handle Pair nodes (in Dict literals): `{{ {key: value} }}`
376
+ */
377
+ handlePair(node) {
378
+ // key is typically a Literal, value may reference a param
379
+ if (node.val)
380
+ this.walk(node.val);
381
+ // walk key too in case it references a variable
382
+ if (node.children) {
383
+ for (const child of node.children) {
384
+ this.walk(child);
385
+ }
386
+ }
387
+ }
388
+ /**
389
+ * Handle FunCall nodes (function calls): `{{ range(10) }}`, etc.
390
+ */
391
+ handleFunCall(node) {
392
+ if (node.name) {
393
+ const nameNode = node.name;
394
+ if (nameNode.typename)
395
+ this.walk(nameNode);
396
+ }
397
+ if (node.args?.children) {
398
+ for (const arg of node.args.children) {
399
+ this.walk(arg);
400
+ }
401
+ }
402
+ }
403
+ /**
404
+ * Handle CallExtension/CallExtensionAsync nodes (custom Nunjucks extensions).
405
+ * MJ uses `{% template "Name" %}` and `{% AIPrompt %}...{% endAIPrompt %}`.
406
+ */
407
+ handleCallExtension(node) {
408
+ // Walk any content args (the body between extension tags)
409
+ if (node.contentArgs) {
410
+ for (const arg of node.contentArgs) {
411
+ this.walk(arg);
412
+ }
413
+ }
414
+ // Walk regular args
415
+ if (node.args?.children) {
416
+ for (const arg of node.args.children) {
417
+ this.walk(arg);
418
+ }
419
+ }
420
+ }
421
+ // ─── Scope management ────────────────────────────────────────────────────
422
+ pushScope(locals) {
423
+ this.scopeStack.push({ locals: new Set(locals) });
424
+ }
425
+ popScope() {
426
+ if (this.scopeStack.length > 1) {
427
+ this.scopeStack.pop();
428
+ }
429
+ }
430
+ currentScope() {
431
+ return this.scopeStack[this.scopeStack.length - 1];
432
+ }
433
+ currentLocals() {
434
+ const all = new Set();
435
+ for (const scope of this.scopeStack) {
436
+ for (const local of scope.locals) {
437
+ all.add(local);
438
+ }
439
+ }
440
+ return all;
441
+ }
442
+ isLocal(name) {
443
+ for (const scope of this.scopeStack) {
444
+ if (scope.locals.has(name))
445
+ return true;
446
+ }
447
+ return false;
448
+ }
449
+ // ─── Parameter accumulation ──────────────────────────────────────────────
450
+ ensureParam(name) {
451
+ let acc = this.params.get(name);
452
+ if (!acc) {
453
+ acc = {
454
+ name,
455
+ types: new Set(),
456
+ usedUnconditionally: false,
457
+ defaultValue: null,
458
+ filters: new Set(),
459
+ properties: new Map(),
460
+ usages: [],
461
+ };
462
+ this.params.set(name, acc);
463
+ }
464
+ return acc;
465
+ }
466
+ recordUsage(name, type, node, accessPath) {
467
+ const acc = this.ensureParam(name);
468
+ acc.types.add(type);
469
+ const isConditional = this.guardedParams.has(name) || this.conditionalDepth > 0;
470
+ if (!isConditional) {
471
+ acc.usedUnconditionally = true;
472
+ }
473
+ acc.usages.push({
474
+ line: node.lineno,
475
+ col: node.colno,
476
+ accessPath,
477
+ isConditional,
478
+ });
479
+ }
480
+ recordPropertyAccess(rootName, propertyPath) {
481
+ if (propertyPath.length === 0)
482
+ return;
483
+ const acc = this.ensureParam(rootName);
484
+ let propMap = acc.properties;
485
+ for (let i = 0; i < propertyPath.length; i++) {
486
+ const propName = propertyPath[i];
487
+ let propAcc = propMap.get(propName);
488
+ if (!propAcc) {
489
+ propAcc = {
490
+ name: propName,
491
+ types: new Set(['Scalar']),
492
+ usedUnconditionally: !this.guardedParams.has(rootName) && this.conditionalDepth === 0,
493
+ children: new Map(),
494
+ };
495
+ propMap.set(propName, propAcc);
496
+ }
497
+ if (!this.guardedParams.has(rootName) && this.conditionalDepth === 0) {
498
+ propAcc.usedUnconditionally = true;
499
+ }
500
+ propMap = propAcc.children;
501
+ }
502
+ }
503
+ markPropertyAsArray(rootName, propertyPath) {
504
+ const acc = this.ensureParam(rootName);
505
+ let propMap = acc.properties;
506
+ for (let i = 0; i < propertyPath.length; i++) {
507
+ const propName = propertyPath[i];
508
+ let propAcc = propMap.get(propName);
509
+ if (!propAcc) {
510
+ propAcc = {
511
+ name: propName,
512
+ types: new Set(),
513
+ usedUnconditionally: true,
514
+ children: new Map(),
515
+ };
516
+ propMap.set(propName, propAcc);
517
+ }
518
+ if (i === propertyPath.length - 1) {
519
+ propAcc.types.add('Array');
520
+ }
521
+ propMap = propAcc.children;
522
+ }
523
+ }
524
+ /**
525
+ * Extract the for-loop variable name from a For AST node.
526
+ */
527
+ extractForLoopVar(node) {
528
+ // In Nunjucks AST, For node has `name` as a Symbol node for the loop variable
529
+ if (node.name && typeof node.name === 'object' && 'value' in node.name) {
530
+ return node.name.value;
531
+ }
532
+ return null;
533
+ }
534
+ // ─── Generic child walking ───────────────────────────────────────────────
535
+ walkChildren(node) {
536
+ if (node.children) {
537
+ for (const child of node.children) {
538
+ this.walk(child);
539
+ }
540
+ }
541
+ }
542
+ walkBody(node) {
543
+ if (node.body)
544
+ this.walk(node.body);
545
+ if (node.else_)
546
+ this.walk(node.else_);
547
+ }
548
+ }
549
+ // ═══════════════════════════════════════════════════
550
+ // Pure helper functions
551
+ // ═══════════════════════════════════════════════════
552
+ /**
553
+ * Walk a LookupVal chain to extract the root symbol and the full property path.
554
+ * `{{ user.address.city }}` → rootName: "user", path: ["user", "address", "city"]
555
+ */
556
+ function resolveLookupChain(node) {
557
+ const path = [];
558
+ let current = node;
559
+ while (current.typename === 'LookupVal') {
560
+ if (current.val && typeof current.val.value === 'string') {
561
+ path.unshift(current.val.value);
562
+ }
563
+ current = current.target;
564
+ }
565
+ if (current.typename === 'Symbol' && typeof current.value === 'string') {
566
+ path.unshift(current.value);
567
+ return { rootName: current.value, path };
568
+ }
569
+ return { rootName: null, path };
570
+ }
571
+ /**
572
+ * Extract the root symbol name from an expression node.
573
+ * Works for Symbol, LookupVal chains, and Filter wrapped expressions.
574
+ */
575
+ function extractRootSymbol(node) {
576
+ if (!node)
577
+ return null;
578
+ if (node.typename === 'Symbol') {
579
+ return typeof node.value === 'string' ? node.value : null;
580
+ }
581
+ if (node.typename === 'LookupVal') {
582
+ const { rootName } = resolveLookupChain(node);
583
+ return rootName;
584
+ }
585
+ if (node.typename === 'Filter' && node.args?.children && node.args.children.length > 0) {
586
+ return extractRootSymbol(node.args.children[0]);
587
+ }
588
+ return null;
589
+ }
590
+ /**
591
+ * Extract all parameter names referenced in a conditional expression.
592
+ * Used to determine which params are guarded by an `{% if %}` block.
593
+ */
594
+ function extractReferencedParams(node, locals) {
595
+ if (!node)
596
+ return [];
597
+ const names = [];
598
+ function collect(n) {
599
+ if (n.typename === 'Symbol' && typeof n.value === 'string') {
600
+ if (!locals.has(n.value) && !isBuiltinSymbol(n.value)) {
601
+ names.push(n.value);
602
+ }
603
+ }
604
+ else if (n.typename === 'LookupVal') {
605
+ const { rootName } = resolveLookupChain(n);
606
+ if (rootName && !locals.has(rootName) && !isBuiltinSymbol(rootName)) {
607
+ names.push(rootName);
608
+ }
609
+ }
610
+ else {
611
+ // Walk children of compound expressions (And, Or, Not, Compare, etc.)
612
+ if (n.left)
613
+ collect(n.left);
614
+ if (n.right)
615
+ collect(n.right);
616
+ if (n.target)
617
+ collect(n.target);
618
+ if (n.expr)
619
+ collect(n.expr);
620
+ if (n.children)
621
+ n.children.forEach(collect);
622
+ if (n.args?.children)
623
+ n.args.children.forEach(collect);
624
+ if (n.ops) {
625
+ for (const op of n.ops) {
626
+ if (op.expr)
627
+ collect(op.expr);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ collect(node);
633
+ return [...new Set(names)];
634
+ }
635
+ /**
636
+ * Determine if a symbol name is a Nunjucks built-in (not a template parameter).
637
+ */
638
+ function isBuiltinSymbol(name) {
639
+ return BUILTIN_SYMBOLS.has(name);
640
+ }
641
+ const BUILTIN_SYMBOLS = new Set([
642
+ 'loop', // Nunjucks loop context variable
643
+ 'true',
644
+ 'false',
645
+ 'null',
646
+ 'none',
647
+ 'undefined',
648
+ 'range', // Built-in function
649
+ 'cycler', // Built-in function
650
+ 'joiner', // Built-in function
651
+ 'caller', // Block variable
652
+ 'super', // Block variable
653
+ ]);
654
+ /**
655
+ * Resolve the final type from a set of observed types.
656
+ * Priority: Array > Object > Scalar (most complex wins).
657
+ */
658
+ function resolveType(types) {
659
+ if (types.has('Array'))
660
+ return 'Array';
661
+ if (types.has('Object'))
662
+ return 'Object';
663
+ if (types.has('Record'))
664
+ return 'Record';
665
+ if (types.has('Entity'))
666
+ return 'Entity';
667
+ return 'Scalar';
668
+ }
669
+ /**
670
+ * Convert the internal property accumulator map into the public PropertyAccess tree.
671
+ */
672
+ function buildPropertyTree(propMap) {
673
+ const result = [];
674
+ for (const acc of propMap.values()) {
675
+ result.push({
676
+ name: acc.name,
677
+ type: resolveType(acc.types),
678
+ optional: !acc.usedUnconditionally,
679
+ children: buildPropertyTree(acc.children),
680
+ });
681
+ }
682
+ result.sort((a, b) => a.name.localeCompare(b.name));
683
+ return result;
684
+ }
685
+ //# sourceMappingURL=parser.js.map