Skip to content

jul-m/PoSh.FluidTemplateEngine

Repository files navigation

PoSh.FluidTemplateEngine

CI PowerShell Gallery PowerShell 7.0+ .NET 8.0+ License

A PowerShell module for easy rendering of Liquid templates using the high-performance Fluid .NET library.


Table of Contents


Features

Feature Description
Liquid Rendering Full Liquid template engine (variables, filters, tags, loops, conditions)
Compiled Templates One-shot or compile-then-render workflow for optimal performance
File-Based Rendering Render templates from files with {% include %} / {% render %} support
Custom Filters Register PowerShell ScriptBlock-based Liquid filters
Custom Tags Register custom Liquid tags (Empty, Identifier, Expression)
Custom Blocks Register custom Liquid blocks with body content
Custom Operators Register custom binary comparison operators
Functions & Macros Built-in support for {% macro %} and {{ func() }} syntax
Parentheses Optional grouping of expressions with parentheses
Strict Modes Strict variables and strict filters (including in includes)
Whitespace Control Full trimming and greedy mode configuration
CLR Type Access Allow-list .NET types for member access in templates
Localization Culture and timezone support for dates/numbers
HTML Encoding Optional HTML encoding on output
Template Caching Fingerprint-based engine caching for optimal performance

Prerequisites

  • PowerShell 7.0+
  • .NET 8.0+ runtime

Installation

From PowerShell Gallery

Install-Module -Name PoSh.FluidTemplateEngine

From Source (Development)

# Build the module
pwsh -NonInteractive ./run.ps1 -Mode Package

# Import
Import-Module ./out/publish/PoSh.FluidTemplateEngine/PoSh.FluidTemplateEngine.psd1 -Force

Quick Start

1. One-Shot Rendering

Format-LiquidString -Source 'Hello {{ name }}!' -Model @{ name = 'Alice' }
# Output: Hello Alice!

2. Compile + Render (Recommended for Repeated Use)

$template = New-FluidTemplate -Source 'Hi {{ name }}, welcome!'
$template | Invoke-FluidTemplate -Model @{ name = 'Bob' }
# Output: Hi Bob, welcome!

3. Render from File with Includes

Set-FluidModuleConfig -TemplateRoot './templates'
Invoke-FluidFile -Path './templates/main.liquid' -Model @{ title = 'Home' }

4. Complex Data Models

$model = @{
    title = 'Products'
    items = @(
        @{ name = 'Widget'; price = 9.99 }
        @{ name = 'Gadget'; price = 24.95 }
    )
}

$source = @'
# {{ title }}
{% for item in items %}
- {{ item.name }}: ${{ item.price }}
{% endfor %}
'@

Format-LiquidString -Source $source -Model $model

Cmdlets Reference

Cmdlet Description
Format-LiquidString Parse + render a Liquid template in one step
New-FluidTemplate Compile a Liquid template into a reusable object
Invoke-FluidTemplate Render a compiled template with a model
Invoke-FluidFile Render a Liquid template from a file
Get-FluidModuleConfig Display the current module configuration
Set-FluidModuleConfig Set module configuration options
Register-FluidType Allow-list a .NET type for member access
Register-LiquidFilter Register a custom Liquid filter (ScriptBlock)
Register-LiquidTag Register a custom Liquid tag
Register-LiquidBlock Register a custom Liquid block
Register-LiquidOperator Register a custom binary operator

Full auto-generated cmdlet documentation is available in docs/Cmdlets/.


Configuration

Use Set-FluidModuleConfig to configure the module globally for the current session:

# View current configuration
Get-FluidModuleConfig

# Reset to defaults
Set-FluidModuleConfig -Reset

All Configuration Options

Parameter Type Default Description
-TemplateRoot string $PWD Root directory for {% include %} / {% render %}
-StrictVariables switch $false Error on undefined variables
-StrictFilters switch $false Error on unknown filters (including in includes)
-UndefinedFormat string $null Fallback format for undefined variables (e.g. [{name} not found])
-MaxSteps int 0 Maximum execution steps (0 = unlimited)
-MaxRecursion int $null Maximum recursion depth for includes/renders
-AllowFunctions switch $false Enable function/macro syntax
-AllowParentheses switch $false Enable expression grouping with parentheses
-Culture string $null Culture for date/number formatting (e.g. fr-FR)
-TimeZoneId string $null Time zone for date parsing (e.g. Europe/Paris)
-Trimming TrimmingFlags None Automatic whitespace trimming rules
-Greedy bool $true When true, trimming removes all successive blank chars
-ModelNamesComparer enum OrdinalIgnoreCase How property names are compared
-IgnoreMemberCasing bool $false Ignore case for CLR member access
-JsonIndented bool $false Indented output for the json filter
-JsonRelaxedEscaping bool $false Relaxed JSON escaping (UnsafeRelaxedJsonEscaping)

