mirror of
https://github.com/OrcaSlicer/OrcaSlicer_WIKI.git
synced 2026-05-17 08:35:46 +03:00
474 lines
16 KiB
PowerShell
474 lines
16 KiB
PowerShell
param(
|
|
[string]$TabCppPath = "https://github.com/OrcaSlicer/OrcaSlicer/blob/main/src/slic3r/GUI/Tab.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 Get-TabCppContent {
|
|
param([string]$Source)
|
|
|
|
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 Tab.cpp from URL: $Source"
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $Source)) {
|
|
throw "Tab.cpp 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
|
|
}
|
|
|
|
$syncProgressActivity = "sync-tab-options-to-wiki.ps1"
|
|
$syncStageTotal = 7
|
|
|
|
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-TabCppContent -Source $TabCppPath
|
|
|
|
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 "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 4 -Status "Building file and anchor entries"
|
|
$entries = New-Object System.Collections.Generic.List[object]
|
|
foreach ($m in $parsedMatches) {
|
|
$variable = $m.Variable
|
|
$ref = $m.Ref
|
|
|
|
if ($ref -notmatch '#') {
|
|
continue
|
|
}
|
|
|
|
$parts = $ref -split '#', 2
|
|
$fileKey = $parts[0].Trim()
|
|
$anchor = $parts[1].Trim().ToLowerInvariant()
|
|
|
|
if ([string]::IsNullOrWhiteSpace($fileKey) -or [string]::IsNullOrWhiteSpace($anchor)) {
|
|
continue
|
|
}
|
|
|
|
$entries.Add([PSCustomObject]@{
|
|
Variable = $variable
|
|
FileKey = $fileKey
|
|
Anchor = $anchor
|
|
Ref = $ref
|
|
})
|
|
}
|
|
|
|
if ($entries.Count -eq 0) {
|
|
Write-Host "No entries with file#anchor format were found." -ForegroundColor Yellow
|
|
Complete-SyncProgress
|
|
exit 0
|
|
}
|
|
|
|
$changes = 0
|
|
$missingFiles = 0
|
|
$missingHeadings = 0
|
|
$alreadyPresent = 0
|
|
$normalizedSections = 0
|
|
|
|
$groupedByFile = $entries | Group-Object -Property FileKey
|
|
$totalFileGroups = $groupedByFile.Count
|
|
$fileNumber = 0
|
|
|
|
Set-SyncStage -Step 5 -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
|
|
}
|
|
|
|
$lines = Get-Content -LiteralPath $targetPath
|
|
$buffer = New-Object System.Collections.Generic.List[string]
|
|
$buffer.AddRange([string[]]$lines)
|
|
$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
|
|
|
|
$vars = New-Object System.Collections.Generic.List[string]
|
|
$seenVars = @{}
|
|
foreach ($entry in $anchorGroup.Group) {
|
|
if (-not $seenVars.ContainsKey($entry.Variable)) {
|
|
$seenVars[$entry.Variable] = $true
|
|
$vars.Add($entry.Variable)
|
|
}
|
|
}
|
|
|
|
$formattedVars = $vars | ForEach-Object { "``$_``" }
|
|
$label = if ($vars.Count -eq 1) { "[Variable](built_in_placeholders_variables):" } else { "[Variables](built_in_placeholders_variables):" }
|
|
$insertLine = "$label " + ($formattedVars -join ", ") + ". " # ending with two spaces so Markdown line break is forced
|
|
|
|
$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
|
|
}
|
|
|
|
$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]
|
|
$hasCanonicalLine = $false
|
|
|
|
for ($k = $idx + 1; $k -le $sectionEnd; $k++) {
|
|
if ($buffer[$k] -match '^\s*(?:\[(?:Variable|Variables)\]\([^\)]+\)|Variables?)\s*:\s*') {
|
|
$metadataLineIndexes.Add($k)
|
|
if ($buffer[$k] -eq $insertLine) {
|
|
$hasCanonicalLine = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
$hasBlankBetween = ($idx + 1) -lt $buffer.Count -and [string]::IsNullOrWhiteSpace($buffer[$idx + 1])
|
|
$varLineIndex = $idx + 2
|
|
$hasHeadingRightAfterVariable = ($varLineIndex + 1) -lt $buffer.Count -and $buffer[$varLineIndex + 1] -match '^#{1,6}\s+'
|
|
$alreadyCanonical = $metadataLineIndexes.Count -eq 1 -and $hasCanonicalLine -and $metadataLineIndexes[0] -eq $varLineIndex -and $hasBlankBetween -and (-not $hasHeadingRightAfterVariable)
|
|
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 ((($idx + 2) -lt $buffer.Count) -and [string]::IsNullOrWhiteSpace($buffer[$idx + 1]) -and $buffer[$idx + 2] -eq $insertLine -and (-not (($idx + 3) -lt $buffer.Count -and $buffer[$idx + 3] -match '^#{1,6}\s+'))) {
|
|
$alreadyPresent++
|
|
continue
|
|
}
|
|
|
|
if (-not (($idx + 1) -lt $buffer.Count -and [string]::IsNullOrWhiteSpace($buffer[$idx + 1]))) {
|
|
$buffer.Insert($idx + 1, "")
|
|
$fileChanged = $true
|
|
}
|
|
|
|
$buffer.Insert($idx + 2, $insertLine)
|
|
if (($idx + 3) -lt $buffer.Count -and $buffer[$idx + 3] -match '^#{1,6}\s+') {
|
|
$buffer.Insert($idx + 3, "")
|
|
$fileChanged = $true
|
|
}
|
|
$changes++
|
|
$fileChanged = $true
|
|
}
|
|
|
|
if ($fileChanged -and -not $DryRun) {
|
|
Set-Content -LiteralPath $targetPath -Value $buffer -Encoding UTF8
|
|
}
|
|
}
|
|
|
|
Set-SyncStage -Step 6 -Status "Finalizing summary"
|
|
Write-Host "Processed: $($entries.Count) entries"
|
|
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 7 -Status "Completed"
|
|
Complete-SyncProgress
|