Skip to content

Commit 1eab0db

Browse files
committed
Update devdocs-generator.ps1
1 parent f515cb4 commit 1eab0db

File tree

1 file changed

+94
-100
lines changed

1 file changed

+94
-100
lines changed

tools/devdocs-generator.ps1

Lines changed: 94 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
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

97
function Update-Progress {
@@ -18,11 +16,7 @@ function Update-Progress {
1816
}
1917

2018
function 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

8776
function 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

10792
function 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-
180202
Update-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-
186206
Update-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
}
194211
Get-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-
203215
Update-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-
208218
Update-Progress "Updating documentation links in JSON" 40
209219
Add-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

220230
Update-Progress "Cleaning up old generated docs" 45
@@ -230,9 +240,9 @@ foreach ($dir in @($tweaksOutputDir, $featuresOutputDir)) {
230240

231241
Update-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

237247
foreach ($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`ntitle: `"$title`"`r`ndescription: `"`"`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

295296
Update-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

301302
foreach ($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`ntitle: `"$title`"`r`ndescription: `"`"`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

Comments
 (0)