DBremen
11/9/2015 - 3:20 PM

Expand zen coding expression for PowerShell

Expand zen coding expression for PowerShell

#requires zenCoding.dll in \resources. Download via https://github.com/madskristensen/zencoding and compile.
function Get-ZenCode{
    [Alias("zenCode")]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true,
                   Position=0)]
        $zenCodeExpr,
        [Parameter(ValueFromPipeline=$true,
                   Position=1)]
        $InputObject,

        [Parameter(Position=2)]
        $outPath="",

        [Parameter(Position=3)]
        [switch]$show
    )
    if (-not ([Management.Automation.PSTypeName]'ZenCoding.HtmlParser').Type)
    {
        Add-Type -Path "$(Split-Path $PSScriptRoot -Parent)\resources\ZenCoding.dll" 
    }
    Add-Type -AssemblyName System.Web
    $allData = @($Input)
    if ($allData){
        $closingCurlyPositions = ([regex]::Matches($zenCodeExpr,'\$_.*?(?=\})(})')).Groups | 
            where {$_.Value -eq '}'} | select -ExpandProperty Index | sort -Descending
        $txtWithinCurlies = [regex]::Matches($zenCodeExpr,'(?<=\{).*?(?=\})')
        
        $pipelineVars = @([regex]::Matches($zenCodeExpr,'(\$_(\.\w+)*)')) 
        $firstIndex = ($pipelineVars | sort index | select -first 1).Index
        $pipelineVars = $pipelineVars | select -ExpandProperty Value
        $txtWithinCurlies = $txtWithinCurlies | select -ExpandProperty Value
        $txtWithinCurliesCount = $txtWithinCurlies | group -AsHashTable -AsString
        #if the expression contains a table, no unqualified pipelinVar ($_) and no headers, add the headers based on the property names
        if ($zenCodeExpr -like '*table*' -and $pipelineVars -notcontains '$_' -and $zenCodeExpr -notlike '*>th*'){
            $headerIndex = ([regex]::Matches($zenCodeExpr,'(?<=\>)table.*?(>)').Groups | 
                where {$_.Value -eq '>' -and $_.Index -lt $firstIndex} | 
                sort Index -Descending).Index + 1
            $headerExpr = 'tr>'
            $headerExpr += (($pipelineVars.SubString(3) | foreach { "th{$_}"}) -join '+') + '^'
            $zenCodeExpr = $zenCodeExpr.Insert($headerIndex,$headerExpr)
            $firstIndex += $headerExpr.Length
        }
        $txtWithinCurlies = $txtWithinCurlies.Trim() | where {$_ -like '*$_*'} | Get-Unique
        $pipelineVars=$pipelineVars.Trim() | Get-Unique
        $htReplacements = @{}
        #foreach ($curlyPos in $closingCurlyPositions){
         #   $zenCodeExpr = $zenCodeExpr.Insert($curlyPos+1,"*$($allData.count)")
        #}
        #add the multiplier and wrap the expression into parenthesis
        $zenCodeExpr = $zenCodeExpr.Insert($zenCodeExpr.Substring(0,$firstIndex).LastIndexOf('>')+1,'(')
        $zenCodeExpr += ")*$($allData.count)"
        foreach ($var in $txtWithinCurlies){
            $guid = [guid]::NewGuid().Guid + '_'
            $htReplacements.Add($guid,$var)
            $zenCodeExpr = $zenCodeExpr -Replace ([regex]::Escape($var) + '\b') ,($guid + '$')
        }
    }
    $zenCodeParser = New-Object ZenCoding.HtmlParser
    $txt = $zenCodeParser.Parse($zenCodeExpr)
    $i=1
    foreach ($item in $allData){
        foreach ($replacement in $htReplacements.GetEnumerator()) { 
            #cannot rely on zencoding numbering therefore use remove/insert instead of replacing all unique instances
            $sb = [scriptblock]::Create($replacement.Value.Replace('$_','$item'))
            $value=$sb.Invoke()
            #do once for each instance of the placeholder within the template
            $count = $txtWithinCurliesCount[$replacement.Value].Count
            for($j=0;$j -lt $count;$j++){
                $firstIndex = $txt.IndexOf($replacement.Key)
                #if the value is an array add a nested table with all its values
                if ($value.Count -gt 1){
                    $searchTxt = $txt.Substring(0,$firstIndex)
                    $startIndex = $searchTxt.LastIndexOf('<')+1
                    $enclosingTag = $searchTxt.Substring($startIndex,$searchTxt.LastIndexOf('>')-$startIndex)
                    #object, insert nested table with headers based on properties
                    if($value | Get-Member CreateObjRef -MemberType Method){
                        $tableExpr = 'table>tr>'
                        $props = ($value | Get-Member | where {$_.MemberType -like '*Property'}).Name
                        $tableExpr += (($props | foreach { "th{$_}"}) -join '+') + '^'
                        $tableExpr += '(tr>' + (($props | foreach { 'td{$_.' + "$_}" }) -join '+') + ')'
                        $replacementStr = $value | zenCode $tableExpr
                    }
                    #values handle according to enclosingtag
                    elseif($enclosingTag -eq 'td'){
                        $replacementStr = $value | zenCode 'table>(tr>td{$_})'
                    }
                    #li assume ul
                    elseif ($enclosingTag -eq 'li'){
                        $expr = 'ul{' + $replacement.Value.Replace('$_.','') + '}>(li{$_})'
                        $replacementStr = $value | zenCode $expr
                    }
                    #just repeat the element and indent the texst
                    else{
                        $expr = '(' + $enclosingTag + '[style=margin-left:2em]{$_})' 
                        $replacementStr = $value | zenCode $expr
                    }

                    $txt=$txt.Remove($firstIndex,$replacement.Key.Length+1).Insert($firstIndex,$replacementStr)
                }
                else{
                    $txt=$txt.Remove($firstIndex,$replacement.Key.Length+1).Insert($firstIndex,$value)
                }
            }
        }
        $i++
    }
    #apply indentation
    $tempRoot = $false
    #hex values do not work in xml
    $escapedTxt= $txt.Replace('0x','hex').Replace('&','&amp;')
    try{
        [xml]$xml=$escapedTxt
    }
    catch{
        #missing unique root element, insert 
        [xml]$xml = "<tempRoot>$escapedTxt</tempRoot>"
        $tempRoot = $true
    }
    $StringWriter = New-Object IO.StringWriter 
    $settings = New-Object XML.XmlWriterSettings
    $settings.Indent = $true
    $settings.IndentChars = "`t"
    $settings.OmitXmlDeclaration = $true
    $XmlWriter = [XMl.XmlTextWriter]::Create($stringWriter,$settings) 
    $xml.WriteContentTo($XmlWriter) 
    $XmlWriter.Flush() 
    $StringWriter.Flush() 
    #remove the root element 
    if ($tempRoot){
        $output = ($StringWriter.ToString() -replace '(?m)\s*</*tempRoot>\s*?','').Trim() -replace '(?m)^\t{1}',''
    }
    else{
        $output = $StringWriter.ToString()
    }
    #create a temp file if show flag is set and no outPath was provided
    if ($show -and $outPath -eq ""){
       $outPath=[IO.Path]::GetTempFileName().Replace('.tmp','.html')
    }
    if ($outPath -ne ""){
        $output | Set-Content $outPath 
    }
    else{
        $output
    }
    if ($show){
        Invoke-Item $outPath
    }
}