HandlebarsPS.psm1

# HandlebarsPS.psm1
# PowerShell wrapper around Handlebars.Net with automatic NuGet download/restore (no nuget.exe required)

#region: internal state
$script:HandlebarsAssemblyLoaded = $false
$script:HandlebarsAssembly       = $null
$script:HandlebarsType           = $null
$script:Handlebars               = $null
#endregion

function Get-ModuleRoot {
    <#
    .SYNOPSIS
        Returns the root folder of this module.
    #>

    return (Split-Path -Parent $PSCommandPath)
}

function Restore-HandlebarsNuGetPackage {
    <#
    .SYNOPSIS
        Downloads and extracts the Handlebars.Net NuGet package (no nuget.exe required).
 
    .PARAMETER PackageId
        NuGet package id (default: Handlebars.Net).
 
    .PARAMETER Version
        Optional specific version, e.g. '3.0.0'. If omitted, latest is used.
    #>

    [CmdletBinding()]
    param(
        [string]$PackageId = 'Handlebars.Net',
        [string]$Version   = ''
    )

    $moduleRoot = Get-ModuleRoot
    $packagesRoot = Join-Path $moduleRoot 'packages'
    $packageFolder = Join-Path $packagesRoot $PackageId

    if (Test-Path $packageFolder) {
        Write-Verbose "Package '$PackageId' already restored at '$packageFolder'."
        return
    }

    if (-not (Test-Path $packagesRoot)) {
        New-Item -ItemType Directory -Path $packagesRoot | Out-Null
    }

    $nupkgPath = Join-Path $moduleRoot "$PackageId.nupkg"

    # Build NuGet download URL
    $url = if ($Version) {
        "https://www.nuget.org/api/v2/package/$PackageId/$Version"
    } else {
        "https://www.nuget.org/api/v2/package/$PackageId"
    }

    Write-Verbose "Downloading NuGet package '$PackageId' from '$url'..."
    try {
        Invoke-WebRequest -Uri $url -OutFile $nupkgPath -UseBasicParsing
    }
    catch {
        throw "Failed to download NuGet package '$PackageId' from '$url': $($_.Exception.Message)"
    }

    Write-Verbose "Expanding NuGet package '$nupkgPath' to '$packageFolder'..."
    try {
        Expand-Archive -Path $nupkgPath -DestinationPath $packageFolder -Force
    }
    catch {
        throw "Failed to extract NuGet package '$nupkgPath': $($_.Exception.Message)"
    }
    finally {
        # Best effort cleanup
        Remove-Item $nupkgPath -Force -ErrorAction SilentlyContinue
    }
}

function Get-HandlebarsDllPath {
    <#
    .SYNOPSIS
        Ensures Handlebars.Net is restored and returns the DLL path.
    #>

    $moduleRoot = Get-ModuleRoot
    $dllPath = Join-Path $moduleRoot 'packages/Handlebars.Net/lib/netstandard2.0/Handlebars.dll'

    if (-not (Test-Path $dllPath)) {
        Write-Verbose "Handlebars.Net.dll not found at '$dllPath'. Restoring package..."
        Restore-HandlebarsNuGetPackage
    }

    if (-not (Test-Path $dllPath)) {
        throw "Handlebars.Net.dll not found after restore. Expected at '$dllPath'."
    }

    return $dllPath
}

function Import-HandlebarsLibrary {
    <#
    .SYNOPSIS
        Loads Handlebars.Net into the current session, restoring from NuGet if needed.
    #>

    [CmdletBinding()]
    param()

    if ($script:HandlebarsAssemblyLoaded) {
        return
    }

    $dllPath = Get-HandlebarsDllPath

    try {
        $script:HandlebarsAssembly = [System.Reflection.Assembly]::LoadFrom((Resolve-Path $dllPath))
    }
    catch {
        throw "Failed to load Handlebars.Net assembly from '$dllPath': $($_.Exception.Message)"
    }

    $script:HandlebarsType = $script:HandlebarsAssembly.GetType('HandlebarsDotNet.Handlebars')
    if (-not $script:HandlebarsType) {
        throw "Could not find type 'HandlebarsDotNet.Handlebars' in the loaded assembly."
    }

    $script:Handlebars = $script:HandlebarsType
    $script:HandlebarsAssemblyLoaded = $true
}

