@mdfriday/foundry
Version:
The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.
1,176 lines • 81.4 kB
JavaScript
"use strict";
/**
* Template functions registry
* Centralized management of template functions following DDD principles
* TypeScript equivalent of Go's valueobject/nsreg.go
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultTemplateRegistry = void 0;
exports.newTemplateRegistry = newTemplateRegistry;
exports.createCoreFuncMap = createCoreFuncMap;
exports.createStandardFuncMap = createStandardFuncMap;
exports.updateEngineDependentFunctions = updateEngineDependentFunctions;
const log_1 = require("../../../../pkg/log");
const crypto = __importStar(require("crypto"));
// Create domain-specific logger for template operations
const log = (0, log_1.getDomainLogger)('template', { component: 'registry' });
const engineDependentFunctions = [];
/**
* Partial function implementation that depends on TemplateEngine
*/
class PartialFunction {
constructor() {
this.engine = null;
this.execute = async (name, context) => {
if (!this.engine) {
log.error(`Partial function called but engine not set: ${name}`);
return `<!-- Partial function called but engine not ready: ${name} -->`;
}
try {
// Ensure name has partials/ prefix if not already present
const templateName = name.startsWith('partials/') ? name : `partials/${name}`;
// Execute partial synchronously using engine's internal components
return await this.executePartial(templateName, context);
}
catch (error) {
log.error(`Partial execution failed for "${name}":`, error);
return `<!-- Partial execution failed: ${name} - ${error.message} -->`;
}
};
}
getFunctionName() {
return 'partial';
}
updateEngine(engine) {
this.engine = engine;
}
/**
* Execute partial synchronously by directly accessing template engine internals
*/
async executePartial(templateName, context) {
if (!this.engine) {
return `<!-- Template engine not available: ${templateName} -->`;
}
try {
// Use the new getPartial method which routes to PartialNamespace
const [templateObj, found, err] = await this.engine.getPartial(templateName);
if (err) {
log.error(`Error getting partial template:`, err);
return `<!-- Error getting partial template: ${templateName} - ${err.message} -->`;
}
if (!found || !templateObj) {
log.warn(`Template not found: ${templateName}`);
return `<!-- Template not found: ${templateName} -->`;
}
// Execute the template directly
const [result, execErr] = await templateObj.Execute(context);
if (execErr) {
log.error(`Template execution error:`, execErr);
return `<!-- Template execution error: ${templateName} - ${execErr.message} -->`;
}
return result;
}
catch (error) {
log.error(`Sync partial execution failed:`, error);
return `<!-- Partial sync execution failed: ${templateName} - ${error.message} -->`;
}
}
}
/**
* Template functions registry implementation
* Following DDD principles - encapsulates template function management logic
*/
class DefaultTemplateRegistry {
/**
* Register core template functions that don't require external services
* Equivalent to Go's RegisterNamespaces()
*/
registerCoreFunctions(funcMap) {
// Crypto functions
this.registerCryptoFunctions(funcMap);
// String manipulation functions
this.registerStringFunctions(funcMap);
// Math functions
this.registerMathFunctions(funcMap);
// Date/time functions
this.registerTimeFunctions(funcMap);
// Collection functions
this.registerCollectionFunctions(funcMap);
// Safe functions
this.registerSafeFunctions(funcMap);
// Reflect functions
this.registerReflectFunctions(funcMap);
// Format functions (printf, errorf, etc.)
this.registerFmtFunctions(funcMap);
// Path functions
this.registerPathFunctions(funcMap);
}
/**
* Register cryptographic hash functions
* Equivalent to Go's crypto packages
*/
registerCryptoFunctions(funcMap) {
// MD5 hash function
funcMap.set('md5', (input) => {
if (!input)
return '';
return crypto.createHash('md5').update(input).digest('hex');
});
}
/**
* Register extended template functions that require external services
* Equivalent to Go's RegisterExtendedNamespaces()
*/
registerExtendedFunctions(funcMap, services) {
// URL functions - require URL service
this.registerURLFunctions(funcMap, services);
// Site functions - require site service
this.registerSiteFunctions(funcMap, services);
this.registerResourcesFunctions(funcMap, services);
// Hugo functions - require hugo info service
this.registerHugoFunctions(funcMap, services);
// Language functions - require language service
this.registerLanguageFunctions(funcMap, services);
}
/**
* Register all available functions with services
* This is the main entry point for function registration
*/
registerAllFunctions(funcMap, services) {
this.registerCoreFunctions(funcMap);
this.registerStringFunctions(funcMap);
this.registerMathFunctions(funcMap);
this.registerTimeFunctions(funcMap);
this.registerCollectionFunctions(funcMap);
// Register engine-dependent functions (with placeholder implementations)
this.registerEngineDependentFunctions(funcMap);
// Register service-based functions if services are provided
if (services) {
this.registerURLFunctions(funcMap, services);
this.registerExtendedFunctions(funcMap, services);
}
}
/**
* Register functions that depend on TemplateEngine
* These functions are registered with placeholder implementations initially
*/
registerEngineDependentFunctions(funcMap) {
// Register partial function
const partialFunction = new PartialFunction();
engineDependentFunctions.push(partialFunction);
funcMap.set(partialFunction.getFunctionName(), partialFunction.execute);
}
/**
* Register URL template functions using URLService
* Equivalent to Go's registerUrls()
*/
registerURLFunctions(funcMap, services) {
// URLs namespace functions
funcMap.set('urls', () => ({
// Parse parses raw URL string into a URL structure
Parse: (rawurl) => {
try {
const u = new URL(rawurl);
return {
Host: u.host,
};
}
catch {
return {
Host: '',
};
}
},
// JoinPath joins path elements into a single path
JoinPath: (...elements) => {
// Remove empty elements and handle trailing slashes
const cleanElements = elements
.filter(e => e !== '')
.map(e => e.replace(/^\/+|\/+$/g, ''));
return cleanElements.join('/');
},
// URLize returns a URL-like string from the given text
URLize: (text) => services.urlize(text),
// Abs returns an absolute URL string
Abs: (url) => services.absURL(url),
// Rel returns a relative URL string
Rel: (url) => services.relURL(url),
// AbsLangURL returns an absolute URL modified with the language prefix
AbsLangURL: (url) => services.absURL(url), // TODO: Add language support
// RelLangURL returns a relative URL modified with the language prefix
RelLangURL: (url) => services.relURL(url), // TODO: Add language support
// Sanitize returns a sanitized string of the URL
Sanitize: (url) => {
try {
const parsed = new URL(url);
return parsed.toString();
}
catch {
// If not a valid URL, at least remove dangerous characters
return url.replace(/[<>"'%{}|\\^`]/g, '');
}
},
// PathEscape escapes special characters in a URL path
PathEscape: (path) => {
return encodeURIComponent(path)
.replace(/%2F/g, '/') // Keep forward slashes
.replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
},
// QueryEscape escapes special characters in a URL query
QueryEscape: (query) => {
return encodeURIComponent(query);
},
// QueryUnescape unescapes special characters in a URL query
QueryUnescape: (query) => {
try {
return decodeURIComponent(query);
}
catch {
return query;
}
},
// IsAbs reports whether the URL is absolute
IsAbs: (urlStr) => {
try {
const url = new URL(urlStr);
return url.protocol !== '';
}
catch {
return false;
}
},
// IsRelative reports whether the URL is relative
IsRelative: (urlStr) => {
try {
new URL(urlStr);
return false;
}
catch {
return true;
}
}
}));
// Individual URL functions for backward compatibility
funcMap.set('absURL', (input) => services.absURL(input));
funcMap.set('relURL', (input) => services.relURL(input));
funcMap.set('urlize', (input) => services.urlize(input));
// Reference functions
funcMap.set('relref', (page, ref) => {
// If ref starts with http:// or https://, return as is
if (/^https?:\/\//.test(ref)) {
return ref;
}
// Remove anchor if present and store it
let anchor = '';
const hashIndex = ref.indexOf('#');
if (hashIndex !== -1) {
anchor = ref.slice(hashIndex);
ref = ref.slice(0, hashIndex);
}
// Remove leading and trailing slashes
ref = ref.replace(/^\/+|\/+$/g, '');
// If it's a relative path starting with ./ or ../, use relURL
if (ref.startsWith('./') || ref.startsWith('../')) {
return services.relURL(ref + anchor);
}
// Handle absolute paths
if (ref.startsWith('/')) {
return services.relURL(ref + anchor);
}
// For other cases, treat as relative path
return services.relURL('/' + ref + anchor);
});
funcMap.set('ref', (page, ref) => {
// Similar to relref but returns absolute URL
// If ref starts with http:// or https://, return as is
if (/^https?:\/\//.test(ref)) {
return ref;
}
// Remove anchor if present and store it
let anchor = '';
const hashIndex = ref.indexOf('#');
if (hashIndex !== -1) {
anchor = ref.slice(hashIndex);
ref = ref.slice(0, hashIndex);
}
// Remove leading and trailing slashes
ref = ref.replace(/^\/+|\/+$/g, '');
// If it's a relative path starting with ./ or ../, use absURL
if (ref.startsWith('./') || ref.startsWith('../')) {
return services.absURL(ref + anchor);
}
// Handle absolute paths
if (ref.startsWith('/')) {
return services.absURL(ref + anchor);
}
// For other cases, treat as relative path
return services.absURL('/' + ref + anchor);
});
// Language-aware URL functions
funcMap.set('absLangURL', (input) => {
// For now, delegate to absURL - can be enhanced for multi-language support
return services.absURL(input);
});
funcMap.set('relLangURL', (input) => {
// For now, delegate to relURL - can be enhanced for multi-language support
return services.relURL(input);
});
}
/**
* Register site template functions using SiteService
* Equivalent to Go's registerSite()
*/
registerSiteFunctions(funcMap, services) {
// Site global object - register as a function that returns the site data
funcMap.set('Site', () => ({
Title: services.title(),
BaseURL: services.baseURL(),
Params: services.params(),
Menus: services.menus(),
IsMultiLingual: services.isMultiLanguage(),
LanguageCode: services.defaultLanguage()
}));
}
registerResourcesFunctions(funcMap, services) {
funcMap.set('resources', () => ({
Get: async (filename) => await services.Get(filename),
Minify: async (resource) => await services.Minify(resource),
Fingerprint: async (resource) => await services.Fingerprint(resource),
ExecuteAsTemplate: async (targetPath, data, resource) => await services.ExecuteAsTemplate(targetPath, data, resource)
}));
}
/**
* Register Hugo template functions using HugoInfoService
* Equivalent to Go's registerHugo()
*/
registerHugoFunctions(funcMap, services) {
funcMap.set('hugo', () => ({
Version: services.version(),
Environment: services.environment(),
Generator: services.generator()
}));
}
/**
* Register language template functions using LanguageService
* Equivalent to Go's registerLang()
*/
registerLanguageFunctions(funcMap, services) {
funcMap.set('i18n', (key) => {
// Placeholder for i18n functionality
return key;
});
}
/**
* Register string manipulation functions
* Equivalent to Go's registerStrings()
*/
registerStringFunctions(funcMap) {
// Humanize converts ID-style strings to human readable format
funcMap.set('humanize', (str) => {
if (!str)
return '';
// Convert from camelCase or PascalCase
str = str.replace(/([a-z])([A-Z])/g, '$1 $2');
// Convert from snake_case or kebab-case
str = str.replace(/[_-]+/g, ' ');
// Remove file extensions
str = str.replace(/\.[^/.]+$/, '');
// Capitalize first letter, rest lowercase
str = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
// Trim spaces
return str.trim();
});
funcMap.set('upper', (str) => str.toUpperCase());
funcMap.set('lower', (str) => str.toLowerCase());
funcMap.set('title', (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()));
funcMap.set('trim', (str) => str.trim());
funcMap.set('replace', (str, old, newStr) => str.replace(new RegExp(old, 'g'), newStr));
// Split function - ensure first parameter is the string to split
funcMap.set('split', (str, sep) => {
// Handle null and undefined cases first
if (str === null || str === undefined) {
log.warn('split function: first argument is null or undefined');
return [];
}
if (typeof str !== 'string') {
log.warn('split function: first argument must be a string, got:', typeof str, 'value:', str);
// If it's an array, try to join it or return as is
if (Array.isArray(str)) {
return str;
}
// Try to convert to string
const stringValue = String(str);
return stringValue.split(sep);
}
return str.split(sep);
});
// SplitRegex function - split string using regex pattern
funcMap.set('splitRegex', (str, regexPattern) => {
// Handle null and undefined cases first
if (str === null || str === undefined) {
log.warn('splitRegex function: first argument is null or undefined');
return [];
}
if (typeof str !== 'string') {
log.warn('splitRegex function: first argument must be a string, got:', typeof str, 'value:', str);
// If it's an array, try to join it or return as is
if (Array.isArray(str)) {
return str;
}
// Try to convert to string
const stringValue = String(str);
try {
const regex = new RegExp(regexPattern);
return stringValue.split(regex);
}
catch (error) {
log.warn('splitRegex function: invalid regex pattern:', regexPattern, 'error:', error);
return [stringValue];
}
}
try {
const regex = new RegExp(regexPattern);
return str.split(regex);
}
catch (error) {
log.warn('splitRegex function: invalid regex pattern:', regexPattern, 'error:', error);
return [str];
}
});
// Join function
funcMap.set('delimit', (arr, sep) => {
if (Array.isArray(arr)) {
return arr.join(sep);
}
return String(arr);
});
// Contains function
funcMap.set('in', (arr, item) => {
if (Array.isArray(arr)) {
return arr.includes(item);
}
return false;
});
// HasPrefix function
funcMap.set('hasPrefix', (str, prefix) => str.startsWith(prefix));
// HasSuffix function
funcMap.set('hasSuffix', (str, suffix) => str.endsWith(suffix));
// Strings namespace - following Go's strings namespace pattern
funcMap.set('strings', () => ({
// Basic string operations
ToLower: (s) => String(s).toLowerCase(),
ToUpper: (s) => String(s).toUpperCase(),
Title: (s) => String(s).replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()),
// Trimming functions
Trim: (s, cutset) => String(s).split('').filter(c => !cutset.includes(c)).join(''),
TrimSpace: (s) => String(s).trim(),
TrimLeft: (cutset, s) => {
const str = String(s);
let i = 0;
while (i < str.length && cutset.includes(str[i])) {
i++;
}
return str.slice(i);
},
TrimRight: (cutset, s) => {
const str = String(s);
let i = str.length - 1;
while (i >= 0 && cutset.includes(str[i])) {
i--;
}
return str.slice(0, i + 1);
},
TrimPrefix: (prefix, s) => {
const str = String(s);
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
},
TrimSuffix: (suffix, s) => {
const str = String(s);
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
},
// Testing functions
Contains: (s, substr) => String(s).includes(substr),
ContainsAny: (s, chars) => {
const str = String(s);
return chars.split('').some(char => str.includes(char));
},
HasPrefix: (s, prefix) => String(s).startsWith(prefix),
HasSuffix: (s, suffix) => String(s).endsWith(suffix),
// Replacement functions
Replace: (s, old, newStr, limit) => {
const str = String(s);
if (limit === undefined) {
return str.replace(new RegExp(old.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newStr);
}
let count = 0;
return str.replace(new RegExp(old.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), (match) => {
if (count < limit) {
count++;
return newStr;
}
return match;
});
},
// Splitting and slicing
Split: (s, delimiter) => String(s).split(delimiter),
SliceString: (s, start, end) => {
const str = String(s);
return end !== undefined ? str.slice(start, end) : str.slice(start);
},
Substr: (s, start, length) => {
const str = String(s);
if (start < 0)
start = str.length + start;
if (start < 0)
start = 0;
if (start >= str.length)
return '';
if (length === undefined)
return str.slice(start);
if (length <= 0)
return '';
return str.slice(start, start + length);
},
// Utility functions
Count: (substr, s) => {
const str = String(s);
if (!substr)
return str.length + 1;
return (str.match(new RegExp(substr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
},
CountWords: (s) => {
const str = String(s).trim();
return str ? str.split(/\s+/).length : 0;
},
CountRunes: (s) => {
const str = String(s);
return str.replace(/\s/g, '').length;
},
RuneCount: (s) => String(s).length,
// Repeat function
Repeat: (n, s) => {
if (n < 0)
throw new Error('strings: negative Repeat count');
return String(s).repeat(n);
},
// First letter upper
FirstUpper: (s) => {
const str = String(s);
return str.charAt(0).toUpperCase() + str.slice(1);
}
}));
}
/**
* Register math functions
* Equivalent to Go's registerMath()
*/
registerMathFunctions(funcMap) {
// Basic arithmetic functions
funcMap.set('add', (a, b) => a + b);
funcMap.set('sub', (a, b) => a - b);
funcMap.set('mul', (a, b) => a * b);
funcMap.set('div', (a, b) => b !== 0 ? a / b : 0);
funcMap.set('mod', (a, b) => b !== 0 ? a % b : 0);
// Math namespace functions - following Go's math package
funcMap.set('math', () => ({
// Abs returns the absolute value of n
Abs: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('the math.Abs function requires a numeric argument');
}
return Math.abs(num);
},
// Add adds the multivalued addends
Add: (...inputs) => {
if (inputs.length < 2) {
throw new Error('must provide at least two numbers');
}
return inputs.reduce((sum, val) => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Add operator can\'t be used with non-numeric values');
}
return sum + num;
}, 0);
},
// Ceil returns the least integer value greater than or equal to n
Ceil: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('Ceil operator can\'t be used with non-numeric value');
}
return Math.ceil(num);
},
// Div divides n1 by n2
Div: (...inputs) => {
if (inputs.length < 2) {
throw new Error('must provide at least two numbers');
}
const result = inputs.reduce((quotient, val, index) => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Div operator can\'t be used with non-numeric values');
}
if (index === 0)
return num;
if (num === 0) {
throw new Error('division by zero');
}
return quotient / num;
});
return result;
},
// Floor returns the greatest integer value less than or equal to n
Floor: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('Floor operator can\'t be used with non-numeric value');
}
return Math.floor(num);
},
// Log returns the natural logarithm of n
Log: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('Log operator can\'t be used with non-numeric value');
}
return Math.log(num);
},
// Max returns the greater of all numbers in inputs
Max: (...inputs) => {
if (inputs.length === 0) {
throw new Error('must provide at least one number');
}
const numbers = inputs.flat().map(val => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Max operator can\'t be used with non-numeric values');
}
return num;
});
return Math.max(...numbers);
},
// Min returns the smaller of all numbers in inputs
Min: (...inputs) => {
if (inputs.length === 0) {
throw new Error('must provide at least one number');
}
const numbers = inputs.flat().map(val => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Min operator can\'t be used with non-numeric values');
}
return num;
});
return Math.min(...numbers);
},
// Mod returns n1 % n2
Mod: (n1, n2) => {
const a = Number(n1);
const b = Number(n2);
if (isNaN(a) || isNaN(b)) {
throw new Error('modulo operator can\'t be used with non-numeric value');
}
if (b === 0) {
throw new Error('the number can\'t be divided by zero at modulo operation');
}
return a % b;
},
// ModBool returns the boolean of n1 % n2. If n1 % n2 == 0, return true
ModBool: (n1, n2) => {
const a = Number(n1);
const b = Number(n2);
if (isNaN(a) || isNaN(b)) {
throw new Error('modulo operator can\'t be used with non-numeric value');
}
if (b === 0) {
throw new Error('the number can\'t be divided by zero at modulo operation');
}
return (a % b) === 0;
},
// Mul multiplies the multivalued numbers
Mul: (...inputs) => {
if (inputs.length < 2) {
throw new Error('must provide at least two numbers');
}
return inputs.reduce((product, val) => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Mul operator can\'t be used with non-numeric values');
}
return product * num;
}, 1);
},
// Pow returns n1 raised to the power of n2
Pow: (n1, n2) => {
const a = Number(n1);
const b = Number(n2);
if (isNaN(a) || isNaN(b)) {
throw new Error('Pow operator can\'t be used with non-numeric value');
}
return Math.pow(a, b);
},
// Rand returns a pseudo-random number in [0.0,1.0)
Rand: () => Math.random(),
// Round returns the integer nearest to n
Round: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('Round operator can\'t be used with non-numeric value');
}
return Math.round(num);
},
// Sqrt returns the square root of n
Sqrt: (n) => {
const num = Number(n);
if (isNaN(num)) {
throw new Error('Sqrt operator can\'t be used with non-numeric value');
}
return Math.sqrt(num);
},
// Sub subtracts multivalued numbers
Sub: (...inputs) => {
if (inputs.length < 2) {
throw new Error('must provide at least two numbers');
}
return inputs.reduce((difference, val, index) => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Sub operator can\'t be used with non-numeric values');
}
if (index === 0)
return num;
return difference - num;
});
},
// Sum returns the sum of all numbers in inputs
Sum: (...inputs) => {
if (inputs.length === 0) {
throw new Error('must provide at least one number');
}
const numbers = inputs.flat().map(val => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Sum operator can\'t be used with non-numeric values');
}
return num;
});
return numbers.reduce((sum, num) => sum + num, 0);
},
// Product returns the product of all numbers in inputs
Product: (...inputs) => {
if (inputs.length === 0) {
throw new Error('must provide at least one number');
}
const numbers = inputs.flat().map(val => {
const num = Number(val);
if (isNaN(num)) {
throw new Error('Product operator can\'t be used with non-numeric values');
}
return num;
});
return numbers.reduce((product, num) => product * num, 1);
}
}));
}
/**
* Register time/date functions
* Equivalent to Go's registerTime()
*/
registerTimeFunctions(funcMap) {
// Time namespace functions - following Go's time package
funcMap.set('time', () => ({
// AsTime converts a value to a time.Time
AsTime: (v) => {
if (!v)
return null;
// If it's already a Date object
if (v instanceof Date)
return v;
// If it's a string, try to parse it
if (typeof v === 'string') {
const date = new Date(v);
return isNaN(date.getTime()) ? null : date;
}
// If it's a number (unix timestamp)
if (typeof v === 'number') {
// Check if it's in seconds (Unix timestamp) or milliseconds
const date = new Date(v < 1e10 ? v * 1000 : v);
return isNaN(date.getTime()) ? null : date;
}
return null;
},
// Format formats a time.Time
Format: (layout, value) => {
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) {
return '';
}
// Go time format parsing - based on reference time: Mon Jan 2 15:04:05 MST 2006
const year = date.getFullYear();
const month = date.getMonth() + 1; // JavaScript months are 0-based
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
const weekday = date.toLocaleString('en-US', { weekday: 'short' });
const monthName = date.toLocaleString('en-US', { month: 'short' });
// Go format patterns in order of precedence (longer patterns first)
const patterns = [
{ pattern: '2006', value: year.toString() },
{ pattern: '06', value: year.toString().slice(-2) },
{ pattern: 'January', value: date.toLocaleString('en-US', { month: 'long' }) },
{ pattern: 'Jan', value: monthName },
{ pattern: '01', value: month.toString().padStart(2, '0') },
{ pattern: '1', value: month.toString() },
{ pattern: 'Monday', value: date.toLocaleString('en-US', { weekday: 'long' }) },
{ pattern: 'Mon', value: weekday },
{ pattern: '02', value: day.toString().padStart(2, '0') },
{ pattern: '2', value: day.toString() },
{ pattern: '15', value: hour.toString().padStart(2, '0') },
{ pattern: '3', value: ((hour + 11) % 12 + 1).toString() },
{ pattern: '04', value: minute.toString().padStart(2, '0') },
{ pattern: '4', value: minute.toString() },
{ pattern: '05', value: second.toString().padStart(2, '0') },
{ pattern: '5', value: second.toString() },
{ pattern: 'PM', value: hour >= 12 ? 'PM' : 'AM' },
{ pattern: 'pm', value: hour >= 12 ? 'pm' : 'am' },
{ pattern: 'MST', value: date.toLocaleString('en-US', { timeZoneName: 'short' }) },
];
let result = layout;
for (const { pattern, value } of patterns) {
result = result.replace(new RegExp(pattern, 'g'), value);
}
return result;
},
// Now returns the current time
Now: () => new Date(),
// Parse parses a formatted string and returns the time value it represents
Parse: (layout, value) => {
try {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
}
catch {
return null;
}
},
// Unix returns the Unix time in seconds
Unix: (value) => {
const date = value instanceof Date ? value : new Date(value);
return Math.floor(date.getTime() / 1000);
},
// UnixNano returns the Unix time in nanoseconds
UnixNano: (value) => {
const date = value instanceof Date ? value : new Date(value);
return date.getTime() * 1e6; // Convert milliseconds to nanoseconds
}
}));
// Now function that returns an object with Format method (Go-style)
funcMap.set('now', () => {
const date = new Date();
return {
Format: (layout) => {
// Go time format parsing - based on reference time: Mon Jan 2 15:04:05 MST 2006
// This represents Unix time 1136239445
const year = date.getFullYear();
const month = date.getMonth() + 1; // JavaScript months are 0-based
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
// Go format patterns in order of precedence (longer patterns first to avoid conflicts)
const patterns = [
// Year patterns
{ pattern: '2006', value: year.toString() }, // 4-digit year
{ pattern: '06', value: year.toString().slice(-2) }, // 2-digit year
// Month patterns
{ pattern: '01', value: month.toString().padStart(2, '0') }, // 2-digit month
{ pattern: '1', value: month.toString() }, // 1-digit month
// Day patterns
{ pattern: '02', value: day.toString().padStart(2, '0') }, // 2-digit day
{ pattern: '2', value: day.toString() }, // 1-digit day
// Hour patterns (24-hour)
{ pattern: '15', value: hour.toString().padStart(2, '0') }, // 2-digit hour (24h)
// Hour patterns (12-hour)
{ pattern: '3', value: ((hour % 12) || 12).toString() }, // 1-digit hour (12h)
// Minute patterns
{ pattern: '04', value: minute.toString().padStart(2, '0') }, // 2-digit minute
{ pattern: '4', value: minute.toString() }, // 1-digit minute
// Second patterns
{ pattern: '05', value: second.toString().padStart(2, '0') }, // 2-digit second
{ pattern: '5', value: second.toString() }, // 1-digit second
];
let result = layout;
// Apply patterns in order, using word boundaries to avoid partial matches
for (const { pattern, value } of patterns) {
// Use a more precise regex that considers word boundaries and context
const regex = new RegExp(`\\b${pattern}\\b`, 'g');
result = result.replace(regex, value);
}
// Handle some special cases that might not have word boundaries
// For patterns that might be standalone or part of other formatting
if (layout === '2006') {
return year.toString(); // Pure year format
}
if (layout === '06') {
return year.toString().slice(-2); // Pure 2-digit year format
}
if (layout === '01') {
return month.toString().padStart(2, '0'); // Pure month format
}
if (layout === '02') {
return day.toString().padStart(2, '0'); // Pure day format
}
if (layout === '15') {
return hour.toString().padStart(2, '0'); // Pure hour format
}
if (layout === '04') {
return minute.toString().padStart(2, '0'); // Pure minute format
}
if (layout === '05') {
return second.toString().padStart(2, '0'); // Pure second format
}
return result;
},
// Also provide direct access to the Date object
getTime: () => date.getTime(),
toString: () => date.toString(),
toISOString: () => date.toISOString()
};
});
funcMap.set('dateFormat', (format, date) => {
// Simple date formatting - can be enhanced with proper library
return date.toLocaleDateString();
});
}
/**
* Register collection functions
* Equivalent to Go's registerCollections()
*/
registerCollectionFunctions(funcMap) {
funcMap.set('len', (obj) => {
if (Array.isArray(obj))
return obj.length;
if (typeof obj === 'string')
return obj.length;
if (obj && typeof obj === 'object')
return Object.keys(obj).length;
return 0;
});
funcMap.set('first', (arr, n) => {
if (!Array.isArray(arr))
return [];
return n ? arr.slice(0, n) : arr.slice(0, 1);
});
funcMap.set('last', (arr, n) => {
if (!Array.isArray(arr))
return [];
return n ? arr.slice(-n) : arr.slice(-1);
});
// Merge function for combining objects/maps
funcMap.set('merge', (...args) => {
const result = {};
for (const arg of args) {
if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
Object.assign(result, arg);
}
}
return result;
});
// Index function to get item at index - supports nested indexing like Go's implementation
// Usage: {{ index array 0 }} or {{ index object "key" }} or {{ index nested "key1" "key2" 0 }}
funcMap.set('index', (item, ...indices) => {
try {
return this.doIndex(item, indices);
}
catch (error) {
log.error(`Index of type ${typeof item} with args [${indices.join(', ')}] failed:`, error);
return null;
}
});
// Reverse function
funcMap.set('reverse', (arr) => {
if (Array.isArray(arr)) {
return [...arr].reverse();
}
return arr;
});
// Append function
funcMap.set('append', (...args) => {
if (args.length < 2) {
return args[0] || [];
}
const result = Array.isArray(args[0]) ? [...args[0]] : [args[0]];
// For each remaining argument, spread it if it's an array, otherwise add as single element
for (let i = 1; i < args.length; i++) {
if (Array.isArray(args[i])) {
result.push(...args[i]);
}
else {
result.push(args[i]);
}
}
return result;
});
// Prepend function
funcMap.set('prepend', (arr, ...items) => {
if (Array.isArray(arr)) {
return [...items, ...arr];
}
return [...items, arr];
});
// Seq creates a sequence of integers from args. It's named and used as GNU's seq.
// Examples:
// 3 => 1, 2, 3
// 1 2 4 => 1, 3
// -3 => -1, -2, -3
// 1 4 => 1, 2, 3, 4
// 1 -2 => 1, 0, -1, -2
funcMap.set('seq', (...args) => {
if (args.length < 1 || args.length > 3) {
throw new Error('invalid number of arguments to Seq');
}
// Convert all arguments to integers
const intArgs = args.map(arg => {
const num = Number(arg);
if (isNaN(num)) {
throw new Error('invalid arguments to Seq');
}
return Math.floor(num);
});
let inc = 1;
let last;
let first = intArgs[0];
if (intArgs.length === 1) {
last = first;
if (last === 0) {
return [];
}
else if (last > 0) {
first = 1;
}
else {
first = -1;
inc = -1;
}
}
else if (intArgs.length === 2) {
last = intArgs[1];
if (last < first) {
inc = -1;
}
}
else {
inc = intArgs[1];
last = intArgs[2];
if (inc === 0) {
throw new Error("'increment' must not be 0");
}
if (first < last && inc < 0) {
throw new Error("'increment' must be > 0");
}
if (first > last && inc > 0) {
throw new Error("'increment' must be < 0");
}
}
// Sanity check
if (last < -100000) {
throw new Error('size of result exceeds limit');
}
const size = Math.floor((last - first) / inc) + 1;
// Sanity check
if (size <= 0 || size > 2000) {
throw new Error('size of result exceeds limit');
}
const seq = new Array(size);
let val = first;
for (let i = 0; i < size; i++) {
seq[i] = val;
val += inc;
if ((inc < 0 && val < last) || (inc > 0 && val > last)) {
break;
}
}
return seq;
});
// Sort returns a sorted copy of the list l
// Equivalent to Go's Sort function in collections namespace
// Usage: sort list [sortByField] [direction]
// Examples:
// {{ sort .Pages }} - sort by default comparison
// {{ sort .Pages "Title" }} - sort by Title field ascending
// {{ sort .Pages "Date" "desc" }} - sort by Date field descending
funcMap.set('sort', (list, ...args) => {
if (list === null || list === undefined) {
throw new Error('sequence must be provided');
}
let arr;
// Handle different input types
if (Array.isArray(list)) {
arr = [...list]; // Create a copy to avoid mutating original
}
else if (typeof list === 'object' && list !== null) {
// Handle objects/maps - convert to array of values
arr = Object.values(list);
}
else {
throw new Error(`can't sort ${typeof list}`);
}
if (arr.length === 0) {
return arr;
}
// Parse arguments
let sortByField = '';
let sortAsc = true;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (i === 0) {
// First argument is the field to sort by
if (typeof arg === 'string') {
sortByField = arg;
}
}
else if (i === 1) {
// Second argument is the sort direction
if (typeof arg === 'string' && arg.toLowerCase() === 'desc') {
sortAsc = false;
}
}
}
// Helper function to get nested property value
c