diff --git a/scripts/lore/check-wikilinks.ps1 b/scripts/lore/check-wikilinks.ps1 index c946f38..6694a7a 100644 --- a/scripts/lore/check-wikilinks.ps1 +++ b/scripts/lore/check-wikilinks.ps1 @@ -38,13 +38,20 @@ foreach ($note in $notes) { $text = Get-Content -Raw -LiteralPath $note.FullName -Encoding utf8 foreach ($match in [regex]::Matches($text, '!??\[\[(?[^\]]+)\]\]')) { $linkCount += 1 - $target = ($match.Groups['target'].Value -split '\|', 2)[0] - $target = ($target -split '#', 2)[0].Trim() - if (-not $target) { continue } + $linkBody = ($match.Groups['target'].Value -split '\|', 2)[0] + $targetParts = $linkBody -split '#', 2 + $target = $targetParts[0].Trim() + $anchor = if ($targetParts.Count -gt 1) { [uri]::UnescapeDataString($targetParts[1].Trim()) } else { $null } $target = $target -replace '\\', '/' $resolved = @() $extension = [System.IO.Path]::GetExtension($target) - if ($extension -and $extension.ToLowerInvariant() -ne '.md') { + if (-not $target -and $anchor) { + $resolved = @($note.FullName) + } + elseif (-not $target) { + continue + } + elseif ($extension -and $extension.ToLowerInvariant() -ne '.md') { $relativeAsset = [System.IO.Path]::GetFullPath((Join-Path $note.DirectoryName $target)) $rootAsset = [System.IO.Path]::GetFullPath((Join-Path $contentFull $target)) if (Test-Path -LiteralPath $relativeAsset -PathType Leaf) { $resolved += $relativeAsset } @@ -71,6 +78,24 @@ foreach ($note in $notes) { elseif ($resolved.Count -gt 1 -and $target -notmatch '[/\\]') { $errors += "$sourceRelative -> [[$($match.Groups['target'].Value)]] is ambiguous across $($resolved.Count) files" } + elseif ($anchor -and $resolved.Count -eq 1 -and [System.IO.Path]::GetExtension($resolved[0]).ToLowerInvariant() -eq '.md') { + $destinationText = Get-Content -Raw -LiteralPath $resolved[0] -Encoding utf8 + if ($anchor.StartsWith('^')) { + $blockId = [regex]::Escape($anchor.Substring(1)) + if ($destinationText -notmatch "(?m)\\^$blockId\\s*$") { + $errors += "$sourceRelative -> [[$($match.Groups['target'].Value)]] has no matching block anchor" + } + } + else { + $wantedHeading = ($anchor -replace '\s+', ' ').Trim().ToLowerInvariant() + $headings = @([regex]::Matches($destinationText, '(?m)^#{1,6}\s+(?.+?)\s*#*\s*$') | ForEach-Object { + (($_.Groups['heading'].Value -replace '\s+', ' ').Trim()).ToLowerInvariant() + }) + if ($headings -notcontains $wantedHeading) { + $errors += "$sourceRelative -> [[$($match.Groups['target'].Value)]] has no matching heading anchor" + } + } + } } } diff --git a/scripts/lore/test-lore-tools.ps1 b/scripts/lore/test-lore-tools.ps1 index ee66002..0b47f7d 100644 --- a/scripts/lore/test-lore-tools.ps1 +++ b/scripts/lore/test-lore-tools.ps1 @@ -12,8 +12,8 @@ if (-not $fixtureFull.StartsWith($tempBase + [System.IO.Path]::DirectorySeparato try { $content = Join-Path $fixtureFull "Kalsa" New-Item -ItemType Directory -Path $content | Out-Null - Set-Content -LiteralPath (Join-Path $content "index.md") -Encoding utf8 -Value "# Fixture`n`nSee [[Institution]]." - Set-Content -LiteralPath (Join-Path $content "Institution.md") -Encoding utf8 -Value "# Institution`n`nA bounded test note." + Set-Content -LiteralPath (Join-Path $content "index.md") -Encoding utf8 -Value "# Fixture`n`nSee [[Institution#Bounded authority]]." + Set-Content -LiteralPath (Join-Path $content "Institution.md") -Encoding utf8 -Value "# Institution`n`n## Bounded authority`n`nA bounded test note." & (Join-Path $PSScriptRoot "verify-seed.ps1") & (Join-Path $PSScriptRoot "check-wikilinks.ps1") -ContentRoot $content @@ -34,6 +34,20 @@ try { } Remove-Item -LiteralPath $broken -Force + $brokenAnchor = Join-Path $content "Broken Anchor.md" + Set-Content -LiteralPath $brokenAnchor -Encoding utf8 -Value "# Broken Anchor`n`nSee [[Institution#No such heading]]." + $caughtBrokenAnchor = $false + try { + & (Join-Path $PSScriptRoot "check-wikilinks.ps1") -ContentRoot $content | Out-Null + } + catch { + $caughtBrokenAnchor = $true + } + if (-not $caughtBrokenAnchor) { + throw "Wikilink checker failed to reject a missing heading anchor" + } + Remove-Item -LiteralPath $brokenAnchor -Force + $forbidden = Join-Path $content "workshop" New-Item -ItemType Directory -Path $forbidden | Out-Null $caughtBoundary = $false @@ -47,7 +61,7 @@ try { throw "Publication checker failed to reject a nested workshop directory" } - Write-Output "Lore tool tests passed, including broken-link and publication-boundary negative checks." + Write-Output "Lore tool tests passed, including broken-link, broken-anchor, and publication-boundary negative checks." } finally { $resolvedFixture = [System.IO.Path]::GetFullPath($fixtureFull)