From 3582d0afb2b7c02d7fa04ae530a29f6459569075 Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Sun, 4 Oct 2026 15:45:15 +0200 Subject: [PATCH] docs: generate the wiki pages from Docs - .github/scripts/Export-WikiContent.ps1 converts Docs, except the contributor guide, into flat wiki pages: Docs/README.md becomes Home, cmdlet pages lose their platyPS metadata, links point to wiki pages or to the files on GitHub, and links in code stay unchanged. It writes a sidebar from the cmdlet groups of Docs/README.md, a footer, and the former page How-to-install, and keeps the page name Version-History that the release notes link to. - Tests/Wiki.Tests.ps1 checks the conversion rules with a sample of Docs and every link and anchor of the wiki generated from the real Docs. - README, CHANGELOG, and the version history mention the wiki again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- .github/scripts/Export-WikiContent.ps1 | 279 +++++++++++++++++++++++++ CHANGELOG.md | 6 + Docs/Version-History.md | 4 +- README.md | 4 + Tests/Wiki.Tests.ps1 | 228 ++++++++++++++++++++ 5 files changed, 519 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/Export-WikiContent.ps1 create mode 100644 Tests/Wiki.Tests.ps1 diff --git a/.github/scripts/Export-WikiContent.ps1 b/.github/scripts/Export-WikiContent.ps1 new file mode 100644 index 0000000..38c3365 --- /dev/null +++ b/.github/scripts/Export-WikiContent.ps1 @@ -0,0 +1,279 @@ +<# +.SYNOPSIS + Converts the documentation in the Docs folder into the pages of the GitHub wiki. + +.DESCRIPTION + Writes a wiki page for every page in Docs except the contributor guide (Contributing.md and the Contributing + folder). Docs/README.md becomes the page Home, and every other page keeps its file name, so a page in + Docs/Cmdlets becomes a page named after its cmdlet. The platyPS metadata at the top of the cmdlet pages is + removed. Relative links point to the wiki pages; links to other files of the repository point to the files on + GitHub. Links in code stay unchanged. + + The script also writes the sidebar, which lists the cmdlets in the groups of the cmdlet list in Docs/README.md; + the footer; and the page How-to-install, which keeps the address of the former wiki page working. + + The script removes everything in DestinationPath except the .git folder, so that pages that no longer exist in + Docs disappear from the wiki. + +.PARAMETER Path + Specifies the Docs folder of the repository. + +.PARAMETER DestinationPath + Specifies the folder to write the wiki pages to, usually a clone of the wiki repository. The script creates the + folder if it doesn't exist. + +.PARAMETER RepositoryUrl + Specifies the address of the repository on GitHub, for links to files that aren't wiki pages. + +.PARAMETER Branch + Specifies the branch for links to files that aren't wiki pages. + +.EXAMPLE + git clone https://github.com/raandree/NTFSSecurity.wiki.git $env:TEMP\wiki + .\.github\scripts\Export-WikiContent.ps1 -Path .\Docs -DestinationPath $env:TEMP\wiki + git -C $env:TEMP\wiki status + + Writes the wiki pages into a clone of the wiki and shows which pages change. +#> +[CmdletBinding(SupportsShouldProcess)] +param ( + [Parameter(Mandatory)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] + [string] + $Path, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $RepositoryUrl = 'https://github.com/raandree/NTFSSecurity', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $Branch = 'master' +) + +$ErrorActionPreference = 'Stop' + +function Resolve-RepositoryPath { + <# + Returns the path of a link target relative to the repository root, with / as separator, or nothing if the + link leaves the repository. + #> + param ( + [Parameter(Mandatory)] + [string] + $Directory, + + [Parameter(Mandatory)] + [string] + $Link + ) + + $segments = New-Object -TypeName 'System.Collections.Generic.List[string]' + foreach ($segment in (('{0}/{1}' -f $Directory, $Link) -split '/')) { + if ($segment -eq '..') { + if ($segments.Count -eq 0) { + return + } + $segments.RemoveAt($segments.Count - 1) + } elseif ($segment -and $segment -ne '.') { + $segments.Add($segment) + } + } + + $segments -join '/' +} + +function ConvertTo-WikiLink { + <# + Returns the wiki address of a link on a page in Directory: the name of a wiki page, the address of a file of + the repository on GitHub, or the link itself if it is absolute or points to the same page. + #> + param ( + [Parameter(Mandatory)] + [string] + $Url, + + [Parameter(Mandatory)] + [string] + $Directory + ) + + if ($Url -match '^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|//|#)') { + return $Url + } + + $linkPath, $anchor = $Url -split '#', 2 + $target = Resolve-RepositoryPath -Directory $Directory -Link ([uri]::UnescapeDataString($linkPath)) + if (-not $target) { + return $Url + } + + $fragment = if ($anchor) { '#' + $anchor } else { '' } + if ($pageNames.ContainsKey($target)) { + return $pageNames[$target] + $fragment + } + + $view = if (Test-Path -LiteralPath (Join-Path -Path $repositoryRoot -ChildPath $target) -PathType Container) { + 'tree' + } else { + 'blob' + } + '{0}/{1}/{2}/{3}{4}' -f $RepositoryUrl.TrimEnd('/'), $view, $Branch, $target, $fragment +} + +function Convert-MarkdownLink { + <# + Returns the Markdown text with the links of a page in Directory converted to wiki addresses. Links in code + spans and fenced code blocks stay unchanged. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'Directory', Justification = 'The match evaluator script block uses it.' + )] + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] + $Markdown, + + [Parameter(Mandatory)] + [string] + $Directory + ) + + $inlinePattern = '(?(?`+).+?\k)|(?!?\[(?:[^\[\]`]|`[^`]*`)*\]\()(?[^)\s]+)(?(?:\s+"[^"]*")?\))' + $definitionPattern = '^(?\s{0,3}\[[^\]]+\]:\s*)(?\S+)(?.*)$' + $convertLink = { + param ($match) + + if ($match.Groups['code'].Success) { + return $match.Value + } + $match.Groups['prefix'].Value + (ConvertTo-WikiLink -Url $match.Groups['url'].Value -Directory $Directory) + + $match.Groups['suffix'].Value + } + + $fence = $null + $lines = foreach ($line in ($Markdown -split '\r?\n')) { + if ($line -match '^\s{0,3}(?`{3,}|~{3,})') { + if (-not $fence) { + $fence = $Matches['fence'] + } elseif ($Matches['fence'].StartsWith($fence)) { + $fence = $null + } + $line + continue + } + if ($fence) { + $line + continue + } + + $line = [regex]::Replace($line, $inlinePattern, $convertLink) + [regex]::Replace($line, $definitionPattern, $convertLink) + } + + $lines -join "`n" +} + +$docsRoot = (Resolve-Path -LiteralPath $Path).ProviderPath.TrimEnd('\', '/') +$repositoryRoot = Split-Path -Path $docsRoot -Parent +$docsName = Split-Path -Path $docsRoot -Leaf +$destinationRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath).TrimEnd('\', '/') + +# Refuse a destination that contains the documentation, such as the repository itself. +$separator = [IO.Path]::DirectorySeparatorChar +if (($docsRoot + $separator).StartsWith($destinationRoot + $separator, [StringComparison]::OrdinalIgnoreCase)) { + throw "The destination '$destinationRoot' contains the documentation in '$docsRoot'. Specify a clone of the wiki." +} + +# Collect the pages: Docs/README.md is the home page, and the contributor guide stays in the repository. +$pageNames = @{} +$pages = foreach ($file in Get-ChildItem -LiteralPath $docsRoot -Filter '*.md' -File -Recurse) { + $repositoryPath = $file.FullName.Substring($repositoryRoot.Length + 1) -replace '\\', '/' + if ($repositoryPath -match ('^{0}/Contributing(?:\.md$|/)' -f [regex]::Escape($docsName))) { + continue + } + + $name = if ($repositoryPath -eq "$docsName/README.md") { 'Home' } else { $file.BaseName } + if ($pageNames.Values -contains $name) { + throw "Two pages in '$docsRoot' would become the wiki page '$name'." + } + $pageNames[$repositoryPath] = $name + + $content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 + $title = if ($content -match '(?m)^#\s+(?.+?)\s*$') { $Matches['title'] } else { $name -replace '-', ' ' } + [pscustomobject]@{ + Name = $name + Title = $title + Directory = $repositoryPath.Substring(0, $repositoryPath.LastIndexOf('/')) + Content = $content + } +} + +# Remove the old pages, but keep the history of the wiki. +if (Test-Path -LiteralPath $destinationRoot) { + foreach ($item in Get-ChildItem -LiteralPath $destinationRoot -Force | Where-Object -Property Name -NE -Value '.git') { + if ($PSCmdlet.ShouldProcess($item.FullName, 'Remove')) { + Remove-Item -LiteralPath $item.FullName -Recurse -Force + } + } +} elseif ($PSCmdlet.ShouldProcess($destinationRoot, 'Create folder')) { + New-Item -ItemType Directory -Path $destinationRoot | Out-Null +} + +$wikiPages = [ordered]@{} +foreach ($page in $pages) { + $content = [regex]::Replace($page.Content, '\A---\r?\n.*?\r?\n---[ \t]*(?:\r?\n|\z)\s*', '', 'Singleline') + $wikiPages[$page.Name] = Convert-MarkdownLink -Markdown $content -Directory $page.Directory +} + +# The sidebar links the other pages and the cmdlets, grouped like the cmdlet list of the home page. +$homePage = $pages | Where-Object -Property Name -EQ -Value 'Home' +$sidebar = New-Object -TypeName 'System.Collections.Generic.List[string]' +$sidebar.Add('### [Home](Home)') +$sidebar.Add('') +foreach ($page in $pages | Where-Object { $_.Directory -eq $docsName -and $_.Name -ne 'Home' } | Sort-Object -Property Name) { + $sidebar.Add(('- [{0}]({1})' -f $page.Title, $page.Name)) +} +if ($homePage) { + $section = $null + foreach ($line in ($homePage.Content -split '\r?\n')) { + if ($line -match '^##\s+(?<title>.+?)\s*$') { + $section = $Matches['title'] + if ($section -eq 'Cmdlets') { + $sidebar.Add('') + $sidebar.Add('### Cmdlets') + } + } elseif ($section -eq 'Cmdlets' -and $line -match '^###\s+(?<title>.+?)\s*$') { + $sidebar.Add('') + $sidebar.Add(('**{0}**' -f $Matches['title'])) + $sidebar.Add('') + } elseif ($section -eq 'Cmdlets' -and $line -match '^\|\s*\[(?<name>[^\]]+)\]\((?<url>[^)\s]+)\)') { + $sidebar.Add(('- [{0}]({1})' -f $Matches['name'], (ConvertTo-WikiLink -Url $Matches['url'] -Directory $docsName))) + } + } +} +$wikiPages['_Sidebar'] = $sidebar -join "`n" + +$wikiPages['_Footer'] = ('This wiki is generated from the [{0}]({1}/tree/{2}/{0}) folder of the repository. ' + + 'To change a page, edit its file there; changes made in the wiki are overwritten.') -f $docsName, $RepositoryUrl.TrimEnd('/'), $Branch + +# The former wiki page How-to-install is linked from outside; it now points to the installation steps. +if ($homePage -and $homePage.Content -match '(?m)^##\s+Installation\s*$') { + $wikiPages['How-to-install'] = "# How to install`n`nThe installation steps are in the [Installation](Home#installation) section of the [Home](Home) page." +} + +$encoding = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList $false +foreach ($name in $wikiPages.Keys) { + $file = Join-Path -Path $destinationRoot -ChildPath "$name.md" + if ($PSCmdlet.ShouldProcess($file, 'Write wiki page')) { + [IO.File]::WriteAllText($file, $wikiPages[$name].TrimEnd() + "`n", $encoding) + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dac731..bc909f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The format is based on ## [Unreleased] +### Added + +- Publish the documentation in the + [wiki](https://github.com/raandree/NTFSSecurity/wiki), generated from the + `Docs` folder after every change, with a sidebar that lists all cmdlets + ## [5.0.0] - 2026-10-04 ### Changed diff --git a/Docs/Version-History.md b/Docs/Version-History.md index ad11586..6f8ea40 100644 --- a/Docs/Version-History.md +++ b/Docs/Version-History.md @@ -1,8 +1,8 @@ # Version history This page lists the changes in NTFSSecurity 4.2.6 and earlier. It replaces -the version history in the former GitHub wiki. For the changes since 4.2.6, -see the [changelog](../CHANGELOG.md). +the hand-written version history that the GitHub wiki kept until 2018. For the +changes since 4.2.6, see the [changelog](../CHANGELOG.md). ## 4.2.5 and 4.2.6 diff --git a/README.md b/README.md index 42ee37f..7173284 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Get-ChildItem2 -Path C:\Data -Recurse | Get-NTFSAccess -ExcludeInherited ## Documentation +Read the documentation in the +[wiki](https://github.com/raandree/NTFSSecurity/wiki), which is generated +from the `Docs` folder, or in the folder itself: + - [Overview](Docs/README.md): features, requirements, and the list of cmdlets - [Concepts](Docs/Concepts.md): security descriptors, access rights, inheritance, privileges, long paths, and module settings diff --git a/Tests/Wiki.Tests.ps1 b/Tests/Wiki.Tests.ps1 new file mode 100644 index 0000000..4b61742 --- /dev/null +++ b/Tests/Wiki.Tests.ps1 @@ -0,0 +1,228 @@ +<# + Tests .github\scripts\Export-WikiContent.ps1, which converts the documentation in Docs into the pages of the + GitHub wiki: with a small sample of Docs for the conversion rules, and with the real Docs for complete pages and + working links. +#> +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' +)] +param () + +BeforeAll { + $exportScript = Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\Export-WikiContent.ps1' + + function Get-GitHubAnchor { + # Returns the anchors that GitHub generates for the headings of a Markdown text. + param ( + [Parameter(Mandatory)] + [string] + $Markdown + ) + + $text = [regex]::Replace($Markdown, '(?ms)^```.*?^```', '') + $seen = @{} + foreach ($match in [regex]::Matches($text, '(?m)^#{1,6}\s+(.+?)\s*#*\s*$')) { + $heading = $match.Groups[1].Value -replace '\[([^\]]*)\]\([^)]*\)', '$1' -replace '[`*]', '' + $anchor = [regex]::Replace($heading.Trim().ToLowerInvariant(), '[^\p{L}\p{Nd}\s_-]', '') -replace ' ', '-' + if ($seen.ContainsKey($anchor)) { + $seen[$anchor]++ + '{0}-{1}' -f $anchor, $seen[$anchor] + } else { + $seen[$anchor] = 0 + $anchor + } + } + } +} + +Describe 'Export-WikiContent.ps1' { + Context 'When it converts a sample of Docs' { + BeforeAll { + $repositoryPath = Join-Path -Path $TestDrive -ChildPath 'repository' + $docsPath = Join-Path -Path $repositoryPath -ChildPath 'Docs' + $wikiPath = Join-Path -Path $TestDrive -ChildPath 'wiki' + foreach ($folder in "$docsPath\Cmdlets", "$docsPath\Contributing", "$wikiPath\.git") { + New-Item -ItemType Directory -Path $folder -Force | Out-Null + } + + # A clone of the wiki with pages that Docs doesn't have + Set-Content -LiteralPath "$wikiPath\.git\config" -Value '[core]' + Set-Content -LiteralPath "$wikiPath\Version-History.textile" -Value '* 4.2.4' + Set-Content -LiteralPath "$wikiPath\Old-Page.md" -Value '# Old page' + + Set-Content -LiteralPath "$repositoryPath\CHANGELOG.md" -Value '# Changelog' + Set-Content -LiteralPath "$docsPath\Contributing.md" -Value '# Contributing' + Set-Content -LiteralPath "$docsPath\Contributing\01-Getting-Started.md" -Value '# Get started' + Set-Content -LiteralPath "$docsPath\Concepts.md" -Value "# Concepts`n`n## Rights`n`nSee [Home](README.md)." + Set-Content -LiteralPath "$docsPath\README.md" -Value @' +# Thing + +[Concepts](Concepts.md#rights), [Get-Thing](Cmdlets/Get-Thing.md), and [`Set-Thing`](Cmdlets/Set-Thing.md). +[Changelog](../CHANGELOG.md), [guide](Contributing.md), and [first steps](Contributing/01-Getting-Started.md). +[Example site](https://example.com/page.md) and [cmdlets](#cmdlets). +Code keeps its links: `[Concepts](Concepts.md)`. + +```powershell +# [Concepts](Concepts.md) +``` + +## Installation + +Install the module. + +## Cmdlets + +### Getting + +| Cmdlet | Description | +| --- | --- | +| [Get-Thing](Cmdlets/Get-Thing.md) | Gets a thing. | + +### Setting + +| Cmdlet | Description | +| --- | --- | +| [Set-Thing](Cmdlets/Set-Thing.md) | Sets a thing. | +'@ + $metadata = "---`nexternal help file: Thing.dll-Help.xml`nonline version: https://example.com`nschema: 2.0.0`n---`n`n" + Set-Content -LiteralPath "$docsPath\Cmdlets\Get-Thing.md" -Value ($metadata + + "# Get-Thing`n`nSee [rights](../Concepts.md#rights), [Set-Thing](Set-Thing.md), and [home](../README.md).") + Set-Content -LiteralPath "$docsPath\Cmdlets\Set-Thing.md" -Value ($metadata + + "# Set-Thing`n`nSee [Get-Thing](Get-Thing.md).") + + & $exportScript -Path $docsPath -DestinationPath $wikiPath -RepositoryUrl 'https://github.com/contoso/Thing' -Branch 'main' + + $homePage = Get-Content -LiteralPath "$wikiPath\Home.md" -Raw + $sidebar = Get-Content -LiteralPath "$wikiPath\_Sidebar.md" -Raw + } + + It 'Should write Home, Concepts, the cmdlet pages, How-to-install, the sidebar, and the footer' { + $expected = 'Home.md', 'Concepts.md', 'Get-Thing.md', 'Set-Thing.md', 'How-to-install.md', '_Sidebar.md', '_Footer.md' + + ((Get-ChildItem -LiteralPath $wikiPath -File).Name | Sort-Object) -join ', ' | + Should -BeExactly (($expected | Sort-Object) -join ', ') + } + + It 'Should keep the .git folder and remove the pages that Docs does not have' { + "$wikiPath\.git\config" | Should -Exist + "$wikiPath\Version-History.textile" | Should -Not -Exist + "$wikiPath\Old-Page.md" | Should -Not -Exist + } + + It 'Should not publish the contributor guide' { + Get-ChildItem -LiteralPath $wikiPath -Recurse -File -Filter '*Get*Started*' | Should -BeNullOrEmpty + "$wikiPath\Contributing.md" | Should -Not -Exist + } + + It 'Should convert <Link> on <Page> into <Expected>' -ForEach @( + @{ Page = 'Home'; Link = '[Concepts](Concepts.md#rights)'; Expected = '[Concepts](Concepts#rights)' } + @{ Page = 'Home'; Link = '[Get-Thing](Cmdlets/Get-Thing.md)'; Expected = '[Get-Thing](Get-Thing)' } + @{ Page = 'Home'; Link = '[`Set-Thing`](Cmdlets/Set-Thing.md)'; Expected = '[`Set-Thing`](Set-Thing)' } + @{ Page = 'Home'; Link = '[Changelog](../CHANGELOG.md)'; Expected = '[Changelog](https://github.com/contoso/Thing/blob/main/CHANGELOG.md)' } + @{ Page = 'Home'; Link = '[guide](Contributing.md)'; Expected = '[guide](https://github.com/contoso/Thing/blob/main/Docs/Contributing.md)' } + @{ Page = 'Home'; Link = '[first steps](Contributing/01-Getting-Started.md)'; Expected = '[first steps](https://github.com/contoso/Thing/blob/main/Docs/Contributing/01-Getting-Started.md)' } + @{ Page = 'Home'; Link = '[Example site](https://example.com/page.md)'; Expected = '[Example site](https://example.com/page.md)' } + @{ Page = 'Home'; Link = '[cmdlets](#cmdlets)'; Expected = '[cmdlets](#cmdlets)' } + @{ Page = 'Concepts'; Link = '[Home](README.md)'; Expected = '[Home](Home)' } + @{ Page = 'Get-Thing'; Link = '[rights](../Concepts.md#rights)'; Expected = '[rights](Concepts#rights)' } + @{ Page = 'Get-Thing'; Link = '[Set-Thing](Set-Thing.md)'; Expected = '[Set-Thing](Set-Thing)' } + @{ Page = 'Get-Thing'; Link = '[home](../README.md)'; Expected = '[home](Home)' } + ) { + $content = Get-Content -LiteralPath "$wikiPath\$Page.md" -Raw + + $content | Should -Match ([regex]::Escape($Expected)) + if ($Link -ne $Expected) { + $content | Should -Not -Match ([regex]::Escape($Link)) + } + } + + It 'Should keep links in code unchanged' { + $homePage | Should -Match ([regex]::Escape('`[Concepts](Concepts.md)`')) + $homePage | Should -Match ([regex]::Escape('# [Concepts](Concepts.md)')) + } + + It 'Should remove the platyPS metadata from the cmdlet pages' { + Get-Content -LiteralPath "$wikiPath\Get-Thing.md" -TotalCount 1 | Should -BeExactly '# Get-Thing' + } + + It 'Should link Home and the other pages in the sidebar' { + $sidebar | Should -Match ([regex]::Escape('[Home](Home)')) + $sidebar | Should -Match ([regex]::Escape('[Concepts](Concepts)')) + } + + It 'Should list the cmdlets in the sidebar under the groups of Docs/README.md' { + $sidebar | Should -Match '(?s)Getting.*\[Get-Thing\]\(Get-Thing\).*Setting.*\[Set-Thing\]\(Set-Thing\)' + } + + It 'Should say in the footer that the wiki is generated from Docs' { + Get-Content -LiteralPath "$wikiPath\_Footer.md" -Raw | + Should -Match ([regex]::Escape('(https://github.com/contoso/Thing/tree/main/Docs)')) + } + + It 'Should keep the address of the former page How-to-install' { + Get-Content -LiteralPath "$wikiPath\How-to-install.md" -Raw | Should -Match ([regex]::Escape('(Home#installation)')) + } + } + + Context 'When it converts the documentation of the repository' { + BeforeAll { + $repositoryPath = Join-Path -Path $PSScriptRoot -ChildPath '..' + $docsPath = Join-Path -Path $repositoryPath -ChildPath 'Docs' + $wikiPath = Join-Path -Path $TestDrive -ChildPath 'wiki-of-the-repository' + + & $exportScript -Path $docsPath -DestinationPath $wikiPath + + $pages = Get-ChildItem -LiteralPath $wikiPath -Filter '*.md' + $cmdletNames = (Get-ChildItem -LiteralPath (Join-Path -Path $docsPath -ChildPath 'Cmdlets') -Filter '*.md').BaseName + } + + It 'Should write a page for every page in Docs except the contributor guide, and one for every cmdlet' { + $topPages = (Get-ChildItem -LiteralPath $docsPath -Filter '*.md' | + Where-Object -Property Name -NotIn -Value 'README.md', 'Contributing.md').BaseName + $expected = @('Home', 'How-to-install', '_Sidebar', '_Footer') + $topPages + $cmdletNames + + ($pages.BaseName | Sort-Object) -join ', ' | Should -BeExactly (($expected | Sort-Object) -join ', ') + } + + It 'Should keep the page name Version-History, which the release notes of 4.2.4 and 4.2.6 link to' { + Join-Path -Path $wikiPath -ChildPath 'Version-History.md' | Should -Exist + } + + It 'Should list every cmdlet in the sidebar' { + $sidebar = Get-Content -LiteralPath (Join-Path -Path $wikiPath -ChildPath '_Sidebar.md') -Raw + + $cmdletNames | Where-Object -FilterScript { $sidebar -notmatch ('\]\({0}\)' -f [regex]::Escape($_)) } | + Should -BeNullOrEmpty + } + + It 'Should link only to existing wiki pages, their anchors, and existing files of the repository' { + $anchors = @{} + foreach ($page in $pages) { + $anchors[$page.BaseName] = @(Get-GitHubAnchor -Markdown (Get-Content -LiteralPath $page.FullName -Raw)) + } + + $brokenLinks = foreach ($page in $pages) { + $text = [regex]::Replace((Get-Content -LiteralPath $page.FullName -Raw), '(?ms)^```.*?^```', '') + $text = [regex]::Replace($text, '`[^`\n]+`', '') + foreach ($match in [regex]::Matches($text, '\]\((?<url>[^)\s]+)\)')) { + $url = $match.Groups['url'].Value + if ($url -match '^https://github\.com/raandree/NTFSSecurity/(?:blob|tree)/master/(?<path>[^#]+)') { + if (-not (Test-Path -LiteralPath (Join-Path -Path $repositoryPath -ChildPath $Matches['path']))) { + '{0}: {1}' -f $page.BaseName, $url + } + } elseif ($url -notmatch '^[a-z]+:') { + $target, $anchor = $url -split '#', 2 + if (-not $target) { + $target = $page.BaseName + } + if (-not $anchors.ContainsKey($target) -or ($anchor -and $anchors[$target] -notcontains $anchor)) { + '{0}: {1}' -f $page.BaseName, $url + } + } + } + } + + $brokenLinks | Should -BeNullOrEmpty + } + } +}