Private/Hyde.Plugins.ps1
|
# Create the in-memory registry used to track plugin hooks and metadata. function newHydePluginRegistry { [CmdletBinding()] [OutputType([hashtable])] param() # Hyde plugins can hook build stages or participate in output path calculation. return @{ Hooks = @{ AfterInitialize = New-Object System.Collections.ArrayList AfterDiscoverDocument = New-Object System.Collections.ArrayList AfterDiscoverStaticFile = New-Object System.Collections.ArrayList BeforeRenderDocument = New-Object System.Collections.ArrayList AfterRenderDocument = New-Object System.Collections.ArrayList BeforeWriteDocument = New-Object System.Collections.ArrayList AfterWriteDocument = New-Object System.Collections.ArrayList BeforeCopyStaticFile = New-Object System.Collections.ArrayList AfterCopyStaticFile = New-Object System.Collections.ArrayList ResolveDocumentOutputPath = New-Object System.Collections.ArrayList ResolveStaticFileOutputPath = New-Object System.Collections.ArrayList } } } # Resolve the configured plugins directory path. function resolveHydePluginDirectory { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context ) $pluginsDirectoryName = if ($Context.Settings.ContainsKey('plugins_dir') -and $Context.Settings.plugins_dir) { $Context.Settings.plugins_dir } else { '_plugins' } return (Join-Path -Path $Context.SourcePath -ChildPath $pluginsDirectoryName) } # Read plugin names from configuration settings. function getHydePluginConfigurationNames { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context ) $configuredNames = New-Object System.Collections.ArrayList foreach ($settingName in @('plugins', 'gems')) { if (-not $Context.Settings.ContainsKey($settingName) -or -not $Context.Settings[$settingName]) { continue } foreach ($pluginName in $Context.Settings[$settingName]) { if ([string]::IsNullOrWhiteSpace([string]$pluginName)) { continue } [void]$configuredNames.Add([string]$pluginName) } } return @($configuredNames.ToArray()) } # Normalize configured plugin names into lookup candidates. function getHydePluginCandidateNames { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [string]$PluginName ) # Hyde accepts Jekyll-style plugin names and translates them into simpler Hyde-friendly names. $candidates = New-Object System.Collections.ArrayList [void]$candidates.Add($PluginName) if ($PluginName.StartsWith('jekyll-', [System.StringComparison]::OrdinalIgnoreCase)) { $trimmedName = $PluginName.Substring(7) if (-not [string]::IsNullOrWhiteSpace($trimmedName)) { [void]$candidates.Add($trimmedName) [void]$candidates.Add("hyde-$trimmedName") } } return @($candidates | Select-Object -Unique) } # Gather the whitelist of plugins permitted under safe mode. function getHydeWhitelistedPluginNames { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context ) if (-not $Context.Settings.ContainsKey('whitelist') -or -not $Context.Settings.whitelist) { return @() } return @($Context.Settings.whitelist | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ }) } # Resolve configured plugin names to actual script files on disk. function resolveHydePluginFiles { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context ) # Hyde plugins are PowerShell scripts under the configured plugin directory. $pluginsDirectory = resolveHydePluginDirectory -Context $Context $builtInPluginsDirectory = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Plugins' if (-not (Test-Path -LiteralPath $pluginsDirectory -PathType Container)) { Write-Verbose "No plugin directory found at '$pluginsDirectory'." $pluginFiles = @() } else { $pluginFiles = @(Get-ChildItem -LiteralPath $pluginsDirectory -Filter '*.ps1' -File | Sort-Object BaseName) } $configuredNames = @(getHydePluginConfigurationNames -Context $Context) if ($configuredNames.Count -eq 0) { return $pluginFiles } $pluginMap = @{} foreach ($pluginFile in $pluginFiles) { $pluginMap[$pluginFile.BaseName] = $pluginFile } $builtInPluginMap = @{} if (Test-Path -LiteralPath $builtInPluginsDirectory -PathType Container) { foreach ($pluginFile in Get-ChildItem -LiteralPath $builtInPluginsDirectory -Filter '*.ps1' -File | Sort-Object BaseName) { $builtInPluginMap[$pluginFile.BaseName] = $pluginFile } } $resolvedFiles = New-Object System.Collections.ArrayList $seenPluginPaths = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase) foreach ($pluginName in $configuredNames) { $resolvedPluginFile = $null foreach ($candidateName in getHydePluginCandidateNames -PluginName $pluginName) { if ($pluginMap.ContainsKey($candidateName)) { $resolvedPluginFile = $pluginMap[$candidateName] break } if ($builtInPluginMap.ContainsKey($candidateName)) { $resolvedPluginFile = $builtInPluginMap[$candidateName] break } } if ($null -eq $resolvedPluginFile) { throw "Could not locate plugin '$pluginName' in '$pluginsDirectory' or '$builtInPluginsDirectory'." } if ($seenPluginPaths.Add($resolvedPluginFile.FullName)) { [void]$resolvedFiles.Add($resolvedPluginFile) } } return @($resolvedFiles.ToArray()) } # Decide whether a plugin is allowed under current safety settings. function testHydePluginAllowed { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context, [Parameter(Mandatory = $true)] [string]$PluginName ) # Safe mode only permits plugins that are explicitly whitelisted. $safeModeEnabled = $Context.Settings.ContainsKey('safe') -and [bool]$Context.Settings.safe if (-not $safeModeEnabled) { return $true } $whitelist = @(getHydeWhitelistedPluginNames -Context $Context) return ($whitelist -contains $PluginName) } # Register a plugin descriptor and hook handlers in the registry. function registerHydePluginDescriptor { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$Descriptor, [Parameter(Mandatory = $true)] [HydeBuildContext]$Context, [Parameter(Mandatory = $true)] [string]$PluginPath ) $pluginName = if ($Descriptor.ContainsKey('Name') -and $Descriptor.Name) { [string]$Descriptor.Name } else { [System.IO.Path]::GetFileNameWithoutExtension($PluginPath) } if ($Descriptor.ContainsKey('Hooks') -and $Descriptor.Hooks) { foreach ($hookName in $Descriptor.Hooks.Keys) { if (-not $Context.PluginRegistry.Hooks.ContainsKey($hookName)) { throw "Plugin '$pluginName' uses unsupported Hyde hook '$hookName'." } $handler = $Descriptor.Hooks[$hookName] if ($handler -isnot [scriptblock]) { throw "Plugin '$pluginName' hook '$hookName' must be a script block." } [void]$Context.PluginRegistry.Hooks[$hookName].Add([pscustomobject]@{ Plugin = $pluginName Path = $PluginPath Action = $handler }) } } if ($Descriptor.ContainsKey('Liquid') -and $Descriptor.Liquid) { $liquidDefinition = $Descriptor.Liquid $dialectDefinitions = @{} # A simple top-level Tags/Filters block targets the JekyllLiquid dialect by default. if (($liquidDefinition.ContainsKey('Tags') -and $liquidDefinition.Tags) -or ($liquidDefinition.ContainsKey('Filters') -and $liquidDefinition.Filters)) { $dialectDefinitions['JekyllLiquid'] = @{ Tags = if ($liquidDefinition.ContainsKey('Tags')) { $liquidDefinition.Tags } else { @{} } Filters = if ($liquidDefinition.ContainsKey('Filters')) { $liquidDefinition.Filters } else { @{} } } } if ($liquidDefinition.ContainsKey('Dialects') -and $liquidDefinition.Dialects) { foreach ($dialectName in $liquidDefinition.Dialects.Keys) { $dialectDefinitions[$dialectName] = $liquidDefinition.Dialects[$dialectName] } } foreach ($dialectName in $dialectDefinitions.Keys) { $dialectDefinition = $dialectDefinitions[$dialectName] if ($dialectDefinition.Tags) { foreach ($tagName in $dialectDefinition.Tags.Keys) { Register-LiquidTag -Registry $Context.LiquidRegistry -Dialect $dialectName -Name ([string]$tagName) -Handler $dialectDefinition.Tags[$tagName] } } if ($dialectDefinition.Filters) { foreach ($filterName in $dialectDefinition.Filters.Keys) { Register-LiquidFilter -Registry $Context.LiquidRegistry -Dialect $dialectName -Name ([string]$filterName) -Handler $dialectDefinition.Filters[$filterName] } } } } [void]$Context.LoadedPlugins.Add([pscustomobject]@{ Name = $pluginName Path = $PluginPath }) } # Load plugin scripts, register hooks, and track loaded plugins. function importHydePlugins { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context ) $pluginFiles = @(resolveHydePluginFiles -Context $Context) if ($pluginFiles.Count -eq 0) { Write-Verbose 'No Hyde plugins selected for loading.' return } foreach ($pluginFile in $pluginFiles) { if (-not (testHydePluginAllowed -Context $Context -PluginName $pluginFile.BaseName)) { Write-Verbose "Skipping plugin '$($pluginFile.BaseName)' because safe mode requires a whitelist entry." continue } Write-Verbose "Loading plugin '$($pluginFile.BaseName)' from '$($pluginFile.FullName)'." try { $descriptor = & $pluginFile.FullName -Context $Context } catch { throw "Could not load plugin '$($pluginFile.BaseName)'. $($_.Exception.Message)" } if ($null -eq $descriptor) { Write-Verbose "Plugin '$($pluginFile.BaseName)' did not register any behavior." continue } if ($descriptor -isnot [hashtable]) { throw "Plugin '$($pluginFile.BaseName)' must return a hashtable descriptor." } registerHydePluginDescriptor -Descriptor $descriptor -Context $Context -PluginPath $pluginFile.FullName } } # Execute a plugin hook for all loaded plugins with optional arguments. function invokeHydePluginHook { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context, [Parameter(Mandatory = $true)] [string]$HookName, [hashtable]$Arguments = @{} ) if (-not $Context.PluginRegistry.Hooks.ContainsKey($HookName)) { throw "Hyde hook '$HookName' is not supported." } foreach ($hook in $Context.PluginRegistry.Hooks[$HookName]) { try { & $hook.Action $Arguments } catch { throw "Plugin '$($hook.Plugin)' failed while running hook '$HookName'. $($_.Exception.Message)" } } } # Resolve a value through plugins that implement a value override hook. function resolveHydePluginValue { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [HydeBuildContext]$Context, [Parameter(Mandatory = $true)] [string]$HookName, [Parameter(Mandatory = $true)] $CurrentValue, [hashtable]$Arguments = @{} ) if (-not $Context.PluginRegistry.Hooks.ContainsKey($HookName)) { throw "Hyde hook '$HookName' is not supported." } $resolvedValue = $CurrentValue foreach ($hook in $Context.PluginRegistry.Hooks[$HookName]) { try { $nextValue = & $hook.Action $resolvedValue $Arguments } catch { throw "Plugin '$($hook.Plugin)' failed while running hook '$HookName'. $($_.Exception.Message)" } if ($null -ne $nextValue) { $resolvedValue = $nextValue } } return $resolvedValue } |