legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
633 lines • 29.8 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any -- Handlebars helper registration requires any for untyped callback arguments injected by the Handlebars runtime */
/**
* Handlebars Template Engine with Custom Helpers
*
* This module provides a Handlebars-based template engine with all
* Legal Markdown helpers registered.
*
* @module handlebars-engine
*/
import Handlebars from 'handlebars';
import { extensionHelpers } from './helpers/index.js';
import { coreHelpers } from '../core/helpers/index.js';
import { fieldSpan } from './tracking/field-span.js';
import { isFieldTrackingEnabled, isAstFieldTrackingEnabled, isLogicBranchHighlightingEnabled, getHelperInvocationInfo, setHelperInvocationInfo, clearHelperInvocationInfo, resetLogicBranchCounter, nextLogicBranchId, } from './tracking/field-tracking-state.js';
import { lmFieldToken, lmLogicStartToken, lmLogicEndToken } from './tracking/tracking-token.js';
// Create Handlebars instance
const handlebarsInstance = Handlebars.create();
const builtinIfHelper = handlebarsInstance.helpers.if;
const builtinUnlessHelper = handlebarsInstance.helpers.unless;
/**
* Best-effort guard for deciding whether to emit tracking span HTML.
*
* Handlebars 4 can provide very similar options objects for top-level calls and
* subexpressions. We therefore also pass non-options sentinels from some helper
* wrappers when we must force raw subexpression values.
*/
function isTopLevelHelperCall(options) {
return Boolean(options &&
typeof options === 'object' &&
'hash' in options &&
'data' in options);
}
function isSourceFieldPath(path) {
if (!path)
return false;
if (path === '.' || path === 'this')
return false;
if (path.startsWith('@') || path.startsWith('../') || path.startsWith('./'))
return false;
return /^[A-Za-z0-9_.\-[\]]+$/.test(path);
}
function collectSourceFields(expression, fields) {
if (!expression || typeof expression !== 'object') {
return;
}
const node = expression;
const type = node['type'];
if (type === 'PathExpression') {
const original = node['original'];
if (typeof original === 'string' && isSourceFieldPath(original)) {
fields.add(original);
}
return;
}
if (type === 'SubExpression' || type === 'MustacheStatement' || type === 'BlockStatement') {
const params = Array.isArray(node['params']) ? node['params'] : [];
for (const param of params) {
collectSourceFields(param, fields);
}
const hash = node['hash'];
if (hash &&
typeof hash === 'object' &&
Array.isArray(hash['pairs'])) {
for (const pair of hash['pairs']) {
const value = pair['value'];
collectSourceFields(value, fields);
}
}
}
}
function indexHelperInvocations(template) {
clearHelperInvocationInfo();
resetLogicBranchCounter();
const walkNode = (node, topLevel) => {
if (!node || typeof node !== 'object')
return;
const typed = node;
const type = typed['type'];
if (type === 'MustacheStatement' || type === 'SubExpression') {
const path = typed['path'];
const helperName = path?.['original'];
const loc = typed['loc'];
if (typeof helperName === 'string' &&
typeof loc?.start?.line === 'number' &&
typeof loc?.start?.column === 'number') {
const sourceFields = new Set();
const params = Array.isArray(typed['params']) ? typed['params'] : [];
for (const param of params) {
collectSourceFields(param, sourceFields);
walkNode(param, false);
}
setHelperInvocationInfo(helperName, loc.start.line, loc.start.column, {
topLevel,
sourceFields: Array.from(sourceFields),
});
}
return;
}
if (type === 'BlockStatement') {
const params = Array.isArray(typed['params']) ? typed['params'] : [];
for (const param of params) {
walkNode(param, false);
}
const program = typed['program'];
const inverse = typed['inverse'];
if (program && typeof program === 'object') {
const body = program['body'];
if (Array.isArray(body)) {
for (const item of body)
walkNode(item, true);
}
}
if (inverse && typeof inverse === 'object') {
const body = inverse['body'];
if (Array.isArray(body)) {
for (const item of body)
walkNode(item, true);
}
}
return;
}
const body = typed['body'];
if (Array.isArray(body)) {
for (const item of body)
walkNode(item, topLevel);
}
};
try {
const parsed = Handlebars.parse(template, { srcName: '__lm_template__' });
walkNode(parsed, true);
}
catch {
clearHelperInvocationInfo();
}
}
/**
* A SafeString subclass that also carries the original raw value.
*
* When a formatting helper (e.g. formatDate, multiply) is used as a
* subexpression, `isTopLevelHelperCall` cannot reliably distinguish it from a
* top-level call (Handlebars 4 passes `data` to both). Rather than suppressing
* the span, we attach the raw value so comparison helpers can unwrap it.
*/
class TrackedValue extends Handlebars.SafeString {
rawValue;
constructor(html, rawValue) {
super(html);
this.rawValue = rawValue;
}
}
/**
* Unwraps a TrackedValue/SafeString to its plain comparable value.
*
* Comparison helpers (eq, gt, etc.) call this on their operands so they still
* work correctly when a formatting helper is used as a subexpression.
*/
function unwrapSafeString(value) {
if (value instanceof TrackedValue)
return value.rawValue;
if (value instanceof Handlebars.SafeString)
return value.toString();
return value;
}
/**
* Wraps a helper's output in a `highlight` field-tracking span when:
* 1. field tracking is currently enabled, AND
* 2. the helper was invoked as a top-level expression (not a subexpression).
*
* Subexpression calls (used inside block helper conditions or as arguments to
* other helpers) must return raw values so they can be compared, counted, or
* passed as arguments without breaking logic.
*/
function wrapTrackedOutput(helperName, result, options) {
if (!isFieldTrackingEnabled())
return result;
if (result === null || result === undefined || result === '')
return result;
const invocationInfo = typeof options?.loc?.start?.line === 'number' && typeof options?.loc?.start?.column === 'number'
? getHelperInvocationInfo(helperName, options.loc.start.line, options.loc.start.column)
: undefined;
const isTopLevel = invocationInfo ? invocationInfo.topLevel : isTopLevelHelperCall(options);
if (!isTopLevel)
return result; // subexpression: return raw value
const dataField = invocationInfo && invocationInfo.sourceFields.length === 1
? invocationInfo.sourceFields[0]
: helperName;
const wrapped = isAstFieldTrackingEnabled()
? lmFieldToken(dataField, String(result), 'highlight')
: fieldSpan(dataField, String(result), 'highlight');
return new TrackedValue(wrapped, result);
}
function stripHandlebarsOptions(value) {
// Handlebars always passes an `options` object as the last argument to every
// helper. That object has a `hash` property. TypeScript cannot statically
// narrow `T` to a type that has `hash`, so `as any` is required here to
// perform the duck-type check at runtime.
if (value && typeof value === 'object' && 'hash' in value) {
return undefined;
}
return value;
}
function isHandlebarsOptions(value) {
// Same rationale as stripHandlebarsOptions: duck-typing `hash` to detect the
// implicit Handlebars options object injected as the final helper argument.
return Boolean(value && typeof value === 'object' && 'hash' in value);
}
function resolveLocaleArg(thisArg, localeArg, optionsArg) {
const safeLocale = stripHandlebarsOptions(localeArg);
if (typeof safeLocale === 'string' && safeLocale.trim()) {
return safeLocale;
}
const options = isHandlebarsOptions(optionsArg)
? optionsArg
: isHandlebarsOptions(localeArg)
? localeArg
: undefined;
const contextLocale = typeof thisArg?.locale === 'string' ? thisArg.locale : undefined;
if (contextLocale && contextLocale.trim()) {
return contextLocale;
}
const rootLocale = typeof options?.data?.root?.locale === 'string' ? options.data.root.locale : undefined;
if (rootLocale && rootLocale.trim()) {
return rootLocale;
}
return undefined;
}
function wrapLogicWinnerBranch(renderedText, branchField, helperName, result) {
const leadingMatch = renderedText.match(/^\s*/);
const trailingMatch = renderedText.match(/\s*$/);
const leadingWhitespace = leadingMatch?.[0] ?? '';
const trailingWhitespace = trailingMatch?.[0] ?? '';
const coreStart = leadingWhitespace.length;
const coreEnd = renderedText.length - trailingWhitespace.length;
const coreText = renderedText.slice(coreStart, coreEnd);
if (!coreText) {
return new Handlebars.SafeString(renderedText);
}
const logicStart = lmLogicStartToken(branchField, helperName, result);
const logicEnd = lmLogicEndToken();
const lines = coreText.split('\n');
const hasStructuralMarkdownLine = lines.some(line => /^(?:\s{0,3}#{1,6}\s+|\s*[-*+]\s+|\s*\d+\.\s+|\s*>\s?)/.test(line));
if (!hasStructuralMarkdownLine) {
return new Handlebars.SafeString(`${leadingWhitespace}${logicStart}${coreText}${logicEnd}${trailingWhitespace}`);
}
const wrappedCore = lines
.map(line => {
if (!line.trim())
return line;
if (/^\s*(```|~~~)/.test(line))
return line;
if (/^\s*\|/.test(line))
return line;
if (/^\s*[-*_]{3,}\s*$/.test(line))
return line;
const prefixPatterns = [
/^(\s{0,3}#{1,6}\s+)(.*)$/,
/^(\s*[-*+]\s+)(.*)$/,
/^(\s*\d+\.\s+)(.*)$/,
/^(\s*>\s?)(.*)$/,
];
for (const pattern of prefixPatterns) {
const match = line.match(pattern);
if (match) {
const [, prefix, body] = match;
if (!body.trim())
return line;
return `${prefix}${logicStart}${body}${logicEnd}`;
}
}
return `${logicStart}${line}${logicEnd}`;
})
.join('\n');
return new Handlebars.SafeString(`${leadingWhitespace}${wrappedCore}${trailingWhitespace}`);
}
// ============================================================================
// CORE HELPERS REGISTRATION
// ============================================================================
handlebarsInstance.registerHelper('today', coreHelpers.today);
handlebarsInstance.registerHelper('formatBasicDate', coreHelpers.formatBasicDate);
handlebarsInstance.registerHelper('parseToday', coreHelpers.parseToday);
handlebarsInstance.registerHelper('if', function (condition, options) {
const evaluatedCondition = unwrapSafeString(condition);
const rendered = builtinIfHelper.call(this, evaluatedCondition, options);
if (!isFieldTrackingEnabled() || !isLogicBranchHighlightingEnabled()) {
return rendered;
}
if (!isAstFieldTrackingEnabled())
return rendered;
if (!options || typeof options.fn !== 'function' || typeof options.inverse !== 'function') {
return rendered;
}
const renderedText = typeof rendered === 'string' ? rendered : String(rendered ?? '');
if (!renderedText.trim())
return rendered;
const branchId = nextLogicBranchId();
return wrapLogicWinnerBranch(renderedText, `logic.branch.${branchId}`, 'if', Boolean(evaluatedCondition));
});
handlebarsInstance.registerHelper('unless', function (condition, options) {
const evaluatedCondition = unwrapSafeString(condition);
const rendered = builtinUnlessHelper.call(this, evaluatedCondition, options);
if (!isFieldTrackingEnabled() || !isLogicBranchHighlightingEnabled()) {
return rendered;
}
if (!isAstFieldTrackingEnabled())
return rendered;
if (!options || typeof options.fn !== 'function' || typeof options.inverse !== 'function') {
return rendered;
}
const renderedText = typeof rendered === 'string' ? rendered : String(rendered ?? '');
if (!renderedText.trim())
return rendered;
const branchId = nextLogicBranchId();
return wrapLogicWinnerBranch(renderedText, `logic.branch.${branchId}`, 'unless', !evaluatedCondition);
});
handlebarsInstance.registerHelper('eq', function (a, b) {
return unwrapSafeString(a) === unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('neq', function (a, b) {
return unwrapSafeString(a) !== unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('gt', function (a, b) {
return unwrapSafeString(a) > unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('gte', function (a, b) {
return unwrapSafeString(a) >= unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('lt', function (a, b) {
return unwrapSafeString(a) < unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('lte', function (a, b) {
return unwrapSafeString(a) <= unwrapSafeString(stripHandlebarsOptions(b));
});
handlebarsInstance.registerHelper('and', function (a, b) {
const safeB = stripHandlebarsOptions(b);
return Boolean(a) && Boolean(safeB);
});
handlebarsInstance.registerHelper('or', function (a, b) {
const safeB = stripHandlebarsOptions(b);
return Boolean(a) || Boolean(safeB);
});
handlebarsInstance.registerHelper('not', function (a) {
return !a;
});
// ============================================================================
// EXTENSION HELPERS REGISTRATION - Date
// ============================================================================
handlebarsInstance.registerHelper('addYears', function (date, n, options) {
const safeN = stripHandlebarsOptions(n);
return wrapTrackedOutput('addYears', extensionHelpers.addYears(unwrapSafeString(date), safeN), options);
});
handlebarsInstance.registerHelper('addMonths', function (date, n, options) {
const safeN = stripHandlebarsOptions(n);
return wrapTrackedOutput('addMonths', extensionHelpers.addMonths(unwrapSafeString(date), safeN), options);
});
handlebarsInstance.registerHelper('addDays', function (date, n, options) {
const safeN = stripHandlebarsOptions(n);
return wrapTrackedOutput('addDays', extensionHelpers.addDays(unwrapSafeString(date), safeN), options);
});
handlebarsInstance.registerHelper('formatDate', function (date, format, locale, options) {
const safeFormat = stripHandlebarsOptions(format);
const resolvedLocale = resolveLocaleArg(this, locale, options ?? locale ?? format);
const result = extensionHelpers.formatDate(unwrapSafeString(date), safeFormat, resolvedLocale);
const topLevelOptions = options ?? locale ?? format;
return wrapTrackedOutput('formatDate', result, topLevelOptions);
});
// ============================================================================
// EXTENSION HELPERS REGISTRATION - Number
// ============================================================================
handlebarsInstance.registerHelper('formatCurrency', function (value, currency, decimals, locale, options) {
// Capture the Handlebars options before any args are nullified.
// The options object migrates toward the front when fewer positional args
// are supplied (e.g. {{formatCurrency amount}} puts options in `currency`).
const hbsOptions = options ??
(isHandlebarsOptions(locale) ? locale : undefined) ??
(isHandlebarsOptions(decimals) ? decimals : undefined) ??
(isHandlebarsOptions(currency) ? currency : undefined);
if (isHandlebarsOptions(locale))
locale = undefined;
if (isHandlebarsOptions(decimals))
decimals = undefined;
if (isHandlebarsOptions(currency))
currency = undefined;
const resolvedLocale = resolveLocaleArg(this, locale, hbsOptions ?? locale ?? decimals);
const result = extensionHelpers.formatCurrency(unwrapSafeString(value), currency, decimals, resolvedLocale);
return wrapTrackedOutput('formatCurrency', result, hbsOptions);
});
handlebarsInstance.registerHelper('formatInteger', function (value, separator, options) {
const safeSeparator = isHandlebarsOptions(separator) ? undefined : separator;
const result = extensionHelpers.formatInteger(unwrapSafeString(value), safeSeparator);
return wrapTrackedOutput('formatInteger', result, options ?? separator);
});
handlebarsInstance.registerHelper('formatPercent', function (value, decimals, symbol, options) {
const safeSymbol = isHandlebarsOptions(symbol) ? undefined : symbol;
const safeDecimals = isHandlebarsOptions(decimals) ? undefined : decimals;
const result = extensionHelpers.formatPercent(unwrapSafeString(value), safeDecimals, safeSymbol);
return wrapTrackedOutput('formatPercent', result, options ?? symbol ?? decimals);
});
handlebarsInstance.registerHelper('formatEuro', function (value, options) {
return wrapTrackedOutput('formatEuro', extensionHelpers.formatEuro(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('formatDollar', function (value, options) {
return wrapTrackedOutput('formatDollar', extensionHelpers.formatDollar(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('formatPound', function (value, options) {
return wrapTrackedOutput('formatPound', extensionHelpers.formatPound(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('numberToWords', function (value, options) {
return wrapTrackedOutput('numberToWords', extensionHelpers.numberToWords(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('ordinal', function (value, locale, options) {
const resolvedLocale = resolveLocaleArg(this, locale, options ?? locale);
const result = extensionHelpers.ordinal(unwrapSafeString(value), resolvedLocale);
return wrapTrackedOutput('ordinal', result, options ?? locale);
});
handlebarsInstance.registerHelper('abs', function (value, options) {
return wrapTrackedOutput('abs', extensionHelpers.abs(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('max', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
const result = extensionHelpers.max(unwrapSafeString(a), unwrapSafeString(b));
return wrapTrackedOutput('max', result, options ?? b);
});
handlebarsInstance.registerHelper('min', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
const result = extensionHelpers.min(unwrapSafeString(a), unwrapSafeString(b));
return wrapTrackedOutput('min', result, options ?? b);
});
handlebarsInstance.registerHelper('round', function (value, decimals, options) {
const safeDecimals = (stripHandlebarsOptions(decimals) ?? 0);
const result = extensionHelpers.round(unwrapSafeString(value), safeDecimals);
return wrapTrackedOutput('round', result, options ?? decimals);
});
// ============================================================================
// EXTENSION HELPERS REGISTRATION - String
// ============================================================================
handlebarsInstance.registerHelper('capitalize', function (str, options) {
return wrapTrackedOutput('capitalize', extensionHelpers.capitalize(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('capitalizeWords', function (str, options) {
return wrapTrackedOutput('capitalizeWords', extensionHelpers.capitalizeWords(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('upper', function (str, options) {
return wrapTrackedOutput('upper', extensionHelpers.upper(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('uppercase', function (str, options) {
return wrapTrackedOutput('uppercase', extensionHelpers.upper(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('lower', function (str, options) {
return wrapTrackedOutput('lower', extensionHelpers.lower(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('lowercase', function (str, options) {
return wrapTrackedOutput('lowercase', extensionHelpers.lower(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('titleCase', function (str, options) {
return wrapTrackedOutput('titleCase', extensionHelpers.titleCase(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('kebabCase', function (str, options) {
return wrapTrackedOutput('kebabCase', extensionHelpers.kebabCase(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('snakeCase', function (str, options) {
return wrapTrackedOutput('snakeCase', extensionHelpers.snakeCase(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('camelCase', function (str, options) {
return wrapTrackedOutput('camelCase', extensionHelpers.camelCase(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('pascalCase', function (str, options) {
return wrapTrackedOutput('pascalCase', extensionHelpers.pascalCase(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('truncate', function (str, length, suffix, options) {
const safeSuffix = isHandlebarsOptions(suffix) ? undefined : suffix;
const result = extensionHelpers.truncate(unwrapSafeString(str), length, safeSuffix);
return wrapTrackedOutput('truncate', result, options ?? suffix);
});
handlebarsInstance.registerHelper('clean', function (str, options) {
return wrapTrackedOutput('clean', extensionHelpers.clean(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('trim', function (str, options) {
return wrapTrackedOutput('trim', extensionHelpers.clean(unwrapSafeString(str)), options);
});
handlebarsInstance.registerHelper('pluralize', function (count, singular, plural, options) {
const safePlural = isHandlebarsOptions(plural) ? undefined : plural;
const result = extensionHelpers.pluralize(count, singular, safePlural);
return wrapTrackedOutput('pluralize', result, options ?? plural);
});
handlebarsInstance.registerHelper('padStart', function (str, length, char, options) {
const safeChar = stripHandlebarsOptions(char) ?? ' ';
const result = extensionHelpers.padStart(unwrapSafeString(str), length, safeChar);
return wrapTrackedOutput('padStart', result, options ?? char);
});
handlebarsInstance.registerHelper('padEnd', function (str, length, char, options) {
const safeChar = stripHandlebarsOptions(char) ?? ' ';
const result = extensionHelpers.padEnd(unwrapSafeString(str), length, safeChar);
return wrapTrackedOutput('padEnd', result, options ?? char);
});
handlebarsInstance.registerHelper('contains', function (str, substring, caseSensitive, options) {
const safeCaseSensitive = stripHandlebarsOptions(caseSensitive) ?? false;
const result = extensionHelpers.contains(unwrapSafeString(str), unwrapSafeString(substring), safeCaseSensitive);
return wrapTrackedOutput('contains', result, options ?? caseSensitive);
});
handlebarsInstance.registerHelper('replaceAll', function (str, search, replace, options) {
const safeReplace = stripHandlebarsOptions(replace) ?? '';
const result = extensionHelpers.replaceAll(unwrapSafeString(str), unwrapSafeString(search), unwrapSafeString(safeReplace));
return wrapTrackedOutput('replaceAll', result, options ?? replace);
});
handlebarsInstance.registerHelper('initials', function (name, options) {
return wrapTrackedOutput('initials', extensionHelpers.initials(unwrapSafeString(name)), options);
});
handlebarsInstance.registerHelper('join', function (arr, separator, options) {
const safeSeparator = (isHandlebarsOptions(separator) ? ', ' : separator) ?? ', ';
const result = extensionHelpers.join(arr, safeSeparator);
return wrapTrackedOutput('join', result, options ?? separator);
});
handlebarsInstance.registerHelper('length', function (value, options) {
return wrapTrackedOutput('length', extensionHelpers.length(unwrapSafeString(value)), options);
});
handlebarsInstance.registerHelper('default', function (value, fallback, options) {
const safeFallback = stripHandlebarsOptions(fallback) ?? '';
const result = extensionHelpers.defaultVal(unwrapSafeString(value), unwrapSafeString(safeFallback));
return wrapTrackedOutput('default', result, options ?? fallback);
});
// ============================================================================
// MATHEMATICAL HELPERS
// ============================================================================
handlebarsInstance.registerHelper('multiply', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
return wrapTrackedOutput('multiply', extensionHelpers.multiply(unwrapSafeString(a), unwrapSafeString(b)), options);
});
handlebarsInstance.registerHelper('divide', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
return wrapTrackedOutput('divide', extensionHelpers.divide(unwrapSafeString(a), unwrapSafeString(b)), options);
});
handlebarsInstance.registerHelper('add', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
return wrapTrackedOutput('add', extensionHelpers.add(unwrapSafeString(a), unwrapSafeString(b)), options);
});
handlebarsInstance.registerHelper('subtract', function (a, b, options) {
if (isHandlebarsOptions(b))
return NaN;
return wrapTrackedOutput('subtract', extensionHelpers.subtract(unwrapSafeString(a), unwrapSafeString(b)), options);
});
handlebarsInstance.registerHelper('modulo', function (a, b, options) {
// Filter out Handlebars options if passed as b
if (isHandlebarsOptions(b)) {
return NaN;
}
return wrapTrackedOutput('modulo', extensionHelpers.modulo(unwrapSafeString(a), unwrapSafeString(b)), options);
});
handlebarsInstance.registerHelper('power', function (a, b, options) {
// Filter out Handlebars options if passed as b
if (isHandlebarsOptions(b)) {
return NaN;
}
return wrapTrackedOutput('power', extensionHelpers.power(unwrapSafeString(a), unwrapSafeString(b)), options);
});
// ============================================================================
// STRING CONCATENATION HELPER
// ============================================================================
handlebarsInstance.registerHelper('concat', function (...args) {
const options = args[args.length - 1];
const values = args.slice(0, -1).map((v) => {
const unwrapped = unwrapSafeString(v);
return unwrapped === null || unwrapped === undefined ? '' : String(unwrapped);
});
const result = values.join('');
return wrapTrackedOutput('concat', result, options);
});
// ============================================================================
// FIELD TRACKING HELPER
// ============================================================================
// Helper for wrapping content with field tracking spans
handlebarsInstance.registerHelper('trackField', function (fieldName, options) {
const value = options.fn(this);
const safeFieldName = String(fieldName);
const safeValue = String(value);
return new handlebarsInstance.SafeString(isAstFieldTrackingEnabled()
? lmFieldToken(safeFieldName, safeValue, 'imported')
: fieldSpan(safeFieldName, safeValue, 'imported'));
});
// ============================================================================
// EXPORTS
// ============================================================================
export { handlebarsInstance };
// Exported for testing - not part of public API
export { resolveLocaleArg as _resolveLocaleArg };
/**
* Compiles and renders a Handlebars template
*
* @param template - The Handlebars template string
* @param data - The data context for template rendering
* @returns Rendered template string
*
* @example
* ```typescript
* const result = compileHandlebarsTemplate(
* 'Hello {{name}}!',
* { name: 'World' }
* );
* // result: "Hello World!"
* ```
*/
export function compileHandlebarsTemplate(template, data) {
indexHelperInvocations(template);
const compiledTemplate = handlebarsInstance.compile(template);
try {
const rendered = compiledTemplate(data);
return typeof rendered === 'string' ? rendered : String(rendered);
}
finally {
clearHelperInvocationInfo();
}
}
/**
* Registers a custom Handlebars helper
*
* @param name - Name of the helper
* @param helperFn - Helper function implementation
*
* @example
* ```typescript
* registerCustomHelper('shout', (text: string) => {
* return text.toUpperCase() + '!';
* });
* ```
*/
export function registerCustomHelper(name, helperFn) {
handlebarsInstance.registerHelper(name, helperFn);
}
//# sourceMappingURL=handlebars-engine.js.map