/* * 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. */ // ---------------------------------------------------------------------------- // https://www.npmjs.com/package/liquidjs import * as liquidjs from 'liquidjs' // https://www.npmjs.com/package/@xpack/logger import { Logger } from '@xpack/logger' // ---------------------------------------------------------------------------- import { LiquidSubstitutionsStrings } from '../data/substitutions-variables.js' import { isJsonObject } from '../functions/is-something.js' import { TemplateError } from './errors.js' // ============================================================================ // https://liquidjs.com/ /** * Configuration parameters for constructing a properties drop instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link LiquidPropertiesDrop}. All properties are mandatory. * * The parameters provide the Liquid engine for nested template evaluation, * the properties map for value lookups, and the logger for diagnostic * output during property resolution and substitution. */ export interface LiquidPropertiesDropConstructorParameters { /** * The Liquid engine used to render nested substitutions. */ engine: liquidjs.Liquid /** * The properties map used for substitutions. */ properties: LiquidSubstitutionsStrings /** * The logger instance for output and diagnostics. */ log: Logger } /** * Liquid drop that resolves `property` values for template substitutions. * * @remarks * This drop exposes properties to the Liquid engine and performs * additional substitutions when a property value itself contains Liquid syntax. * * Implements the Liquid Drop pattern to provide lazy property resolution and * recursive template evaluation. When a template accesses `properties.foo`, * the Liquid engine calls {@link LiquidPropertiesDrop.liquidMethodMissing} * which: * *
    *
  1. Looks up the property value in the properties map.
  2. *
  3. Checks if the value contains Liquid syntax * (\{\{ or \{%).
  4. *
  5. If yes, recursively evaluates the value as a Liquid template.
  6. *
  7. Returns the final resolved value.
  8. *
* * This enables multi-level property references where one property can * reference another, which can reference yet another, with the engine * automatically resolving the entire chain. */ export class LiquidPropertiesDrop extends liquidjs.Drop { // -------------------------------------------------------------------------- // Public Members. // -------------------------------------------------------------------------- // Protected Members. /** * The logger instance for output and diagnostics. */ protected readonly _log: Logger /** * The properties map used for substitutions. */ protected readonly _properties: LiquidSubstitutionsStrings /** * The Liquid engine used to render nested substitutions. */ protected readonly _engine: liquidjs.Liquid // -------------------------------------------------------------------------- // Constructor. /** * Constructs a properties drop. * * @param engine - The Liquid engine used to render nested substitutions. * @param properties - The properties map used for substitutions. * @param log - The logger instance for output and diagnostics. */ constructor({ engine, properties, log, }: LiquidPropertiesDropConstructorParameters) { super() log.trace(`${LiquidPropertiesDrop.name}()`) this._log = log this._engine = engine this._properties = properties } // -------------------------------------------------------------------------- // Public Methods. /** * Resolves a missing property and performs nested substitutions. * * @remarks * Called by the Liquid engine when a property is accessed that doesn't * exist as a regular method. This implements the Drop pattern for dynamic * property resolution. * * Resolution process: * *
    *
  1. Validate the property exists, throw InputError if * not.
  2. *
  3. Retrieve the property value (string, array, or object).
  4. *
  5. If object, return as-is for Liquid to access nested properties.
  6. *
  7. If array, join elements into a single string for processing.
  8. *
  9. If the value contains Liquid syntax, recursively render it with the * current context to resolve nested references.
  10. *
  11. Return the final resolved value.
  12. *
* * Array values are concatenated without separators, allowing properties * to span multiple lines in JSON while producing a single string output. * * @param key - The property name requested by the template. * @param context - The Liquid rendering context. * @returns The resolved property value. * * @throws {@link TemplateError} * If the property is not defined. */ override async liquidMethodMissing( key: string, context: liquidjs.Context // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { // console.log(key) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this._properties[key] === undefined) { throw new TemplateError(`"properties.${key}" not defined`) } const log = this._log const value = this._properties[key] log.trace( `${LiquidPropertiesDrop.name}.liquidMethodMissing('${key}') in (`, value, ')' ) let result: string | string[] if (isJsonObject(value)) { return value } // If the property value is an array, merge them into a single string. const valueString = Array.isArray(value) ? value.join('') : value if (valueString.includes('{{') || valueString.includes('{%')) { result = (await this._engine.parseAndRender( valueString, context )) as string } else { result = value } log.trace( `${LiquidPropertiesDrop.name}.liquidMethodMissing('${key}')` + ` => (`, result, ')' ) return result } } // ============================================================================ /** * Configuration parameters for constructing a matrix drop instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link LiquidMatrixDrop}. All properties are mandatory. * * The parameters provide the Liquid engine for nested template evaluation, * the matrix parameters map for value lookups, and the logger for diagnostic * output during matrix parameter resolution and substitution. */ export interface LiquidMatrixDropConstructorParameters { /** * The Liquid engine used to render nested substitutions. */ engine: liquidjs.Liquid /** * The matrix parameters map used for substitutions. */ matrix: LiquidSubstitutionsStrings /** * The logger instance for output and diagnostics. */ log: Logger } /** * Liquid drop that resolves `matrix` parameter values for templates. * * @remarks * This drop exposes matrix values to the Liquid engine and performs * nested substitutions when a matrix value contains Liquid syntax. * * Matrix parameters are used during template expansion to generate multiple * actions or build configurations from a single template definition. Each * expanded instance receives a specific combination of matrix values. * * Implements the same Drop pattern as {@link LiquidPropertiesDrop} but * for matrix-scoped variables. When a template accesses `matrix.arch`, the * engine calls {@link LiquidMatrixDrop.liquidMethodMissing} which resolves * the parameter value and recursively evaluates any nested Liquid syntax. * * Matrix parameters are isolated per template instance, ensuring that each * generated action or configuration has access only to its specific matrix * combination. */ export class LiquidMatrixDrop extends liquidjs.Drop { // -------------------------------------------------------------------------- // Protected Members. /** * The logger instance for output and diagnostics. */ protected readonly _log: Logger /** * The matrix parameters map used for substitutions. */ protected readonly _matrix: LiquidSubstitutionsStrings /** * The Liquid engine used to render nested substitutions. */ protected readonly _engine: liquidjs.Liquid // -------------------------------------------------------------------------- // Constructor. /** * Constructs a matrix drop. * * @param engine - The Liquid engine used to render nested substitutions. * @param matrix - The matrix parameters map used for substitutions. * @param log - The logger instance for output and diagnostics. */ constructor({ engine, matrix, log }: LiquidMatrixDropConstructorParameters) { super() log.trace(`${LiquidMatrixDrop.name}()`) this._log = log this._engine = engine this._matrix = matrix } // -------------------------------------------------------------------------- // Public Methods. /** * Resolves a missing matrix parameter and performs nested substitutions. * * @remarks * Called by the Liquid engine when a matrix parameter is accessed that * doesn't exist as a regular method. This implements the Drop pattern for * dynamic matrix parameter resolution. * * Resolution process: * *
    *
  1. Validate the parameter exists, throw InputError if * not.
  2. *
  3. Retrieve the parameter value (string, array, or object).
  4. *
  5. If object, return as-is for Liquid to access nested properties.
  6. *
  7. If array, join elements into a single string for processing.
  8. *
  9. If the value contains Liquid syntax, recursively render it with the * current context to resolve nested references.
  10. *
  11. Return the final resolved value.
  12. *
* * This mirrors the behavior of {@link LiquidPropertiesDrop} but operates * on matrix parameters instead of properties. Matrix values can reference * other substitution variables, enabling complex template expansions. * * @param key - The matrix parameter name requested by the template. * @param context - The Liquid rendering context. * @returns The resolved matrix parameter value. * * @throws {@link TemplateError} * If the matrix parameter is not defined. */ override async liquidMethodMissing( key: string, context: liquidjs.Context // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { // console.log(key) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this._matrix[key] === undefined) { throw new TemplateError(`"matrix.${key}" not defined`) } const log = this._log const value = this._matrix[key] log.trace( `${LiquidMatrixDrop.name}.liquidMethodMissing('${key}') in (`, value, ')' ) let result: string | string[] if (isJsonObject(value)) { return value } // If the property value is an array, merge them into a single string. const valueString = Array.isArray(value) ? value.join('') : value if (valueString.includes('{{') || valueString.includes('{%')) { result = (await this._engine.parseAndRender( valueString, context )) as string } else { result = value } log.trace( `${LiquidMatrixDrop.name}.liquidMethodMissing('${key}')` + ` => (`, result, ')' ) return result } } // ----------------------------------------------------------------------------