2024-08-07 17:55:23 +02:00
<#
. DESCRIPTION
2026-02-22 23:19:43 +00:00
Generates Hugo markdown docs from config / tweaks . json and config / feature . json .
Run by the GitHub Actions docs workflow before Hugo build .
2024-08-07 17:55:23 +02:00
#>
function Update-Progress {
param (
[ Parameter ( Mandatory , position = 0 ) ]
[ string ] $StatusMessage ,
[ Parameter ( Mandatory , position = 1 ) ]
[ ValidateRange ( 0 , 100 ) ]
2026-02-12 20:33:11 +00:00
[ int ] $Percent
2024-08-07 17:55:23 +02:00
)
2026-02-12 20:33:11 +00:00
Write-Progress -Activity " Generating Dev Docs " -Status $StatusMessage -PercentComplete $Percent
2024-08-07 17:55:23 +02:00
}
2026-02-12 20:33:11 +00:00
function Get-RawJsonBlock {
2026-02-22 23:19:43 +00:00
# Returns the raw JSON text and 1-based start line for an item, excluding the "link" property.
2024-08-07 17:55:23 +02:00
param (
[ Parameter ( Mandatory ) ]
2026-02-12 20:33:11 +00:00
[ string ] $ItemName ,
[ Parameter ( Mandatory ) ]
[ AllowEmptyString ( ) ]
[ string[] ] $JsonLines
2024-08-07 17:55:23 +02:00
)
2026-02-12 20:33:11 +00:00
$escapedName = [ regex ] :: Escape ( $ItemName )
2026-02-22 23:19:43 +00:00
$startIndex = -1
2026-02-12 20:33:11 +00:00
$startIndent = " "
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
for ( $i = 0 ; $i -lt $JsonLines . Count ; $i + + ) {
if ( $JsonLines [ $i ] -match " ^(\s*) `" $escapedName `" \s*:\s*\{ " ) {
2026-02-22 23:19:43 +00:00
$startIndex = $i
2026-02-12 20:33:11 +00:00
$startIndent = $matches [ 1 ]
break
2024-08-07 17:55:23 +02:00
}
}
2026-02-12 20:33:11 +00:00
if ( $startIndex -eq -1 ) {
Write-Warning " Could not find ' $ItemName ' in JSON "
return $null
}
$escapedIndent = [ regex ] :: Escape ( $startIndent )
2026-02-22 23:19:43 +00:00
$endIndex = -1
2026-02-12 20:33:11 +00:00
for ( $i = ( $startIndex + 1 ) ; $i -lt $JsonLines . Count ; $i + + ) {
if ( $JsonLines [ $i ] -match " ^ $escapedIndent \} " ) {
$endIndex = $i
break
}
}
if ( $endIndex -eq -1 ) {
Write-Warning " Could not find closing brace for ' $ItemName ' "
return $null
}
2026-02-22 23:19:43 +00:00
# Strip trailing "link" property and blank lines before returning
2026-02-12 20:33:11 +00:00
$lastContentIndex = $endIndex - 1
while ( $lastContentIndex -gt $startIndex ) {
$trimmed = $JsonLines [ $lastContentIndex ] . Trim ( )
if ( $trimmed -eq " " -or $trimmed -match '^"link"' ) {
$lastContentIndex - -
} else {
break
}
}
return @ {
2026-02-22 23:19:43 +00:00
LineNumber = $startIndex + 1
RawText = ( $JsonLines [ $startIndex . . $lastContentIndex ] -join " `r `n " )
2024-08-07 17:55:23 +02:00
}
}
2026-02-12 20:33:11 +00:00
function Get-ButtonFunctionMapping {
2026-02-22 23:19:43 +00:00
# Parses Invoke-WPFButton.ps1 and returns a hashtable of button name -> function name.
2024-08-07 17:55:23 +02:00
param (
2026-02-12 20:33:11 +00:00
[ Parameter ( Mandatory ) ]
[ string ] $ButtonFilePath
2024-08-07 17:55:23 +02:00
)
2026-02-12 20:33:11 +00:00
$mapping = @ { }
2026-02-22 23:19:43 +00:00
foreach ( $line in ( Get-Content -Path $ButtonFilePath ) ) {
2026-02-12 20:33:11 +00:00
if ( $line -match '^\s*"(\w+)"\s*\{(Invoke-\w+)' ) {
$mapping [ $matches [ 1 ] ] = $matches [ 2 ]
2024-08-07 17:55:23 +02:00
}
}
2026-02-12 20:33:11 +00:00
return $mapping
2024-08-07 17:55:23 +02:00
}
function Add-LinkAttributeToJson {
2026-02-22 23:19:43 +00:00
# Updates only the "link" property for each entry in a JSON config file.
# Reads via ConvertFrom-Json for metadata, then edits lines directly to avoid reformatting.
2024-08-07 17:55:23 +02:00
param (
2026-02-12 20:33:11 +00:00
[ Parameter ( Mandatory ) ]
[ string ] $JsonFilePath ,
[ Parameter ( Mandatory ) ]
[ string ] $UrlPrefix ,
[ Parameter ( Mandatory ) ]
[ string ] $ItemNameToCut
2024-08-07 17:55:23 +02:00
)
2026-02-22 23:19:43 +00:00
$jsonData = Get-Content -Path $JsonFilePath -Raw | ConvertFrom-Json
$lines = [ System.Collections.Generic.List[string] ] ( Get-Content -Path $JsonFilePath )
2024-08-07 17:55:23 +02:00
foreach ( $item in $jsonData . PSObject . Properties ) {
2026-02-22 23:19:43 +00:00
$itemName = $item . Name
$category = $item . Value . category -replace '[^a-zA-Z0-9]' , '-'
2026-02-12 20:33:11 +00:00
$displayName = $itemName -replace $ItemNameToCut , ''
2026-02-22 23:19:43 +00:00
$newLink = " $UrlPrefix / $( $category . ToLower ( ) ) / $( $displayName . ToLower ( ) ) "
$escapedName = [ regex ] :: Escape ( $itemName )
2024-08-07 17:55:23 +02:00
2026-02-22 23:19:43 +00:00
# Find item start line
$startIdx = -1
for ( $i = 0 ; $i -lt $lines . Count ; $i + + ) {
if ( $lines [ $i ] -match " ^\s* `" $escapedName `" \s*:\s*\{ " ) {
$startIdx = $i
break
}
}
if ( $startIdx -eq -1 ) { continue }
# Derive indentation: propIndent is one level deeper than the item start.
# Used to target only top-level properties and skip nested object braces.
$null = $lines [ $startIdx ] -match '^(\s*)'
$propIndent = $matches [ 1 ] + ' '
$propIndentLen = $propIndent . Length
$escapedPropIndent = [ regex ] :: Escape ( $propIndent )
# Scan forward: update existing "link" or find the closing brace to insert one.
# Closing brace is matched by indent <= propIndentLen to handle inconsistent formatting.
$linkUpdated = $false
$closeBraceIdx = -1
for ( $j = $startIdx + 1 ; $j -lt $lines . Count ; $j + + ) {
if ( $lines [ $j ] -match " ^ $escapedPropIndent `" link `" \s*: " ) {
$lines [ $j ] = $lines [ $j ] -replace '"link"\s*:\s*"[^"]*"' , " `" link `" : `" $newLink `" "
$linkUpdated = $true
break
}
if ( $lines [ $j ] -match '^\s*\}' ) {
$null = $lines [ $j ] -match '^(\s*)'
if ( $matches [ 1 ] . Length -le $propIndentLen ) {
$closeBraceIdx = $j
break
}
}
}
if ( -not $linkUpdated -and $closeBraceIdx -ne -1 ) {
# Insert "link" before the closing brace
$prevPropIdx = $closeBraceIdx - 1
while ( $prevPropIdx -gt $startIdx -and $lines [ $prevPropIdx ] . Trim ( ) -eq '' ) { $prevPropIdx - - }
if ( $lines [ $prevPropIdx ] -notmatch ',\s*$' ) {
$lines [ $prevPropIdx ] = $lines [ $prevPropIdx ] . TrimEnd ( ) + ','
}
$lines . Insert ( $closeBraceIdx , " $propIndent `" link `" : `" $newLink `" " )
}
2024-08-07 17:55:23 +02:00
}
2026-02-22 23:19:43 +00:00
Set-Content -Path $JsonFilePath -Value $lines -Encoding utf8
2024-08-07 17:55:23 +02:00
}
2026-02-12 20:33:11 +00:00
# ==============================================================================
2026-02-22 23:19:43 +00:00
# Main
2026-02-12 20:33:11 +00:00
# ==============================================================================
$scriptDir = if ( $PSScriptRoot ) { $PSScriptRoot } else { ( Get-Location ) . Path }
2026-02-22 23:19:43 +00:00
$repoRoot = Resolve-Path " $scriptDir /.. "
2026-02-12 20:33:11 +00:00
2026-02-22 23:19:43 +00:00
$tweaksJsonPath = " $repoRoot /config/tweaks.json "
$featuresJsonPath = " $repoRoot /config/feature.json "
$tweaksOutputDir = " $repoRoot /docs/content/dev/tweaks "
$featuresOutputDir = " $repoRoot /docs/content/dev/features "
2026-02-12 20:33:11 +00:00
$publicFunctionsDir = " $repoRoot /functions/public "
$privateFunctionsDir = " $repoRoot /functions/private "
$itemnametocut = 'WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?'
2026-02-22 23:19:43 +00:00
$baseUrl = " https://winutil.christitus.com "
2026-02-12 20:33:11 +00:00
2026-02-22 23:19:43 +00:00
# Categories with generated docs
2026-02-12 20:33:11 +00:00
$documentedCategories = @ (
" Essential Tweaks " ,
" z__Advanced Tweaks - CAUTION " ,
" Customize Preferences " ,
" Performance Plans " ,
" Features " ,
" Fixes " ,
2026-02-22 23:19:43 +00:00
" Legacy Windows Panels " ,
" Powershell Profile Powershell 7+ Only " ,
" Remote Access "
2026-02-12 20:33:11 +00:00
)
2026-02-22 23:19:43 +00:00
# Categories where Button entries embed a PS function instead of raw JSON
$functionEmbedCategories = @ (
" Fixes " ,
" Powershell Profile Powershell 7+ Only " ,
" Remote Access "
)
2026-02-12 20:33:11 +00:00
2024-08-07 17:55:23 +02:00
Update-Progress " Loading JSON files " 10
2026-02-22 23:19:43 +00:00
$tweaks = Get-Content -Path $tweaksJsonPath -Raw | ConvertFrom-Json
2026-02-12 20:33:11 +00:00
$features = Get-Content -Path $featuresJsonPath -Raw | ConvertFrom-Json
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
Update-Progress " Loading function files " 20
$functionFiles = @ { }
2026-02-22 23:19:43 +00:00
Get-ChildItem -Path $publicFunctionsDir -Filter * . ps1 | ForEach-Object {
$functionFiles [ $_ . BaseName ] = @ { Content = ( Get-Content -Path $_ . FullName -Raw ) . TrimEnd ( ) ; RelativePath = " functions/public/ $( $_ . Name ) " }
2024-08-07 17:55:23 +02:00
}
2026-02-12 20:33:11 +00:00
Get-ChildItem -Path $privateFunctionsDir -Filter * . ps1 | ForEach-Object {
2026-02-22 23:19:43 +00:00
$functionFiles [ $_ . BaseName ] = @ { Content = ( Get-Content -Path $_ . FullName -Raw ) . TrimEnd ( ) ; RelativePath = " functions/private/ $( $_ . Name ) " }
2024-08-07 17:55:23 +02:00
}
2026-02-12 20:33:11 +00:00
Update-Progress " Building button-to-function mapping " 30
$buttonFunctionMap = Get-ButtonFunctionMapping -ButtonFilePath " $publicFunctionsDir /Invoke-WPFButton.ps1 "
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
Update-Progress " Updating documentation links in JSON " 40
Add-LinkAttributeToJson -JsonFilePath $tweaksJsonPath -UrlPrefix " $baseUrl /dev/tweaks " -ItemNameToCut $itemnametocut
2026-02-22 23:19:43 +00:00
Add-LinkAttributeToJson -JsonFilePath $featuresJsonPath -UrlPrefix " $baseUrl /dev/features " -ItemNameToCut $itemnametocut
2024-08-07 17:55:23 +02:00
2026-02-22 23:19:43 +00:00
# Reload lines after link update so line numbers in docs are accurate
2026-02-12 20:33:11 +00:00
$tweaksLines = Get-Content -Path $tweaksJsonPath
$featuresLines = Get-Content -Path $featuresJsonPath
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
# ==============================================================================
2026-02-22 23:19:43 +00:00
# Clean up old generated .md files (preserve _index.md)
2026-02-12 20:33:11 +00:00
# ==============================================================================
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
Update-Progress " Cleaning up old generated docs " 45
foreach ( $dir in @ ( $tweaksOutputDir , $featuresOutputDir ) ) {
Get-ChildItem -Path $dir -Recurse -Filter * . md | Where-Object {
$_ . Name -ne " _index.md "
} | Remove-Item -Force
2024-08-07 17:55:23 +02:00
}
2026-02-12 20:33:11 +00:00
# ==============================================================================
# Generate Tweak Documentation
# ==============================================================================
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
Update-Progress " Generating tweak documentation " 50
2024-08-07 17:55:23 +02:00
2026-02-22 23:19:43 +00:00
$tweakNames = $tweaks . PSObject . Properties . Name
2026-02-12 20:33:11 +00:00
$totalTweaks = $tweakNames . Count
2026-02-22 23:19:43 +00:00
$tweakCount = 0
2024-08-07 17:55:23 +02:00
2026-02-12 20:33:11 +00:00
foreach ( $itemName in $tweakNames ) {
$item = $tweaks . $itemName
$tweakCount + +
if ( $item . category -notin $documentedCategories ) { continue }
$category = $item . category -replace '[^a-zA-Z0-9]' , '-'
$displayName = $itemName -replace $itemnametocut , ''
$categoryDir = " $tweaksOutputDir / $category "
$filename = " $categoryDir / $displayName .md "
2026-02-22 23:19:43 +00:00
if ( -Not ( Test-Path -Path $categoryDir ) ) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
2026-02-12 20:33:11 +00:00
2026-02-22 23:19:43 +00:00
$title = $item . Content -replace '"' , '\"'
2026-02-17 18:28:36 +00:00
$content = " --- `r `n title: `" $title `" `r `n description: `" `" `r `n --- `r `n `r `n "
2026-02-12 20:33:11 +00:00
if ( $item . Type -eq " Button " ) {
$funcName = $buttonFunctionMap [ $itemName ]
if ( $funcName -and $functionFiles . ContainsKey ( $funcName ) ) {
2026-02-22 23:19:43 +00:00
$func = $functionFiles [ $funcName ]
2026-02-12 20:33:11 +00:00
$content + = " `` `` `` powershell {filename= `" $( $func . RelativePath ) `" ,linenos=inline,linenostart=1} `r `n "
$content + = $func . Content + " `r `n "
$content + = " `` `` `` `r `n "
}
} else {
$jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $tweaksLines
if ( $jsonBlock ) {
$content + = " `` `` `` json {filename= `" config/tweaks.json `" ,linenos=inline,linenostart= $( $jsonBlock . LineNumber ) } `r `n "
$content + = $jsonBlock . RawText + " `r `n "
$content + = " `` `` `` `r `n "
}
if ( $item . registry ) {
$content + = " `r `n ## Registry Changes `r `n `r `n "
$content + = " Applications and System Components store and retrieve configuration data to modify windows settings, so we can use the registry to change many settings in one place. `r `n `r `n "
$content + = " You can find information about the registry on [Wikipedia](https://www.wikiwand.com/en/Windows_Registry) and [Microsoft's Website](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry). `r `n "
}
}
Set-Content -Path $filename -Value $content -Encoding utf8 -NoNewline
2026-02-22 23:19:43 +00:00
$percent = [ Math ] :: Min ( 70 , 50 + [ int ] ( ( $tweakCount / $totalTweaks ) * 20 ) )
2026-02-12 20:33:11 +00:00
Update-Progress " Generating tweak documentation ( $tweakCount / $totalTweaks ) " $percent
}
# ==============================================================================
# Generate Feature Documentation
# ==============================================================================
Update-Progress " Generating feature documentation " 70
2026-02-22 23:19:43 +00:00
$featureNames = $features . PSObject . Properties . Name
2026-02-12 20:33:11 +00:00
$totalFeatures = $featureNames . Count
2026-02-22 23:19:43 +00:00
$featureCount = 0
2026-02-12 20:33:11 +00:00
foreach ( $itemName in $featureNames ) {
$item = $features . $itemName
$featureCount + +
if ( $item . category -notin $documentedCategories ) { continue }
if ( $itemName -eq " WPFFeatureInstall " ) { continue }
$category = $item . category -replace '[^a-zA-Z0-9]' , '-'
$displayName = $itemName -replace $itemnametocut , ''
$categoryDir = " $featuresOutputDir / $category "
$filename = " $categoryDir / $displayName .md "
2026-02-22 23:19:43 +00:00
if ( -Not ( Test-Path -Path $categoryDir ) ) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
2026-02-12 20:33:11 +00:00
2026-02-22 23:19:43 +00:00
$title = $item . Content -replace '"' , '\"'
2026-02-17 18:28:36 +00:00
$content = " --- `r `n title: `" $title `" `r `n description: `" `" `r `n --- `r `n `r `n "
2026-02-12 20:33:11 +00:00
2026-02-22 23:19:43 +00:00
if ( $item . category -in $functionEmbedCategories ) {
$funcName = if ( $item . function ) { $item . function } else { $buttonFunctionMap [ $itemName ] }
2026-02-12 20:33:11 +00:00
if ( $funcName -and $functionFiles . ContainsKey ( $funcName ) ) {
2026-02-22 23:19:43 +00:00
$func = $functionFiles [ $funcName ]
2026-02-12 20:33:11 +00:00
$content + = " `` `` `` powershell {filename= `" $( $func . RelativePath ) `" ,linenos=inline,linenostart=1} `r `n "
$content + = $func . Content + " `r `n "
$content + = " `` `` `` `r `n "
}
} else {
$jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $featuresLines
if ( $jsonBlock ) {
$content + = " `` `` `` json {filename= `" config/feature.json `" ,linenos=inline,linenostart= $( $jsonBlock . LineNumber ) } `r `n "
$content + = $jsonBlock . RawText + " `r `n "
$content + = " `` `` `` `r `n "
}
}
Set-Content -Path $filename -Value $content -Encoding utf8 -NoNewline
2026-02-22 23:19:43 +00:00
$percent = [ Math ] :: Min ( 90 , 70 + [ int ] ( ( $featureCount / $totalFeatures ) * 20 ) )
2026-02-12 20:33:11 +00:00
Update-Progress " Generating feature documentation ( $featureCount / $totalFeatures ) " $percent
}
2024-08-07 17:55:23 +02:00
Update-Progress " Process Completed " 100
2026-02-12 20:33:11 +00:00
Write-Host " Documentation generation complete. "