Public/ConvertTo-LiquidAst.ps1

<#
.SYNOPSIS
Parses a Liquid template into an Abstract Syntax Tree (AST).
.DESCRIPTION
Tokenizes and parses a Liquid template into a structured AST object for analysis or tooling.
This is the primary PowerLiquid entry point for parse-only inspection of Liquid syntax.
Tokens and AST nodes preserve line, column, and character-index ranges for diagnostics.
Supports multiple dialects and extension registries.
.PARAMETER Template
The Liquid template source to parse.
.PARAMETER Dialect
The Liquid dialect to parse with.
.PARAMETER Registry
The extension registry containing custom tags and filters.
.PARAMETER IncludeTokens
Include the raw token stream in the AST output. Tokens also include source locations for diagnostics.
.OUTPUTS
PowerLiquid.Ast
.EXAMPLE
$ast = ConvertTo-LiquidAst -Template '{% if page.title %}{{ page.title }}{% endif %}' -Dialect JekyllLiquid
.EXAMPLE
$ast = ConvertTo-LiquidAst -Template '{{ user.name }}' -IncludeTokens
.NOTES
AST root:
- Dialect
- Nodes
- Tokens when -IncludeTokens is specified

Location object:
- StartLine
- StartColumn
- EndLine
- EndColumn
- StartIndex
- EndIndex

Common node shapes:
- Text: Type, Value, Location
- Output: Type, Expression, Location
- Assign: Type, Name, Expression, Location
- Capture: Type, Name, Nodes, Location
- If: Type, Branches, Else, Location
- Unless: Type, Condition, Nodes, Else, Location
- For: Type, VariableName, CollectionExpression, Nodes, Else, Location
- Include: Type, TargetExpression, Parameters, Location
- IncludeRelative: Type, TargetExpression, Parameters, Location
- CustomTag: Type, Name, Markup, Location

Security:
- ConvertTo-LiquidAst is parse-only.
- It does not evaluate context data, includes, filters, or custom tag handlers.
- Prefer it for editor tooling, diagnostics, linting, and safe inspection of untrusted templates.
#>

function ConvertTo-LiquidAst {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Template,

        [ValidateSet('Liquid', 'JekyllLiquid')]
        [string]$Dialect = 'Liquid',

        [hashtable]$Registry = (New-LiquidExtensionRegistry),

        [switch]$IncludeTokens
    )

    try {
        # Validate the dialect up front so parsing behavior stays consistent with rendering.
        AssertLiquidDialect -Dialect $Dialect

        Write-Verbose "Parsing template to AST with dialect '$Dialect'"

        # Tokenize first so the AST API can optionally return both the raw token stream and the nested node tree.
        $tokens = ConvertTo-LiquidToken -Template $Template
        Write-Verbose "Tokenized template into $($tokens.Count) tokens"

        # Parse the nested node tree and then attach token-derived source locations for diagnostics.
        $index = 0
        $nodes = parseLiquidNode -Tokens $tokens -Index ([ref]$index) -Registry $Registry
        $tokenIndex = 0
        addLiquidAstLocation -Nodes $nodes -Tokens $tokens -TokenIndex ([ref]$tokenIndex)
        Write-Verbose "Parsed $($nodes.Count) AST nodes"

        # Expose a stable root object so hosts can rely on one entry shape instead of a raw node array.
        $ast = [pscustomobject]@{
            PSTypeName = 'PowerLiquid.Ast'
            Dialect    = $Dialect
            Nodes      = @($nodes)
        }

        if ($IncludeTokens) {
            Add-Member -InputObject $ast -MemberType NoteProperty -Name Tokens -Value @($tokens)
            Write-Verbose "Included token stream in AST output"
        }

        Write-Verbose "AST parsing completed successfully"
        return $ast
    } catch {
        throw "ConvertTo-LiquidAst failed: $($_.Exception.Message)"
    }
}