Nov
8
Written by:
Jeff
11/8/2007 12:30 PM
Inspired in part by the
Windows PowerShell Graphical Help File, I wrote a PowerShell script that uses the PowerShell XML help files to generate HTML help topics that are then compiled into a CHM with Html Help Workshop. The advantage of the resulting file over the Windows PowerShell Graphical Help File is that help is generated for all Cmdlets installed on your system, not just the core Cmdlets that come with Windows PowerShell. The help manual also includes help for PSProviders and all of the "about" topics. The original formatting for the "about" topics is preserved, so they don't look quite as nice as the other topics. I tested this after installing the
PowerShell Community Extensions and the
PowerShell cmdlets for Active Directory by Quest Software, even though I personally don't use either snap-in. I would be interested to hear how the help looks for any other snap-ins available out there.
The resulting CHM is fully searchable, and all Cmdlet and "about" topic names link to their topic, so jumping from one topic to another is much easier.

The script generates all of the HTML topic files, a CSS file, an Html Help Contents file, and an Html Help Project file. The CHM is automatically compiled at the end of the script, but it would be fairly easy to update these files and then re-create the manual if you don't, for example, like the color scheme I chose.
Here is the script:
# Compile-Help.ps1
# by Jeff Hillman
#
# this script uses the text and XML PowerShell help files to generate HTML help
# for all PowerShell Cmdlets, PSProviders, and "about" topics. the help topics
# are compiled into a .chm file using HTML Help Workshop.
param( [string] $outDirectory = ".\PSHelp", [switch] $GroupByPSSnapIn )
function Html-Encode( [string] $value )
{
# System.Web.HttpUtility.HtmlEncode() doesn't quite get everything, and
# I don't want to load the System.Web assembly just for this. I'm sure
# I missed something here, but these are the characters I saw that needed
# to be encoded most often
$value = $value -replace "&(?![\w#]+;)", "&"
$value = $value -replace "<(?!!--)", "<"
$value = $value -replace "(?<!--)>", ">"
$value = $value -replace "’", "'"
$value = $value -replace '["“”]', """
$value = $value -replace "\n", "<br />"
$value
}
function Capitalize-Words( [string] $value )
{
$capitalizedString = ""
# convert the string to lower case and split it into individual words. for each one,
# capitalize the first character, and append it to the converted string
[regex]::Split( $value.ToLower(), "\s" ) | ForEach-Object {
$capitalizedString += ( [string]$_.Chars( 0 ) ).ToUpper() + $_.SubString( 1 ) + " "
}
$capitalizedString.Trim()
}
function Get-ParagraphedHtml( [string] $xmlText )
{
$value = ""
if ( $xmlText -match "<(\w+:)?para" )
{
$value = ""
$options = [System.Text.RegularExpressions.RegexOptions]::Singleline
foreach ( $match in [regex]::Matches( $xmlText,
"<(?:\w+:)?para[^>]*>(?<Text>.*?)</(?:\w+:)?para>", $options ) )
{
$value += "<p>$( Html-Encode $match.Groups[ 'Text' ].Value )</p>"
}
}
else
{
$value = Html-Encode $xmlText
}
$value
}
function Get-SyntaxHtml( [xml] $syntaxXml )
{
$syntaxHtml = ""
# generate the HTML for each form of the Cmdlet syntax
foreach ( $syntaxItem in $syntaxXml.syntax.syntaxItem )
{
if ( $syntaxHtml -ne "" )
{
$syntaxHtml += "<br /><br />`n"
}
$syntaxHtml += " $( $syntaxItem.name.get_InnerText().Trim() ) "
if ( $syntaxItem.parameter )
{
foreach ( $parameter in $syntaxItem.parameter )
{
$required = [bool]::Parse( $parameter.required )
$syntaxHtml += "<nobr>[-$( $parameter.name.get_InnerText().Trim() )"
if ( $required )
{
$syntaxHtml += "]"
}
if ( $parameter.parameterValue )
{
$syntaxHtml +=
" <$( $parameter.parameterValue.get_InnerText().Trim() )>"
}
if ( !$required )
{
$syntaxHtml += "]"
}
$syntaxHtml += "</nobr> "
}
}
$syntaxHtml += " <nobr>[<CommonParameters>]</nobr>"
}
$syntaxHtml.Trim()
}
function Get-ParameterHtml( [xml] $parameterXml )
{
$parameterHtml = ""
# generate HTML for each parameter
foreach ( $parameter in $parameterXml.parameters.parameter )
{
if ( $parameterHtml -ne "" )
{
$parameterHtml += " <br /><br />`n"
}
$parameterHtml +=
" <nobr><span class=`"boldtext`">-$( $parameter.name.get_InnerText().Trim() )"
if ( $parameter.parameterValue )
{
$parameterHtml += " <$( $parameter.parameterValue.get_InnerText().Trim() )>"
}
$parameterHtml += "</span></nobr>`n"
$parameterHtml += @"
<br />
<div id="contenttext">
$( Get-ParagraphedHtml $parameter.description.get_InnerXml().Trim() )
"@
if ( $parameter.possibleValues )
{
foreach ( $possibleValue in $parameter.possibleValues.possibleValue )
{
$parameterHtml += @"
$( $possibleValue.value.Trim() )<br />
"@
if ( $possibleValue.description.get_InnerText().Trim() -ne "" )
{
$parameterHtml += @"
<div id="contenttext">
$( Get-ParagraphedHtml $possibleValue.description.get_InnerXml().Trim() )
</div>
"@
}
}
}
$parameterHtml += @"
<br />
</div>
<table class="parametertable">
<tr>
<td>Required</td>
<td>$( $parameter.required )</td>
</tr>
<tr>
<td>Position</td>
<td>$( $parameter.position )</td>
</tr>
<tr>
<td>Accepts pipeline input</td>
<td>$( $parameter.pipelineInput )</td>
</tr>
<tr>
<td>Accepts wildcard characters</td>
<td>$( $parameter.globbing )</td>
</tr>
"@
if ( $parameter.defaultValue )
{
if( $parameter.defaultValue.get_InnerText().Trim() -ne "" )
{
$parameterHtml += @"
<tr>
<td>Default Value</td>
<td>$( $parameter.defaultValue.get_InnerText().Trim() )</td>
</tr>
"@
}
}
$parameterHtml += @"
</table>
"@
}
if ( $parameterHtml -ne "" )
{
$parameterHtml += " <br /><br />`n"
}
$parameterHtml += @"
<nobr><span class="boldtext"><CommonParameters></span></nobr>
<br />
<div id="contenttext">
<p>
For more information about common parameters, type "Get-Help about_commonparameters".
</p>
</div>
"@
$parameterHtml.Trim()
}
function Get-InputHtml( [xml] $inputXml )
{
$inputHtml = ""
$inputCount = 0
# generate HTML for each input type
foreach ( $inputType in $inputXml.inputTypes.inputType )
{
if ( $inputHtml -ne "" )
{
$inputHtml += " <br /><br />`n"
}
if ( $inputType.type.name.get_InnerText().Trim() -ne "" -or
$inputType.type.description.get_InnerText().Trim() -ne "" )
{
$inputHtml += " $( $inputType.type.name.get_InnerText().Trim() )`n"
$inputHtml += @"
<div id="contenttext">
$( Get-ParagraphedHtml $inputType.type.description.get_InnerXml().Trim() )
</div>
"@
$inputCount++
}
}
$inputHtml.Trim()
$inputCount
}
function Get-ReturnHtml( [xml] $returnXml )
{
$returnHtml = ""
$returnCount = 0
# generate HTML for each return value
foreach ( $returnValue in $returnXml.returnValues.returnValue )
{
if ( $returnHtml -ne "" )
{
$returnHtml += " <br /><br />`n"
}
if ( $returnValue.type.name.get_InnerText().Trim() -ne "" -or
$returnValue.type.description.get_InnerText().Trim() -ne "" )
{
$returnHtml += " $( $returnValue.type.name.get_InnerText().Trim() )`n"
$returnHtml += @"
<div id="contenttext">
$( Get-ParagraphedHtml $returnValue.type.description.get_InnerXml().Trim() )
</div>
"@
$returnCount++
}
}
$returnHtml.Trim()
$returnCount
}
function Get-ExampleHtml( [xml] $exampleXml )
{
$exampleHtml = ""
$exampleTotalCount = 0
$exampleCount = 0
foreach ( $example in $exampleXml.examples.example )
{
$exampleTotalCount++
}
# generate HTML for each example
foreach ( $example in $exampleXml.examples.example )
{
if ( $example.code -and $example.code.get_InnerText().Trim() -ne "" )
{
if ( $exampleHtml -ne "" )
{
$exampleHtml += " <br />`n"
}
if ( $exampleTotalCount -gt 1 )
{
$exampleHtml +=
" <nobr><span class=`"boldtext`">Example $( $exampleCount + 1 )</span></nobr>`n"
}
$exampleCodeHtml = "$( Html-Encode $example.introduction.get_InnerText().Trim() )" +
"$( Html-Encode $example.code.get_InnerText().Trim() )"
$exampleHtml += " <div class=`"syntaxregion`">$exampleCodeHtml</div>`n"
$foundFirstPara = $false
foreach ( $para in $example.remarks.para )
{
if ( $para.get_InnerText().Trim() -ne "" )
{
# the first para is generally the description of the example.
# other para tags usually contain sample output
if ( !$foundFirstPara )
{
$exampleHtml += @"
<div id="contenttext">
<p>
$( Html-Encode $para.get_InnerText().Trim() )
</p>
</div>
"@
$foundFirstPara = $true
}
else
{
$exampleHtml += @"
<pre class="syntaxregion">$( $( ( Html-Encode $para.get_InnerText().Trim() ) -replace "<br />", "`n" ) )</pre>
"@
}
}
}
$exampleCount++
}
}
$exampleHtml.Trim()
$exampleCount
}
function Get-TaskExampleHtml( [xml] $exampleXml )
{
$exampleHtml = ""
$exampleCount = 0
$exampleTotalCount = 0
foreach ( $example in $exampleXml.examples.example )
{
$exampleTotalCount++
}
# generate HTML for each example
foreach ( $example in $exampleXml.examples.example )
{
if ( $exampleHtml -ne "" )
{
$exampleHtml += " <br />`n"
}
if ( $exampleTotalCount -gt 1 )
{
$exampleHtml += " <nobr><span class=`"boldtext`">Example $( $exampleCount + 1 )</span></nobr>`n"
}
$exampleHtml += " <div>$( Get-ParagraphedHtml $example.introduction.get_InnerXml().Trim() )</div>`n"
$exampleCodeHtml = ( Html-Encode $example.code.Trim() ) -replace "<br />", "`n"
$exampleHtml += " <pre class=`"syntaxregion`">$exampleCodeHtml</pre>"
$exampleHtml += " <div>$( Get-ParagraphedHtml $example.remarks.get_InnerXml().Trim() )</div>`n"
$exampleCount++
}
$exampleHtml.Trim()
}
function Get-LinkHtml( [xml] $linkXml )
{
$linkHtml = ""
$linkCount = 0
# generate HTML for each related link
foreach ( $navigationLink in $linkXml.relatedLinks.navigationLink )
{
if ( $navigationLink.linkText -and `
( $helpHash.Keys | Foreach-Object { $_.ToUpper() } ) -contains $navigationLink.linkText.Trim().ToUpper() )
{
$linkHtml += " $( $navigationLink.linkText.Trim() )<br />`n"
$linkCount++
}
}
$linkHtml.Trim()
$linkCount
}
function Get-TaskHtml( [xml] $taskXml )
{
$taskHtml = ""
$taskCount = 0
foreach ( $task in $taskXml.tasks.task )
{
if ( $taskHtml -ne "" )
{
$taskHtml += " <br />`n"
}
$taskHtml += " <nobr><span class=`"bold