From 4b48ba1004b15a5c65c07dd6efeb2bc4c23dcc0f Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Fri, 22 Aug 2025 11:23:59 -0300 Subject: [PATCH] Wiki Validation Workflow Action (#10447) Wiki Action --- .github/workflows/validate-documentation.yml | 330 +++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 .github/workflows/validate-documentation.yml diff --git a/.github/workflows/validate-documentation.yml b/.github/workflows/validate-documentation.yml new file mode 100644 index 0000000000..d0992a66c2 --- /dev/null +++ b/.github/workflows/validate-documentation.yml @@ -0,0 +1,330 @@ +name: Validate Documentation + +on: + pull_request: + paths: + - 'src/slic3r/GUI/Tab.cpp' + - 'doc/**/*.md' + workflow_dispatch: + +jobs: + validate: + runs-on: windows-latest + name: Check Documentation + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: | + src/slic3r/GUI/Tab.cpp + doc/**/*.md + + - name: Run validation + if: steps.changed-files.outputs.any_changed == 'true' + shell: pwsh + run: | + # Helper Functions + function Normalize-Fragment($fragment) { + return $fragment.ToLower().Trim() -replace '[^a-z0-9\s-]', '' -replace ' ', '-' -replace '^-+|-+$', '' + } + + function Add-BrokenReference($sourceFile, $line, $target, $issue, $type) { + return @{ + SourceFile = $sourceFile + Line = $line + Target = $target + Issue = $issue + Type = $type + } + } + + function Validate-Fragment($fragment, $availableAnchors, $sourceFile, $line, $target, $type) { + $cleanFragment = $fragment.StartsWith('#') ? $fragment.Substring(1) : $fragment + $normalizedFragment = Normalize-Fragment $cleanFragment + + if ($availableAnchors -notcontains $normalizedFragment) { + return Add-BrokenReference $sourceFile $line $target "Fragment does not exist" $type + } + return $null + } + + # Initialize + $tabFile = Join-Path $PWD "src/slic3r/GUI/Tab.cpp" + $docDir = Join-Path $PWD 'doc' + $brokenReferences = @() + $docIndex = @{} + + Write-Host "Validating documentation..." -ForegroundColor Blue + + # Validate paths + $hasTabFile = Test-Path $tabFile + if (-not $hasTabFile) { Write-Host "::warning::Tab.cpp file not found at: $tabFile" } + if (-not (Test-Path $docDir)) { Write-Host "::error::doc folder does not exist"; exit 1 } + + # Build documentation index + $mdFiles = Get-ChildItem -Path $docDir -Filter *.md -Recurse -File -ErrorAction SilentlyContinue + + foreach ($mdFile in $mdFiles) { + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($mdFile.Name) + $relPath = (Resolve-Path $mdFile.FullName).Path.Substring($docDir.Length).TrimStart('\', '/') + $content = Get-Content -Path $mdFile.FullName -Encoding UTF8 -Raw + $lines = Get-Content -Path $mdFile.FullName -Encoding UTF8 + + # Extract anchors + $anchors = @() + $anchors += [regex]::Matches($content, '(?i)]*(?:name|id)\s*=\s*[`"'']([^`"'']+)[`"'']') | + ForEach-Object { $_.Groups[1].Value.ToLower() } + $anchors += [regex]::Matches($content, '(?m)^#+\s+(.+)$') | + ForEach-Object { Normalize-Fragment $_.Groups[1].Value.Trim() } + + # Parse links + $links = @() + $inCodeFence = $false + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + if ($line.TrimStart() -match '^(```|~~~)') { + $inCodeFence = -not $inCodeFence + continue + } + if ($inCodeFence) { continue } + + $lineForParsing = [regex]::Replace($line, '`[^`]*`', '') + foreach ($linkMatch in [regex]::Matches($lineForParsing, '(?append_single_option_line\s*\(\s*(?:"([^"]+)"|([^,]+?))\s*,\s*"([^"]+)"\s*\)' + $lines = Get-Content -Path $tabFile -Encoding UTF8 + + for ($i = 0; $i -lt $lines.Count; $i++) { + foreach ($match in [regex]::Matches($lines[$i], $regex)) { + $arg2Full = $match.Groups[3].Value.Trim() + + if ($arg2Full.Contains('##')) { + $brokenReferences += Add-BrokenReference "Tab.cpp" ($i + 1) $arg2Full "Use single # for fragments." "Link" + continue + } + + $arg2Parts = $arg2Full -split '#', 2 + $docBase = $arg2Parts[0].Trim() + $fragment = ($arg2Parts.Length -gt 1) ? $arg2Parts[1].Trim() : $null + + if (-not $docIndex.ContainsKey($docBase)) { + $brokenReferences += Add-BrokenReference "Tab.cpp" ($i + 1) $docBase "File does not exist" "Link" + } elseif ($fragment) { + $validationResult = Validate-Fragment $fragment $docIndex[$docBase].Anchors "Tab.cpp" ($i + 1) "$docBase#$fragment" "Link" + if ($validationResult) { $brokenReferences += $validationResult } + } + } + } + } + + # Validate markdown links + foreach ($baseName in $docIndex.Keys) { + foreach ($link in $docIndex[$baseName].Links) { + if (-not $docIndex.ContainsKey($link.TargetBase)) { + $brokenReferences += Add-BrokenReference $link.SourceFile $link.Line "$($link.TargetBase).md" "File does not exist" "Link" + } elseif ($link.Fragment) { + $validationResult = Validate-Fragment $link.Fragment $docIndex[$link.TargetBase].Anchors $link.SourceFile $link.Line "$($link.TargetBase)#$($link.Fragment)" "Link" + if ($validationResult) { $brokenReferences += $validationResult } + } + } + } + + # Validate images + Write-Host "Validating images..." -ForegroundColor Blue + $expectedUrlPattern = '^https://github\.com/SoftFever/OrcaSlicer/blob/main/([^?]+)\?raw=true$' + + foreach ($file in $mdFiles) { + $lines = Get-Content $file.FullName -Encoding UTF8 + $relPath = (Resolve-Path $file.FullName).Path.Substring($docDir.Length).TrimStart('\', '/') + + $inCodeFence = $false + for ($lineNumber = 0; $lineNumber -lt $lines.Count; $lineNumber++) { + $line = $lines[$lineNumber] + if ($line.TrimStart() -match '^(```|~~~)') { + $inCodeFence = -not $inCodeFence + continue + } + if ($inCodeFence) { continue } + + $lineForParsing = [regex]::Replace($line, '`[^`]*`', '') + + # Process markdown and HTML images + $imagePatterns = @( + @{ Pattern = "!\[([^\]]*)\]\(([^)]+)\)"; Type = "Markdown"; AltGroup = 1; UrlGroup = 2 } + @{ Pattern = ']*>'; Type = "HTML"; AltGroup = -1; UrlGroup = -1 } + ) + + foreach ($pattern in $imagePatterns) { + foreach ($match in [regex]::Matches($lineForParsing, $pattern.Pattern)) { + $altText = "" + $url = "" + + if ($pattern.Type -eq "Markdown") { + $altText = $match.Groups[$pattern.AltGroup].Value + $url = $match.Groups[$pattern.UrlGroup].Value + } else { + # Extract from HTML + $imgTag = $match.Value + if ($imgTag -match 'alt\s*=\s*[`"'']([^`"'']*)[`"'']') { $altText = $matches[1] } + if ($imgTag -match 'src\s*=\s*[`"'']([^`"'']*)[`"'']') { $url = $matches[1] } + } + + if (-not $altText.Trim() -and $url) { + $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $match.Value "[$($pattern.Type)] Missing alt text for image" "Image" + } elseif ($url -and $altText) { + # Validate URL format and file existence + if ($url -match $expectedUrlPattern) { + $relativePathInUrl = $matches[1] + $fileNameFromUrl = [System.IO.Path]::GetFileNameWithoutExtension($relativePathInUrl) + + if ($altText -ne $fileNameFromUrl) { + $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $match.Value "[$($pattern.Type)] Alt text `"$altText`" ≠ filename `"$fileNameFromUrl`"" "Image" + } + + $expectedImagePath = Join-Path $PWD ($relativePathInUrl -replace "/", "\") + if (-not (Test-Path $expectedImagePath)) { + $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $match.Value "[$($pattern.Type)] Image not found at path: $relativePathInUrl" "Image" + } + } else { + $urlIssues = @() + if (-not $url.StartsWith('https://github.com/SoftFever/OrcaSlicer/blob/main/')) { $urlIssues += "URL must start with expected prefix" } + if (-not $url.EndsWith('?raw=true')) { $urlIssues += "URL must end with '?raw=true'" } + if ($url -match '^https?://(?!github\.com/SoftFever/OrcaSlicer)') { $urlIssues += "External URLs not allowed" } + + $issueText = "[$($pattern.Type)] URL format issues: " + ($urlIssues -join '; ') + $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $match.Value $issueText "Image" + } + } + } + } + } + } + + # Report results + $linkErrors = $brokenReferences | Where-Object { $_.Type -eq "Link" } + $imageErrors = $brokenReferences | Where-Object { $_.Type -eq "Image" } + + if ($brokenReferences.Count -gt 0) { + Write-Host "::error::Documentation validation failed" + + # Build error summary for PR comment + $errorSummary = "" + + # Report link errors + if ($linkErrors) { + Write-Host "::group::🔗 Link Validation Errors" + $errorSummary += "## 🔗 Link Validation Errors`n`n" + $linkErrors | Group-Object SourceFile | ForEach-Object { + Write-Host "📄 $($_.Name):" -ForegroundColor Yellow + $errorSummary += "**📄 doc/$($_.Name):**`n" + $_.Group | Sort-Object Line | ForEach-Object { + Write-Host " Line $($_.Line): $($_.Target) - $($_.Issue)" -ForegroundColor Red + Write-Host "::error file=doc/$($_.SourceFile),line=$($_.Line)::$($_.Target) - $($_.Issue)" + $errorSummary += "- Line $($_.Line): ``$($_.Target)`` - $($_.Issue)`n" + } + $errorSummary += "`n" + } + Write-Host "::endgroup::" + } + + # Report image errors + if ($imageErrors) { + Write-Host "::group::🖼️ Image Validation Errors" + $errorSummary += "## 🖼️ Image Validation Errors`n`n" + $imageErrors | Group-Object SourceFile | ForEach-Object { + Write-Host "📄 $($_.Name):" -ForegroundColor Yellow + $errorSummary += "**📄 doc/$($_.Name):**`n" + $_.Group | Sort-Object Line | ForEach-Object { + Write-Host " Line $($_.Line): $($_.Issue)" -ForegroundColor Red + Write-Host "::error file=doc/$($_.SourceFile),line=$($_.Line)::$($_.Issue)" + $errorSummary += "- Line $($_.Line): $($_.Issue)`n" + } + $errorSummary += "`n" + } + Write-Host "::endgroup::" + } + + # Export error summary for PR comment + Add-Content -Path $env:GITHUB_ENV -Value "VALIDATION_ERRORS<