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<