mirror of
https://github.com/OrcaSlicer/OrcaSlicer_WIKI.git
synced 2026-05-17 00:25:45 +03:00
792 lines
27 KiB
PowerShell
792 lines
27 KiB
PowerShell
param(
|
|
[string]$TabCppPath = "https://github.com/OrcaSlicer/OrcaSlicer/blob/main/src/slic3r/GUI/Tab.cpp",
|
|
[string]$PrintConfigCppPath = "https://github.com/OrcaSlicer/OrcaSlicer/blob/main/src/libslic3r/PrintConfig.cpp",
|
|
[string]$WikiRoot = $PSScriptRoot,
|
|
[switch]$DryRun
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function ConvertTo-AnchorSlug {
|
|
param([string]$Heading)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Heading)) {
|
|
return ""
|
|
}
|
|
|
|
$text = $Heading.Trim().ToLowerInvariant()
|
|
# Keep letters, digits, spaces and hyphens; strip punctuation to mimic markdown anchors.
|
|
$text = [regex]::Replace($text, "[^a-z0-9\s-]", "")
|
|
$text = [regex]::Replace($text, "[\s-]+", "-")
|
|
return $text.Trim('-')
|
|
}
|
|
|
|
function Find-HeadingLineIndex {
|
|
param(
|
|
[string[]]$Lines,
|
|
[string]$Anchor
|
|
)
|
|
|
|
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
|
$line = $Lines[$i]
|
|
if ($line -match '^(#{1,6})\s+(.+?)\s*$') {
|
|
$headingText = $Matches[2].Trim()
|
|
$headingText = $headingText -replace '\s+#+$', ''
|
|
$slug = ConvertTo-AnchorSlug -Heading $headingText
|
|
if ($slug -eq $Anchor) {
|
|
return $i
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
function Find-FirstHeadingLineIndex {
|
|
param([string[]]$Lines)
|
|
|
|
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
|
if ($Lines[$i] -match '^(#{1,6})\s+(.+?)\s*$') {
|
|
return $i
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
function ConvertTo-DocFileKey {
|
|
param([string]$RefFile)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($RefFile)) {
|
|
return ""
|
|
}
|
|
|
|
$normalizedRef = $RefFile.Trim() -replace '\\', '/'
|
|
$leaf = ($normalizedRef -split '/')[(-1)]
|
|
if ([string]::IsNullOrWhiteSpace($leaf)) {
|
|
return ""
|
|
}
|
|
|
|
if ([System.IO.Path]::GetExtension($leaf).ToLowerInvariant() -eq '.md') {
|
|
return [System.IO.Path]::GetFileNameWithoutExtension($leaf)
|
|
}
|
|
|
|
return $leaf
|
|
}
|
|
|
|
function Get-CppSourceContent {
|
|
param(
|
|
[string]$Source,
|
|
[string]$Description
|
|
)
|
|
|
|
if ($Source -match '^https?://') {
|
|
$url = $Source
|
|
# Convert GitHub blob URL to a raw URL so the script receives C++ source text.
|
|
if ($url -match '^https://github\.com/([^/]+)/([^/]+)/blob/(.+)$') {
|
|
$owner = $Matches[1]
|
|
$repo = $Matches[2]
|
|
$path = $Matches[3]
|
|
$url = "https://raw.githubusercontent.com/$owner/$repo/$path"
|
|
}
|
|
|
|
try {
|
|
return (Invoke-WebRequest -Uri $url -UseBasicParsing).Content
|
|
}
|
|
catch {
|
|
throw "Failed to download $Description from URL: $Source"
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $Source)) {
|
|
throw "$Description not found: $Source"
|
|
}
|
|
|
|
return Get-Content -LiteralPath $Source -Raw
|
|
}
|
|
|
|
function Get-StringVectors {
|
|
param([string]$Content)
|
|
|
|
$result = @{}
|
|
$vectorPattern = '(?s)const\s+std::vector\s*<\s*std::string\s*>\s*(?<name>\w+)\s*\{(?<vals>.*?)\}\s*;'
|
|
$vectorMatches = [regex]::Matches($Content, $vectorPattern)
|
|
|
|
foreach ($vm in $vectorMatches) {
|
|
$name = $vm.Groups['name'].Value
|
|
$valsRaw = $vm.Groups['vals'].Value
|
|
$vals = [regex]::Matches($valsRaw, '"(?<v>[^"]+)"') | ForEach-Object { $_.Groups['v'].Value }
|
|
if ($vals.Count -gt 0) {
|
|
$result[$name] = @($vals)
|
|
}
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-OptionModesByVariable {
|
|
param([string]$Content)
|
|
|
|
$result = @{}
|
|
$lines = $Content -split "`r?`n"
|
|
$currentVariable = $null
|
|
$addPattern = '(?:\b\w+\s*=\s*)*def\s*=\s*this->add(?:_nullable)?\(\s*"(?<variable>[^"]+)"\s*,'
|
|
$modePattern = 'def->mode\s*=\s*(?<mode>comSimple|comAdvanced|comExpert|comDevelop)\s*;'
|
|
|
|
foreach ($line in $lines) {
|
|
if ($line -match $addPattern) {
|
|
$capturedVariable = [string]$Matches['variable']
|
|
if ([string]::IsNullOrWhiteSpace($capturedVariable)) {
|
|
$currentVariable = $null
|
|
}
|
|
else {
|
|
$currentVariable = $capturedVariable.Trim()
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($currentVariable) -and $line -match $modePattern) {
|
|
$result[$currentVariable] = [string]$Matches['mode']
|
|
}
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Convert-ConfigOptionModeToLabel {
|
|
param([string]$Mode)
|
|
|
|
switch ($Mode) {
|
|
'comSimple' { return 'Simple' }
|
|
'comAdvanced' { return 'Advanced' }
|
|
'comExpert' { return 'Expert' }
|
|
'comDevelop' { return 'Developer' }
|
|
default { return $null }
|
|
}
|
|
}
|
|
|
|
function Remove-StaleSectionMetadata {
|
|
param(
|
|
[System.Collections.Generic.List[string]]$Buffer,
|
|
[hashtable]$ExpectedAnchors,
|
|
[string]$MetadataLinePattern
|
|
)
|
|
|
|
$sectionsPruned = 0
|
|
$headings = New-Object System.Collections.Generic.List[object]
|
|
|
|
for ($i = 0; $i -lt $Buffer.Count; $i++) {
|
|
$line = $Buffer[$i]
|
|
if ($line -match '^(#{1,6})\s+(.+?)\s*$') {
|
|
$headingText = $Matches[2].Trim()
|
|
$headingText = $headingText -replace '\s+#+$', ''
|
|
$slug = ConvertTo-AnchorSlug -Heading $headingText
|
|
if (-not [string]::IsNullOrWhiteSpace($slug)) {
|
|
$headings.Add([PSCustomObject]@{
|
|
Index = $i
|
|
Anchor = $slug
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($headings.Count -eq 0) {
|
|
return 0
|
|
}
|
|
|
|
for ($h = $headings.Count - 1; $h -ge 0; $h--) {
|
|
$sectionStart = $headings[$h].Index
|
|
$sectionAnchor = $headings[$h].Anchor
|
|
|
|
if ($ExpectedAnchors.ContainsKey($sectionAnchor)) {
|
|
continue
|
|
}
|
|
|
|
$sectionEnd = if ($h -lt ($headings.Count - 1)) { $headings[$h + 1].Index - 1 } else { $Buffer.Count - 1 }
|
|
if ($sectionEnd -le $sectionStart) {
|
|
continue
|
|
}
|
|
|
|
$metadataIndexes = New-Object System.Collections.Generic.List[int]
|
|
for ($k = $sectionStart + 1; $k -le $sectionEnd; $k++) {
|
|
if ($Buffer[$k] -match $MetadataLinePattern) {
|
|
$metadataIndexes.Add($k)
|
|
}
|
|
}
|
|
|
|
if ($metadataIndexes.Count -eq 0) {
|
|
continue
|
|
}
|
|
|
|
for ($r = $metadataIndexes.Count - 1; $r -ge 0; $r--) {
|
|
$Buffer.RemoveAt($metadataIndexes[$r])
|
|
}
|
|
|
|
$sectionsPruned++
|
|
}
|
|
|
|
return $sectionsPruned
|
|
}
|
|
|
|
$syncProgressActivity = "sync-tab-options-to-wiki.ps1"
|
|
$syncStageTotal = 8
|
|
|
|
function Set-SyncStage {
|
|
param(
|
|
[int]$Step,
|
|
[string]$Status
|
|
)
|
|
|
|
$percent = if ($syncStageTotal -gt 0) {
|
|
[Math]::Max(0, [Math]::Min(100, [int](($Step / [double]$syncStageTotal) * 100)))
|
|
}
|
|
else {
|
|
0
|
|
}
|
|
|
|
Write-Progress -Id 1 -Activity $syncProgressActivity -Status $Status -PercentComplete $percent
|
|
}
|
|
|
|
function Set-SyncDetail {
|
|
param(
|
|
[string]$Status,
|
|
[int]$Current,
|
|
[int]$Total
|
|
)
|
|
|
|
$percent = if ($Total -gt 0) {
|
|
[Math]::Max(0, [Math]::Min(100, [int](($Current / [double]$Total) * 100)))
|
|
}
|
|
else {
|
|
0
|
|
}
|
|
|
|
Write-Progress -Id 2 -ParentId 1 -Activity "Processing markdown mappings" -Status $Status -PercentComplete $percent
|
|
}
|
|
|
|
function Complete-SyncProgress {
|
|
Write-Progress -Id 2 -Activity "Processing markdown mappings" -Completed
|
|
Write-Progress -Id 1 -Activity $syncProgressActivity -Completed
|
|
}
|
|
|
|
Set-SyncStage -Step 0 -Status "Validating input paths"
|
|
|
|
if (-not (Test-Path -LiteralPath $WikiRoot)) {
|
|
Complete-SyncProgress
|
|
throw "Wiki root not found: $WikiRoot"
|
|
}
|
|
|
|
Set-SyncStage -Step 1 -Status "Loading Tab.cpp content"
|
|
$tabContent = Get-CppSourceContent -Source $TabCppPath -Description "Tab.cpp"
|
|
|
|
Set-SyncStage -Step 2 -Status "Parsing option mappings from Tab.cpp"
|
|
$patternSingle = 'append_single_option_line\(\s*"(?<variable>[^"]+)"\s*,\s*"(?<ref>[^"]+)"(?:\s*,\s*(?<indexer>[^\)]+))?\s*\)'
|
|
$patternOption = 'append_option_line\(\s*[^,]+\s*,\s*"(?<variable>[^"]+)"\s*,\s*"(?<ref>[^"]+)"(?:\s*,\s*(?<indexer>[^\)]+))?\s*\)'
|
|
$patternAppendLineBlock = '(?s)(?<obj>\w+)\.label_path\s*=\s*"(?<ref>[^"]+)"\s*;(?<body>.*?)(?:\w+->)?append_line\(\s*\k<obj>\s*\)\s*;'
|
|
$patternAppendLineAssignedBlock = '(?s)(?<obj>\w+)\s*=\s*\{.*?\}\s*;(?<body>.*?)(?:\w+->)?append_line\(\s*\k<obj>\s*\)\s*;'
|
|
$patternForBlock = '(?s)for\s*\(\s*const\s+std::string\s*&\s*(?<iter>\w+)\s*:\s*(?<collection>\w+)\s*\)\s*\{(?<body>.*?)\}'
|
|
|
|
$singleMatches = [regex]::Matches($tabContent, $patternSingle)
|
|
$optionMatches = [regex]::Matches($tabContent, $patternOption)
|
|
$appendLineMatches = [regex]::Matches($tabContent, $patternAppendLineBlock)
|
|
$appendLineAssignedMatches = [regex]::Matches($tabContent, $patternAppendLineAssignedBlock)
|
|
$forMatches = [regex]::Matches($tabContent, $patternForBlock)
|
|
$stringVectors = Get-StringVectors -Content $tabContent
|
|
|
|
$rawEntries = New-Object System.Collections.Generic.List[object]
|
|
|
|
foreach ($m in $singleMatches) {
|
|
$variable = $m.Groups['variable'].Value.Trim()
|
|
$indexer = $m.Groups['indexer'].Value.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($indexer) -and $indexer -match '^[A-Za-z_]\w*$') {
|
|
$variable = "${variable}[$indexer]"
|
|
}
|
|
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
Ref = $m.Groups['ref'].Value.Trim()
|
|
Index = [int]$m.Index
|
|
})
|
|
}
|
|
|
|
foreach ($m in $optionMatches) {
|
|
$variable = $m.Groups['variable'].Value.Trim()
|
|
$indexer = $m.Groups['indexer'].Value.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($indexer) -and $indexer -match '^[A-Za-z_]\w*$') {
|
|
$variable = "${variable}[$indexer]"
|
|
}
|
|
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
Ref = $m.Groups['ref'].Value.Trim()
|
|
Index = [int]$m.Index
|
|
})
|
|
}
|
|
|
|
foreach ($m in $appendLineMatches) {
|
|
$obj = $m.Groups['obj'].Value
|
|
$ref = $m.Groups['ref'].Value.Trim()
|
|
$body = $m.Groups['body'].Value
|
|
$optPattern = '(?s)' + [regex]::Escape($obj) + '\.append_option\(\s*.*?get_option\("(?<variable>[^"]+)"(?:\s*,\s*(?<indexer>[^\)]+))?\)\s*\)\s*;'
|
|
$optMatches = [regex]::Matches($body, $optPattern)
|
|
|
|
foreach ($om in $optMatches) {
|
|
$variable = $om.Groups['variable'].Value.Trim()
|
|
$indexer = $om.Groups['indexer'].Value.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($indexer) -and $indexer -match '^[A-Za-z_]\w*$') {
|
|
$variable = "${variable}[$indexer]"
|
|
}
|
|
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
Ref = $ref
|
|
Index = [int]($m.Index + $om.Index)
|
|
})
|
|
}
|
|
}
|
|
|
|
foreach ($m in $appendLineAssignedMatches) {
|
|
$obj = $m.Groups['obj'].Value
|
|
$body = $m.Groups['body'].Value
|
|
|
|
$labelPattern = [regex]::Escape($obj) + '\.label_path\s*=\s*"(?<ref>[^"]+)"\s*;'
|
|
$labelMatch = [regex]::Match($body, $labelPattern)
|
|
if (-not $labelMatch.Success) {
|
|
continue
|
|
}
|
|
|
|
$ref = $labelMatch.Groups['ref'].Value.Trim()
|
|
$optPattern = '(?s)' + [regex]::Escape($obj) + '\.append_option\(\s*.*?get_option\("(?<variable>[^"]+)"(?:\s*,\s*(?<indexer>[^\)]+))?\)\s*\)\s*;'
|
|
$optMatches = [regex]::Matches($body, $optPattern)
|
|
|
|
foreach ($om in $optMatches) {
|
|
$variable = $om.Groups['variable'].Value.Trim()
|
|
$indexer = $om.Groups['indexer'].Value.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($indexer) -and $indexer -match '^[A-Za-z_]\w*$') {
|
|
$variable = "${variable}[$indexer]"
|
|
}
|
|
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
Ref = $ref
|
|
Index = [int]($m.Index + $om.Index)
|
|
})
|
|
}
|
|
}
|
|
|
|
foreach ($fm in $forMatches) {
|
|
$iter = $fm.Groups['iter'].Value
|
|
$collection = $fm.Groups['collection'].Value
|
|
$body = $fm.Groups['body'].Value
|
|
|
|
if (-not $stringVectors.ContainsKey($collection)) {
|
|
continue
|
|
}
|
|
|
|
$values = $stringVectors[$collection]
|
|
$prefixPattern = 'append_option_line\(\s*[^,]+\s*,\s*"(?<prefix>[^"]*)"\s*\+\s*' + [regex]::Escape($iter) + '\s*,\s*"(?<ref>[^"]+)"\s*\)'
|
|
$suffixPattern = 'append_option_line\(\s*[^,]+\s*,\s*' + [regex]::Escape($iter) + '\s*\+\s*"(?<suffix>[^"]*)"\s*,\s*"(?<ref>[^"]+)"\s*\)'
|
|
|
|
$prefixMatches = [regex]::Matches($body, $prefixPattern)
|
|
foreach ($pm in $prefixMatches) {
|
|
$prefix = $pm.Groups['prefix'].Value
|
|
$ref = $pm.Groups['ref'].Value.Trim()
|
|
|
|
foreach ($v in $values) {
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = "$prefix$v"
|
|
Ref = $ref
|
|
Index = [int]($fm.Index + $pm.Index)
|
|
})
|
|
}
|
|
}
|
|
|
|
$suffixMatches = [regex]::Matches($body, $suffixPattern)
|
|
foreach ($sm in $suffixMatches) {
|
|
$suffix = $sm.Groups['suffix'].Value
|
|
$ref = $sm.Groups['ref'].Value.Trim()
|
|
|
|
foreach ($v in $values) {
|
|
$rawEntries.Add([PSCustomObject]@{
|
|
Variable = "$v$suffix"
|
|
Ref = $ref
|
|
Index = [int]($fm.Index + $sm.Index)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
$parsedMatches = @($rawEntries | Sort-Object -Property Index)
|
|
|
|
if ($parsedMatches.Count -eq 0) {
|
|
Write-Host "No supported option-to-doc mappings were found." -ForegroundColor Yellow
|
|
Complete-SyncProgress
|
|
exit 0
|
|
}
|
|
|
|
Set-SyncStage -Step 3 -Status "Loading and parsing option modes from PrintConfig.cpp"
|
|
$optionModesByVariable = @{}
|
|
if (-not [string]::IsNullOrWhiteSpace($PrintConfigCppPath)) {
|
|
try {
|
|
$printConfigContent = Get-CppSourceContent -Source $PrintConfigCppPath -Description "PrintConfig.cpp"
|
|
$optionModesByVariable = Get-OptionModesByVariable -Content $printConfigContent
|
|
}
|
|
catch {
|
|
Write-Host "[WARN] $($_.Exception.Message). Continuing without option mode annotations." -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
Set-SyncStage -Step 4 -Status "Scanning markdown files"
|
|
$mdFiles = Get-ChildItem -LiteralPath $WikiRoot -Recurse -File -Filter '*.md' |
|
|
Where-Object { $_.FullName -notmatch '[\\/]wiki[\\/]' }
|
|
|
|
$mdByName = @{}
|
|
foreach ($file in $mdFiles) {
|
|
$key = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
|
|
if (-not $mdByName.ContainsKey($key)) {
|
|
$mdByName[$key] = New-Object System.Collections.Generic.List[string]
|
|
}
|
|
$mdByName[$key].Add($file.FullName)
|
|
}
|
|
|
|
Set-SyncStage -Step 5 -Status "Building file and anchor entries"
|
|
$entries = New-Object System.Collections.Generic.List[object]
|
|
$firstHeadingAnchorToken = '__first_heading__'
|
|
foreach ($m in $parsedMatches) {
|
|
$variable = $m.Variable
|
|
$baseVariable = [regex]::Match($variable, '^[^\[]+').Value
|
|
$mode = $null
|
|
if (-not [string]::IsNullOrWhiteSpace($baseVariable) -and $optionModesByVariable.ContainsKey($baseVariable)) {
|
|
$mode = $optionModesByVariable[$baseVariable]
|
|
}
|
|
|
|
$ref = $m.Ref
|
|
|
|
$rawFileKey = $null
|
|
$anchor = $null
|
|
if ($ref -match '#') {
|
|
$parts = $ref -split '#', 2
|
|
$rawFileKey = $parts[0].Trim()
|
|
$anchorPart = $parts[1].Trim().ToLowerInvariant()
|
|
if ([string]::IsNullOrWhiteSpace($anchorPart)) {
|
|
$anchor = $firstHeadingAnchorToken
|
|
}
|
|
else {
|
|
$anchor = $anchorPart
|
|
}
|
|
}
|
|
else {
|
|
$rawFileKey = $ref.Trim()
|
|
$anchor = $firstHeadingAnchorToken
|
|
}
|
|
|
|
$fileKey = ConvertTo-DocFileKey -RefFile $rawFileKey
|
|
|
|
if ([string]::IsNullOrWhiteSpace($fileKey) -or [string]::IsNullOrWhiteSpace($anchor)) {
|
|
continue
|
|
}
|
|
|
|
$entries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
FileKey = $fileKey
|
|
Anchor = $anchor
|
|
Ref = $ref
|
|
Mode = $mode
|
|
})
|
|
}
|
|
|
|
if ($entries.Count -eq 0) {
|
|
Write-Host "No entries with markdown file references were found." -ForegroundColor Yellow
|
|
Complete-SyncProgress
|
|
exit 0
|
|
}
|
|
|
|
$changes = 0
|
|
$missingFiles = 0
|
|
$missingHeadings = 0
|
|
$alreadyPresent = 0
|
|
$normalizedSections = 0
|
|
$entriesWithMode = @($entries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Mode) }).Count
|
|
$metadataLinePattern = '^\s*(?:\[(?:Variable|Variables|Mode|Modes)\]\([^\)]+\)|`[^`]+`\s+\[(?:Variable|Variables)\]\([^\)]+\)|Variables?|Modes?)\s*:\s*'
|
|
|
|
$managedTargetPaths = @{}
|
|
foreach ($fileKey in $mdByName.Keys) {
|
|
$candidates = $mdByName[$fileKey]
|
|
$managedPath = $candidates[0]
|
|
if ($candidates.Count -gt 1) {
|
|
$managedPath = ($candidates | Sort-Object Length | Select-Object -First 1)
|
|
}
|
|
|
|
$managedTargetPaths[$managedPath] = $true
|
|
}
|
|
|
|
$expectedAnchorsByPath = @{}
|
|
$processedTargetPaths = @{}
|
|
|
|
$groupedByFile = $entries | Group-Object -Property FileKey
|
|
$totalFileGroups = $groupedByFile.Count
|
|
$fileNumber = 0
|
|
|
|
Set-SyncStage -Step 6 -Status "Applying mappings to markdown files ($totalFileGroups files)"
|
|
|
|
foreach ($group in $groupedByFile) {
|
|
$fileNumber++
|
|
$fileKey = $group.Name
|
|
Set-SyncDetail -Status "File $fileNumber/$($totalFileGroups): $fileKey" -Current $fileNumber -Total $totalFileGroups
|
|
|
|
if (-not $mdByName.ContainsKey($fileKey)) {
|
|
Write-Host "[WARN] Markdown file not found for '$fileKey'" -ForegroundColor Yellow
|
|
$missingFiles++
|
|
continue
|
|
}
|
|
|
|
$candidates = $mdByName[$fileKey]
|
|
$targetPath = $candidates[0]
|
|
if ($candidates.Count -gt 1) {
|
|
$targetPath = ($candidates | Sort-Object Length | Select-Object -First 1)
|
|
Write-Host "[WARN] Multiple files matched '$fileKey'. Using: $targetPath" -ForegroundColor Yellow
|
|
}
|
|
|
|
$processedTargetPaths[$targetPath] = $true
|
|
if (-not $expectedAnchorsByPath.ContainsKey($targetPath)) {
|
|
$expectedAnchorsByPath[$targetPath] = @{}
|
|
}
|
|
|
|
$lines = Get-Content -LiteralPath $targetPath
|
|
$buffer = New-Object System.Collections.Generic.List[string]
|
|
$buffer.AddRange([string[]]$lines)
|
|
$firstHeadingIndex = Find-FirstHeadingLineIndex -Lines $buffer.ToArray()
|
|
$firstHeadingAnchor = $null
|
|
if ($firstHeadingIndex -ge 0 -and $buffer[$firstHeadingIndex] -match '^(#{1,6})\s+(.+?)\s*$') {
|
|
$headingText = $Matches[2].Trim()
|
|
$headingText = $headingText -replace '\s+#+$', ''
|
|
$firstHeadingAnchor = ConvertTo-AnchorSlug -Heading $headingText
|
|
}
|
|
$fileChanged = $false
|
|
|
|
$groupedByAnchor = $group.Group | Group-Object -Property Anchor
|
|
$anchorCount = $groupedByAnchor.Count
|
|
$anchorNumber = 0
|
|
|
|
foreach ($anchorGroup in $groupedByAnchor) {
|
|
$anchorNumber++
|
|
Set-SyncDetail -Status "File $fileNumber/$($totalFileGroups): $fileKey | Anchor $anchorNumber/$($anchorCount): $($anchorGroup.Name)" -Current $fileNumber -Total $totalFileGroups
|
|
$anchor = $anchorGroup.Name
|
|
$resolvedAnchor = $anchor
|
|
|
|
$vars = New-Object System.Collections.Generic.List[string]
|
|
$seenVars = @{}
|
|
$varModes = @{}
|
|
foreach ($entry in $anchorGroup.Group) {
|
|
if (-not $seenVars.ContainsKey($entry.Variable)) {
|
|
$seenVars[$entry.Variable] = $true
|
|
$vars.Add($entry.Variable)
|
|
}
|
|
|
|
$modeLabel = Convert-ConfigOptionModeToLabel -Mode $entry.Mode
|
|
if (-not [string]::IsNullOrWhiteSpace($modeLabel) -and -not $varModes.ContainsKey($entry.Variable)) {
|
|
$varModes[$entry.Variable] = $modeLabel
|
|
}
|
|
}
|
|
|
|
$formattedVars = $vars | ForEach-Object { "``$_``" }
|
|
$variableLabel = if ($vars.Count -eq 1) { "[Variable](built_in_placeholders_variables):" } else { "[Variables](built_in_placeholders_variables):" }
|
|
$insertVariableLine = "$variableLabel " + ($formattedVars -join ", ") + ". " # ending with two spaces so Markdown line break is forced
|
|
|
|
$distinctModes = New-Object System.Collections.Generic.List[string]
|
|
$seenModes = @{}
|
|
$modeToVars = @{}
|
|
$varsWithoutMode = New-Object System.Collections.Generic.List[string]
|
|
|
|
foreach ($varName in $vars) {
|
|
$formattedVarName = "``$varName``"
|
|
if (-not $varModes.ContainsKey($varName)) {
|
|
$varsWithoutMode.Add($formattedVarName)
|
|
continue
|
|
}
|
|
|
|
$modeName = $varModes[$varName]
|
|
if (-not $seenModes.ContainsKey($modeName)) {
|
|
$seenModes[$modeName] = $true
|
|
$distinctModes.Add($modeName)
|
|
$modeToVars[$modeName] = New-Object System.Collections.Generic.List[string]
|
|
}
|
|
|
|
$modeToVars[$modeName].Add($formattedVarName)
|
|
}
|
|
|
|
$canonicalLines = New-Object System.Collections.Generic.List[string]
|
|
|
|
if ($distinctModes.Count -gt 1) {
|
|
$canonicalLines.Add("[Modes](option_mode): ")
|
|
foreach ($modeName in $distinctModes) {
|
|
$modeVars = [System.Collections.Generic.List[string]]$modeToVars[$modeName]
|
|
$groupVariableLabel = if ($modeVars.Count -eq 1) { "[Variable](built_in_placeholders_variables):" } else { "[Variables](built_in_placeholders_variables):" }
|
|
$canonicalLines.Add("``$modeName`` $groupVariableLabel " + ($modeVars -join ", ") + ". ")
|
|
}
|
|
|
|
if ($varsWithoutMode.Count -gt 0) {
|
|
$leftoverVariableLabel = if ($varsWithoutMode.Count -eq 1) { "[Variable](built_in_placeholders_variables):" } else { "[Variables](built_in_placeholders_variables):" }
|
|
$canonicalLines.Add("$leftoverVariableLabel " + ($varsWithoutMode -join ", ") + ". ")
|
|
}
|
|
}
|
|
else {
|
|
$insertModeLine = $null
|
|
if ($distinctModes.Count -gt 0) {
|
|
$formattedModes = $distinctModes | ForEach-Object { "``$_``" }
|
|
$modeLabel = if ($distinctModes.Count -eq 1) { "[Mode](option_mode):" } else { "[Modes](option_mode):" }
|
|
$insertModeLine = "$modeLabel " + ($formattedModes -join ", ") + ". " # ending with two spaces so Markdown line break is forced
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($insertModeLine)) {
|
|
$canonicalLines.Add($insertModeLine)
|
|
}
|
|
$canonicalLines.Add($insertVariableLine)
|
|
}
|
|
|
|
$idx = -1
|
|
if ($anchor -eq $firstHeadingAnchorToken) {
|
|
if ($firstHeadingIndex -lt 0 -or [string]::IsNullOrWhiteSpace($firstHeadingAnchor)) {
|
|
$sourceRef = $anchorGroup.Group[0].Ref
|
|
Write-Host "[WARN] First heading not found in $targetPath (from '$sourceRef')" -ForegroundColor Yellow
|
|
$missingHeadings++
|
|
continue
|
|
}
|
|
|
|
$idx = $firstHeadingIndex
|
|
$resolvedAnchor = $firstHeadingAnchor
|
|
}
|
|
else {
|
|
$idx = Find-HeadingLineIndex -Lines $buffer.ToArray() -Anchor $anchor
|
|
}
|
|
|
|
if ($idx -lt 0) {
|
|
$sourceRef = $anchorGroup.Group[0].Ref
|
|
Write-Host "[WARN] Heading anchor '$anchor' not found in $targetPath (from '$sourceRef')" -ForegroundColor Yellow
|
|
$missingHeadings++
|
|
continue
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($resolvedAnchor)) {
|
|
$expectedAnchorsByPath[$targetPath][$resolvedAnchor] = $true
|
|
}
|
|
|
|
$nextHeading = -1
|
|
for ($j = $idx + 1; $j -lt $buffer.Count; $j++) {
|
|
if ($buffer[$j] -match '^#{1,6}\s+') {
|
|
$nextHeading = $j
|
|
break
|
|
}
|
|
}
|
|
|
|
$sectionEnd = if ($nextHeading -ge 0) { $nextHeading - 1 } else { $buffer.Count - 1 }
|
|
$metadataLineIndexes = New-Object System.Collections.Generic.List[int]
|
|
|
|
for ($k = $idx + 1; $k -le $sectionEnd; $k++) {
|
|
if ($buffer[$k] -match $metadataLinePattern) {
|
|
$metadataLineIndexes.Add($k)
|
|
}
|
|
}
|
|
|
|
$hasBlankBetween = ($idx + 1) -lt $buffer.Count -and [string]::IsNullOrWhiteSpace($buffer[$idx + 1])
|
|
$metadataStart = $idx + 2
|
|
$lastMetadataIndex = $metadataStart + $canonicalLines.Count - 1
|
|
$hasHeadingRightAfterMetadata = ($lastMetadataIndex + 1) -lt $buffer.Count -and $buffer[$lastMetadataIndex + 1] -match '^#{1,6}\s+'
|
|
$isCanonicalLayout = $hasBlankBetween -and $metadataLineIndexes.Count -eq $canonicalLines.Count
|
|
if ($isCanonicalLayout) {
|
|
for ($ci = 0; $ci -lt $canonicalLines.Count; $ci++) {
|
|
$expectedIndex = $metadataStart + $ci
|
|
if ($expectedIndex -ge $buffer.Count -or $metadataLineIndexes[$ci] -ne $expectedIndex -or $buffer[$expectedIndex] -ne $canonicalLines[$ci]) {
|
|
$isCanonicalLayout = $false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
$alreadyCanonical = $isCanonicalLayout -and (-not $hasHeadingRightAfterMetadata)
|
|
if ($alreadyCanonical) {
|
|
$alreadyPresent++
|
|
continue
|
|
}
|
|
|
|
if ($metadataLineIndexes.Count -gt 0) {
|
|
for ($r = $metadataLineIndexes.Count - 1; $r -ge 0; $r--) {
|
|
$buffer.RemoveAt($metadataLineIndexes[$r])
|
|
}
|
|
$normalizedSections++
|
|
$fileChanged = $true
|
|
}
|
|
|
|
if (-not (($idx + 1) -lt $buffer.Count -and [string]::IsNullOrWhiteSpace($buffer[$idx + 1]))) {
|
|
$buffer.Insert($idx + 1, "")
|
|
$fileChanged = $true
|
|
}
|
|
|
|
$insertIndex = $idx + 2
|
|
foreach ($metadataLine in $canonicalLines) {
|
|
$buffer.Insert($insertIndex, $metadataLine)
|
|
$insertIndex++
|
|
}
|
|
|
|
if ($insertIndex -lt $buffer.Count -and $buffer[$insertIndex] -match '^#{1,6}\s+') {
|
|
$buffer.Insert($insertIndex, "")
|
|
$fileChanged = $true
|
|
}
|
|
|
|
$changes++
|
|
$fileChanged = $true
|
|
}
|
|
|
|
$staleSectionsPruned = Remove-StaleSectionMetadata -Buffer $buffer -ExpectedAnchors $expectedAnchorsByPath[$targetPath] -MetadataLinePattern $metadataLinePattern
|
|
if ($staleSectionsPruned -gt 0) {
|
|
$normalizedSections += $staleSectionsPruned
|
|
$fileChanged = $true
|
|
}
|
|
|
|
if ($fileChanged -and -not $DryRun) {
|
|
Set-Content -LiteralPath $targetPath -Value $buffer -Encoding UTF8
|
|
}
|
|
}
|
|
|
|
foreach ($managedTargetPath in $managedTargetPaths.Keys) {
|
|
if ($processedTargetPaths.ContainsKey($managedTargetPath)) {
|
|
continue
|
|
}
|
|
|
|
$lines = Get-Content -LiteralPath $managedTargetPath
|
|
$buffer = New-Object System.Collections.Generic.List[string]
|
|
$buffer.AddRange([string[]]$lines)
|
|
|
|
$expectedAnchors = @{}
|
|
if ($expectedAnchorsByPath.ContainsKey($managedTargetPath)) {
|
|
$expectedAnchors = $expectedAnchorsByPath[$managedTargetPath]
|
|
}
|
|
|
|
$staleSectionsPruned = Remove-StaleSectionMetadata -Buffer $buffer -ExpectedAnchors $expectedAnchors -MetadataLinePattern $metadataLinePattern
|
|
if ($staleSectionsPruned -le 0) {
|
|
continue
|
|
}
|
|
|
|
$normalizedSections += $staleSectionsPruned
|
|
if (-not $DryRun) {
|
|
Set-Content -LiteralPath $managedTargetPath -Value $buffer -Encoding UTF8
|
|
}
|
|
}
|
|
|
|
Set-SyncStage -Step 7 -Status "Finalizing summary"
|
|
Write-Host "Processed: $($entries.Count) entries"
|
|
Write-Host "Entries with option mode: $entriesWithMode"
|
|
Write-Host "Inserted: $changes"
|
|
Write-Host "Skipped (already present): $alreadyPresent"
|
|
Write-Host "Normalized sections: $normalizedSections"
|
|
Write-Host "Missing markdown file matches: $missingFiles"
|
|
Write-Host "Missing heading anchors: $missingHeadings"
|
|
|
|
if ($DryRun) {
|
|
Write-Host "Dry run only. No files were modified." -ForegroundColor Cyan
|
|
}
|
|
|
|
Set-SyncStage -Step 8 -Status "Completed"
|
|
Complete-SyncProgress
|