diff --git a/.github/workflows/validate-documentation.yml b/.github/workflows/validate-documentation.yml deleted file mode 100644 index d4c4cfc9c0..0000000000 --- a/.github/workflows/validate-documentation.yml +++ /dev/null @@ -1,368 +0,0 @@ -name: Validate Documentation - -on: - pull_request: - paths: - - 'src/slic3r/GUI/Tab.cpp' - - 'doc/**/*.md' - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - validate: - runs-on: windows-latest - name: Check Documentation - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v47 - 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 - } - - function Get-ImagesFromLine($line) { - $images = @() - $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] } - } - - $images += @{ - Match = $match.Value - Type = $pattern.Type - AltText = $altText - Url = $url - StartIndex = $match.Index - Length = $match.Length - } - } - } - - return $images - } - - # 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, '`[^`]*`', '') - - # Get all images from this line to skip them in link processing - $imagesInLine = Get-ImagesFromLine $line - $imageRanges = @() - foreach ($img in $imagesInLine) { - # Exclude the entire image syntax from link processing - $imageRanges += @{ Start = $img.StartIndex; End = $img.StartIndex + $img.Length } - } - - # Find all markdown links, but exclude those that are part of images - 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/OrcaSlicer/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 } - - # Use the unified image detection function - $imagesInLine = Get-ImagesFromLine $line - - foreach ($image in $imagesInLine) { - $altText = $image.AltText - $url = $image.Url - $imageMatch = $image.Match - $imageType = $image.Type - - if (-not $altText.Trim() -and $url) { - $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $imageMatch "[$imageType] 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) $imageMatch "[$imageType] Alt text `"$altText`" ≠ filename `"$fileNameFromUrl`"" "Image" - } - - $expectedImagePath = Join-Path $PWD ($relativePathInUrl -replace "/", "\") - if (-not (Test-Path $expectedImagePath)) { - $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $imageMatch "[$imageType] Image not found at path: $relativePathInUrl" "Image" - } - } else { - $urlIssues = @() - if (-not $url.StartsWith('https://github.com/OrcaSlicer/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/OrcaSlicer/OrcaSlicer)') { $urlIssues += "External URLs not allowed" } - - $issueText = "[$imageType] URL format issues: " + ($urlIssues -join '; ') - $brokenReferences += Add-BrokenReference $relPath ($lineNumber + 1) $imageMatch $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<