typebars 1.1.0 → 1.1.2
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.
- package/dist/cjs/analyzer.js +1 -1
- package/dist/cjs/analyzer.js.map +1 -1
- package/dist/cjs/parser.d.ts +8 -0
- package/dist/cjs/parser.js +1 -1
- package/dist/cjs/parser.js.map +1 -1
- package/dist/esm/analyzer.js +1 -1
- package/dist/esm/analyzer.js.map +1 -1
- package/dist/esm/parser.d.ts +8 -0
- package/dist/esm/parser.js +1 -1
- package/dist/esm/parser.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/parser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/parser.ts"],"sourcesContent":["import Handlebars from \"handlebars\";\nimport { TemplateParseError } from \"./errors.ts\";\n\n// ─── Root Token ──────────────────────────────────────────────────────────────\n// Special token `$root` references the entire input schema / data context.\n// It cannot be followed by property access (e.g. `$root.name` is invalid).\nexport const ROOT_TOKEN = \"$root\";\n\n// ─── Regex for detecting a template identifier (e.g. \"meetingId:1\") ──────────\n// The identifier is always an integer (positive, zero, or negative),\n// separated from the variable name by a `:`. The `:` and number are on\n// the **last** segment of the path (Handlebars splits on `.`).\nconst IDENTIFIER_RE = /^(.+):(-?\\d+)$/;\n\n// ─── Template Parser ─────────────────────────────────────────────────────────\n// Thin wrapper around the Handlebars parser. Centralizing the parser call\n// here allows us to:\n// 1. Wrap errors into our own hierarchy (`TemplateParseError`)\n// 2. Expose AST introspection helpers (e.g. `isSingleExpression`)\n// 3. Isolate the direct Handlebars dependency from the rest of the codebase\n//\n// AST caching is handled at the `Typebars` instance level (via its own\n// configurable LRU cache), not here. This module only parses and wraps errors.\n\n// ─── Regex for detecting a numeric literal (integer or decimal, signed) ──────\n// Intentionally conservative: no scientific notation (1e5), no hex (0xFF),\n// no separators (1_000). We only want to recognize what a human would write\n// as a numeric value in a template.\nconst NUMERIC_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Parses a template string and returns the Handlebars AST.\n *\n * This function does not cache results — caching is managed at the\n * `Typebars` instance level via its own configurable LRU cache.\n *\n * @param template - The template string to parse (e.g. `\"Hello {{name}}\"`)\n * @returns The root AST node (`hbs.AST.Program`)\n * @throws {TemplateParseError} if the template syntax is invalid\n */\nexport function parse(template: string): hbs.AST.Program {\n\ttry {\n\t\treturn Handlebars.parse(template);\n\t} catch (error: unknown) {\n\t\t// Handlebars throws a plain Error with a descriptive message.\n\t\t// We transform it into a TemplateParseError for uniform handling.\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\n\t\t// Handlebars sometimes includes the position in the message —\n\t\t// attempt to extract it to enrich our error.\n\t\tconst locMatch = message.match(/line\\s+(\\d+).*?column\\s+(\\d+)/i);\n\t\tconst loc = locMatch\n\t\t\t? {\n\t\t\t\t\tline: parseInt(locMatch[1] ?? \"0\", 10),\n\t\t\t\t\tcolumn: parseInt(locMatch[2] ?? \"0\", 10),\n\t\t\t\t}\n\t\t\t: undefined;\n\n\t\tthrow new TemplateParseError(message, loc);\n\t}\n}\n\n/**\n * Determines whether the AST represents a template consisting of a single\n * expression `{{expression}}` with no text content around it.\n *\n * This matters for return type inference:\n * - Template `{{value}}` → returns the raw type of `value` (number, object…)\n * - Template `Hello {{name}}` → always returns `string` (concatenation)\n *\n * @param ast - The parsed AST of the template\n * @returns `true` if the template is a single expression\n */\nexport function isSingleExpression(ast: hbs.AST.Program): boolean {\n\tconst { body } = ast;\n\n\t// Exactly one node, and it's a MustacheStatement (not a block, not text)\n\treturn body.length === 1 && body[0]?.type === \"MustacheStatement\";\n}\n\n/**\n * Extracts the path segments from a Handlebars `PathExpression`.\n *\n * Handlebars decomposes `user.address.city` into `{ parts: [\"user\", \"address\", \"city\"] }`.\n * This function safely extracts those segments.\n *\n * @param expr - The expression to extract the path from\n * @returns The path segments, or an empty array if the expression is not\n * a `PathExpression`\n */\nexport function extractPathSegments(expr: hbs.AST.Expression): string[] {\n\tif (expr.type === \"PathExpression\") {\n\t\treturn (expr as hbs.AST.PathExpression).parts;\n\t}\n\treturn [];\n}\n\n/**\n * Checks whether an AST expression is a `PathExpression` pointing to `this`\n * (used inside `{{#each}}` blocks).\n */\nexport function isThisExpression(expr: hbs.AST.Expression): boolean {\n\tif (expr.type !== \"PathExpression\") return false;\n\tconst path = expr as hbs.AST.PathExpression;\n\treturn path.original === \"this\" || path.original === \".\";\n}\n\n/**\n * Checks whether an AST expression is a `PathExpression` whose first\n * segment is the `$root` token.\n *\n * This covers both the valid `{{$root}}` case (single segment) and the\n * invalid `{{$root.name}}` case (multiple segments). The caller must\n * check `parts.length` to distinguish valid from invalid usage.\n *\n * **Note:** This does NOT match `{{$root:2}}` because Handlebars parses\n * it as `parts: [\"$root:2\"]` (identifier still attached). Use\n * `isRootSegments()` on cleaned segments instead when identifier\n * extraction has already been performed.\n */\nexport function isRootExpression(expr: hbs.AST.Expression): boolean {\n\tif (expr.type !== \"PathExpression\") return false;\n\tconst path = expr as hbs.AST.PathExpression;\n\treturn path.parts.length > 0 && path.parts[0] === ROOT_TOKEN;\n}\n\n/**\n * Checks whether an array of **cleaned** path segments represents a\n * `$root` reference (i.e. exactly one segment equal to `\"$root\"`).\n *\n * This is the segment-level counterpart of `isRootExpression()` and is\n * meant to be called **after** `extractExpressionIdentifier()` has\n * stripped the `:N` suffix. It correctly handles both `{{$root}}` and\n * `{{$root:2}}`.\n *\n * @param cleanSegments - Path segments with identifiers already removed\n * @returns `true` if the segments represent a `$root` reference\n */\nexport function isRootSegments(cleanSegments: string[]): boolean {\n\treturn cleanSegments.length === 1 && cleanSegments[0] === ROOT_TOKEN;\n}\n\n/**\n * Checks whether cleaned segments represent a **path traversal** on\n * `$root` (e.g. `$root.name`, `$root.address.city`).\n *\n * Path traversal on `$root` is forbidden — users should write `{{name}}`\n * instead of `{{$root.name}}`.\n *\n * @param cleanSegments - Path segments with identifiers already removed\n * @returns `true` if the first segment is `$root` and there are additional segments\n */\nexport function isRootPathTraversal(cleanSegments: string[]): boolean {\n\treturn cleanSegments.length > 1 && cleanSegments[0] === ROOT_TOKEN;\n}\n\n// ─── Filtering Semantically Significant Nodes ───────────────────────────────\n// In a Handlebars AST, formatting (newlines, indentation) produces\n// `ContentStatement` nodes whose value is purely whitespace. These nodes\n// have no semantic impact and must be ignored during type inference to\n// correctly detect \"effectively a single block\" or \"effectively a single\n// expression\" cases.\n\n/**\n * Returns the semantically significant statements of a Program by\n * filtering out `ContentStatement` nodes that contain only whitespace.\n */\nexport function getEffectiveBody(\n\tprogram: hbs.AST.Program,\n): hbs.AST.Statement[] {\n\treturn program.body.filter(\n\t\t(s) =>\n\t\t\t!(\n\t\t\t\ts.type === \"ContentStatement\" &&\n\t\t\t\t(s as hbs.AST.ContentStatement).value.trim() === \"\"\n\t\t\t),\n\t);\n}\n\n/**\n * Determines whether a Program effectively consists of a single\n * `BlockStatement` (ignoring surrounding whitespace).\n *\n * Recognized examples:\n * ```\n * {{#if x}}...{{/if}}\n *\n * {{#each items}}...{{/each}}\n * ```\n *\n * @returns The single `BlockStatement`, or `null` if the program contains\n * other significant nodes.\n */\nexport function getEffectivelySingleBlock(\n\tprogram: hbs.AST.Program,\n): hbs.AST.BlockStatement | null {\n\tconst effective = getEffectiveBody(program);\n\tif (effective.length === 1 && effective[0]?.type === \"BlockStatement\") {\n\t\treturn effective[0] as hbs.AST.BlockStatement;\n\t}\n\treturn null;\n}\n\n/**\n * Determines whether a Program effectively consists of a single\n * `MustacheStatement` (ignoring surrounding whitespace).\n *\n * Example: ` {{age}} ` → true\n */\nexport function getEffectivelySingleExpression(\n\tprogram: hbs.AST.Program,\n): hbs.AST.MustacheStatement | null {\n\tconst effective = getEffectiveBody(program);\n\tif (effective.length === 1 && effective[0]?.type === \"MustacheStatement\") {\n\t\treturn effective[0] as hbs.AST.MustacheStatement;\n\t}\n\treturn null;\n}\n\n// ─── Handlebars Expression Detection ─────────────────────────────────────────\n// Fast heuristic to determine whether a string contains Handlebars expressions.\n// Used by `excludeTemplateExpression` filtering to skip dynamic entries.\n\n/**\n * Determines whether a string contains Handlebars expressions.\n *\n * A string contains template expressions if it includes `{{` (opening\n * delimiter for Handlebars mustache/block statements). This is a fast\n * substring check — **no parsing is performed**.\n *\n * Used by `excludeTemplateExpression` to filter out properties whose\n * values are dynamic templates.\n *\n * **Limitation:** This is a simple `includes(\"{{\")` check, not a full\n * parser. It will produce false positives for strings that contain `{{`\n * as literal text (e.g. `\"Use {{name}} syntax\"` in documentation strings)\n * or in escaped contexts. For the current use case (filtering object/array\n * entries that are likely templates), this trade-off is acceptable because:\n * - It avoids the cost of parsing every string value\n * - False positives only cause an entry to be excluded from the output\n * schema (conservative behavior)\n *\n * @param value - The string to check\n * @returns `true` if the string contains at least one `{{` sequence\n */\nexport function hasHandlebarsExpression(value: string): boolean {\n\treturn value.includes(\"{{\");\n}\n\n// ─── Fast-Path Detection ─────────────────────────────────────────────────────\n// For templates consisting only of text and simple expressions (no blocks,\n// no helpers with parameters), we can bypass Handlebars entirely and perform\n// a simple variable replacement via string concatenation.\n\n/**\n * Determines whether an AST can be executed via the fast-path (direct\n * concatenation without going through `Handlebars.compile()`).\n *\n * The fast-path is possible when the template only contains:\n * - `ContentStatement` nodes (static text)\n * - Simple `MustacheStatement` nodes (no params, no hash)\n *\n * This excludes:\n * - Block helpers (`{{#if}}`, `{{#each}}`, etc.)\n * - Inline helpers (`{{uppercase name}}`)\n * - Sub-expressions\n *\n * @param ast - The parsed AST of the template\n * @returns `true` if the template can use the fast-path\n */\nexport function canUseFastPath(ast: hbs.AST.Program): boolean {\n\treturn ast.body.every(\n\t\t(s) =>\n\t\t\ts.type === \"ContentStatement\" ||\n\t\t\t(s.type === \"MustacheStatement\" &&\n\t\t\t\t(s as hbs.AST.MustacheStatement).params.length === 0 &&\n\t\t\t\t!(s as hbs.AST.MustacheStatement).hash),\n\t);\n}\n\n// ─── Literal Detection in Text Content ───────────────────────────────────────\n// When a program contains only ContentStatements (no expressions), we try\n// to detect whether the concatenated and trimmed text is a typed literal\n// (number, boolean, null). This enables correct type inference for branches\n// like `{{#if x}} 42 {{/if}}`.\n\n/**\n * Attempts to detect the type of a raw text literal.\n *\n * @param text - The trimmed text from a ContentStatement or group of ContentStatements\n * @returns The detected JSON Schema type, or `null` if it's free-form text (string).\n */\nexport function detectLiteralType(\n\ttext: string,\n): \"number\" | \"boolean\" | \"null\" | null {\n\tif (NUMERIC_LITERAL_RE.test(text)) return \"number\";\n\tif (text === \"true\" || text === \"false\") return \"boolean\";\n\tif (text === \"null\") return \"null\";\n\treturn null;\n}\n\n/**\n * Coerces a raw string from Handlebars rendering to its actual type\n * if it represents a literal (number, boolean, null).\n * Returns the raw (untrimmed) string otherwise.\n */\nexport function coerceLiteral(raw: string): unknown {\n\tconst trimmed = raw.trim();\n\tconst type = detectLiteralType(trimmed);\n\tif (type === \"number\") return Number(trimmed);\n\tif (type === \"boolean\") return trimmed === \"true\";\n\tif (type === \"null\") return null;\n\t// Not a typed literal — return the raw string without trimming,\n\t// as whitespace may be significant (e.g. output of an #each block).\n\treturn raw;\n}\n\n// ─── Template Identifier Parsing ─────────────────────────────────────────────\n// Syntax `{{key:N}}` where N is an integer (positive, zero, or negative).\n// The identifier allows resolving a variable from a specific data source\n// (e.g. a workflow node identified by its number).\n\n/** Result of parsing a path segment with a potential identifier */\nexport interface ParsedIdentifier {\n\t/** The variable name, without the `:N` suffix */\n\tkey: string;\n\t/** The numeric identifier, or `null` if absent */\n\tidentifier: number | null;\n}\n\n/**\n * Parses an individual path segment to extract the key and optional identifier.\n *\n * @param segment - A raw path segment (e.g. `\"meetingId:1\"` or `\"meetingId\"`)\n * @returns An object `{ key, identifier }`\n *\n * @example\n * ```\n * parseIdentifier(\"meetingId:1\") // → { key: \"meetingId\", identifier: 1 }\n * parseIdentifier(\"meetingId\") // → { key: \"meetingId\", identifier: null }\n * parseIdentifier(\"meetingId:0\") // → { key: \"meetingId\", identifier: 0 }\n * ```\n */\nexport function parseIdentifier(segment: string): ParsedIdentifier {\n\tconst match = segment.match(IDENTIFIER_RE);\n\tif (match) {\n\t\treturn {\n\t\t\tkey: match[1] ?? segment,\n\t\t\tidentifier: parseInt(match[2] ?? \"0\", 10),\n\t\t};\n\t}\n\treturn { key: segment, identifier: null };\n}\n\n/** Result of extracting the identifier from a complete expression */\nexport interface ExpressionIdentifier {\n\t/** Cleaned path segments (without the `:N` suffix on the last one) */\n\tcleanSegments: string[];\n\t/** The numeric identifier extracted from the last segment, or `null` */\n\tidentifier: number | null;\n}\n\n/**\n * Extracts the identifier from a complete expression (array of segments).\n *\n * The identifier is always on the **last** segment of the path, because\n * Handlebars splits on `.` before the `:`.\n *\n * @param segments - The raw path segments (e.g. `[\"user\", \"name:1\"]`)\n * @returns An object `{ cleanSegments, identifier }`\n *\n * @example\n * ```\n * extractExpressionIdentifier([\"meetingId:1\"])\n * // → { cleanSegments: [\"meetingId\"], identifier: 1 }\n *\n * extractExpressionIdentifier([\"user\", \"name:1\"])\n * // → { cleanSegments: [\"user\", \"name\"], identifier: 1 }\n *\n * extractExpressionIdentifier([\"meetingId\"])\n * // → { cleanSegments: [\"meetingId\"], identifier: null }\n * ```\n */\nexport function extractExpressionIdentifier(\n\tsegments: string[],\n): ExpressionIdentifier {\n\tif (segments.length === 0) {\n\t\treturn { cleanSegments: [], identifier: null };\n\t}\n\n\tconst lastSegment = segments[segments.length - 1] as string;\n\tconst parsed = parseIdentifier(lastSegment);\n\n\tif (parsed.identifier !== null) {\n\t\tconst cleanSegments = [...segments.slice(0, -1), parsed.key];\n\t\treturn { cleanSegments, identifier: parsed.identifier };\n\t}\n\n\treturn { cleanSegments: segments, identifier: null };\n}\n"],"names":["Handlebars","TemplateParseError","ROOT_TOKEN","IDENTIFIER_RE","NUMERIC_LITERAL_RE","parse","template","error","message","Error","String","locMatch","match","loc","line","parseInt","column","undefined","isSingleExpression","ast","body","length","type","extractPathSegments","expr","parts","isThisExpression","path","original","isRootExpression","isRootSegments","cleanSegments","isRootPathTraversal","getEffectiveBody","program","filter","s","value","trim","getEffectivelySingleBlock","effective","getEffectivelySingleExpression","hasHandlebarsExpression","includes","canUseFastPath","every","params","hash","detectLiteralType","text","test","coerceLiteral","raw","trimmed","Number","parseIdentifier","segment","key","identifier","extractExpressionIdentifier","segments","lastSegment","parsed","slice"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/parser.ts"],"sourcesContent":["import Handlebars from \"handlebars\";\nimport { TemplateParseError } from \"./errors.ts\";\n\n// ─── Root Token ──────────────────────────────────────────────────────────────\n// Special token `$root` references the entire input schema / data context.\n// It cannot be followed by property access (e.g. `$root.name` is invalid).\nexport const ROOT_TOKEN = \"$root\";\n\n// ─── Regex for detecting a template identifier (e.g. \"meetingId:1\") ──────────\n// The identifier is always an integer (positive, zero, or negative),\n// separated from the variable name by a `:`. The `:` and number are on\n// the **last** segment of the path (Handlebars splits on `.`).\nconst IDENTIFIER_RE = /^(.+):(-?\\d+)$/;\n\n// ─── Template Parser ─────────────────────────────────────────────────────────\n// Thin wrapper around the Handlebars parser. Centralizing the parser call\n// here allows us to:\n// 1. Wrap errors into our own hierarchy (`TemplateParseError`)\n// 2. Expose AST introspection helpers (e.g. `isSingleExpression`)\n// 3. Isolate the direct Handlebars dependency from the rest of the codebase\n//\n// AST caching is handled at the `Typebars` instance level (via its own\n// configurable LRU cache), not here. This module only parses and wraps errors.\n\n// ─── Regex for detecting a numeric literal (integer or decimal, signed) ──────\n// Intentionally conservative: no scientific notation (1e5), no hex (0xFF),\n// no separators (1_000). We only want to recognize what a human would write\n// as a numeric value in a template.\nconst NUMERIC_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Parses a template string and returns the Handlebars AST.\n *\n * This function does not cache results — caching is managed at the\n * `Typebars` instance level via its own configurable LRU cache.\n *\n * @param template - The template string to parse (e.g. `\"Hello {{name}}\"`)\n * @returns The root AST node (`hbs.AST.Program`)\n * @throws {TemplateParseError} if the template syntax is invalid\n */\nexport function parse(template: string): hbs.AST.Program {\n\ttry {\n\t\treturn Handlebars.parse(template);\n\t} catch (error: unknown) {\n\t\t// Handlebars throws a plain Error with a descriptive message.\n\t\t// We transform it into a TemplateParseError for uniform handling.\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\n\t\t// Handlebars sometimes includes the position in the message —\n\t\t// attempt to extract it to enrich our error.\n\t\tconst locMatch = message.match(/line\\s+(\\d+).*?column\\s+(\\d+)/i);\n\t\tconst loc = locMatch\n\t\t\t? {\n\t\t\t\t\tline: parseInt(locMatch[1] ?? \"0\", 10),\n\t\t\t\t\tcolumn: parseInt(locMatch[2] ?? \"0\", 10),\n\t\t\t\t}\n\t\t\t: undefined;\n\n\t\tthrow new TemplateParseError(message, loc);\n\t}\n}\n\n/**\n * Determines whether the AST represents a template consisting of a single\n * expression `{{expression}}` with no text content around it.\n *\n * This matters for return type inference:\n * - Template `{{value}}` → returns the raw type of `value` (number, object…)\n * - Template `Hello {{name}}` → always returns `string` (concatenation)\n *\n * @param ast - The parsed AST of the template\n * @returns `true` if the template is a single expression\n */\nexport function isSingleExpression(ast: hbs.AST.Program): boolean {\n\tconst { body } = ast;\n\n\t// Exactly one node, and it's a MustacheStatement (not a block, not text)\n\treturn body.length === 1 && body[0]?.type === \"MustacheStatement\";\n}\n\n/**\n * Extracts the path segments from a Handlebars `PathExpression`.\n *\n * Handlebars decomposes `user.address.city` into `{ parts: [\"user\", \"address\", \"city\"] }`.\n * This function safely extracts those segments.\n *\n * @param expr - The expression to extract the path from\n * @returns The path segments, or an empty array if the expression is not\n * a `PathExpression`\n */\nexport function extractPathSegments(expr: hbs.AST.Expression): string[] {\n\tif (expr.type === \"PathExpression\") {\n\t\treturn (expr as hbs.AST.PathExpression).parts;\n\t}\n\treturn [];\n}\n\n/**\n * Checks whether an AST expression is a `PathExpression` pointing to `this`\n * (used inside `{{#each}}` blocks).\n */\nexport function isThisExpression(expr: hbs.AST.Expression): boolean {\n\tif (expr.type !== \"PathExpression\") return false;\n\tconst path = expr as hbs.AST.PathExpression;\n\treturn path.original === \"this\" || path.original === \".\";\n}\n\n/**\n * Checks whether an AST expression is a Handlebars `@data` variable\n * (e.g. `@index`, `@first`, `@last`, `@key`).\n *\n * These variables are injected by Handlebars at runtime inside block helpers\n * and are not part of the user-provided input schema.\n */\nexport function isDataExpression(expr: hbs.AST.Expression): boolean {\n\tif (expr.type !== \"PathExpression\") return false;\n\treturn (expr as hbs.AST.PathExpression).data === true;\n}\n\n/**\n * Checks whether an AST expression is a `PathExpression` whose first\n * segment is the `$root` token.\n *\n * This covers both the valid `{{$root}}` case (single segment) and the\n * invalid `{{$root.name}}` case (multiple segments). The caller must\n * check `parts.length` to distinguish valid from invalid usage.\n *\n * **Note:** This does NOT match `{{$root:2}}` because Handlebars parses\n * it as `parts: [\"$root:2\"]` (identifier still attached). Use\n * `isRootSegments()` on cleaned segments instead when identifier\n * extraction has already been performed.\n */\nexport function isRootExpression(expr: hbs.AST.Expression): boolean {\n\tif (expr.type !== \"PathExpression\") return false;\n\tconst path = expr as hbs.AST.PathExpression;\n\treturn path.parts.length > 0 && path.parts[0] === ROOT_TOKEN;\n}\n\n/**\n * Checks whether an array of **cleaned** path segments represents a\n * `$root` reference (i.e. exactly one segment equal to `\"$root\"`).\n *\n * This is the segment-level counterpart of `isRootExpression()` and is\n * meant to be called **after** `extractExpressionIdentifier()` has\n * stripped the `:N` suffix. It correctly handles both `{{$root}}` and\n * `{{$root:2}}`.\n *\n * @param cleanSegments - Path segments with identifiers already removed\n * @returns `true` if the segments represent a `$root` reference\n */\nexport function isRootSegments(cleanSegments: string[]): boolean {\n\treturn cleanSegments.length === 1 && cleanSegments[0] === ROOT_TOKEN;\n}\n\n/**\n * Checks whether cleaned segments represent a **path traversal** on\n * `$root` (e.g. `$root.name`, `$root.address.city`).\n *\n * Path traversal on `$root` is forbidden — users should write `{{name}}`\n * instead of `{{$root.name}}`.\n *\n * @param cleanSegments - Path segments with identifiers already removed\n * @returns `true` if the first segment is `$root` and there are additional segments\n */\nexport function isRootPathTraversal(cleanSegments: string[]): boolean {\n\treturn cleanSegments.length > 1 && cleanSegments[0] === ROOT_TOKEN;\n}\n\n// ─── Filtering Semantically Significant Nodes ───────────────────────────────\n// In a Handlebars AST, formatting (newlines, indentation) produces\n// `ContentStatement` nodes whose value is purely whitespace. These nodes\n// have no semantic impact and must be ignored during type inference to\n// correctly detect \"effectively a single block\" or \"effectively a single\n// expression\" cases.\n\n/**\n * Returns the semantically significant statements of a Program by\n * filtering out `ContentStatement` nodes that contain only whitespace.\n */\nexport function getEffectiveBody(\n\tprogram: hbs.AST.Program,\n): hbs.AST.Statement[] {\n\treturn program.body.filter(\n\t\t(s) =>\n\t\t\t!(\n\t\t\t\ts.type === \"ContentStatement\" &&\n\t\t\t\t(s as hbs.AST.ContentStatement).value.trim() === \"\"\n\t\t\t),\n\t);\n}\n\n/**\n * Determines whether a Program effectively consists of a single\n * `BlockStatement` (ignoring surrounding whitespace).\n *\n * Recognized examples:\n * ```\n * {{#if x}}...{{/if}}\n *\n * {{#each items}}...{{/each}}\n * ```\n *\n * @returns The single `BlockStatement`, or `null` if the program contains\n * other significant nodes.\n */\nexport function getEffectivelySingleBlock(\n\tprogram: hbs.AST.Program,\n): hbs.AST.BlockStatement | null {\n\tconst effective = getEffectiveBody(program);\n\tif (effective.length === 1 && effective[0]?.type === \"BlockStatement\") {\n\t\treturn effective[0] as hbs.AST.BlockStatement;\n\t}\n\treturn null;\n}\n\n/**\n * Determines whether a Program effectively consists of a single\n * `MustacheStatement` (ignoring surrounding whitespace).\n *\n * Example: ` {{age}} ` → true\n */\nexport function getEffectivelySingleExpression(\n\tprogram: hbs.AST.Program,\n): hbs.AST.MustacheStatement | null {\n\tconst effective = getEffectiveBody(program);\n\tif (effective.length === 1 && effective[0]?.type === \"MustacheStatement\") {\n\t\treturn effective[0] as hbs.AST.MustacheStatement;\n\t}\n\treturn null;\n}\n\n// ─── Handlebars Expression Detection ─────────────────────────────────────────\n// Fast heuristic to determine whether a string contains Handlebars expressions.\n// Used by `excludeTemplateExpression` filtering to skip dynamic entries.\n\n/**\n * Determines whether a string contains Handlebars expressions.\n *\n * A string contains template expressions if it includes `{{` (opening\n * delimiter for Handlebars mustache/block statements). This is a fast\n * substring check — **no parsing is performed**.\n *\n * Used by `excludeTemplateExpression` to filter out properties whose\n * values are dynamic templates.\n *\n * **Limitation:** This is a simple `includes(\"{{\")` check, not a full\n * parser. It will produce false positives for strings that contain `{{`\n * as literal text (e.g. `\"Use {{name}} syntax\"` in documentation strings)\n * or in escaped contexts. For the current use case (filtering object/array\n * entries that are likely templates), this trade-off is acceptable because:\n * - It avoids the cost of parsing every string value\n * - False positives only cause an entry to be excluded from the output\n * schema (conservative behavior)\n *\n * @param value - The string to check\n * @returns `true` if the string contains at least one `{{` sequence\n */\nexport function hasHandlebarsExpression(value: string): boolean {\n\treturn value.includes(\"{{\");\n}\n\n// ─── Fast-Path Detection ─────────────────────────────────────────────────────\n// For templates consisting only of text and simple expressions (no blocks,\n// no helpers with parameters), we can bypass Handlebars entirely and perform\n// a simple variable replacement via string concatenation.\n\n/**\n * Determines whether an AST can be executed via the fast-path (direct\n * concatenation without going through `Handlebars.compile()`).\n *\n * The fast-path is possible when the template only contains:\n * - `ContentStatement` nodes (static text)\n * - Simple `MustacheStatement` nodes (no params, no hash)\n *\n * This excludes:\n * - Block helpers (`{{#if}}`, `{{#each}}`, etc.)\n * - Inline helpers (`{{uppercase name}}`)\n * - Sub-expressions\n *\n * @param ast - The parsed AST of the template\n * @returns `true` if the template can use the fast-path\n */\nexport function canUseFastPath(ast: hbs.AST.Program): boolean {\n\treturn ast.body.every(\n\t\t(s) =>\n\t\t\ts.type === \"ContentStatement\" ||\n\t\t\t(s.type === \"MustacheStatement\" &&\n\t\t\t\t(s as hbs.AST.MustacheStatement).params.length === 0 &&\n\t\t\t\t!(s as hbs.AST.MustacheStatement).hash),\n\t);\n}\n\n// ─── Literal Detection in Text Content ───────────────────────────────────────\n// When a program contains only ContentStatements (no expressions), we try\n// to detect whether the concatenated and trimmed text is a typed literal\n// (number, boolean, null). This enables correct type inference for branches\n// like `{{#if x}} 42 {{/if}}`.\n\n/**\n * Attempts to detect the type of a raw text literal.\n *\n * @param text - The trimmed text from a ContentStatement or group of ContentStatements\n * @returns The detected JSON Schema type, or `null` if it's free-form text (string).\n */\nexport function detectLiteralType(\n\ttext: string,\n): \"number\" | \"boolean\" | \"null\" | null {\n\tif (NUMERIC_LITERAL_RE.test(text)) return \"number\";\n\tif (text === \"true\" || text === \"false\") return \"boolean\";\n\tif (text === \"null\") return \"null\";\n\treturn null;\n}\n\n/**\n * Coerces a raw string from Handlebars rendering to its actual type\n * if it represents a literal (number, boolean, null).\n * Returns the raw (untrimmed) string otherwise.\n */\nexport function coerceLiteral(raw: string): unknown {\n\tconst trimmed = raw.trim();\n\tconst type = detectLiteralType(trimmed);\n\tif (type === \"number\") return Number(trimmed);\n\tif (type === \"boolean\") return trimmed === \"true\";\n\tif (type === \"null\") return null;\n\t// Not a typed literal — return the raw string without trimming,\n\t// as whitespace may be significant (e.g. output of an #each block).\n\treturn raw;\n}\n\n// ─── Template Identifier Parsing ─────────────────────────────────────────────\n// Syntax `{{key:N}}` where N is an integer (positive, zero, or negative).\n// The identifier allows resolving a variable from a specific data source\n// (e.g. a workflow node identified by its number).\n\n/** Result of parsing a path segment with a potential identifier */\nexport interface ParsedIdentifier {\n\t/** The variable name, without the `:N` suffix */\n\tkey: string;\n\t/** The numeric identifier, or `null` if absent */\n\tidentifier: number | null;\n}\n\n/**\n * Parses an individual path segment to extract the key and optional identifier.\n *\n * @param segment - A raw path segment (e.g. `\"meetingId:1\"` or `\"meetingId\"`)\n * @returns An object `{ key, identifier }`\n *\n * @example\n * ```\n * parseIdentifier(\"meetingId:1\") // → { key: \"meetingId\", identifier: 1 }\n * parseIdentifier(\"meetingId\") // → { key: \"meetingId\", identifier: null }\n * parseIdentifier(\"meetingId:0\") // → { key: \"meetingId\", identifier: 0 }\n * ```\n */\nexport function parseIdentifier(segment: string): ParsedIdentifier {\n\tconst match = segment.match(IDENTIFIER_RE);\n\tif (match) {\n\t\treturn {\n\t\t\tkey: match[1] ?? segment,\n\t\t\tidentifier: parseInt(match[2] ?? \"0\", 10),\n\t\t};\n\t}\n\treturn { key: segment, identifier: null };\n}\n\n/** Result of extracting the identifier from a complete expression */\nexport interface ExpressionIdentifier {\n\t/** Cleaned path segments (without the `:N` suffix on the last one) */\n\tcleanSegments: string[];\n\t/** The numeric identifier extracted from the last segment, or `null` */\n\tidentifier: number | null;\n}\n\n/**\n * Extracts the identifier from a complete expression (array of segments).\n *\n * The identifier is always on the **last** segment of the path, because\n * Handlebars splits on `.` before the `:`.\n *\n * @param segments - The raw path segments (e.g. `[\"user\", \"name:1\"]`)\n * @returns An object `{ cleanSegments, identifier }`\n *\n * @example\n * ```\n * extractExpressionIdentifier([\"meetingId:1\"])\n * // → { cleanSegments: [\"meetingId\"], identifier: 1 }\n *\n * extractExpressionIdentifier([\"user\", \"name:1\"])\n * // → { cleanSegments: [\"user\", \"name\"], identifier: 1 }\n *\n * extractExpressionIdentifier([\"meetingId\"])\n * // → { cleanSegments: [\"meetingId\"], identifier: null }\n * ```\n */\nexport function extractExpressionIdentifier(\n\tsegments: string[],\n): ExpressionIdentifier {\n\tif (segments.length === 0) {\n\t\treturn { cleanSegments: [], identifier: null };\n\t}\n\n\tconst lastSegment = segments[segments.length - 1] as string;\n\tconst parsed = parseIdentifier(lastSegment);\n\n\tif (parsed.identifier !== null) {\n\t\tconst cleanSegments = [...segments.slice(0, -1), parsed.key];\n\t\treturn { cleanSegments, identifier: parsed.identifier };\n\t}\n\n\treturn { cleanSegments: segments, identifier: null };\n}\n"],"names":["Handlebars","TemplateParseError","ROOT_TOKEN","IDENTIFIER_RE","NUMERIC_LITERAL_RE","parse","template","error","message","Error","String","locMatch","match","loc","line","parseInt","column","undefined","isSingleExpression","ast","body","length","type","extractPathSegments","expr","parts","isThisExpression","path","original","isDataExpression","data","isRootExpression","isRootSegments","cleanSegments","isRootPathTraversal","getEffectiveBody","program","filter","s","value","trim","getEffectivelySingleBlock","effective","getEffectivelySingleExpression","hasHandlebarsExpression","includes","canUseFastPath","every","params","hash","detectLiteralType","text","test","coerceLiteral","raw","trimmed","Number","parseIdentifier","segment","key","identifier","extractExpressionIdentifier","segments","lastSegment","parsed","slice"],"mappings":"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"}
|