Skip to content

Instantly share code, notes, and snippets.

@DBremen
Last active August 16, 2022 03:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DBremen/06aeb0bcdcc1d57f04d6 to your computer and use it in GitHub Desktop.
Save DBremen/06aeb0bcdcc1d57f04d6 to your computer and use it in GitHub Desktop.
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment