Formatting PowerShell Script for My Blog

In a previous post, I described how I format code for my blog (i.e. by copying it from Visual Studio or SQL Server Management Studio and then running a simple console application to convert the RTF clipboard text to HTML).

I recently started doing some significant work with PowerShell and therefore I wanted to have a similar method of formatting PowerShell scripts for various blog posts. Note that the approach I currently use for other code snippets wouldn't work very well with PowerShell scripts, primarily because Visual Studio doesn't currently provide syntax highlighting for PowerShell scripts.

Consequently I went looking for a secondary solution for formatting PowerShell scripts. A little searching on the Internet quickly led to a blog post by Lee Holmes:

More PowerShell Syntax Highlighting

https://www.leeholmes.com/blog/MorePowerShellSyntaxHighlighting.aspx

While Lee's script outputs nicely formatted script as HTML, I decided to modify his approach a little. It's not that there's really anything wrong with Lee's script. Rather, given the simplicity of parsing PowerShell scripts into various tokens, I decided that it would be preferable (and not to mention a great PowerShell exercise for me) to output semantic HTML that is subsequently formatted with CSS instead of via inline style attributes.

Here is the updated script that I ended up with (I've also attached it to this post for easier downloading):

 ################################################################################
# Set-ClipboardScriptHtmlBlock.ps1
#
# Copies the entire contents of the currently selected Windows PowerShell ISE
# editor window to the clipboard. The copied data can be pasted into any
# application that supports pasting in UnicodeText or HTML format. Text pasted
# in HTML format will be formatted according to the specified CSS styles.
#
# Originally based on the Set-ClipboardScript.ps1 developed by Lee Holmes:
#
#    https://www.leeholmes.com/blog/MorePowerShellSyntaxHighlighting.aspx
#
# Updated to output semantic HTML (instead of inline styles) and to apply
# formatting via CSS.
#
################################################################################
 
[CmdletBinding()]
param($path)
 
function Get-ScriptName
{
    $myInvocation.ScriptName
}
 
if($path -and ([Threading.Thread]::CurrentThread.ApartmentState -ne "STA"))
{
    PowerShell -NoProfile -STA -File (Get-ScriptName) $path
    return
}

$cssClassMappings = @{
    'Attribute' = 'attribute'
    'Command' = 'command'
    'CommandArgument' = 'commandArgument'
    'CommandParameter' = 'commandParameter'
    'Comment' = 'comment'
    'GroupEnd' = $null
    'GroupStart' = $null
    'Keyword' = 'keyword'
    'LineContinuation' = 'lineContinuation'
    'LoopLabel' = 'loopLabel'
    'Member' = 'member'
    'NewLine' = 'newLine'
    'Number' = 'number'
    'Operator' = 'operator'
    'Position' = 'position'
    'StatementSeparator' = 'statementSeparator'
    'String' = 'string'
    'Type' = 'userType'
    'Unknown' = $null
    'Variable' = 'variable'    
}

$styles = "<style type='text/css'>
code .attribute {
 color: #2b91af;
}
code .command {
 color: #00f;
}
code .commandArgument {
 color: #8a2be2;
}
code .commandParameter {
 color: #000080;
}
code .comment {
 color: #008000;
}
code .keyword {
 color: #00f;
}
code .number {
 color: #800080;
}
code .operator {
 color: #666;
}
code .string {
 color: #a31515;
}
code .userType {
 color: #2b91af;
}
code .variable {
 color: #ff4500;
}
div.codeBlock, div.consoleBlock, div.logExcerpt {
 background-color: #F4F4F4;
 border: 1px solid gray;
 cursor: text;
 font-family: 'Courier New',courier,monospace;
 margin: 10px 2px;
 max-height: 250px;
 overflow: auto;
 padding: 4px;
 width: 97.5%;
}
div.codeBlock pre, div.logExcerpt pre {
 margin: 0;
}
</style>"
 
Add-Type -Assembly System.Web
Add-Type -Assembly PresentationCore
 
# Generate an HTML span and append it to HTML string builder
$currentLine = 1
function Append-HtmlSpan ($block, $tokenType)
{
    if (($tokenType -eq 'NewLine') -or ($tokenType -eq 'LineContinuation'))
    {
        if($tokenType -eq 'LineContinuation')
        {
            $null = $codeBuilder.Append('`')
        }
        
        $null = $codeBuilder.Append("`r`n")
        $SCRIPT:currentLine++
    }
    else
    {
        $block = [System.Web.HttpUtility]::HtmlEncode($block)
        
        if($tokenType -eq 'String')
        {
            $lines = $block -split "`r`n"
            $block = ""
 
            $multipleLines = $false
            foreach($line in $lines)
            {
                if($multipleLines)
                {
                    $block += "`r`n"
                    
                    $SCRIPT:currentLine++
                }
 
                $newText = $line.TrimStart()
                $newText = " " * ($line.Length - $newText.Length) + $newText                    
                $block += $newText
                $multipleLines = $true
            }
        }
        
        $cssClass = $cssClassMappings[$tokenType]
        
        If ($cssClass -ne $null)
        {
            $null = $codeBuilder.Append(
                "<span class='$cssClass'>$block</span>")
        }
        Else
        {
            $null = $codeBuilder.Append($block)
        }
    }
}

function GetHtmlClipboardFormat($html)
{
    $header = @"
Version:1.0
StartHTML:0000000000
EndHTML:0000000000
StartFragment:0000000000
EndFragment:0000000000
StartSelection:0000000000
EndSelection:0000000000
SourceURL:file:///about:blank
<!DOCTYPE HTML PUBLIC `"-//W3C//DTD HTML 4.0 Transitional//EN`">
<HTML>
<HEAD>
<TITLE>HTML Clipboard</TITLE>
__STYLES__
</HEAD>
<BODY>
<!--StartFragment-->
__HTML__
<!--EndFragment-->
</BODY>
</HTML>
"@

    $header = $header.Replace("__STYLES__", $styles)
 
    $startFragment = $header.IndexOf("<!--StartFragment-->") +
        "<!--StartFragment-->".Length + 2
    $endFragment = $header.IndexOf("<!--EndFragment-->") +
        $html.Length - "__HTML__".Length
    $startHtml = $header.IndexOf("<!DOCTYPE")
    $endHtml = $header.Length + $html.Length - "__HTML__".Length
    $header = $header -replace "StartHTML:0000000000",
        ("StartHTML:{0:0000000000}" -f $startHtml)
    $header = $header -replace "EndHTML:0000000000",
        ("EndHTML:{0:0000000000}" -f $endHtml)
    $header = $header -replace "StartFragment:0000000000",
        ("StartFragment:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndFragment:0000000000",
        ("EndFragment:{0:0000000000}" -f $endFragment)
    $header = $header -replace "StartSelection:0000000000",
        ("StartSelection:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndSelection:0000000000",
        ("EndSelection:{0:0000000000}" -f $endFragment)    
    $header = $header.Replace("__HTML__", $html)
    
    Write-Verbose $header
    $header
}
 
function Main
{
    $text = $null
    
    if($path)
    {
        $text = (Get-Content $path) -join "`r`n"
    }
    else
    {
        if (-not $psise.CurrentFile)
        {
            Write-Error 'No script is available for copying.'
            return
        }
        
        $text = $psise.CurrentFile.Editor.Text
    }
 
    trap { break }
 
    # Do syntax parsing.
    $errors = $null
    $tokens = [system.management.automation.psparser]::Tokenize($text,
        [ref] $errors)
 
    # Initialize HTML builder.
    $codeBuilder = new-object system.text.stringbuilder
    $SCRIPT:currentLine++
    
    # Iterate over the tokens and set the colors appropriately.
    $position = 0
    foreach ($token in $tokens)
    {
        if ($position -lt $token.Start)
        {
            $block = $text.Substring($position, ($token.Start - $position))
            $tokenType = 'Unknown'
            Append-HtmlSpan $block $tokenType
        }
        
        $block = $text.Substring($token.Start, $token.Length)
        $tokenType = $token.Type.ToString()
        Append-HtmlSpan $block $tokenType
        
        $position = $token.Start + $token.Length
    }
 
    # Copy console screen buffer contents to clipboard in two formats -
    # text and HTML.
    $code = $codeBuilder.ToString()
    
    $codeBlock =
        "<div class='codeBlock'><pre><code>" + $code + "</code></pre></div>"
    
    $html = GetHtmlClipboardFormat($codeBlock)
        
    $dataObject = New-Object Windows.DataObject
    $dataObject.SetText([string]$codeBlock, [Windows.TextDataFormat]"UnicodeText")
    
    $dataObject.SetText([string]$html, [Windows.TextDataFormat]"Html")
 
    [Windows.Clipboard]::SetDataObject($dataObject, $true)
}
 
. Main

Now, all I have to do is run the Set-ClipboardScriptHtmlBlock script from within the Windows PowerShell ISE (with a different script window active) and the contents of the active script are copied to the clipboard. From there I can paste the clipboard contents into the source window of Expression Web (currently my blog editor of choice).

I then specify rules similar to the following in the custom CSS for my blog:

 code .attribute {
   color: #2b91af;
}
code .command {
   color: #00f;
}
code .commandArgument {
   color: #8a2be2;
}
code .commandParameter {
   color: #000080;
}
code .comment {
   color: #008000;
}
code .keyword {
   color: #00f;
}
code .number {
   color: #800080;
}
code .operator {
   color: #666;
}
code .string {
   color: #a31515;
}
code .userType {
   color: #2b91af;
}
code .variable {
   color: #ff4500;
}
div.codeBlock {
   background-color: #F4F4F4;
   border: 1px solid gray;
   cursor: text;
   font-family: 'Courier New',courier,monospace;
   margin: 10px 2px;
   max-height: 250px;
   overflow: auto;
   padding: 4px;
   width: 97.5%;
}
div.codeBlock pre {
   margin: 0;
}

Note that this CSS is also embedded in the generated HTML to support pasting into other applications (e.g. Microsoft Word). Also note that I generalized the class names for the CSS rules a little bit, just in case I later decide to generate semantic markup for other types of code (e.g. C#).

If you want your PowerShell scripts to appear differently in your blog posts, all you would need to do is tweak the CSS accordingly.

Kudos to Lee for providing such a solid foundation to build upon!

Set-ClipboardScriptHtmlBlock.zip