Sharpdown.psm1
|
Set-StrictMode -Version Latest #### <h1 style="color: rgb(220, 166, 87);">🎆 Sharpdown</h1> #### #### > A polyglot commenting language for generated markdown from code comments #### #### --- #### #### | Language | File Types | #### | --- | --- | $script:SharpDownConfig = @{ CSharp = @{ Marker = '////' Fence = 'csharp' DeclPattern = '^(public|protected|internal|static|abstract|sealed|override|async)\b' #### | `CSharp` | .cs | Extensions = @('.cs') ExcludeRegex = '[\\/](bin|obj)[\\/]' } Sql = @{ Marker = '----' Fence = 'sql' DeclPattern = '^(CREATE|ALTER|DROP|SELECT|INSERT|UPDATE|DELETE|GRANT|REVOKE|WITH|COMMENT|CALL|TRUNCATE|MERGE)\b' #### | `Sql` | .sql | Extensions = @('.sql') ExcludeRegex = '[\\/](bin|obj)[\\/]' } JavaScript = @{ Marker = '////' Fence = 'javascript' DeclPattern = '^(export|import|class|function|async\s+function|type|interface)\b' #### | `JavaScript` | .js, .mjs, .ts, .tsx | Extensions = @('.js', '.mjs', '.ts', '.tsx') ExcludeRegex = '[\\/](node_modules|dist|build|\.next|coverage)[\\/]' } PowerShell = @{ Marker = '####' Fence = 'powershell' DeclPattern = '^(function|filter|class|enum|workflow|describe|it)\b' #### | `PowerShell` | .ps1, .psm1 | Extensions = @('.ps1', '.psm1') ExcludeRegex = '[\\/](bin|obj)[\\/]' } } #### <h2 style="color: #DCA657;">ConvertTo-RedactedPath</h2> #### function ConvertTo-RedactedPath { #### Swap the user's home directory for `~` so log lines never spill private paths. #### #### <b style="color: #D2A8FF;">Parameters</b> #### - `[string]`: __Path__ #### - *Path to redact.* #### param([Parameter(Mandatory)][string]$Path) #### <b style="color: #369FFF;">Returns</b> #### - `[string]` #### - *Same path with `$HOME` replaced by `~`.* $Path.Replace($HOME, '~') } #### --- #### <h2 style="color: #DCA657;">Convert-SharpDownContent</h2> #### function Convert-SharpDownContent { #### Stream a source file's marker comments and declaration lines as Markdown. #### #### <b style="color: #D2A8FF;">Parameters</b> #### param( #### #### - `[string]`: __Source__ #### - *Path to the source file.* [Parameter(Mandatory)][string]$Source, #### - `[hashtable]`: __Config__ #### - *Language entry from `$script:SharpDownConfig`.* [Parameter(Mandatory)][hashtable]$Config, #### - `[switch]`: __API__ #### - *Suppress auto-generated declaration fences. Only marker lines emit.* [switch]$API ) #### #### | Key | Description | #### | --- | --- | #### | `Marker` | The line prefix that signals a doc comment. | $marker = $Config.Marker #### | `Fence` | The code fence label applied to declarations. | $fence = $Config.Fence #### | `DeclPattern` | The regex that marks a line as a declaration. | $declPattern = $Config.DeclPattern #### #### --- #### Read and trim each source line so the tests ignore indentation. #### Get-Content $Source | ForEach-Object { $_.Trim() } | ForEach-Object { #### --- #### - A marker line is doc text: emit it with the prefix removed. #### if ($_.StartsWith($marker)) { $_.Substring($marker.Length) } #### - Otherwise a declaration line becomes a fenced signature, unless `-API`. #### elseif (-not $API -and $_ -match $declPattern) { #### - Drop the trailing brace so the fence holds just the declaration. #### $line = $_ -replace '\s*\{\s*$', '' #### - Fence the cleaned signature with the language's label. "``````$fence`n$line`n``````" #### } } #### <b style="color: #369FFF;">Returns</b> #### - `[string[]]` #### - *Doc lines, with declarations fenced unless `-API` is set.* } #### --- #### <h2 style="color: #DCA657;">Resolve-SharpDownTarget</h2> #### function Resolve-SharpDownTarget { #### Compute the mirrored Markdown output path for a source file under `-Recurse`. #### #### <b style="color: #D2A8FF;">Parameters</b> #### param( #### #### - `[string]`: __Root__ #### - *Walk root passed to `-Recurse`.* [Parameter(Mandatory)][string]$Root, #### - `[string]`: __Source__ #### - *Absolute path of the source file.* [Parameter(Mandatory)][string]$Source, #### - `[string]`: __ProjectName__ #### - *Leaf of `Root`. Stripped if it leads the relative path.* [Parameter(Mandatory)][string]$ProjectName, #### - `[string]`: __OutRoot__ #### - *Output root passed as `-OutPath`.* [Parameter(Mandatory)][string]$OutRoot ) #### $sep = [System.IO.Path]::DirectorySeparatorChar $parts = [System.IO.Path]::GetRelativePath($Root, $Source).Split($sep) if ($parts.Length -gt 1 -and $parts[0].ToLower() -eq 'src') { $parts = $parts[1..($parts.Length - 1)] } if ($parts.Length -gt 1 -and $parts[0] -eq $ProjectName) { $parts = $parts[1..($parts.Length - 1)] } Join-Path $OutRoot ([System.IO.Path]::ChangeExtension([string]::Join($sep, $parts), '.md')) #### <b style="color: #369FFF;">Returns</b> #### - `[string]` #### - *Absolute path of the `.md` to write.* } #### --- #### <h2 style="color: #DCA657;">Write-SharpDownFile</h2> #### function Write-SharpDownFile { #### Convert a single source file and write the result. Skips with a warning when the source has no SharpDown content. Creates the output directory on demand. #### #### <b style="color: #D2A8FF;">Parameters</b> #### param( #### #### - `[string]`: __Source__ #### - *Path to the source file.* [Parameter(Mandatory)][string]$Source, #### - `[string]`: __Target__ #### - *Path of the `.md` to write.* [Parameter(Mandatory)][string]$Target, #### - `[hashtable]`: __Config__ #### - *Language entry from `$script:SharpDownConfig`.* [Parameter(Mandatory)][hashtable]$Config, #### - `[switch]`: __API__ #### - *Forwarded to `Convert-SharpDownContent`. Suppresses auto-fenced declarations.* [switch]$API ) #### $output = Convert-SharpDownContent -Source $Source -Config $Config -API:$API if (-not $output) { Write-Warning "No SharpDown content in $(ConvertTo-RedactedPath $Source)" return } $dir = Split-Path -Parent $Target if ($dir -and -not (Test-Path $dir)) { [void](New-Item -ItemType Directory -Force -Path $dir) } $output | Out-File -FilePath $Target -Encoding UTF8 [PSCustomObject]@{ Source = ConvertTo-RedactedPath $Source Target = ConvertTo-RedactedPath $Target Lines = @($output).Count } #### <b style="color: #369FFF;">Returns</b> #### - `[PSCustomObject]` #### - `[string]`: __Source__ #### - *Redacted source path.* #### - `[string]`: __Target__ #### - *Redacted target path.* #### - `[int]`: __Lines__ #### - *Number of lines emitted to the target.* } #### --- #### <h2 style="color: #DCA657;">Invoke-SharpDownFile</h2> #### function Invoke-SharpDownFile { #### Validate the File-mode contract and convert one source. #### #### <b style="color: #D2A8FF;">Parameters</b> #### param( #### #### - `[string]`: __Path__ #### - *Source file path. Must be a leaf.* [Parameter(Mandatory)][string]$Path, #### - `[string]`: __OutPath__ #### - *Target `.md` path. Must not be an existing directory.* [Parameter(Mandatory)][string]$OutPath, #### - `[hashtable]`: __Config__ #### - *Language entry from `$script:SharpDownConfig`.* [Parameter(Mandatory)][hashtable]$Config, #### - `[switch]`: __API__ #### - *Forwarded to `Write-SharpDownFile`.* [switch]$API ) #### #### <b style="color: #C22514;">Throws</b> #### - When `Path` is not a file. #### - When `OutPath` resolves to an existing directory. if (-not (Test-Path $Path -PathType Leaf)) { throw "Path '$(ConvertTo-RedactedPath $Path)' is not a file." } if (Test-Path $OutPath -PathType Container) { throw "OutPath '$(ConvertTo-RedactedPath $OutPath)' is a directory. File mode writes to a file path." } Write-SharpDownFile -Source (Resolve-Path $Path).ProviderPath -Target $OutPath -Config $Config -API:$API } #### <h2 style="color: #DCA657;">Invoke-SharpDownTree</h2> #### function Invoke-SharpDownTree { #### Validate the Directory-mode contract and walk a source tree. #### #### <b style="color: #D2A8FF;">Parameters</b> #### param( #### #### - `[string]`: __Path__ #### - *Source root. Must be a directory.* [Parameter(Mandatory)][string]$Path, #### - `[string]`: __OutPath__ #### - *Output root. Must not be an existing file.* [Parameter(Mandatory)][string]$OutPath, #### - `[hashtable]`: __Config__ #### - *Language entry from `$script:SharpDownConfig`.* [Parameter(Mandatory)][hashtable]$Config, #### - `[string]`: __Language__ #### - *Language name used in the no-files warning.* [Parameter(Mandatory)][string]$Language, #### - `[switch]`: __API__ #### - *Forwarded to `Write-SharpDownFile` for each source file under the tree.* [switch]$API ) #### #### <b style="color: #C22514;">Throws</b> #### - When `Path` is not a directory. #### - When `OutPath` resolves to an existing file. if (-not (Test-Path $Path -PathType Container)) { throw "Path '$(ConvertTo-RedactedPath $Path)' is not a directory." } if (Test-Path $OutPath -PathType Leaf) { throw "OutPath '$(ConvertTo-RedactedPath $OutPath)' is a file. -Recurse writes a tree under a directory." } $root = (Resolve-Path $Path).ProviderPath $projectName = Split-Path -Leaf $root #### | Key | Description | #### | --- | --- | $files = Get-ChildItem -Path $root -Recurse -File | #### | `Extensions` | The set of file extensions walked under `-Recurse`. | Where-Object { $_.Extension -in $Config.Extensions } | #### | `ExcludeRegex` | The directory pattern skipped under `-Recurse`. | Where-Object { $_.FullName -notmatch $Config.ExcludeRegex } if (-not $files) { Write-Warning "No $Language files found under $(ConvertTo-RedactedPath $root)" return } Write-Host "SharpDown ($Language): $(@($files).Count) file(s) -> $(ConvertTo-RedactedPath $OutPath)" foreach ($file in $files) { $target = Resolve-SharpDownTarget -Root $root -Source $file.FullName -ProjectName $projectName -OutRoot $OutPath Write-SharpDownFile -Source $file.FullName -Target $target -Config $Config -API:$API } } #### <h2 style="color: #DCA657;">ConvertTo-SharpDown</h2> #### function ConvertTo-SharpDown { #### Public entry point. Convert a single source or a tree of sources to Markdown. #### #### Two parameter sets: #### #### - **File** is the default. `-Path` is a single source file, `-OutPath` is the `.md` target. #### - **Directory** requires `-Recurse`. `-Path` is a source directory, `-OutPath` is the output root. `-Path` accepts pipeline input from `Get-Item` and `Get-ChildItem -Directory`. #### #### <b style="color: #D2A8FF;">Parameters</b> #### [CmdletBinding(DefaultParameterSetName = 'File')] param( #### - `[string]`: __Language__ #### - *One of `CSharp`, `Sql`, `JavaScript`, `PowerShell`. Defaults to `CSharp`.* [ValidateSet('CSharp', 'Sql', 'JavaScript', 'PowerShell')] [string]$Language = 'CSharp', #### - `[string]`: __Path__ #### - *Source file in File set, source directory in Directory set. Pipeline-bound only in Directory set.* [Parameter(Mandatory = $true, ParameterSetName = 'File')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Directory')] [Alias('FullName', 'PSPath')] [string]$Path, #### - `[string]`: __OutPath__ #### - *Output file in File set, output root in Directory set.* [Parameter(Mandatory = $true)] [string]$OutPath, #### - `[switch]`: __Recurse__ #### - *Selects the Directory set.* [Parameter(ParameterSetName = 'Directory')] [switch]$Recurse, #### - `[switch]`: __API__ #### - *Suppresses auto-generated declaration fences. Use for HTTP endpoint files where the doc IS the wire contract, not the language signatures.* [switch]$API ) #### begin { $config = $script:SharpDownConfig[$Language] } process { switch ($PSCmdlet.ParameterSetName) { 'File' { Invoke-SharpDownFile -Path $Path -OutPath $OutPath -Config $config -API:$API } 'Directory' { Invoke-SharpDownTree -Path $Path -OutPath $OutPath -Config $config -Language $Language -API:$API } } } #### <b style="color: #369FFF;">Returns</b> #### - `[PSCustomObject[]]` #### - `[string]`: __Source__ #### - *Redacted source path.* #### - `[string]`: __Target__ #### - *Redacted target path.* #### - `[int]`: __Lines__ #### - *Number of lines emitted to the target.* } Export-ModuleMember -Function 'ConvertTo-SharpDown' |