UNPKG

@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
"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