11<#
22 . DESCRIPTION
3- Generates Hugo-compatible markdown files for the development documentation
4- based on config/tweaks.json and config/feature.json.
5- Each JSON entry gets its own .md file with the raw JSON snippet or PowerShell function embedded.
6- Called by the GitHub Actions docs workflow before Hugo build.
3+ Generates Hugo markdown docs from config/tweaks.json and config/feature.json.
4+ Run by the GitHub Actions docs workflow before Hugo build.
75#>
86
97function Update-Progress {
@@ -18,11 +16,7 @@ function Update-Progress {
1816}
1917
2018function Get-RawJsonBlock {
21- <#
22- . SYNOPSIS
23- Extracts the raw JSON text for a specific item from a JSON file's lines.
24- Returns the line number and raw text, excluding the "link" property and closing brace.
25- #>
19+ # Returns the raw JSON text and 1-based start line for an item, excluding the "link" property.
2620 param (
2721 [Parameter (Mandatory )]
2822 [string ]$ItemName ,
@@ -32,13 +26,12 @@ function Get-RawJsonBlock {
3226 )
3327
3428 $escapedName = [regex ]::Escape($ItemName )
35- $startIndex = -1
29+ $startIndex = -1
3630 $startIndent = " "
3731
38- # Find the line containing "ItemName": {
3932 for ($i = 0 ; $i -lt $JsonLines.Count ; $i ++ ) {
4033 if ($JsonLines [$i ] -match " ^(\s*)`" $escapedName `" \s*:\s*\{" ) {
41- $startIndex = $i
34+ $startIndex = $i
4235 $startIndent = $matches [1 ]
4336 break
4437 }
@@ -49,9 +42,8 @@ function Get-RawJsonBlock {
4942 return $null
5043 }
5144
52- # Find the closing } at the same indentation level
5345 $escapedIndent = [regex ]::Escape($startIndent )
54- $endIndex = -1
46+ $endIndex = -1
5547 for ($i = ($startIndex + 1 ); $i -lt $JsonLines.Count ; $i ++ ) {
5648 if ($JsonLines [$i ] -match " ^$escapedIndent \}" ) {
5749 $endIndex = $i
@@ -64,7 +56,7 @@ function Get-RawJsonBlock {
6456 return $null
6557 }
6658
67- # Walk backwards from closing brace to exclude "link" property and empty lines
59+ # Strip trailing "link" property and blank lines before returning
6860 $lastContentIndex = $endIndex - 1
6961 while ($lastContentIndex -gt $startIndex ) {
7062 $trimmed = $JsonLines [$lastContentIndex ].Trim()
@@ -75,28 +67,21 @@ function Get-RawJsonBlock {
7567 }
7668 }
7769
78- $rawLines = $JsonLines [$startIndex .. $lastContentIndex ]
79- $rawText = $rawLines -join " `r`n "
80-
8170 return @ {
82- LineNumber = $startIndex + 1 # 1-based
83- RawText = $rawText
71+ LineNumber = $startIndex + 1
72+ RawText = ( $JsonLines [ $startIndex .. $lastContentIndex ] -join " `r`n " )
8473 }
8574}
8675
8776function Get-ButtonFunctionMapping {
88- <#
89- . SYNOPSIS
90- Parses Invoke-WPFButton.ps1 to build a hashtable mapping button names to function names.
91- #>
77+ # Parses Invoke-WPFButton.ps1 and returns a hashtable of button name -> function name.
9278 param (
9379 [Parameter (Mandatory )]
9480 [string ]$ButtonFilePath
9581 )
9682
9783 $mapping = @ {}
98- $lines = Get-Content - Path $ButtonFilePath
99- foreach ($line in $lines ) {
84+ foreach ($line in (Get-Content - Path $ButtonFilePath )) {
10085 if ($line -match ' ^\s*"(\w+)"\s*\{(Invoke-\w+)' ) {
10186 $mapping [$matches [1 ]] = $matches [2 ]
10287 }
@@ -105,11 +90,8 @@ function Get-ButtonFunctionMapping {
10590}
10691
10792function Add-LinkAttributeToJson {
108- <#
109- . SYNOPSIS
110- Updates the "link" property on each top-level entry in a JSON config file
111- to point to the corresponding documentation page URL.
112- #>
93+ # Updates only the "link" property for each entry in a JSON config file.
94+ # Reads via ConvertFrom-Json for metadata, then edits lines directly to avoid reformatting.
11395 param (
11496 [Parameter (Mandatory )]
11597 [string ]$JsonFilePath ,
@@ -119,43 +101,85 @@ function Add-LinkAttributeToJson {
119101 [string ]$ItemNameToCut
120102 )
121103
122- $jsonText = Get-Content - Path $JsonFilePath - Raw
123- $jsonData = $jsonText | ConvertFrom-Json
104+ $jsonData = Get-Content - Path $JsonFilePath - Raw | ConvertFrom-Json
105+ $lines = [ System.Collections.Generic.List [ string ]]( Get-Content - Path $JsonFilePath )
124106
125107 foreach ($item in $jsonData.PSObject.Properties ) {
126- $itemName = $item.Name
127- $itemDetails = $item.Value
128- $category = $itemDetails.category -replace ' [^a-zA-Z0-9]' , ' -'
108+ $itemName = $item.Name
109+ $category = $item.Value.category -replace ' [^a-zA-Z0-9]' , ' -'
129110 $displayName = $itemName -replace $ItemNameToCut , ' '
130- $docLink = " $UrlPrefix /$ ( $category.ToLower ()) /$ ( $displayName.ToLower ()) "
111+ $newLink = " $UrlPrefix /$ ( $category.ToLower ()) /$ ( $displayName.ToLower ()) "
112+ $escapedName = [regex ]::Escape($itemName )
113+
114+ # Find item start line
115+ $startIdx = -1
116+ for ($i = 0 ; $i -lt $lines.Count ; $i ++ ) {
117+ if ($lines [$i ] -match " ^\s*`" $escapedName `" \s*:\s*\{" ) {
118+ $startIdx = $i
119+ break
120+ }
121+ }
122+ if ($startIdx -eq -1 ) { continue }
123+
124+ # Derive indentation: propIndent is one level deeper than the item start.
125+ # Used to target only top-level properties and skip nested object braces.
126+ $null = $lines [$startIdx ] -match ' ^(\s*)'
127+ $propIndent = $matches [1 ] + ' '
128+ $propIndentLen = $propIndent.Length
129+ $escapedPropIndent = [regex ]::Escape($propIndent )
130+
131+ # Scan forward: update existing "link" or find the closing brace to insert one.
132+ # Closing brace is matched by indent <= propIndentLen to handle inconsistent formatting.
133+ $linkUpdated = $false
134+ $closeBraceIdx = -1
135+ for ($j = $startIdx + 1 ; $j -lt $lines.Count ; $j ++ ) {
136+ if ($lines [$j ] -match " ^$escapedPropIndent `" link`" \s*:" ) {
137+ $lines [$j ] = $lines [$j ] -replace ' "link"\s*:\s*"[^"]*"' , " `" link`" : `" $newLink `" "
138+ $linkUpdated = $true
139+ break
140+ }
141+ if ($lines [$j ] -match ' ^\s*\}' ) {
142+ $null = $lines [$j ] -match ' ^(\s*)'
143+ if ($matches [1 ].Length -le $propIndentLen ) {
144+ $closeBraceIdx = $j
145+ break
146+ }
147+ }
148+ }
149+
150+ if (-not $linkUpdated -and $closeBraceIdx -ne -1 ) {
151+ # Insert "link" before the closing brace
152+ $prevPropIdx = $closeBraceIdx - 1
153+ while ($prevPropIdx -gt $startIdx -and $lines [$prevPropIdx ].Trim() -eq ' ' ) { $prevPropIdx -- }
131154
132- $itemDetails | Add-Member - NotePropertyName " link" - NotePropertyValue $docLink - Force
155+ if ($lines [$prevPropIdx ] -notmatch ' ,\s*$' ) {
156+ $lines [$prevPropIdx ] = $lines [$prevPropIdx ].TrimEnd() + ' ,'
157+ }
158+ $lines.Insert ($closeBraceIdx , " $propIndent `" link`" : `" $newLink `" " )
159+ }
133160 }
134161
135- $jsonText = ($jsonData | ConvertTo-Json - Depth 100 ).replace(' \n' , " `n " ).replace(' \r' , " `r " )
136- Set-Content - Path $JsonFilePath - Value $jsonText - Encoding utf8
162+ Set-Content - Path $JsonFilePath - Value $lines - Encoding utf8
137163}
138164
139165# ==============================================================================
140- # Main Script
166+ # Main
141167# ==============================================================================
142168
143- # Use PSScriptRoot if available (running as a script file), otherwise assume CWD is tools/
144169$scriptDir = if ($PSScriptRoot ) { $PSScriptRoot } else { (Get-Location ).Path }
145- $repoRoot = Resolve-Path " $scriptDir /.."
170+ $repoRoot = Resolve-Path " $scriptDir /.."
146171
147- # Paths
148- $tweaksJsonPath = " $repoRoot /config/tweaks.json"
149- $featuresJsonPath = " $repoRoot /config/feature.json"
150- $tweaksOutputDir = " $repoRoot /docs/content/dev/tweaks"
151- $featuresOutputDir = " $repoRoot /docs/content/dev/features"
172+ $tweaksJsonPath = " $repoRoot /config/tweaks.json"
173+ $featuresJsonPath = " $repoRoot /config/feature.json"
174+ $tweaksOutputDir = " $repoRoot /docs/content/dev/tweaks"
175+ $featuresOutputDir = " $repoRoot /docs/content/dev/features"
152176$publicFunctionsDir = " $repoRoot /functions/public"
153177$privateFunctionsDir = " $repoRoot /functions/private"
154178
155179$itemnametocut = ' WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?'
156- $baseUrl = " https://winutil.christitus.com"
180+ $baseUrl = " https://winutil.christitus.com"
157181
158- # Categories that should have generated documentation
182+ # Categories with generated docs
159183$documentedCategories = @ (
160184 " Essential Tweaks" ,
161185 " z__Advanced Tweaks - CAUTION" ,
@@ -168,53 +192,39 @@ $documentedCategories = @(
168192 " Remote Access"
169193)
170194
171- # Categories whose Button entries should embed the PowerShell function (not raw JSON)
195+ # Categories where Button entries embed a PS function instead of raw JSON
172196$functionEmbedCategories = @ (
173197 " Fixes" ,
174198 " Powershell Profile Powershell 7+ Only" ,
175199 " Remote Access"
176200)
177201
178- # --- Load data ---
179-
180202Update-Progress " Loading JSON files" 10
181- $tweaks = Get-Content - Path $tweaksJsonPath - Raw | ConvertFrom-Json
203+ $tweaks = Get-Content - Path $tweaksJsonPath - Raw | ConvertFrom-Json
182204$features = Get-Content - Path $featuresJsonPath - Raw | ConvertFrom-Json
183205
184- # --- Load function files (content + relative path) ---
185-
186206Update-Progress " Loading function files" 20
187207$functionFiles = @ {}
188- Get-ChildItem - Path $publicFunctionsDir - Filter * .ps1 | ForEach-Object {
189- $functionFiles [$_.BaseName ] = @ {
190- Content = (Get-Content - Path $_.FullName - Raw).TrimEnd()
191- RelativePath = " functions/public/$ ( $_.Name ) "
192- }
208+ Get-ChildItem - Path $publicFunctionsDir - Filter * .ps1 | ForEach-Object {
209+ $functionFiles [$_.BaseName ] = @ { Content = (Get-Content - Path $_.FullName - Raw).TrimEnd(); RelativePath = " functions/public/$ ( $_.Name ) " }
193210}
194211Get-ChildItem - Path $privateFunctionsDir - Filter * .ps1 | ForEach-Object {
195- $functionFiles [$_.BaseName ] = @ {
196- Content = (Get-Content - Path $_.FullName - Raw).TrimEnd()
197- RelativePath = " functions/private/$ ( $_.Name ) "
198- }
212+ $functionFiles [$_.BaseName ] = @ { Content = (Get-Content - Path $_.FullName - Raw).TrimEnd(); RelativePath = " functions/private/$ ( $_.Name ) " }
199213}
200214
201- # --- Build button-to-function mapping ---
202-
203215Update-Progress " Building button-to-function mapping" 30
204216$buttonFunctionMap = Get-ButtonFunctionMapping - ButtonFilePath " $publicFunctionsDir /Invoke-WPFButton.ps1"
205217
206- # --- Update link attributes in JSON files ---
207-
208218Update-Progress " Updating documentation links in JSON" 40
209219Add-LinkAttributeToJson - JsonFilePath $tweaksJsonPath - UrlPrefix " $baseUrl /dev/tweaks" - ItemNameToCut $itemnametocut
210- Add-LinkAttributeToJson - JsonFilePath $featuresJsonPath - UrlPrefix " $baseUrl /dev/features" - ItemNameToCut $itemnametocut
220+ Add-LinkAttributeToJson - JsonFilePath $featuresJsonPath - UrlPrefix " $baseUrl /dev/features" - ItemNameToCut $itemnametocut
211221
212- # Reload JSON lines after link update ( so line numbers are accurate)
222+ # Reload lines after link update so line numbers in docs are accurate
213223$tweaksLines = Get-Content - Path $tweaksJsonPath
214224$featuresLines = Get-Content - Path $featuresJsonPath
215225
216226# ==============================================================================
217- # Clean up old generated .md files (keep _index.md)
227+ # Clean up old generated .md files (preserve _index.md)
218228# ==============================================================================
219229
220230Update-Progress " Cleaning up old generated docs" 45
@@ -230,9 +240,9 @@ foreach ($dir in @($tweaksOutputDir, $featuresOutputDir)) {
230240
231241Update-Progress " Generating tweak documentation" 50
232242
233- $tweakNames = $tweaks.PSObject.Properties.Name
243+ $tweakNames = $tweaks.PSObject.Properties.Name
234244$totalTweaks = $tweakNames.Count
235- $tweakCount = 0
245+ $tweakCount = 0
236246
237247foreach ($itemName in $tweakNames ) {
238248 $item = $tweaks .$itemName
@@ -245,46 +255,37 @@ foreach ($itemName in $tweakNames) {
245255 $categoryDir = " $tweaksOutputDir /$category "
246256 $filename = " $categoryDir /$displayName .md"
247257
248- if (-Not (Test-Path - Path $categoryDir )) {
249- New-Item - ItemType Directory - Path $categoryDir | Out-Null
250- }
258+ if (-Not (Test-Path - Path $categoryDir )) { New-Item - ItemType Directory - Path $categoryDir | Out-Null }
251259
252- # Hugo frontmatter
253- $title = $item.Content -replace ' "' , ' \"'
260+ $title = $item.Content -replace ' "' , ' \"'
254261 $content = " ---`r`n title: `" $title `"`r`n description: `"`"`r`n ---`r`n`r`n "
255262
256263 if ($item.Type -eq " Button" ) {
257- # Button-type tweak: embed the mapped PowerShell function
258264 $funcName = $buttonFunctionMap [$itemName ]
259265 if ($funcName -and $functionFiles.ContainsKey ($funcName )) {
260- $func = $functionFiles [$funcName ]
266+ $func = $functionFiles [$funcName ]
261267 $content += " `````` powershell {filename=`" $ ( $func.RelativePath ) `" ,linenos=inline,linenostart=1}`r`n "
262268 $content += $func.Content + " `r`n "
263269 $content += " ```````r`n "
264270 }
265271 } else {
266- # Standard tweak: embed raw JSON block
267272 $jsonBlock = Get-RawJsonBlock - ItemName $itemName - JsonLines $tweaksLines
268273 if ($jsonBlock ) {
269274 $content += " `````` json {filename=`" config/tweaks.json`" ,linenos=inline,linenostart=$ ( $jsonBlock.LineNumber ) }`r`n "
270275 $content += $jsonBlock.RawText + " `r`n "
271276 $content += " ```````r`n "
272277 }
273278
274- # Registry Changes section
275279 if ($item.registry ) {
276280 $content += " `r`n ## Registry Changes`r`n`r`n "
277281 $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 "
278282 $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 "
279283 }
280-
281-
282284 }
283285
284286 Set-Content - Path $filename - Value $content - Encoding utf8 - NoNewline
285287
286- $percent = 50 + [int ](($tweakCount / $totalTweaks ) * 20 )
287- if ($percent -gt 70 ) { $percent = 70 }
288+ $percent = [Math ]::Min(70 , 50 + [int ](($tweakCount / $totalTweaks ) * 20 ))
288289 Update-Progress " Generating tweak documentation ($tweakCount /$totalTweaks )" $percent
289290}
290291
@@ -294,42 +295,36 @@ foreach ($itemName in $tweakNames) {
294295
295296Update-Progress " Generating feature documentation" 70
296297
297- $featureNames = $features.PSObject.Properties.Name
298+ $featureNames = $features.PSObject.Properties.Name
298299$totalFeatures = $featureNames.Count
299- $featureCount = 0
300+ $featureCount = 0
300301
301302foreach ($itemName in $featureNames ) {
302303 $item = $features .$itemName
303304 $featureCount ++
304305
305306 if ($item.category -notin $documentedCategories ) { continue }
306-
307- # Skip pure UI buttons that don't need docs
308307 if ($itemName -eq " WPFFeatureInstall" ) { continue }
309308
310309 $category = $item.category -replace ' [^a-zA-Z0-9]' , ' -'
311310 $displayName = $itemName -replace $itemnametocut , ' '
312311 $categoryDir = " $featuresOutputDir /$category "
313312 $filename = " $categoryDir /$displayName .md"
314313
315- if (-Not (Test-Path - Path $categoryDir )) {
316- New-Item - ItemType Directory - Path $categoryDir | Out-Null
317- }
314+ if (-Not (Test-Path - Path $categoryDir )) { New-Item - ItemType Directory - Path $categoryDir | Out-Null }
318315
319- $title = $item.Content -replace ' "' , ' \"'
316+ $title = $item.Content -replace ' "' , ' \"'
320317 $content = " ---`r`n title: `" $title `"`r`n description: `"`"`r`n ---`r`n`r`n "
321318
322319 if ($item.category -in $functionEmbedCategories ) {
323- # Button-driven categories: embed the PowerShell function file
324320 $funcName = if ($item.function ) { $item.function } else { $buttonFunctionMap [$itemName ] }
325321 if ($funcName -and $functionFiles.ContainsKey ($funcName )) {
326- $func = $functionFiles [$funcName ]
322+ $func = $functionFiles [$funcName ]
327323 $content += " `````` powershell {filename=`" $ ( $func.RelativePath ) `" ,linenos=inline,linenostart=1}`r`n "
328324 $content += $func.Content + " `r`n "
329325 $content += " ```````r`n "
330326 }
331327 } else {
332- # Features category: embed raw JSON block
333328 $jsonBlock = Get-RawJsonBlock - ItemName $itemName - JsonLines $featuresLines
334329 if ($jsonBlock ) {
335330 $content += " `````` json {filename=`" config/feature.json`" ,linenos=inline,linenostart=$ ( $jsonBlock.LineNumber ) }`r`n "
@@ -340,8 +335,7 @@ foreach ($itemName in $featureNames) {
340335
341336 Set-Content - Path $filename - Value $content - Encoding utf8 - NoNewline
342337
343- $percent = 70 + [int ](($featureCount / $totalFeatures ) * 20 )
344- if ($percent -gt 90 ) { $percent = 90 }
338+ $percent = [Math ]::Min(90 , 70 + [int ](($featureCount / $totalFeatures ) * 20 ))
345339 Update-Progress " Generating feature documentation ($featureCount /$totalFeatures )" $percent
346340}
347341
0 commit comments