Verify wikilink heading anchors

This commit is contained in:
Meta Vens 2026-08-22 13:47:05 +02:00
commit d2f514cf16
2 changed files with 46 additions and 7 deletions

View file

@ -38,13 +38,20 @@ foreach ($note in $notes) {
$text = Get-Content -Raw -LiteralPath $note.FullName -Encoding utf8
foreach ($match in [regex]::Matches($text, '!??\[\[(?<target>[^\]]+)\]\]')) {
$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+(?<heading>.+?)\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"
}
}
}
}
}

View file

@ -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)