DocumentWMI.psm1

function New-WmiHelp {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Namespace = 'root\cimv2',
        [Parameter(Position = 1)]
        [string]$OutputPath = '.',
        [string]$TargetClass,
        [hashtable]$Metadata,
        [switch]$CreateValueTables,
        [switch]$IncludeInventoryClasses,
        [switch]$IncludePropertyTable
    )

    ###########
    # Helper functions
    ###########

    function hash2yaml {
        param(
            [Parameter(Mandatory=$true,
                    Position=0)]
            [hashtable]$meta,

            [Parameter(ParameterSetName='AsCode',
                    Mandatory=$true)]
            [switch]$AsCodeBlock,

            [Parameter(ParameterSetName='AsYaml',
                    Mandatory=$true)]
            [switch]$AsYamlDoc
        )

        if ($AsCodeBlock) {
            $header = '```yaml'
            $footer = '```'
        }

        if ($AsYamlDoc) {
            $header = '---'
            $footer = '---'
        }

        $header
        ForEach ($key in ($meta.keys | Sort-Object)) {
            '{0}: {1}' -f $key, $meta.$key
        }
        $footer
    }
    function Add-SingleLine {
        param([string]$line)
        $null = $mdContent.AppendLine($line)
    }
    function Add-MultipleLines {
        [Parameter(Mandatory=$true)]
        param([string[]]$yaml)

        foreach ($line in $yaml) {
            $null = $mdContent.AppendLine($line)
        }
    }
    function Write-QualifierBlock {
        param(
            [System.Management.QualifierDataCollection]$qualifiers,
            [string[]]$exclusions
        )

        ### Create collection of value qualifiers
        if ($CreateValueTables) {
            $vhash = @{}
            $exclusions += $valueQualifiers
            foreach ($q in $qualifiers) {
                if ($q.name -in $valueQualifiers) {
                    $vhash.Add("$($q.Name)","$($q.Value -join '~!~')")
                }
            }
        }

        ### Create collection of qualifier (may exclude value qualifiers)
        $qhash = @{}
        foreach ($q in $qualifiers) {
            if ($q.name -notin $exclusions) {
                $qhash.Add("$($q.Name)","$($q.Value -join ', ')")
            }
        }

        ### Output values as a table
        if ($CreateValueTables) {
            if ($vhash.ContainsKey('Values')) {
                Add-MultipleLines 'Possible values include:', ''
                $hasMap = $false
                if ($vhash.ContainsKey('ValueMap')) {
                    $hasMap = $true
                    $ValueMap = $vhash.ValueMap -split '~!~'
                }
                $Values = $vhash.Values -split '~!~'
                Add-MultipleLines '| ValueMap | Value |', '|---|---|'
                for ($x=0; $x -lt $Values.Count; $x++) {
                    if ($hasMap) {
                        Add-SingleLine ('| {0} | {1} |' -f $ValueMap[$x], $Values[$x])
                    } else {
                        Add-SingleLine ('| {0} | {1} |' -f $x, $Values[$x])
                    }
                }
                Add-SingleLine ''
            }
            elseif ($vhash.ContainsKey('BitValues')){
                Add-MultipleLines 'Possible BitValues include:', ''
                $hasMap = $false
                if ($vhash.ContainsKey('BitMap')) {
                    $hasMap = $true
                    $ValueMap = $vhash.BitMap -split '~!~'
                }
                $Values = $vhash.BitValues -split '~!~'
                Add-MultipleLines '| BitMap | Value |', '|---|---|'
                for ($x=0; $x -lt $Values.Count; $x++) {
                    if ($hasMap) {
                        Add-SingleLine ('| {0} | {1} |' -f $ValueMap[$x], $Values[$x])
                    } else {
                        Add-SingleLine ('| {0} | {1} |' -f $x, $Values[$x])
                    }
                }
                Add-SingleLine ''
            }
            elseif ($vhash.ContainsKey('Enumeration')) {
                Add-MultipleLines 'Possible enumeration values include:', ''
                $vhash.Enumeration -split ',' | ForEach-Object {
                    Add-SingleLine ('- {0}' -f $_.Trim())
                }
                Add-SingleLine ''
            }
            elseif ($vhash.ContainsKey('Bits')) {
                Add-MultipleLines 'Possible bit values include:', ''
                $vhash.Bits -split ',' | ForEach-Object {
                    Add-SingleLine ('- {0}' -f $_.Trim())
                }
                Add-SingleLine ''
            }
        }

        ### Output qualifiers
        Add-MultipleLines (hash2yaml $qhash -AsCodeBlock)
        Add-SingleLine ''
    }
    function Get-MethodSyntax {
        param ([System.Management.MethodData]$method)

        $name = $method.Name
        $rvType = ''
        $parms = @()
        $text = @()

        foreach ($i in $method.InParameters.Properties) {
            $parms += " [in] $($i.Type) $($i.Name.ToString())"
        }
        foreach ($o in $method.OutParameters.Properties) {
            if ($o.Name -eq 'ReturnValue') {
                $rvType = $o.Type
            } else {
                $parms += " [out] $($o.Type) $($o.Name)"
            }
        }

        $text += 'Syntax'
        $text += ''
        $text += '```c'
        if ($parms.Count -eq 0) {
            $text += "$rvType $name();"
        } else {
            $text += "$rvType $name("
            for ($x=0; $x -lt $parms.Count-1; $x++) {
                $text += $parms[$x] + ','
            }
            $text += $parms[$x]
            $text += ');'
        }
        $text += '```'
        $text
    }
    function Get-QualifierValue($Qualifiers, $Name) {
        if ($Name -eq 'Description') {
            $r = '{{ Insert description }}'
        } else {
            $r = $null
        }
        foreach ($q in $Qualifiers) {
            if ($q.Name -eq $Name) {
                $r = $q.Value
                break
            }
        }

        if ($r) {
            $output = @()
            $r -split "`n" | ForEach-Object {
                $tmp = ($_ -replace '\r').Trim()
                if ($tmp -ne '') { $output += $tmp }
            }
            $r = $output -join "`r`n`r`n"
        }
        $r
    }
    function Test-QualifierExists($Qualifiers, $Name) {
        $r = $false

        foreach ($q in $Qualifiers) {
            if ($q.Name -eq $Name) {
                $r = $true
                break
            }
        }

        return $r
    }

    ###########
    # Initialize WMI
    ###########

    if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory $OutputPath -Force }

    $classQuery = 'SELECT * FROM meta_class WHERE NOT __Class LIKE "[_][_]%" AND NOT __Class LIKE "CIM[_]%" AND NOT __Class LIKE "Win32_Perf%" AND NOT __Class LIKE "MSFT[_]%" AND NOT __Class LIKE "SMS_CM_RES_COLL_%"'
    if (-not $IncludeInventoryClasses) {
        $classQuery += ' AND NOT __Class LIKE "SMS_G_System%" AND NOT __Class LIKE "SMS_GH_System%" AND NOT __Class LIKE "SMS_GEH_System%"'
    }

    If ($TargetClass) {
        $classQuery = "SELECT * FROM meta_class WHERE __Class = '{0}'" -f $TargetClass
    }

    $scope = [System.Management.ManagementScope]::new($Namespace)
    $objectQuery = [System.Management.ObjectQuery]::new($classQuery)

    $enumOptions = [System.Management.EnumerationOptions]::new()
    $enumOptions.EnumerateDeep = $true
    $enumOptions.UseAmendedQualifiers = $true

    $searcher = [System.Management.ManagementObjectSearcher]::new($scope, $objectQuery, $enumOptions)

    $valueQualifiers = 'Values', 'ValueMap', 'Enumeration', 'BitMap', 'BitValues', 'Bits'

    $articleMetadata = @{
        title = ''
        description = ''
        'ms.date' = get-date -Format 'MM/dd/yyyy'
    }

    if ($null -ne $Metadata) {
        foreach ($key in $Metadata.keys) {
            if ($articleMetadata.ContainsKey($key)) {
                $articleMetadata[$key] = $Metadata[$key]
            } else {
                $articleMetadata.Add($key,$Metadata[$key])
            }
        }
    }

    ###########
    # Process class information
    ###########

    foreach ($class in $searcher.Get()) {
        if ($class.Name -eq '') {
            $class.Name = $class.ClassPath.ClassName
        }
        Write-Host "== $($class.Name) =="

        $classFileName = "$($class.Name).md".ToLower()

        $mdContent = [System.Text.StringBuilder]::new()

        $articleMetadata.title = "$($class.Name) class"
        $articleMetadata.description = "$($class.Name) class in $Namespace"
        Add-MultipleLines (hash2yaml $articleMetadata -AsYamlDoc)
        Add-MultipleLines "# $($class.Name) class", ''

        $hasQualifers = $false
        foreach ($q in $class.Qualifiers) {
            if ($q.name -notin 'Description','ResID','ResDLL','Icon') {
                $hasQualifers = $true
            }
        }

        if ($hasQualifers) {
            Add-MultipleLines 'Class Qualifiers', ''
            Write-QualifierBlock -qualifiers $class.Qualifiers -exclusions 'Description','ResID','ResDLL','Icon'
        }

        if ($IncludePropertyTable) {
            Add-MultipleLines 'Property list', ''

            foreach ($p in $class.Properties) {
                $anchor = '#' + $p.Name.ToLower()
                Add-SingleLine "- [$($p.Name)]($anchor)"
            }
            Add-SingleLine ''
        }

        $classDescription = Get-QualifierValue -Qualifiers ($class.Qualifiers) -Name "Description"
        Add-MultipleLines '## Description', '', $classDescription, '', '## Properties', ''

        foreach ($p in $class.Properties) {
            Write-Host " Property: $($p.Name)"
            $propDescription = Get-QualifierValue -Qualifiers ($p.Qualifiers) -Name "Description"

            Add-MultipleLines "### $($p.Name)", '', $propDescription, ''
            Write-QualifierBlock -qualifiers $p.Qualifiers -exclusions 'Description','ResID','ResDLL'
        }

        Add-MultipleLines '## Methods', ''

        if ($class.Methods.Count -eq 0) {
            Add-MultipleLines 'This class has no methods.', ''
        }
        else {
            foreach ($m in ($class.Methods | Sort-Object Name)) {
                $methodFileName = "$($class.Name)-$($m.Name)-method.md".ToLower()
                Add-SingleLine "- [$($m.Name)]($methodFileName)"
            }
            Add-SingleLine ''
        }

        Add-MultipleLines '## Notes', '',
                        '{{ Insert optional notes }}', '',
                        '## Related links', '',
                        '{{ Insert optional links }}'

        $mdContent.ToString() | Out-File (Join-Path $OutputPath $classFileName) -Encoding utf8 -Force

    ###########
    # Process method information
    ###########

        if ($class.Methods.Count -gt 0) {

            foreach ($m in $class.Methods) {
                Write-Host " Method: $($m.Name)"
                $methodFileName = "$($class.Name)-$($m.Name)-method.md".ToLower()
                $mdContent = [System.Text.StringBuilder]::new()

                $articleMetadata.title = "$($m.Name)() method of $($class.Name) class"
                $articleMetadata.description = "$($m.Name)() method of $($class.Name) class in $Namespace"
                Add-MultipleLines (hash2yaml $articleMetadata -AsYamlDoc)
                Add-MultipleLines "# $($m.Name)() method of $($class.Name) class", ''

                $hasQualifers = $false
                foreach ($q in $m.Qualifiers) {
                    if ($q.name -ne 'Description') {
                        $hasQualifers = $true
                    }
                }

                if ($hasQualifers) {
                    Add-MultipleLines 'Method qualifiers', ''
                    Write-QualifierBlock -qualifiers $m.Qualifiers -exclusions 'Description'
                }

                Add-MultipleLines (Get-MethodSyntax $m)
                Add-SingleLine ''

                $methodDescription = Get-QualifierValue -Qualifiers ($m.Qualifiers) -Name "Description"
                $methodStatic = Test-QualifierExists -Qualifiers ($m.Qualifiers) -Name "Static"

                $lines = '## Description', '', $methodDescription, '', '> [!NOTE]'
                if ($methodStatic) {
                    $lines += '> This method is static, which means you can use this method without creating an instance of this class.'
                }
                else {
                    $lines += '> This method is not static, which means you need an instance of this class to execute this method.'
                }
                Add-MultipleLines $lines

                Add-MultipleLines '', '## Input Parameters', ''

                if ($m.InParameters.Properties.Count -eq 0) {
                    Add-MultipleLines 'This method has no input parameters.', ''
                }
                else {
                    foreach ($i in $m.InParameters.Properties) {
                        $iDescription = Get-QualifierValue -Qualifiers ($i.Qualifiers) -Name "Description"
                        Add-MultipleLines "### $($i.Name)", '', $iDescription, ''
                        Write-QualifierBlock -qualifiers $i.Qualifiers -exclusions 'Description','In'
                    }
                }

                Add-MultipleLines '## Output Parameters', ''
                if ($m.OutParameters.Properties.Count -eq 0) {
                    Add-MultipleLines 'This method has no output parameters.', ''
                }
                else {
                    foreach ($o in $m.OutParameters.Properties) {
                        if ($o.Name -eq 'ReturnValue') {
                            $rvdesc = Get-QualifierValue -Qualifiers ($o.Qualifiers) -Name "Description"
                            $rvtype = Get-QualifierValue -Qualifiers ($o.Qualifiers) -Name "CIMTYPE"
                            if ($m.OutParameters.Properties.Count -eq 1) {
                                Add-MultipleLines 'This method has no output parameters.', ''
                            }
                        } else {
                            $oDescription = Get-QualifierValue -Qualifiers ($o.Qualifiers) -Name "Description"
                            Add-MultipleLines "### $($o.Name)", '', $oDescription, ''
                            Write-QualifierBlock -qualifiers $o.Qualifiers -exclusions 'Description','ResID','ResDLL','Icon'
                        }
                    }
                }

                Add-MultipleLines '## Return value', '',
                                $rvdesc, '',
                                "Type: $rvtype", '',
                                '## Notes', '',
                                '{{ Insert optional notes }}', '',
                                '## Related links', '',
                                '{{ Insert optional links }}'

                $mdContent.ToString() | Out-File (Join-Path $OutputPath $methodFileName) -Encoding utf8 -Force
            }
        }
        Write-Host
    }
}

