Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1
📖 [Docs]: Style guides cover image accessibility, PowerShell matching, and requirement IDs#16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Marius Storhaug (MariusStorhaug)
merged 25 commits into
main
from
docs/15-style-guide-authoring-gapsJul 6, 2026
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
b1f585d
Add reference-style link, backtick, and image alt-text guidance to th…
MariusStorhaug cdeb3dd
Add matching-operator and read-only-constant idioms to the PowerShell…
MariusStorhaug c9a2a3a
Name the F#/N# bold requirement-identifier convention in Spec-Driven …
MariusStorhaug 97e6028
Render requirement identifiers in bold in Spec-Driven Development
MariusStorhaug e640c27
Clarify read-only versus constant in the PowerShell constants idiom
MariusStorhaug cf16a05
Align spec template requirement identifiers with the no-period bold c…
MariusStorhaug da0de18
Adopt FR/NFR identifiers with stable {#id} anchors, BCP 14 language, …
MariusStorhaug ddbbc41
Recognize explicit attr_list heading ids in the documentation link ch…
MariusStorhaug 3c6490c
Clarify the BCP 14 keyword set is not exhaustive in the spec guide
MariusStorhaug 62e0a16
Validate reference-style link definitions in the documentation link c…
MariusStorhaug a775139
Add the 'prefer .NET for the actual work' principle to the PowerShell…
MariusStorhaug 96e375c
Use native .NET for path resolution and existence checks in the link …
MariusStorhaug d889b58
Fully qualify [System.IO.Path]::GetFullPath in the PowerShell standard
MariusStorhaug 033f133
Report the normalized link target in link-checker error messages
MariusStorhaug c000fbb
Merge branch 'main' into docs/15-style-guide-authoring-gaps
MariusStorhaug 35334b9
Conform the documentation link checker to the PowerShell coding standard
MariusStorhaug 4cdc775
Validate anchor fragments case-sensitively in the link checker
MariusStorhaug 2227499
Support angle-bracketed reference-style destinations in the link checker
MariusStorhaug 351cba4
Merge main and align the PowerShell .NET guidance with #18
MariusStorhaug a144b19
Require a [Parameter()] attribute and a blank line per parameter, and…
MariusStorhaug 5ca4264
Add comment-based help and full parameter blocks to the link checker'…
MariusStorhaug 000a7f0
Scope comment-based help to public functions and match the Markdown e…
MariusStorhaug 41e1564
Strip single-quoted and parenthesised link titles, not just double-qu…
MariusStorhaug cf42bf4
Describe what [Parameter()] does without the inaccurate advanced-func…
MariusStorhaug 04637dd
Require comment-based help on every function, private included, and o…
MariusStorhaug File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -14,7 +14,8 @@ | ||
| - A heading anchor ('target.md#section', or a same-page '#section') must match | ||
| a heading in the target file. Slugs are computed the same way the site's | ||
| Markdown processor does, including the '_1', '_2' suffixes for duplicate | ||
| headings. | ||
| headings; an explicit attr_list id ('## Heading { #id }') is recognised as | ||
| the heading's anchor. | ||
| External links (http, https, mailto, tel), absolute paths, links inside fenced | ||
| code blocks, and links inside inline code spans are ignored on purpose. | ||
| @@ -37,27 +38,75 @@ $Root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) | ||
| $Docs = Join-Path $Root 'src/docs' | ||
| function ConvertTo-Slug { | ||
| param([string]$Heading) | ||
| # Mirror the site's Markdown TOC slugifier (python-markdown default): drop | ||
| # non-ASCII, remove punctuation except word characters / whitespace / hyphen, | ||
| # lowercase, then collapse whitespace and hyphen runs into a single hyphen. | ||
| $ascii = -join ([char[]] $Heading | Where-Object { [int] $_ -lt 128 }) | ||
| <# | ||
| .SYNOPSIS | ||
| Convert a heading to the anchor slug the site's Markdown processor emits. | ||
| .DESCRIPTION | ||
| Mirror python-markdown's default TOC slugifier: drop non-ASCII characters, | ||
| remove punctuation except word characters, whitespace, and hyphens, | ||
| lowercase the result, then collapse whitespace and hyphen runs into a | ||
| single hyphen. | ||
| .EXAMPLE | ||
| ConvertTo-Slug -Heading 'Prefer .NET for the actual work' | ||
| Returns 'prefer-net-for-the-actual-work'. | ||
| .OUTPUTS | ||
| [string] | ||
| #> | ||
| [CmdletBinding()] | ||
| param( | ||
| # The heading text to slugify. | ||
| [Parameter(Mandatory)] | ||
| [string] $Heading | ||
| ) | ||
| $ascii = $Heading -replace '[^\x00-\x7F]', '' | ||
| $clean = ($ascii -replace '[^\w\s-]', '').Trim().ToLowerInvariant() | ||
| return ($clean -replace '[\s-]+', '-') | ||
| } | ||
| function Get-HeadingSlug { | ||
| param([string]$Path) | ||
| # The anchor slugs a page exposes, matching the duplicate-slug suffixing | ||
| # ('_1', '_2', ...) the Markdown processor applies to repeated headings. | ||
| <# | ||
| .SYNOPSIS | ||
| Get the anchor slugs a Markdown file exposes. | ||
| .DESCRIPTION | ||
| Return each heading's anchor, matching the duplicate-slug suffixing | ||
| ('_1', '_2', ...) the Markdown processor applies to repeated headings. A | ||
| heading may also carry an explicit attr_list id ('## Heading { #id }'), | ||
| which the site renderer uses as the anchor verbatim, overriding the text | ||
| slug; those are recognised so links to '#id' validate. Fenced code blocks | ||
| are skipped. | ||
| .EXAMPLE | ||
| Get-HeadingSlug -Path ./src/docs/index.md | ||
| Returns the anchor slugs and explicit ids defined in index.md. | ||
| .OUTPUTS | ||
| [System.Collections.Generic.List[string]] | ||
| #> | ||
| [CmdletBinding()] | ||
| param( | ||
| # Path to the Markdown file to scan for heading anchors. | ||
| [Parameter(Mandatory)] | ||
| [string] $Path | ||
| ) | ||
| $slugs = [System.Collections.Generic.List[string]]::new() | ||
| $seen = @{} | ||
| $inFence = $false | ||
| foreach ($line in [System.IO.File]::ReadAllLines($Path)) { | ||
| if ($line -match '^\s*```') { $inFence = -not $inFence; continue } | ||
| if ($inFence) { continue } | ||
| if ($line -match '^#{1,6}\s+(.+?)\s*$') { | ||
| $base = ConvertTo-Slug $matches[1] | ||
| $text = $matches[1] | ||
| # An explicit attr_list id ('{ #id }' or '{: #id ... }') wins over | ||
| # the text slug, exactly as python-markdown's attr_list assigns it. | ||
| if ($text -match '\{\s*:?\s*#([-\w]+)[^}]*\}\s*$') { | ||
| $slugs.Add($matches[1]) | ||
| continue | ||
| } | ||
| $base = ConvertTo-Slug $text | ||
| if (-not $base) { continue } | ||
| if ($seen.ContainsKey($base)) { $seen[$base]++; $slugs.Add("${base}_$($seen[$base])") } | ||
| else { $seen[$base] = 0; $slugs.Add($base) } | ||
| @@ -66,15 +115,98 @@ function Get-HeadingSlug { | ||
| return $slugs | ||
| } | ||
| # Parse each target file's anchors once. | ||
| $slugCache = @{} | ||
| function Get-CachedSlug { | ||
| param([string]$Path) | ||
| <# | ||
| .SYNOPSIS | ||
| Get a file's heading slugs, parsing each file only once. | ||
| .DESCRIPTION | ||
| Memoise Get-HeadingSlug in the script-scoped $slugCache so a file that is | ||
| linked from many places is scanned a single time. | ||
| .EXAMPLE | ||
| Get-CachedSlug -Path ./src/docs/index.md | ||
| Returns index.md's anchor slugs, reading the file only on the first call. | ||
| .OUTPUTS | ||
| [System.Collections.Generic.List[string]] | ||
| #> | ||
| [CmdletBinding()] | ||
| param( | ||
| # Path to the Markdown file whose slugs are wanted. | ||
| [Parameter(Mandatory)] | ||
| [string] $Path | ||
| ) | ||
| if (-not $slugCache.ContainsKey($Path)) { $slugCache[$Path] = Get-HeadingSlug $Path } | ||
| return $slugCache[$Path] | ||
| } | ||
| $linkPattern = '\[[^\]]*\]\(([^)]+)\)' | ||
| function Get-LinkTargetIssue { | ||
| <# | ||
| .SYNOPSIS | ||
| Get the problem with a single relative Markdown link target, if any. | ||
| .DESCRIPTION | ||
| Validate one inline or reference-style link target: external links, | ||
| absolute site paths, and empty targets are ignored; a relative file must | ||
| exist; and a '#fragment' must match a heading anchor (case-sensitively) | ||
| either in the target file or on the same page. Return a human-readable | ||
| message when the target does not resolve, or nothing when it is valid. | ||
| .EXAMPLE | ||
| Get-LinkTargetIssue -Target '../reference/bar.md#setup' -File $file -Rel 'docs/foo.md' -LineNo 12 | ||
| Returns a message when bar.md or its '#setup' anchor is missing, otherwise nothing. | ||
| .OUTPUTS | ||
| [string] | ||
| #> | ||
| [CmdletBinding()] | ||
| param( | ||
| # The raw link target - a destination and an optional '#fragment'. | ||
| [Parameter(Mandatory)] | ||
| [string] $Target, | ||
| # The Markdown file the link appears in, used to resolve relative paths. | ||
| [Parameter(Mandatory)] | ||
| [System.IO.FileInfo] $File, | ||
| # The file's repository-relative path, for the reported message. | ||
| [Parameter(Mandatory)] | ||
| [string] $Rel, | ||
| # The 1-based line number the link is on, for the reported message. | ||
| [Parameter(Mandatory)] | ||
| [int] $LineNo | ||
| ) | ||
| $t = ($Target.Trim() -replace '\s+("[^"]*"|''[^'']*''|\([^)]*\))$', '') -replace '^<', '' -replace '>$', '' | ||
| if (-not $t) { return } | ||
| if ($t -match '^(https?:|mailto:|tel:|//)') { return } | ||
| $path, $frag = $t -split '#', 2 | ||
| if (-not $path) { | ||
| if ($frag -and ($frag -cnotin (Get-CachedSlug $File.FullName))) { | ||
| "${Rel}:${LineNo}: '#$frag' - no heading with that anchor on this page" | ||
| } | ||
| return | ||
| } | ||
| if ($path.StartsWith('/')) { return } # absolute site path - not resolvable here | ||
| $resolved = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($File.DirectoryName, $path)) | ||
| if (-not ([System.IO.File]::Exists($resolved) -or [System.IO.Directory]::Exists($resolved))) { | ||
| "${Rel}:${LineNo}: '$t' - target does not exist" | ||
| return | ||
| } | ||
| if ($frag -and $resolved.EndsWith('.md', [System.StringComparison]::OrdinalIgnoreCase) -and ($frag -cnotin (Get-CachedSlug $resolved))) { | ||
| "${Rel}:${LineNo}: '$t' - no heading '#$frag' in the target file" | ||
| } | ||
| } | ||
| # Inline links '[text](target)' and reference-style definitions '[label]: target'. | ||
| # The inline target may carry an optional title ("...", '...', or (...)); the | ||
| # nested-paren alternative keeps a parenthesised title from being truncated. The | ||
| # definition destination is either an angle-bracketed path (which may contain | ||
| # spaces) or a bare non-whitespace token. | ||
| $linkPattern = '\[[^\]]*\]\(([^()]*(?:\([^()]*\)[^()]*)*)\)' | ||
| $refDefPattern = '^\s*\[[^\]]+\]:\s+(<[^>]+>|\S+)' | ||
| $broken = [System.Collections.Generic.List[string]]::new() | ||
MariusStorhaug marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| foreach ($file in (Get-ChildItem -LiteralPath $Docs -Recurse -File -Filter *.md | Sort-Object FullName)) { | ||
| @@ -87,27 +219,16 @@ foreach ($file in (Get-ChildItem -LiteralPath $Docs -Recurse -File -Filter *.md | ||
| if ($inFence) { continue } | ||
| # Remove inline code spans so links shown as examples are not validated. | ||
| $scrubbed = $line -replace '`[^`]*`', '' | ||
| $lineNo = $n + 1 | ||
| foreach ($m in [regex]::Matches($scrubbed, $linkPattern)) { | ||
| $target = $m.Groups[1].Value.Trim() -replace '\s+"[^"]*"$', '' # strip optional link title | ||
| if (-not $target) { continue } | ||
| if ($target -match '^(https?:|mailto:|tel:|//)') { continue } | ||
| $lineNo = $n + 1 | ||
| $path, $frag = $target -split '#', 2 | ||
| if (-not $path) { | ||
| if ($frag -and ($frag -notin (Get-CachedSlug $file.FullName))) { | ||
| $broken.Add("${rel}:${lineNo}: '#$frag' - no heading with that anchor on this page") | ||
| } | ||
| continue | ||
| } | ||
| if ($path.StartsWith('/')) { continue } # absolute site path - not resolvable here | ||
| $resolved = [System.IO.Path]::GetFullPath((Join-Path $file.DirectoryName $path)) | ||
| if (-not (Test-Path -LiteralPath $resolved)) { | ||
| $broken.Add("${rel}:${lineNo}: '$target' - target does not exist") | ||
| continue | ||
| } | ||
| if ($frag -and $resolved.EndsWith('.md') -and ($frag -notin (Get-CachedSlug $resolved))) { | ||
| $broken.Add("${rel}:${lineNo}: '$target' - no heading '#$frag' in the target file") | ||
| } | ||
| $issue = Get-LinkTargetIssue -Target $m.Groups[1].Value -File $file -Rel $rel -LineNo $lineNo | ||
| if ($issue) { $broken.Add($issue) } | ||
| } | ||
| # Reference-style link definitions ('[label]: target') carry a relative | ||
| # target too; validate it the same way so those links do not slip past CI. | ||
| if ($scrubbed -match $refDefPattern) { | ||
| $issue = Get-LinkTargetIssue -Target $matches[1] -File $file -Rel $rel -LineNo $lineNo | ||
| if ($issue) { $broken.Add($issue) } | ||
| } | ||
| } | ||
| } | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -53,7 +53,10 @@ These rules are disabled or widened so they do not flag valid documentation — | ||
| - **Use sentence-style headings.** | ||
| - **Surround headings, lists, and fenced blocks with a blank line** for readability, even though the linter no longer enforces it. | ||
| - **Prefer relative links** within a repository; use the canonical published URL for cross-repository references. | ||
| - **Give a repeated or long link a reference-style definition** (`[text][ref]`, with `[ref]: url` listed below) so the prose stays readable and one edit updates every use. | ||
MariusStorhaug marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| - **Tag every code fence with a language** (` ```bash `, ` ```yaml `) so it is highlighted and converts cleanly when published. | ||
| - **Wrap code, commands, filenames, and identifiers in backticks** rather than bold or italic, so they read as code and do not lean on the emphasis the linter now allows freely. | ||
| - **Give every image descriptive alt text** — `` — so it serves screen readers and still says something when the image fails to load; use a relative path for images kept in the repository. | ||
| ## PowerShell code samples | ||
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
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
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
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.