/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved. * * Permission to use, copy, modify, and/or distribute this software * for any purpose is hereby granted, under the terms of the MIT license. * * If a copy of the license was not distributed with this file, it can * be obtained from https://opensource.org/license/mit. */ // ---------------------------------------------------------------------------- import * as os from 'node:os' import * as path from 'node:path' import * as util from 'node:util' // https://www.npmjs.com/package/liquidjs import * as liquidjs from 'liquidjs' // ---------------------------------------------------------------------------- import { isJsonObject } from '../functions/is-something.js' import { PlatformDetector } from './platform-detector.js' // ============================================================================ /** * Liquid engine configured for xpm templates. * * @remarks * This class extends the Liquid engine and registers custom filters * for path manipulation, string formatting, and convenience helpers used across * xpm templates. * * The engine is configured with strict parsing options to catch template * errors early during development. Custom filters are organized into * categories: * *
    *
  1. Path manipulation: Platform-specific and cross-platform path * operations * (basename, dirname, join, * relative, normalize) for default, POSIX, and * Win32 paths.
  2. *
  3. String formatting: Utilities for printf-style formatting and * filename * sanitization.
  4. *
  5. Array/string conversion: Filters for joining and splitting * lines.
  6. *
  7. Object introspection: Filters for extracting object keys.
  8. *
* * These filters enable templates to perform complex path manipulations and * string transformations without requiring external dependencies or custom * template tags. */ export class LiquidEngine extends liquidjs.Liquid { // -------------------------------------------------------------------------- // Private Members. /** * The platform detector instance for platform-specific behaviour. */ private readonly platformDetector: PlatformDetector // -------------------------------------------------------------------------- // Constructor. /** * Constructs a Liquid engine instance with xpm-specific settings and * filters. * * @remarks * The constructor configures strict parsing options and registers * filters for path handling, formatting, and list operations. * * Configuration options: * * * * Filter registration: * * * * @param platformDetector - The platform detector instance for * platform-specific behaviour. Defaults to a new {@link PlatformDetector} * instance. */ constructor({ platformDetector = new PlatformDetector(), options = {}, }: { platformDetector?: PlatformDetector options?: liquidjs.LiquidOptions } = {}) { super({ strictFilters: true, strictVariables: true, trimTagLeft: false, trimTagRight: false, trimOutputLeft: false, trimOutputRight: false, greedy: false, lenientIf: true, cache: false, ...options, // Allow overriding defaults with provided options. }) this.platformDetector = platformDetector // https://liquidjs.com/api/classes/liquid_.liquid.html#registerFilter // https://nodejs.org/dist/latest-v16.x/docs/api/path.html // Add the main path manipulation functions. this.registerFilter('path_basename', (p: string, ...arg) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument path.basename(p, ...arg) ) this.registerFilter('path_dirname', (p: string) => path.dirname(p)) this.registerFilter('path_normalize', (p: string) => path.normalize(p)) // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.registerFilter('path_join', (p, ...args) => path.join(p, ...args)) this.registerFilter('path_relative', (from: string, to: string) => path.relative(from, to) ) this.registerFilter('path_posix_basename', (p: string, ...arg) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument path.posix.basename(p, ...arg) ) this.registerFilter('path_posix_dirname', (p: string) => path.posix.dirname(p) ) this.registerFilter('path_posix_normalize', (p: string) => path.posix.normalize(p) ) this.registerFilter('path_posix_join', (p, ...args) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument path.posix.join(p, ...args) ) this.registerFilter('path_posix_relative', (from: string, to: string) => path.posix.relative(from, to) ) this.registerFilter('path_win32_basename', (p: string, ...arg) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument path.win32.basename(p, ...arg) ) this.registerFilter('path_win32_dirname', (p: string) => path.win32.dirname(p) ) this.registerFilter('path_win32_normalize', (p: string) => path.win32.normalize(p) ) this.registerFilter('path_win32_join', (p, ...args) => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument path.win32.join(p, ...args) ) this.registerFilter('path_win32_relative', (from: string, to: string) => path.win32.relative(from, to) ) // https://nodejs.org/dist/latest-v16.x/docs/api/util.html this.registerFilter('util_format', (format, ...args) => { // console.log([...args]) // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return util.format(format, ...args) }) // Custom action. this.registerFilter( 'to_filename', // Replace non alphanumeric chars with dashes to make the paths // comply with filesystem names. (input: string): string => { /* c8 ignore start - windows specific code cannot be tested on other platforms */ const fixed = this.platformDetector.isWindows() ? input.replace(/[^a-zA-Z0-9\\:]+/g, '-') : input.replace(/[^a-zA-Z0-9/]+/g, '-') /* c8 ignore stop */ return fixed.replace(/--/g, '-') } ) this.registerFilter('join_lines', (input: string[]): string => { // Convert an array into a string with each element on a separate line. if (Array.isArray(input)) { return input.join(os.EOL) } return String(input) }) // Convert a string with lines into an array. this.registerFilter('split_lines', (input: string | string[]): string[] => { if (Array.isArray(input)) { // If already an array, first flatten it, then split it. // This is needed in case any of the lines include EOLs. return input.join(os.EOL).split(os.EOL) } return input.split(os.EOL) }) this.registerFilter('keys', (input: unknown): string[] | string => { if (isJsonObject(input)) { const keys = Object.keys(input as object) // console.log('input object', input) // console.log('input keys', keys) return keys } else if (Array.isArray(input)) { const keys = Object.keys(input) return keys } else { return String(input) } }) } } // ----------------------------------------------------------------------------