function New-WmiHelpToc {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Position=0)]
        [string]$SourcePath = '.',
        [Parameter(Position=1)]
        [string]$OutputPath = '.',
        [string]$TocBasePath,
        [int]$StartDepth = 0
    )

    ###########
    # Helper functions
    ###########
    function get-yamlblock {
        param($mdpath)
        $doc = Get-Content $mdpath
        $start = $end = -1
        $hdr = ""

        for ($x = 0; $x -lt 30; $x++) {
        if ($doc[$x] -eq '---') {
            if ($start -eq -1) {
            $start = $x + 1
            } else {
            if ($end -eq -1) {
                $end = $x - 1
                break
            }
            }
        }
        }
        if ($end -gt $start) {
        $hdr = $doc[$start..$end]
        $hdr
        }
    }
    function get-metadata {
        param(
            $path,
            [switch]$Recurse
        )

        foreach ($file in (dir -rec:$Recurse -file $path)) {
            $ignorelist = 'keywords','helpviewer_keywords','ms.assetid'
            $lines = get-yamlblock $file
            $meta = @{}
            foreach ($line in $lines) {
                $i = $line.IndexOf(':')
                if ($i -ne -1) {
                    $key = $line.Substring(0,$i)
                    if (!$ignorelist.Contains($key)) {
                        $value = $line.Substring($i+1).replace('"','')
                        switch ($key) {
                            'title' {
                                $value = $value.split('|')[0].trim()
                            }
                            'ms.date' {
                                $value = Get-Date $value -Format 'MM/dd/yyyy'
                            }
                            Default {
                                $value = $value.trim()
                            }
                        }
                        $meta.Add($key,$value)
                    }
                }
            }
            [pscustomobject]$meta
        }
    }

    ###########
    # Validate Parameters
    ###########
    if (-not (Test-Path $SourcePath -PathType Container)) {
        Write-Error ('Source path is not a valid folder: {0}' -f $SourcePath)
        exit 2 # ERROR_FILE_NOT_FOUND
    }
    if (-not (Test-Path $OutputPath -PathType Container)) {
        Write-Error ('Output path is not a valid folder: {0}' -f $SourcePath)
        exit 2 # ERROR_FILE_NOT_FOUND
    }

    ###########
    # Initialize
    ###########

    $TocFile = Join-Path $OutputPath 'toc.yml'
    $SourcePath = Join-Path $SourcePath '*.md'
    if ($TocBasePath -ne '') {
        if (-not $TocBasePath.EndsWith('/')) {
            $TocBasePath += '/'
        }
    }

    $hasMethods = $false
    $indentLevel = $StartDepth
    $tab = ' '
    $nodeTemplate = @'
{indent}- name: {title}
{indent} items:
'@

    $itemTemplate = @'
{indent}- name: {title}
{indent} href: {filepath}
'@


    ###########
    # Collect files
    ###########
    Write-Verbose 'Collecting class files...'
    $classFiles = dir $SourcePath -Exclude *-method.md | Select-Object BaseName, FullName, Name

    Write-Verbose 'Collecting method files...'
    $methodFiles = dir -path $SourcePath -Include "*-method.md" | Select-Object BaseName, FullName, Name
    foreach ($class in $classFiles) {
        $m = $methodFiles | Where-Object Name -like "$($class.BaseName)-*"
        $class | Add-Member -MemberType NoteProperty -Name Methods -Value $m
    }

    ###########
    # Process files
    ###########
    Write-Verbose "Creating TOC file: $TocFile"
    $null = New-Item $TocFile -Force

    foreach ($class in $classFiles) {
        $filemetadata = get-metadata -path $class.FullName
        $title = $filemetadata.title
        $hasMethods = $class.Methods.Count -gt 0

        if ($hasMethods) {
            $node = $nodeTemplate -replace '{indent}', ($tab * $indentLevel)
            $node = $node -replace '{title}', $title
            Add-Content -Path $TocFile -Value $node
            $indentLevel++
        }
        $node = $itemTemplate -replace '{indent}', ($tab * $indentLevel)
        $node = $node -replace '{title}', $title
        $node = $node -replace '{filepath}', ($TocBasePath + $class.Name)
        Add-Content -Path $TocFile -Value $node

        if ($hasMethods) {
            foreach($method in $class.Methods){
                $filemetadata = get-metadata -path $method.FullName
                $title = $filemetadata.title
                $node = $itemTemplate -replace '{indent}', ($tab * $indentLevel)
                $node = $node -replace '{title}', $title
                $node = $node -replace '{filepath}', ($TocBasePath + $method.Name)
                Add-Content -Path $TocFile -Value $node
            }
            $indentLevel--
        }
    }

    Get-Item $TocFile
}