Files
winutil/tools/devdocs-generator.ps1
T
Sean (ANGRYxScotsman)andGitHub e96f990c60 Fix alignment issues, update links, and enhance documentation (#4898)
* Fix the same arrow-icon alignment issue on the "Read the Docs" button

Same root cause as the hero/final-CTA buttons: the label was plain
markdown text, which MDX wrapped in a <p>, and the icon was missing
the size/nudge applied elsewhere. Match it to the other two buttons.

* Rename known-issues page slug to knownissues

Update the sidebar config, nav dropdown, and every internal link that
pointed at /known-issues/ to match.

* Fix image paths in root-level markdown to match the new docs site

The Hugo docs site (docs/assets/images/...) has been replaced by the
Astro site (docs/src/assets/...); update CONTRIBUTING.md,
READMEITALIAN.md, and README.md to point at the new asset locations.

* Remove unused houston.webp asset

Leftover Starlight default-theme asset with no references anywhere
in the docs site.

* Fix docs site reference in generator docs

Updates `tools/devdocs-generator.md` to describe link generation correctly: links now point to the Astro/Starlight documentation site instead of the old Hugo site.

* Point docs homepage CTAs to guides hub

Updates the docs landing page CTA links to use `/guides/` instead of `/guides/getting-started/` in the hero action and both “Get Started”/“Read the Docs” buttons, routing users to the guides index.

* Add reusable YouTube embeds to docs guides

Introduces a new `YouTubeEmbed.astro` component for responsive 16:9 video embeds using the privacy-enhanced `youtube-nocookie.com` domain. The User Guide index and Tweaks guide now import and use this component instead of plain YouTube links, and the component includes styling that overrides Starlight’s iframe reset so embedded videos render at the correct size.

* Fix YouTube embed iframe height via not-content

Add the `not-content` class to the embed wrapper so Starlight's markdown reset no longer overrides `iframe { height: auto }`. Drop the `@layer starlight.core` wrapper and `!important` overrides that were compensating for the same issue.

* Clarify docs comments across Astro components

Adds and refines inline comments in the docs site config, custom Starlight components, and theme styles to better explain layout structure, navigation dropdown behavior, hero composition, and theme initialization. Also normalizes a few multi-line comments into single-line form for readability without changing functionality.

* Set secondary docs button background color

Update `.wu-btn-secondary` in the docs theme to use `var(--sl-color-gray-6)` instead of a transparent background, improving button visibility and contrast while keeping existing border and hover behavior.

* Move User Guides into docs dropdown

Updates the docs header navigation so the “User Guides” link is grouped under the existing “Documentation” dropdown instead of appearing as a separate top-level nav item. This keeps related documentation links together and simplifies the top navigation.

* Fix hero margin and center trust section

Remove the automatic 1.5rem top margin Starlight adds between the hero and the content column. Also flex-center the trust section so its content is properly aligned.

* Disable edit links in generated dev docs

Update `tools/devdocs-generator.ps1` to include `editUrl: false` in the frontmatter for generated tweak and feature docs pages. This prevents Starlight from showing edit links on these auto-generated pages.

* Move docs update PRs to pre-release flow

The docs Pages workflow is simplified to only build and deploy the docs site: it now triggers only on docs changes, drops extra permissions/default shell settings, and removes the dev-docs generation + auto-PR steps. The pre-release workflow now owns that automation by expanding its PR step to include generated code-reference docs alongside JSON link updates, with updated commit/title/body text and a documentation label.

* Shorten generated docs source note

Simplify the autogenerated DevDocs notice so generated pages only warn against direct edits while still linking back to the source file.

* Improve docs header dropdown behavior

Reworked the header nav dropdown triggers to use native `<details>/<summary>` instead of buttons, then updated menu visibility logic to use `details[open]` for no-JS support on touch and keyboard while preserving hover behavior. Also renamed the “Developer Docs” link to “Code Reference” and tightened related CSS comments.

* Refresh contribution and docs workflow docs

Updated contributor-facing docs to use consistent Markdown admonition blocks, cleaned Mermaid fence formatting, and fixed minor wording/capitalization in contribution steps. Clarified `devdocs-generator` behavior so workflow ownership is explicit: generation now documented as part of `pre-release.yaml` (via docs-update PR), while `docs.yaml` is documented as build/deploy only.

* Fix contributing guide grammar

Correct the "it's" to "its" typo in both contributing guides so the pull request guidance is consistent across the GitHub and docs versions.
2026-08-03 10:23:31 -05:00

438 lines
17 KiB
PowerShell

<#
.DESCRIPTION
Generates Astro/Starlight markdown docs from config/tweaks.json and config/feature.json.
Run by the GitHub Actions docs workflow before the Astro build.
#>
function Update-Progress {
param (
[Parameter(Mandatory, position=0)]
[string]$StatusMessage,
[Parameter(Mandatory, position=1)]
[ValidateRange(0,100)]
[int]$Percent
)
Write-Progress -Activity "Generating Dev Docs" -Status $StatusMessage -PercentComplete $Percent
}
function Get-RawJsonBlock {
# Returns the raw JSON text and 1-based start line for an item, excluding the "link" property.
param (
[Parameter(Mandatory)]
[string]$ItemName,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string[]]$JsonLines
)
$escapedName = [regex]::Escape($ItemName)
$startIndex = -1
for ($i = 0; $i -lt $JsonLines.Count; $i++) {
if ($JsonLines[$i] -match "^(\s*)`"$escapedName`"\s*:\s*\{") {
$startIndex = $i
break
}
}
if ($startIndex -eq -1) {
Write-Warning "Could not find '$ItemName' in JSON"
return $null
}
# Use brace-depth tracking to find the closing brace
$endIndex = -1
$depth = 1 # We're starting inside the opening brace
for ($i = ($startIndex + 1); $i -lt $JsonLines.Count; $i++) {
$line = $JsonLines[$i]
# Count braces in this line, ignoring those in strings
$inString = $false
$chars = $line.ToCharArray()
for ($k = 0; $k -lt $chars.Count; $k++) {
if ($chars[$k] -eq '"' -and ($k -eq 0 -or $chars[$k-1] -ne '\')) {
$inString = -not $inString
} elseif (-not $inString) {
if ($chars[$k] -eq '{') { $depth++ }
elseif ($chars[$k] -eq '}') { $depth-- }
}
}
# Found the closing brace of the item
if ($depth -eq 0) {
$endIndex = $i
break
}
}
if ($endIndex -eq -1) {
Write-Warning "Could not find closing brace for '$ItemName'"
return $null
}
# Strip trailing "link" property and blank lines before returning
$lastContentIndex = $endIndex - 1
while ($lastContentIndex -gt $startIndex) {
$trimmed = $JsonLines[$lastContentIndex].Trim()
if ($trimmed -eq "" -or $trimmed -match '^"link"') {
$lastContentIndex--
} else {
break
}
}
# Include the item's own closing brace, stripped of the trailing comma that
# only exists to separate it from the next sibling in the parent object.
$closingLine = $JsonLines[$endIndex] -replace ',\s*$', ''
return @{
LineNumber = $startIndex + 1
RawText = (($JsonLines[$startIndex..$lastContentIndex] + $closingLine) -join "`r`n")
}
}
function Get-GeneratedFromNote {
# Builds the Starlight ":::note" aside pointing back at the source file for an entry.
param (
[Parameter(Mandatory)]
[string]$SourceRelativePath
)
$githubUrl = "https://github.com/ChrisTitusTech/winutil/blob/main/$SourceRelativePath"
$note = ":::note`r`n"
$note += "This page is generated from [``$SourceRelativePath``]($githubUrl). Do not edit this page directly.`r`n"
$note += ":::`r`n`r`n"
return $note
}
function Get-ButtonFunctionMapping {
# Parses Invoke-WPFButton.ps1 and returns a hashtable of button name -> function name.
param (
[Parameter(Mandatory)]
[string]$ButtonFilePath
)
$mapping = @{}
foreach ($line in (Get-Content -Path $ButtonFilePath)) {
if ($line -match '^\s*"(\w+)"\s*\{(Invoke-\w+)') {
$mapping[$matches[1]] = $matches[2]
}
}
return $mapping
}
function Add-LinkAttributeToJson {
# 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.
param (
[Parameter(Mandatory)]
[string]$JsonFilePath,
[Parameter(Mandatory)]
[string]$UrlPrefix,
[Parameter(Mandatory)]
[string]$ItemNameToCut
)
$jsonData = Get-Content -Path $JsonFilePath -Raw | ConvertFrom-Json
$lines = [System.Collections.Generic.List[string]](Get-Content -Path $JsonFilePath)
foreach ($item in $jsonData.PSObject.Properties) {
$itemName = $item.Name
$category = $item.Value.category -replace '[^a-zA-Z0-9]', '-'
$displayName = $itemName -replace $ItemNameToCut, ''
$newLink = "$UrlPrefix/$($category.ToLower())/$($displayName.ToLower())"
$escapedName = [regex]::Escape($itemName)
# 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 used by top-level properties in the item.
# Prefer existing property indentation to avoid inheriting bad key indentation.
$null = $lines[$startIdx] -match '^(\s*)'
$propIndent = $matches[1] + ' '
$depthProbe = 1
for ($p = $startIdx + 1; $p -lt $lines.Count; $p++) {
$probeLine = $lines[$p]
if ($depthProbe -eq 1 -and $probeLine -match '^(\s*)"[^"]+"\s*:') {
$propIndent = $matches[1]
break
}
$inStringProbe = $false
$probeChars = $probeLine.ToCharArray()
for ($q = 0; $q -lt $probeChars.Count; $q++) {
if ($probeChars[$q] -eq '"' -and ($q -eq 0 -or $probeChars[$q-1] -ne '\')) {
$inStringProbe = -not $inStringProbe
} elseif (-not $inStringProbe) {
if ($probeChars[$q] -eq '{') { $depthProbe++ }
elseif ($probeChars[$q] -eq '}') { $depthProbe-- }
}
}
if ($depthProbe -eq 0) { break }
}
# Scan forward: remove any existing "link" property and find the closing brace.
# Use brace-depth tracking to properly handle nested structures like arrays.
$closeBraceIdx = -1
$depth = 1 # We're starting inside the opening brace of the item
$linesToRemove = @()
for ($j = $startIdx + 1; $j -lt $lines.Count; $j++) {
$line = $lines[$j]
# Check for existing "link" property at top-level (depth 1 before processing braces on this line)
# Match at any indentation level (user may have manually changed indentation)
if ($depth -eq 1 -and $line -match '^\s*"link"\s*:') {
# Mark this line for removal
$linesToRemove += $j
}
# Count braces in this line, ignoring those in strings
$inString = $false
$chars = $line.ToCharArray()
for ($k = 0; $k -lt $chars.Count; $k++) {
if ($chars[$k] -eq '"' -and ($k -eq 0 -or $chars[$k-1] -ne '\')) {
$inString = -not $inString
} elseif (-not $inString) {
if ($chars[$k] -eq '{') { $depth++ }
elseif ($chars[$k] -eq '}') { $depth-- }
}
}
# Found the closing brace of the item
if ($depth -eq 0) {
$closeBraceIdx = $j
break
}
}
# Remove old "link" lines in reverse order to preserve indices
foreach ($idx in ($linesToRemove | Sort-Object -Descending)) {
# If the line before had a trailing comma (from the link property), remove it
if ($idx -gt $startIdx) {
$prevLine = $lines[$idx - 1]
if ($prevLine -match ',\s*$' -and $lines[$idx].Trim() -match '^}') {
$lines[$idx - 1] = $prevLine -replace ',\s*$', ''
}
}
$lines.RemoveAt($idx)
if ($idx -lt $closeBraceIdx) {
$closeBraceIdx--
}
}
# Now insert "link" before the closing brace (consistent position for all items)
if ($closeBraceIdx -ne -1) {
$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`"")
}
}
Set-Content -Path $JsonFilePath -Value $lines -Encoding utf8
}
# ==============================================================================
# Main
# ==============================================================================
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$repoRoot = Resolve-Path "$scriptDir/.."
$tweaksJsonPath = "$repoRoot/config/tweaks.json"
$featuresJsonPath = "$repoRoot/config/feature.json"
$tweaksOutputDir = "$repoRoot/docs/src/content/docs/code-reference/tweaks"
$featuresOutputDir = "$repoRoot/docs/src/content/docs/code-reference/features"
$publicFunctionsDir = "$repoRoot/functions/public"
$privateFunctionsDir = "$repoRoot/functions/private"
$itemnametocut = 'WPF(WinUtil|Toggle|Features?|Tweaks?|Panel|Fix(es)?)?'
$baseUrl = "https://winutil.christitus.com"
# Categories with generated docs
$documentedCategories = @(
"Essential Tweaks",
"z__Advanced Tweaks - CAUTION",
"Customize Preferences",
"Performance Plans",
"Features",
"Fixes",
"Legacy Windows Panels",
"Powershell Profile Powershell 7+ Only",
"Remote Access"
)
# Categories where Button entries embed a PS function instead of raw JSON
$functionEmbedCategories = @(
"Fixes",
"Powershell Profile Powershell 7+ Only",
"Remote Access"
)
Update-Progress "Loading JSON files" 10
$tweaks = Get-Content -Path $tweaksJsonPath -Raw | ConvertFrom-Json
$features = Get-Content -Path $featuresJsonPath -Raw | ConvertFrom-Json
Update-Progress "Loading function files" 20
$functionFiles = @{}
Get-ChildItem -Path $publicFunctionsDir -Filter *.ps1 | ForEach-Object {
$functionFiles[$_.BaseName] = @{ Content = (Get-Content -Path $_.FullName -Raw).TrimEnd(); RelativePath = "functions/public/$($_.Name)" }
}
Get-ChildItem -Path $privateFunctionsDir -Filter *.ps1 | ForEach-Object {
$functionFiles[$_.BaseName] = @{ Content = (Get-Content -Path $_.FullName -Raw).TrimEnd(); RelativePath = "functions/private/$($_.Name)" }
}
Update-Progress "Building button-to-function mapping" 30
$buttonFunctionMap = Get-ButtonFunctionMapping -ButtonFilePath "$publicFunctionsDir/Invoke-WPFButton.ps1"
Update-Progress "Updating documentation links in JSON" 40
Add-LinkAttributeToJson -JsonFilePath $tweaksJsonPath -UrlPrefix "$baseUrl/code-reference/tweaks" -ItemNameToCut $itemnametocut
Add-LinkAttributeToJson -JsonFilePath $featuresJsonPath -UrlPrefix "$baseUrl/code-reference/features" -ItemNameToCut $itemnametocut
# Reload lines after link update so line numbers in docs are accurate
$tweaksLines = Get-Content -Path $tweaksJsonPath
$featuresLines = Get-Content -Path $featuresJsonPath
# ==============================================================================
# Clean up old generated .mdx files
# ==============================================================================
Update-Progress "Cleaning up old generated docs" 45
foreach ($dir in @($tweaksOutputDir, $featuresOutputDir)) {
if (-Not (Test-Path -Path $dir)) { continue }
Get-ChildItem -Path $dir -Recurse -Filter *.mdx | Where-Object {
# No category index.mdx pages exist yet. If one is added later as a
# category landing page, uncomment this line to keep it from being wiped.
# $_.Name -ne "index.mdx"
$true
} | Remove-Item -Force
}
# ==============================================================================
# Generate Tweak Documentation
# ==============================================================================
Update-Progress "Generating tweak documentation" 50
$tweakNames = $tweaks.PSObject.Properties.Name
$totalTweaks = $tweakNames.Count
$tweakCount = 0
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.mdx"
if (-Not (Test-Path -Path $categoryDir)) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
$title = $item.Content -replace '"', '\"'
$description = if ($item.Description) { $item.Description -replace '"', '\"' } else { '' }
$content = "---`r`ntitle: `"$title`"`r`ndescription: `"$description`"`r`neditUrl: false`r`n---`r`n`r`n"
if ($item.Type -eq "Button") {
$funcName = $buttonFunctionMap[$itemName]
if ($funcName -and $functionFiles.ContainsKey($funcName)) {
$func = $functionFiles[$funcName]
$content += Get-GeneratedFromNote -SourceRelativePath $func.RelativePath
$content += "``````powershell title=`"$($func.RelativePath)`"`r`n"
$content += $func.Content + "`r`n"
$content += "```````r`n"
}
} else {
$jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $tweaksLines
if ($jsonBlock) {
$content += Get-GeneratedFromNote -SourceRelativePath "config/tweaks.json"
$content += "``````json title=`"config/tweaks.json`"`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://en.wikipedia.org/wiki/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
$percent = [Math]::Min(70, 50 + [int](($tweakCount / $totalTweaks) * 20))
Update-Progress "Generating tweak documentation ($tweakCount/$totalTweaks)" $percent
}
# ==============================================================================
# Generate Feature Documentation
# ==============================================================================
Update-Progress "Generating feature documentation" 70
$featureNames = $features.PSObject.Properties.Name
$totalFeatures = $featureNames.Count
$featureCount = 0
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.mdx"
if (-Not (Test-Path -Path $categoryDir)) { New-Item -ItemType Directory -Path $categoryDir | Out-Null }
$title = $item.Content -replace '"', '\"'
$description = if ($item.Description) { $item.Description -replace '"', '\"' } else { '' }
$content = "---`r`ntitle: `"$title`"`r`ndescription: `"$description`"`r`neditUrl: false`r`n---`r`n`r`n"
if ($item.category -in $functionEmbedCategories) {
$funcName = if ($item.function) { $item.function } else { $buttonFunctionMap[$itemName] }
if ($funcName -and $functionFiles.ContainsKey($funcName)) {
$func = $functionFiles[$funcName]
$content += Get-GeneratedFromNote -SourceRelativePath $func.RelativePath
$content += "``````powershell title=`"$($func.RelativePath)`"`r`n"
$content += $func.Content + "`r`n"
$content += "```````r`n"
}
} else {
$jsonBlock = Get-RawJsonBlock -ItemName $itemName -JsonLines $featuresLines
if ($jsonBlock) {
$content += Get-GeneratedFromNote -SourceRelativePath "config/feature.json"
$content += "``````json title=`"config/feature.json`"`r`n"
$content += $jsonBlock.RawText + "`r`n"
$content += "```````r`n"
}
}
Set-Content -Path $filename -Value $content -Encoding utf8 -NoNewline
$percent = [Math]::Min(90, 70 + [int](($featureCount / $totalFeatures) * 20))
Update-Progress "Generating feature documentation ($featureCount/$totalFeatures)" $percent
}
Update-Progress "Process Completed" 100
Write-Host "Documentation generation complete."