mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-12-24 08:38:40 -07:00
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 46 to 47. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/v46...v47) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-version: '47' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
335 lines
16 KiB
YAML
335 lines
16 KiB
YAML
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
|
|
}
|
|
|
|
# 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@v8
|
|
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
|
|
})
|