Custom Filters

Register PowerShell ScriptBlock-based Liquid filters using Register-LiquidFilter. The ScriptBlock receives the input value as the first argument, followed by any filter arguments.

# Simple filter
Register-LiquidFilter -Name 'shout' -ScriptBlock {
    param($input)
    $input.ToString().ToUpperInvariant()
}

Format-LiquidString -Source '{{ name | shout }}' -Model @{ name = 'hello' }
# Output: HELLO
# Filter with arguments
Register-LiquidFilter -Name 'prefix' -ScriptBlock {
    param($input, $prefix)
    "$prefix$input"
}

Format-LiquidString -Source "{{ name | prefix: '>> ' }}" -Model @{ name = 'World' }
# Output: >> World

Custom Tags

Tags are self-closing Liquid elements (no {% end... %}). Register them using Register-LiquidTag.

Three types are supported:

Empty Tag — No Parameter

Register-LiquidTag -Name 'timestamp' -Type Empty -ScriptBlock {
    (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
}

Format-LiquidString -Source 'Generated on {% timestamp %}' -Model @{}
# Output: Generated on 2026-02-28 14:30:00

Identifier Tag — Takes an Identifier

Register-LiquidTag -Name 'hello' -Type Identifier -ScriptBlock {
    param($identifier)
    "Hello $identifier!"
}

Format-LiquidString -Source '{% hello world %}' -Model @{}
# Output: Hello world!

Expression Tag — Takes an Evaluated Expression

Register-LiquidTag -Name 'echo' -Type Expression -ScriptBlock {
    param($value)
    ">> $value <<"
}

Format-LiquidString -Source "{% echo 'test' | upcase %}" -Model @{}
# Output: >> TEST <<

Custom Blocks

Blocks are Liquid elements with body content between {% name %}...{% endname %}. Register them using Register-LiquidBlock.

The ScriptBlock receives the rendered body as a string parameter.

Empty Block — Wraps Content

Register-LiquidBlock -Name 'card' -Type Empty -ScriptBlock {
    param($body)
    "<div class='card'>$body</div>"
}

Format-LiquidString -Source '{% card %}Important{% endcard %}' -Model @{}
# Output: <div class='card'>Important</div>

Identifier Block — Named Wrapper

Register-LiquidBlock -Name 'tag' -Type Identifier -ScriptBlock {
    param($identifier, $body)
    "<$identifier>$body</$identifier>"
}

Format-LiquidString -Source '{% tag section %}Content{% endtag %}' -Model @{}
# Output: <section>Content</section>

Expression Block — Parameterized Block

Register-LiquidBlock -Name 'repeat' -Type Expression -ScriptBlock {
    param($value, $body)
    $body * [int]$value
}

Format-LiquidString -Source '{% repeat 3 %}Go! {% endrepeat %}' -Model @{}
# Output: Go! Go! Go! 

Custom Operators

Operators are used in {% if %} conditions to compare values. Register custom binary operators using Register-LiquidOperator.

The ScriptBlock receives $left and $right operands and must return a boolean.

# XOR operator
Register-LiquidOperator -Name 'xor' -ScriptBlock {
    param($left, $right)
    [bool]$left -xor [bool]$right
}

Format-LiquidString -Source '{% if true xor false %}Yes{% endif %}' -Model @{}
# Output: Yes

Format-LiquidString -Source '{% if true xor true %}Yes{% else %}No{% endif %}' -Model @{}
# Output: No
# Regex match operator
Register-LiquidOperator -Name 'matches' -ScriptBlock {
    param($left, $right)
    $left -match $right
}

Format-LiquidString -Source "{% if name matches '^A' %}Starts with A{% endif %}" -Model @{ name = 'Alice' }
# Output: Starts with A

Functions & Macros

Fluid supports functions and macros when enabled via AllowFunctions.

Enabling Functions

Set-FluidModuleConfig -AllowFunctions

Using Macros

Define reusable template fragments with {% macro %}:

Set-FluidModuleConfig -AllowFunctions

$source = @'
{% macro greet(name, greeting='Hello') %}
{{ greeting }} {{ name }}!
{% endmacro %}

{{ greet('Alice') }}
{{ greet('Bob', greeting='Hi') }}
'@

Format-LiquidString -Source $source -Model @{}

Importing Functions from External Templates

{% from 'forms' import field %}

{{ field('user') }}
{{ field('pass', type='password') }}

Whitespace Control

Hyphens in Templates

Use hyphens (-) in tags and output values to strip whitespace:

{%- assign name = "Bill" -%}
{{ name }}

Automatic Trimming

Configure automatic whitespace trimming without hyphens:

# Trim right side of tags and left side of output values
Set-FluidModuleConfig -Trimming TagRight, OutputLeft

# Disable greedy mode (only strip spaces before first newline)
Set-FluidModuleConfig -Greedy $false

Available TrimmingFlags: None, TagLeft, TagRight, OutputLeft, OutputRight, TagBoth, OutputBoth, All

Greedy Mode

When enabled (default), trimming removes all successive blank characters. When disabled, only spaces before the first newline are stripped.


Encoding

By default, output is not encoded. Use -HtmlEncode to activate HTML encoding:

# No encoding (default)
Format-LiquidString -Source '{{ val }}' -Model @{ val = '<script>alert(1)</script>' }
# Output: <script>alert(1)</script>

# HTML encoded
Format-LiquidString -Source '{{ val }}' -Model @{ val = '<script>alert(1)</script>' } -HtmlEncode
# Output: &lt;script&gt;alert(1)&lt;/script&gt;

Type Registration (CLR Access)

By default, Fluid only allows access to dictionary/hashtable properties. To use .NET objects in templates, register their types first:

# Register all public properties
Register-FluidType -Type ([System.Version])

# Register specific properties only
Register-FluidType -TypeName 'System.Diagnostics.Process' -Member Id, ProcessName

# Use in templates
Register-FluidType -TypeName 'System.Version' -Member Major, Minor, Build
Format-LiquidString -Source '{{ v.Major }}.{{ v.Minor }}.{{ v.Build }}' -Model @{ v = [Version]'1.2.3' }
# Output: 1.2.3

Case Sensitivity

# Ignore casing on registered type members
Set-FluidModuleConfig -IgnoreMemberCasing $true

Strict Modes

Strict Variables

Error when accessing undefined variables:

Set-FluidModuleConfig -StrictVariables
Format-LiquidString -Source '{{ missing }}' -Model @{} -ErrorAction Stop
# Throws: Undefined variable 'missing'

Undefined Format (Fallback)

When strict variables is disabled, provide a fallback format for undefined variables:

Set-FluidModuleConfig -UndefinedFormat '[{name} not found]'
Format-LiquidString -Source '{{ missing }}' -Model @{}
# Output: [missing not found]

Strict Filters

Error when using unregistered filters (including in included templates):

Set-FluidModuleConfig -StrictFilters
Format-LiquidString -Source "{{ 'hello' | unknown_filter }}" -Model @{} -ErrorAction Stop
# Throws: Undefined filter 'unknown_filter'

Include & Render

Use {% include %} and {% render %} to embed partial templates:

# Set the root directory for template resolution
Set-FluidModuleConfig -TemplateRoot './templates'

# Or specify per-call
Invoke-FluidFile -Path './templates/main.liquid' -Model $data -TemplateRoot './templates'
<!-- main.liquid -->
{% include 'header' %}
<main>{{ content }}</main>
{% include 'footer' %}

The template root is automatically inferred from the file's directory when using Invoke-FluidFile, unless explicitly overridden.


Advanced Configuration

Expression Grouping (Parentheses)

Set-FluidModuleConfig -AllowParentheses

Format-LiquidString -Source '{{ 1 | plus: (2 | times: 3) }}' -Model @{}
# Output: 7

Localization

# Set culture for date/number formatting
Set-FluidModuleConfig -Culture 'fr-FR'
Format-LiquidString -Source '{{ 1234.56 }}' -Model @{}

Time Zones

Set-FluidModuleConfig -TimeZoneId 'Europe/Paris'
Format-LiquidString -Source "{{ 'now' | date: '%Y-%m-%d %H:%M' }}" -Model @{}

JSON Options

# Indented JSON output
Set-FluidModuleConfig -JsonIndented $true

# Relaxed escaping (no escaping of <, >, &, etc.)
Set-FluidModuleConfig -JsonRelaxedEscaping $true

Format-LiquidString -Source '{{ data | json }}' -Model @{ data = @{ key = 'value' } }

Model Names Comparison

# Case-sensitive property names
Set-FluidModuleConfig -ModelNamesComparer Ordinal

Available modes: OrdinalIgnoreCase (default), Ordinal, InvariantCultureIgnoreCase, InvariantCulture, CurrentCultureIgnoreCase, CurrentCulture

Execution Limits

# Limit template execution steps (prevent infinite loops)
Set-FluidModuleConfig -MaxSteps 100000

# Limit recursion depth for includes/renders
Set-FluidModuleConfig -MaxRecursion 20

Build & Development

Build

# Build the module (compile + package)
pwsh -NonInteractive ./run.ps1 -Mode Package

# Generate cmdlet documentation
pwsh -NonInteractive ./run.ps1 -Mode Documentation

# Run tests
pwsh -NonInteractive ./run.ps1 -Mode Tests

CI/CD

This project uses GitHub Actions for continuous integration:

  • CI — Builds and tests on every push/PR across Windows, macOS, and Linux.
  • Release — Publishes to PowerShell Gallery and creates GitHub Releases on version tags (v*).

To publish a release:

git tag v0.1.0
git push origin v0.1.0

Note: Publishing to PSGallery requires a PSGALLERY_API_KEY secret in the release GitHub environment.

Documentation

Project Structure

├── src/                           # C# source code
│   ├── Cmdlets/                   # PowerShell cmdlet implementations
│   │   ├── FormatLiquidStringCmdlet.cs
│   │   ├── NewFluidTemplateCmdlet.cs
│   │   ├── InvokeFluidTemplateCmdlet.cs
│   │   ├── InvokeFluidFileCmdlet.cs
│   │   ├── SetFluidModuleConfigCmdlet.cs
│   │   ├── GetFluidModuleConfigCmdlet.cs
│   │   ├── RegisterFluidTypeCmdlet.cs
│   │   ├── RegisterLiquidFilterCmdlet.cs
│   │   ├── RegisterLiquidTagCmdlet.cs
│   │   ├── RegisterLiquidBlockCmdlet.cs
│   │   └── RegisterLiquidOperatorCmdlet.cs
│   └── Core/                      # Internal engine & utilities
│       ├── FluidManager.cs        # Singleton engine (parser + options cache)
│       ├── FluidModuleConfiguration.cs
│       ├── PsModelConverter.cs    # PS → Fluid model bridge
│       ├── StrictFiltersValidator.cs  # AST Visitor for filter validation
│       ├── ValidatingTemplateCache.cs # Decorator for strict filter checks on includes
│       ├── CompiledFluidTemplate.cs
│       ├── ScriptBlockBinaryExpression.cs  # Custom operator support
│       └── ...
├── tests/                         # Pester tests
├── examples/                      # Usage examples
├── docs/                          # Generated documentation
├── assets/                        # Liquid templates for doc generation
├── lib/                           # Build helpers
└── run.ps1                        # Build/test/docs orchestrator

Architecture

The module is a binary PowerShell module compiled from C# targeting .NET 8.

Engine Lifecycle

  1. Configuration is stored in a PowerShell global variable ($Global:FluidModuleConfiguration)
  2. The FluidParser and TemplateOptions are lazily created and cached based on a fingerprint of all configuration values
  3. When configuration changes, the engine is automatically rebuilt on next use
  4. Custom filters, tags, blocks, and operators are stored independently and applied when the engine is rebuilt

Key Design Decisions

  • Fingerprint-based caching — The engine (parser + options) is only rebuilt when configuration actually changes
  • ScriptBlock bridge — PowerShell ScriptBlocks are transparently wrapped into Fluid delegates for filters, tags, blocks, and operators
  • AST VisitorStrictFiltersValidator uses Fluid's AstVisitor pattern to walk the template AST and detect unknown filters
  • Decorator patternValidatingTemplateCache decorates Fluid's template cache to enforce strict filter validation on included templates
  • PS → Fluid model conversionPsModelConverter recursively converts Hashtables, PSCustomObjects, and collections into Fluid-compatible dictionaries

Dependencies

Package Version Role
Fluid.Core 2.31.0 Liquid template engine
PowerShellStandard.Library 5.1.1 PowerShell Standard API
System.Management.Automation 7.4.0 PowerShell 7.4 API
Microsoft.Extensions.FileProviders.Physical 8.0.0 File system provider for includes

Contributing

Contributions are welcome! Please read the Contributing Guide before submitting a pull request.

License

Apache License 2.0

About

A PowerShell module for easy rendering of Liquid templates using the high-performance Fluid .NET library.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors