Public/Hyde.ps1

<#
.SYNOPSIS
PowerShell static site generator. The ugly Mr. Hyde to the popular Jekyll.
.DESCRIPTION
Hyde is a PowerShell static site generator inspired by Jekyll. Created as a fun project to only generate a private webpage, but useful as an example when teaching about PowerShell.
 
The current implementation supports:
- `New`
- `New-Theme`
- loading Hyde defaults from `globalConfig.yaml`
- loading site settings from `_config.yml`
- loading site and built-in plugins
- discovering documents and static files
- copying HTML and static files to the destination site
- loading recursive YAML, JSON, CSV, and TSV files from `_data` into nested `site.data` paths with collision and parse error reporting
- parsing YAML front matter
- rendering Markdown documents to HTML
- rendering single-level layouts through the Liquid module
- rendering plugin-provided Liquid tags and filters
- rendering Jekyll-style `include_relative` from post content under `_posts`
- collections
- Jekyll-style permalinks for pages, collections, and posts
- cleaning generated output and cache directories
- basic doctor-style site validation
 
We may never support:
- all plugins
- syntax highlighting
- theme installation workflows
 
Due to the nature of PowerShell, there is no intention to support:
- serve command
- file watch mode
.PARAMETER Command
Chooses which top-level Hyde action to run.
 
Available options are:
- `New`
- `New-Theme`
- `Build`
- `Clean`
- `Doctor`
- `Help`
.PARAMETER Source
Overrides the configured source directory for the site.
Supported by: `Build`, `Doctor`
.PARAMETER SourcePath
Uses the given site source directory only to read configuration for `Clean`.
Supported by: `Clean`
.PARAMETER Destination
Overrides the configured destination directory for generated output.
Supported by: `Build`, `Clean`, `New`, `New-Theme`
.PARAMETER Blank
Creates a minimal new-site scaffold.
Supported by: `New`
.PARAMETER Portable
Creates a reusable theme package scaffold without preview content pages.
Supported by: `New-Theme`
.PARAMETER Environment
Sets the build environment value exposed internally during the build.
Supported by: `Build`
.PARAMETER Quiet
Suppresses Hyde information messages during execution.
Supported by: `Build`, `Clean`, `Doctor`, `New`, `New-Theme`
.EXAMPLE
Hyde Build
 
Builds the site using paths from configuration.
.EXAMPLE
Hyde Build -Source . -Destination .\_site
 
Builds the site from the current directory into `.\_site`.
.EXAMPLE
Hyde Clean
 
Removes the generated destination folder, metadata file, and cache directories for the site.
.EXAMPLE
Hyde Clean -SourcePath .\site
 
Reads `.\site\_config.yml` to determine the generated destination path to clean.
.EXAMPLE
Hyde New mysite
 
Creates a new Hyde site scaffold at `.\mysite`.
.EXAMPLE
Hyde New mysite -Blank
 
Creates a minimal new Hyde site scaffold at `.\mysite`.
.EXAMPLE
Hyde New-Theme mytheme
 
Creates a previewable Hyde theme scaffold at `./mytheme`.
.EXAMPLE
Hyde New-Theme mytheme -Portable
 
Creates a reusable Hyde theme scaffold at `./mytheme` without preview content pages.
.EXAMPLE
Hyde Doctor
 
Checks the site for common problems such as invalid front matter, missing layouts, and output-path conflicts.
.EXAMPLE
Hyde Help
 
Shows command help for the module command.
#>

function Hyde {
    [CmdletBinding()]
    param(
        # Chooses the top-level Hyde action to run from the imported module.
        [Parameter(Position = 0)]
        [ValidateSet('New', 'New-Theme', 'Build', 'Clean', 'Doctor', 'Help')]
        [string]$Command
    )

    dynamicparam {
        $dynamicParameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

        function newHydeDynamicParameter {
            param(
                [Parameter(Mandatory = $true)]
                [string]$Name,

                [Parameter(Mandatory = $true)]
                [Type]$Type,

                [string[]]$Aliases = @()
            )

            $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $parameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
            [void]$attributeCollection.Add($parameterAttribute)

            if ($Aliases.Count -gt 0) {
                $aliasAttribute = [System.Management.Automation.AliasAttribute]::new($Aliases)
                [void]$attributeCollection.Add($aliasAttribute)
            }

            return [System.Management.Automation.RuntimeDefinedParameter]::new($Name, $Type, $attributeCollection)
        }

        # Dynamic parameters remain the best fit for a "Hyde build" command shape because
        # parameter sets cannot branch on the value of a positional string argument.
        switch ($Command) {
            'New' {
                $dynamicParameters.Add('Destination', (newHydeDynamicParameter -Name 'Destination' -Type ([string]) -Aliases @('Path')))
                $dynamicParameters['Destination'].Attributes[0].Position = 1
                $dynamicParameters.Add('Blank', (newHydeDynamicParameter -Name 'Blank' -Type ([switch])))
                $dynamicParameters.Add('Quiet', (newHydeDynamicParameter -Name 'Quiet' -Type ([switch])))
            }
            'New-Theme' {
                $dynamicParameters.Add('Destination', (newHydeDynamicParameter -Name 'Destination' -Type ([string]) -Aliases @('Path')))
                $dynamicParameters['Destination'].Attributes[0].Position = 1
                $dynamicParameters.Add('Portable', (newHydeDynamicParameter -Name 'Portable' -Type ([switch])))
                $dynamicParameters.Add('Quiet', (newHydeDynamicParameter -Name 'Quiet' -Type ([switch])))
            }
            'Build' {
                $dynamicParameters.Add('Source', (newHydeDynamicParameter -Name 'Source' -Type ([string])))
                $dynamicParameters.Add('Destination', (newHydeDynamicParameter -Name 'Destination' -Type ([string])))
                $dynamicParameters.Add('Environment', (newHydeDynamicParameter -Name 'Environment' -Type ([string]) -Aliases @('JEKYLL_ENV', 'HYDE_ENV')))
                $dynamicParameters.Add('Quiet', (newHydeDynamicParameter -Name 'Quiet' -Type ([switch])))
            }
            'Clean' {
                $dynamicParameters.Add('SourcePath', (newHydeDynamicParameter -Name 'SourcePath' -Type ([string])))
                $dynamicParameters.Add('Destination', (newHydeDynamicParameter -Name 'Destination' -Type ([string])))
                $dynamicParameters.Add('Quiet', (newHydeDynamicParameter -Name 'Quiet' -Type ([switch])))
            }
            'Doctor' {
                $dynamicParameters.Add('Source', (newHydeDynamicParameter -Name 'Source' -Type ([string])))
                $dynamicParameters.Add('Quiet', (newHydeDynamicParameter -Name 'Quiet' -Type ([switch])))
            }
        }

        return $dynamicParameters
    }

    begin {
        Set-StrictMode -Version Latest
        $ErrorActionPreference = 'Stop'

        if ($PSBoundParameters.ContainsKey('Quiet') -and $VerbosePreference -eq 'Continue') {
            throw "It doesn't make sense to ask for verbose output AND to keep quiet!"
        }

        switch ($Command) {
            'New' {
                if (-not $PSBoundParameters.ContainsKey('Destination') -or [string]::IsNullOrWhiteSpace([string]$PSBoundParameters['Destination'])) {
                    throw "The New command requires a destination path."
                }

                $commandParameters = @{
                    Destination = [string]$PSBoundParameters['Destination']
                    Quiet       = [bool]($PSBoundParameters.ContainsKey('Quiet') -and $PSBoundParameters['Quiet'])
                }

                if ($PSBoundParameters.ContainsKey('Blank')) {
                    $commandParameters['Blank'] = [bool]$PSBoundParameters['Blank']
                }

                if ($VerbosePreference -eq 'Continue') {
                    $commandParameters['Verbose'] = $true
                }

                New-StaticSite @commandParameters
            }
            'New-Theme' {
                if (-not $PSBoundParameters.ContainsKey('Destination') -or [string]::IsNullOrWhiteSpace([string]$PSBoundParameters['Destination'])) {
                    throw "The New-Theme command requires a destination path."
                }

                $commandParameters = @{
                    Destination = [string]$PSBoundParameters['Destination']
                    Quiet       = [bool]($PSBoundParameters.ContainsKey('Quiet') -and $PSBoundParameters['Quiet'])
                }

                if ($PSBoundParameters.ContainsKey('Portable')) {
                    $commandParameters['Portable'] = [bool]$PSBoundParameters['Portable']
                }

                if ($VerbosePreference -eq 'Continue') {
                    $commandParameters['Verbose'] = $true
                }

                New-StaticSiteTheme @commandParameters
            }
            'Build' {
                $commandParameters = @{
                    Environment = if ($PSBoundParameters.ContainsKey('Environment')) { [string]$PSBoundParameters['Environment'] } else { 'development' }
                    Quiet       = [bool]($PSBoundParameters.ContainsKey('Quiet') -and $PSBoundParameters['Quiet'])
                }

                if ($VerbosePreference -eq 'Continue') {
                    $commandParameters['Verbose'] = $true
                }

                if ($PSBoundParameters.ContainsKey('Source')) {
                    $commandParameters['Source'] = [string]$PSBoundParameters['Source']
                }

                if ($PSBoundParameters.ContainsKey('Destination')) {
                    $commandParameters['Destination'] = [string]$PSBoundParameters['Destination']
                }

                Publish-StaticSite @commandParameters
            }
            'Clean' {
                $commandParameters = @{
                    Quiet = [bool]($PSBoundParameters.ContainsKey('Quiet') -and $PSBoundParameters['Quiet'])
                }

                if ($VerbosePreference -eq 'Continue') {
                    $commandParameters['Verbose'] = $true
                }

                if ($PSBoundParameters.ContainsKey('SourcePath')) {
                    $commandParameters['SourcePath'] = [string]$PSBoundParameters['SourcePath']
                }

                if ($PSBoundParameters.ContainsKey('Destination')) {
                    $commandParameters['Destination'] = [string]$PSBoundParameters['Destination']
                }

                Clear-StaticSite @commandParameters
            }
            'Doctor' {
                $commandParameters = @{
                    Quiet = [bool]($PSBoundParameters.ContainsKey('Quiet') -and $PSBoundParameters['Quiet'])
                }

                if ($VerbosePreference -eq 'Continue') {
                    $commandParameters['Verbose'] = $true
                }

                if ($PSBoundParameters.ContainsKey('Source')) {
                    $commandParameters['Source'] = [string]$PSBoundParameters['Source']
                }

                Test-StaticSite @commandParameters
            }
            'Help' {
                Get-Help -Name Hyde
            }
            default {
                throw "Choose one of: Build, New, New-Theme, Clean, Doctor, Help. Use 'Help' to see command documentation."
            }
        }
    }
}