function New-HandlebarsTemplate {
    <#
    .SYNOPSIS
        Compiles a Handlebars template and returns a callable delegate.
 
    .PARAMETER Template
        Handlebars template string.
 
    .EXAMPLE
        $tmpl = New-HandlebarsTemplate -Template 'Hello {{name}}'
        Invoke-HandlebarsTemplate -TemplateDelegate $tmpl -Data @{ name = 'World' }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Template
    )

    Import-HandlebarsLibrary

    try {
        $delegate = $script:Handlebars::Compile($Template)
    }
    catch {
        throw "Failed to compile Handlebars template: $($_.Exception.Message)"
    }

    return $delegate
}

function Invoke-HandlebarsTemplate {
    <#
    .SYNOPSIS
        Renders a Handlebars template with the given data.
 
    .DESCRIPTION
        You can:
        - Use -Template to compile from text on each call, or
        - Use -TemplateDelegate for a precompiled template.
 
        Data can be a hashtable or any object.
 
    .PARAMETER Template
        Handlebars template string.
 
    .PARAMETER TemplateDelegate
        Compiled template returned by New-HandlebarsTemplate.
 
    .PARAMETER Data
        Data context (hashtable or object).
 
    .EXAMPLE
        Invoke-HandlebarsTemplate -Template 'Hello {{name}}' -Data @{ name = 'World' }
 
    .EXAMPLE
        $tmpl = New-HandlebarsTemplate -Template 'Hello {{name}}'
        Invoke-HandlebarsTemplate -TemplateDelegate $tmpl -Data @{ name = 'World' }
    #>

    [CmdletBinding(DefaultParameterSetName = 'ByTemplateText')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByTemplateText')]
        [string]$Template,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByDelegate')]
        [object]$TemplateDelegate,

        [Parameter(Mandatory = $true)]
        [object]$Data
    )

    Import-HandlebarsLibrary

    if ($Data -is [System.Collections.IDictionary]) {
        $Data = [PSCustomObject]$Data
    }

    $delegateToUse = $TemplateDelegate
    if ($PSCmdlet.ParameterSetName -eq 'ByTemplateText') {
        $delegateToUse = New-HandlebarsTemplate -Template $Template
    }

    if (-not $delegateToUse) {
        throw "No template delegate available to invoke."
    }

    try {
        return $delegateToUse.Invoke($Data)
    }
    catch {
        throw "Failed to render Handlebars template: $($_.Exception.Message)"
    }
}

function Register-HandlebarsHelper {
    <#
    .SYNOPSIS
        Registers a custom Handlebars helper.
 
    .DESCRIPTION
        ScriptBlock signature:
            param($context, $args)
        - $context: current Handlebars context
        - $args : array of arguments passed from the template
 
        Return a string or any value; it will be written into the template output.
 
    .PARAMETER Name
        Helper name as used in the template.
 
    .PARAMETER ScriptBlock
        ScriptBlock implementing the helper logic.
 
    .EXAMPLE
        Register-HandlebarsHelper -Name 'upper' -ScriptBlock {
            param($context, $args)
            $args[0].ToString().ToUpper()
        }
 
        Invoke-HandlebarsTemplate -Template 'Hi {{upper name}}' -Data @{ name = 'john' }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [ScriptBlock]$ScriptBlock
    )

    Import-HandlebarsLibrary

    $callback = [HandlebarsDotNet.HelperFunction]{
        param($output, $context, $arguments)

        $result = & $ScriptBlock $context $arguments

        if ($null -ne $result) {
            $output.WriteSafeString($result.ToString())
        }
    }

    try {
        [HandlebarsDotNet.Handlebars]::RegisterHelper($Name, $callback)
    }
    catch {
        throw "Failed to register Handlebars helper '$Name': $($_.Exception.Message)"
    }
}

Export-ModuleMember -Function `
    Import-HandlebarsLibrary, `
    New-HandlebarsTemplate, `
    Invoke-HandlebarsTemplate, `
    Register-HandlebarsHelper