Wiki 13: (mini update) Layer time variability #161
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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@v46 | |
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)<a\s+[^>]*(?: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, '(?<!!)[^\]]*\]\(([^)]+)\)')) { | |
$destRaw = $linkMatch.Groups[1].Value.Trim() | |
# Handle internal fragments | |
if ($destRaw.StartsWith('#')) { | |
$fragment = $destRaw.Substring(1) | |
if ($fragment.Contains('#')) { | |
$brokenReferences += Add-BrokenReference $relPath ($i + 1) $destRaw "Internal link must use only one #." "Link" | |
} else { | |
$validationResult = Validate-Fragment $fragment $anchors $relPath ($i + 1) $destRaw "Link" | |
if ($validationResult) { $brokenReferences += $validationResult } | |
} | |
continue | |
} | |
# Skip external URLs | |
if ($destRaw -match '^(?:https?:|mailto:|data:|#|\\)') { continue } | |
# Check for double ## | |
if ($destRaw.Contains('##')) { | |
$brokenReferences += Add-BrokenReference $relPath ($i + 1) $destRaw "Use single # for fragments." "Link" | |
continue | |
} | |
# Parse file and fragment | |
$destParts = $destRaw -split '#', 2 | |
$destNoFragment = $destParts[0] | |
$fragment = ($destParts.Length -gt 1) ? $destParts[1] : $null | |
if ($destNoFragment) { | |
$leaf = ($destNoFragment -split '[\\/]')[-1] | |
if ($leaf) { | |
$targetBase = $leaf.ToLower().EndsWith('.md') ? $leaf.Substring(0, $leaf.Length - 3) : $leaf | |
$targetBase = $targetBase.Trim() | |
if ($targetBase) { | |
$linkInfo = @{ | |
TargetBase = $targetBase | |
Fragment = $fragment | |
Line = $i + 1 | |
SourceFile = $relPath | |
} | |
$links += $linkInfo | |
} | |
} | |
} | |
} | |
} | |
$docIndex[$baseName] = @{ Anchors = $anchors; Links = $links } | |
} | |
# Parse Tab.cpp references | |
if ($hasTabFile) { | |
$regex = 'optgroup->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 = '<img\s+[^>]*>'; 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<<EOF" | |
Add-Content -Path $env:GITHUB_ENV -Value $errorSummary | |
Add-Content -Path $env:GITHUB_ENV -Value "EOF" | |
exit 1 | |
} else { | |
Write-Host "::notice::All documentation is valid!" | |
exit 0 | |
} | |
- name: Comment on PR | |
if: failure() && github.event_name == 'pull_request' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const validationErrors = process.env.VALIDATION_ERRORS || ''; | |
const body = `❌ **Documentation validation failed** | |
${validationErrors || 'Please check the workflow logs for details about the validation errors.'}`; | |
github.rest.issues.createComment({ | |
issue_number: context.issue.number, | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
body: body | |
}) |