parser.ps1
|
<#
.SYNOPSIS PugPS - A Pug to HTML converter for PowerShell and Pode. .DESCRIPTION Copyright (c) 2026 Nabil Redmann Licensed under the MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. #> function Get-PugErrorContext { param( [string]$Path, [int]$LineNumber, [string]$Detail, [int]$ContextRange = 2 ) if (-not (Test-Path $Path)) { return "Error: $Detail`n(File not found: $($Path):$($LineNumber))" } $allLines = Get-Content $Path $errorIdx = $LineNumber - 1 $start = [Math]::Max(0, $errorIdx - $ContextRange) $end = [Math]::Min($allLines.Count - 1, $errorIdx + $ContextRange) $maxDigits = ($end + 1).ToString().Length $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine("$($Path):$LineNumber") for ($i = $start; $i -le $end; $i++) { $currentLineNum = $i + 1 $lineText = $allLines[$i] $marker = if ($currentLineNum -eq $LineNumber) { "> " } else { " " } $lineNumStr = $currentLineNum.ToString().PadLeft($maxDigits) [void]$sb.AppendLine("$marker$lineNumStr | $lineText") } [void]$sb.AppendLine("`n$Detail") return $sb.ToString() } function New-PugError { param( [Parameter(Mandatory=$true)] [PSObject]$LineObj, [Parameter(Mandatory=$true)] [string]$Detail, [int]$ContextRange = 2 ) $niceMsg = Get-PugErrorContext -Path $LineObj.Path -LineNumber $LineObj.Line -Detail $Detail -ContextRange $ContextRange $ex = New-Object System.Exception($niceMsg) $ex | Add-Member -NotePropertyName "Detail" -NotePropertyValue $Detail -Force $ex | Add-Member -NotePropertyName "Path" -NotePropertyValue $LineObj.Path -Force $ex | Add-Member -NotePropertyName "Line" -NotePropertyValue $LineObj.Line -Force return $ex } function Convert-PugToPowerShell { [CmdletBinding(DefaultParameterSetName='Path')] param( [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, HelpMessage="The path to the Pug template file.")] [string]$Path, [Parameter(ParameterSetName='Content', Mandatory=$true, ValueFromPipeline=$true, HelpMessage="The raw Pug template content.")] [string[]]$InputContent, [Parameter(Mandatory=$false, HelpMessage="The default file extension to use for included files if not specified.")] [string]$Extension = 'pug', [Parameter(Mandatory=$false, HelpMessage="The root directory used to resolve absolute include/extend paths (those starting with / or \). If empty, absolute paths are resolved relative to the current file.")] [string]$BaseDir = "", [Parameter(Mandatory=$false, HelpMessage="When true (default), boolean attributes are rendered as 'attr'. When false, they are rendered as 'attr=''attr'''.")] [bool]$Properties = $true, [Parameter(Mandatory=$false, HelpMessage="When true, standard void tags (like img, br) are rendered with a self-closing slash (e.g., <img />). Default is false.")] [bool]$VoidTagsSelfClosing = $false, [Parameter(Mandatory=$false, HelpMessage="When true, empty container tags (like div, span) with no content or children are rendered as self-closing (e.g., <div />). Default is false.")] [bool]$ContainerTagsSelfClosing = $false, [Parameter(Mandatory=$false, HelpMessage="When true, CamelCase in PUG is converted to kebab-case. Default is true.")] [bool]$KebabCaseHTML = $true, [Parameter(Mandatory=$false, HelpMessage="Number of context lines to show before and after the error line.")] [int]$ErrorContextRange = 2, [Parameter(Mandatory=$false, HelpMessage="When passed in, it will be used to store the list of all included files.")] [hashtable]$RefIncludes = @{} # path -> System.IO.FileInfo ) begin { $accumulatedContent = New-Object System.Collections.Generic.List[string] } process { if ($PSCmdlet.ParameterSetName -eq 'Content') { if ($InputContent) { $accumulatedContent.AddRange($InputContent -split "`n") } } } end { if ($PSCmdlet.ParameterSetName -eq 'Path') { if (-not (Test-Path $Path)) { throw "Template not found: $Path" } } else { $Path = "Stream" # Default BaseDir to PWD if we are processing content directly and BaseDir wasn't provided if ([string]::IsNullOrEmpty($BaseDir)) { $BaseDir = (Get-Location).Path } } $voidTags = @('area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr') $literalTags = @('pre', 'code', 'textarea', 'xmp') # Regex updated: Tags must start with a letter [a-zA-Z]. # Prevents numeric start (123tag) or symbol start (&tag) from being parsed as tags. $tagRegex = '^([a-zA-Z][a-zA-Z0-9_-]*(?::[a-zA-Z0-9_-]+)*|#[a-zA-Z0-9_-]+|\.[a-zA-Z0-9\._-]+)(?:#([a-zA-Z0-9_-]+))?(?:\.([a-zA-Z0-9\._-]+))?(?:\((.*)\))?(\/)?(!?=)?\s?(.*)?$' # --- PHASE 1: Resolve Inheritance & Includes --- function Resolve-SourceRecursive { param( [string]$CurrentPath, [string[]]$InjectContent = $null ) $rawLines = if ($InjectContent) { $InjectContent } else { Get-Content $CurrentPath } $lines = New-Object System.Collections.Generic.List[PSObject] for ($r=0; $r -lt $rawLines.Count; $r++) { $lines.Add([PSCustomObject]@{ Text = [string]$rawLines[$r] Path = $CurrentPath Line = $r + 1 }) } $processed = New-Object System.Collections.Generic.List[PSObject] # Determine directory for relative resolution $currentFileDir = if ($InjectContent) { # If content is injected (Stream), use PWD or BaseDir if set globally if ($BaseDir) { $BaseDir } else { (Get-Location).Path } } else { Split-Path $CurrentPath } $ResolvePath = { param([string]$target) if ($target -match '^(/|\\)') { $stripped = $target.TrimStart('/\') if (![string]::IsNullOrEmpty($BaseDir)) { return Join-Path $BaseDir $stripped } return Join-Path $currentFileDir $stripped } return Join-Path $currentFileDir $target } if ($lines.Count -gt 0 -and $lines[0].Text -match '^extends\s+(.+)') { $parentName = $matches[1].Trim() $parentPath = &$ResolvePath $parentName if (-not (Test-Path $parentPath)) { $tryPath = "$parentPath.$Extension" if (Test-Path $tryPath) { $parentPath = $tryPath } else { $parentPath = "$parentPath.pug" } } $parentLineObjs = Resolve-SourceRecursive -CurrentPath $parentPath $childBlocks = @{} $childMixins = New-Object System.Collections.Generic.List[PSObject] $i = 1 while ($i -lt $lines.Count) { $lineObj = $lines[$i] $lineText = $lineObj.Text if ([string]::IsNullOrWhiteSpace($lineText)) { $i++; continue } $indent = ($lineText -split '\S', 2)[0].Length # Ignore unbuffered comment blocks in scan if ($lineText -match '^(\s*)//-') { $cIndent = $matches[1].Length $i++ while ($i -lt $lines.Count) { if ($lines[$i].Text.Trim().Length -eq 0) { $i++; continue } $nIndent = ($lines[$i].Text -split '\S', 2)[0].Length if ($nIndent -gt $cIndent) { $i++ } else { break } } continue } if ($lineText -match '^\s*block\s+([a-zA-Z0-9_-]+)') { $blockName = $matches[1] $blockContent = New-Object System.Collections.Generic.List[PSObject] $i++ while ($i -lt $lines.Count) { $nextObj = $lines[$i] if ($nextObj.Text.Trim().Length -eq 0) { $blockContent.Add($nextObj) $i++ continue } if (($nextObj.Text -split '\S', 2)[0].Length -le $indent) { break } $blockContent.Add($nextObj) $i++ } $childBlocks[$blockName] = $blockContent } elseif ($lineText -match '^\s*mixin\s+') { $childMixins.Add($lineObj) $mIndent = $indent $i++ while ($i -lt $lines.Count) { $nextObj = $lines[$i] if ($nextObj.Text.Trim().Length -eq 0) { $childMixins.Add($nextObj); $i++; continue } if (($nextObj.Text -split '\S', 2)[0].Length -le $mIndent) { break } $childMixins.Add($nextObj) $i++ } } else { $i++ } } foreach($m in $childMixins) { $processed.Add($m) } foreach ($pObj in $parentLineObjs) { if ($pObj.Text -match '^(\s*)block\s+([a-zA-Z0-9_-]+)') { $pIndent = $matches[1].Length $pBlockName = $matches[2] if ($childBlocks.ContainsKey($pBlockName)) { $cObjs = $childBlocks[$pBlockName] if ($cObjs.Count -gt 0) { $minC = 999 foreach($co in $cObjs) { if ($co.Text.Trim().Length -gt 0) { $val = ($co.Text -split '\S', 2)[0].Length if ($val -lt $minC) { $minC = $val } } } if ($minC -eq 999) { $minC = 0 } foreach($co in $cObjs) { $newObj = $co.PSObject.Copy() if ($co.Text.Trim().Length -eq 0) { $newObj.Text = "" } else { $currC = ($co.Text -split '\S', 2)[0].Length $calcIndent = [Math]::Max(0, ($currC - $minC)) $newIndent = " " * ($pIndent + $calcIndent) $newObj.Text = $newIndent + $co.Text.Trim() } $processed.Add($newObj) } } } else { $processed.Add($pObj) } } else { $processed.Add($pObj) } } $lines = $processed $processed = New-Object System.Collections.Generic.List[PSObject] } # Include Logic $i = 0 while ($i -lt $lines.Count) { $lineObj = $lines[$i] $line = $lineObj.Text # Detect block comments (both unbuffered //- and buffered //) to skip include resolution inside them if ($line -match '^(\s*)//') { $commentIndent = $matches[1].Length $processed.Add($lineObj) $i++ while ($i -lt $lines.Count) { $nLine = $lines[$i] if ([string]::IsNullOrWhiteSpace($nLine.Text)) { $processed.Add($nLine); $i++; continue } $nIndent = ($nLine.Text -split '\S', 2)[0].Length # If line is indented deeper than the comment start, it is part of the comment block if ($nIndent -gt $commentIndent) { $processed.Add($nLine) $i++ } else { break } } continue } if ($line -match '^(\s*)include(:[a-zA-Z0-9_:-]+)?\s+(.+)') { $indentStr = $matches[1] $filterChain = $matches[2] $incFileName = $matches[3].Trim() $incPath = &$ResolvePath $incFileName if (-not (Test-Path $incPath) -and $incFileName -notmatch '\.[a-zA-Z0-9]+$') { $tryPath = "$incPath.$Extension" if (Test-Path $tryPath) { $incPath = $tryPath } } if (Test-Path $incPath) { if (-not [string]::IsNullOrEmpty($filterChain)) { $newHeader = $lineObj.PSObject.Copy() $newHeader.Text = "$indentStr$filterChain" $processed.Add($newHeader) $incRaw = Get-Content $incPath for($k=0; $k -lt $incRaw.Count; $k++) { $processed.Add([PSCustomObject]@{ Text = "$indentStr $($incRaw[$k])" Path = $incPath Line = $k + 1 }) } } else { $regexExt = ($Extension -replace '\.', '\.') + '$' if ($incPath -match $regexExt -or $incPath -match '\.pug$') { $incLines = Resolve-SourceRecursive -CurrentPath $incPath foreach ($il in $incLines) { $newObj = $il.PSObject.Copy() $newObj.Text = $indentStr + $il.Text $processed.Add($newObj) } } else { $incRaw = Get-Content $incPath for($k=0; $k -lt $incRaw.Count; $k++) { $processed.Add([PSCustomObject]@{ Text = "$indentStr| $($incRaw[$k])" Path = $incPath Line = $k + 1 }) } } } } else { throw (New-PugError -LineObj $lineObj -Detail "Included file not found: $incPath" -ContextRange $ErrorContextRange) } $i++ } else { $processed.Add($lineObj) $i++ } } return $processed } function Get-BalancedContent([string]$str, [int]$startIdx, [char]$open, [char]$close) { $depth = 0 for ($j = $startIdx; $j -lt $str.Length; $j++) { if ($str[$j] -eq $open) { $depth++ } elseif ($str[$j] -eq $close) { $depth-- if ($depth -eq 0) { return @{ Content = $str.Substring($startIdx + 1, $j - $startIdx - 1); EndIdx = $j } } } } return $null } function Get-PSFilterPipeline([string]$chain, [ref]$endIdx) { $pipelineParts = New-Object System.Collections.Generic.List[string] $idx = 0 while ($idx -lt $chain.Length) { if ($chain[$idx] -match '\s') { break } if ($chain[$idx] -eq ':') { $idx++ } $start = $idx while ($idx -lt $chain.Length -and $chain[$idx] -match '[a-zA-Z0-9_-]') { $idx++ } $funcName = $chain.Substring($start, $idx - $start) $argsStr = "" if ($idx -lt $chain.Length -and $chain[$idx] -eq '(') { $b = Get-BalancedContent $chain $idx '(' ')' if ($b) { $rawArgs = $b.Content $psArgs = New-Object System.Collections.Generic.List[string] $pairs = Get-PugAttributePairs $rawArgs foreach ($pair in $pairs) { if ($pair -match '^([^=]+)=(.*)$') { $k = $matches[1].Trim(); $v = $matches[2].Trim() $psArgs.Add("-$k $v") } else { $psArgs.Add($pair) } } $argsStr = " " + ($psArgs -join " ") $idx = $b.EndIdx + 1 } else { $idx++ } } if (![string]::IsNullOrEmpty($funcName)) { $pipelineParts.Add("$funcName$argsStr") } if ($idx -lt $chain.Length -and $chain[$idx] -ne ':') { break } } $endIdx.Value = $idx return $pipelineParts -join " | " } function ConvertTo-PugInterpolatedTag { param([string]$inner, [bool]$xmlMode) if ($inner -match $tagRegex) { $tagMatch = $matches $tagName = "div" if ($tagMatch[1] -and $tagMatch[1] -notmatch '^[#.]') { $tagName = $tagMatch[1] } # OPTIMIZATION: Convert CamelCase to kebab-case if not XML if (-not $xmlMode -and $KebabCaseHTML -and $tagName -cmatch '[A-Z]') { $tagName = ($tagName -creplace '([a-zA-Z0-9-])([A-Z])', '$1-$2').ToLower() } $id = if ($tagMatch[1] -match '^#') { $tagMatch[1].Substring(1) } else { $tagMatch[2] } $rawClass = "" if ($tagMatch[1] -match '^\.') { $rawClass = $tagMatch[1] } if ($tagMatch[3]) { $rawClass += "." + $tagMatch[3] } $classes = if ($rawClass) { $rawClass.Split('.') | Where-Object { $_ } | ForEach-Object { "'$_'" } } else { @() } $attrString = $tagMatch[4] $explicitSlash = [bool]$tagMatch[5] $operator = $tagMatch[6] $inlineContent = $tagMatch[7] $isVoid = $voidTags -contains $tagName $hasContent = ![string]::IsNullOrEmpty($inlineContent) $parts = New-Object System.Collections.Generic.List[string] $parts.Add("'<" + $tagName + "'") if ($id) { $parts.Add("' id=`"$id`"'") } if ($classes.Count -gt 0) { $parts.Add("(Out-PugAttr 'class' @($($classes -join ',')) `$false)") } if ($attrString) { foreach ($pair in (Get-PugAttributePairs $attrString)) { if ($pair -match '^([^!=]+)\s*(!?=)\s*(.*)$') { $ak = $matches[1].Trim(); $ao = $matches[2]; $val = $matches[3].Trim() $parts.Add("(Out-PugAttr '$ak' ($val) " + $(if ($ao -eq '=') { '$true' } else { '$false' }) + ")") } else { $parts.Add("(Out-PugAttr '$pair' `$true `$false)") } } } if ($explicitSlash -and !$hasContent) { $parts.Add("' />'") } else { if ($isVoid) { $parts.Add("`$(if (`$pug_voidSelfClose) { ' />' } else { '>' })") } elseif (!$hasContent) { $parts.Add("`$(if (`$pug_containerSelfClose) { ' />' } else { '>' })") } else { $parts.Add("'>'") } if ($hasContent) { if ($operator -eq '=') { $parts.Add("(Out-PugEnc ($inlineContent))") } elseif ($operator -eq '!=') { $parts.Add("($inlineContent)") } else { $parts.Add((Out-PSEscaped $inlineContent $xmlMode)) } } if (!$isVoid) { if (!$hasContent) { $parts.Add("`$(if (-not `$pug_containerSelfClose) { '</$tagName>' } else { '' })") } else { $parts.Add("'</$tagName>'") } } } return "($($parts -join ' + '))" } return "''" } function Out-PSEscaped { param([string]$text, [bool]$xmlMode) if ($null -eq $text) { return '""' } $res = ""; $idx = 0 while ($idx -lt $text.Length) { $char = $text[$idx] if (($char -eq '\' -or $char -eq '`') -and ($idx + 1 -lt $text.Length)) { $next = $text[$idx+1] if ($next -eq '$') { $res += '`$'; $idx += 2; continue } if ($next -eq '`') { $res += '``'; $idx += 2; continue } if ($next -eq '\') { $res += '\'; $idx += 2; continue } } if ($char -eq '#' -and ($idx + 1 -lt $text.Length)) { $next = $text[$idx+1] if ($next -eq '(' -or $next -eq '{') { $closeChar = $(if($next -eq '('){')'} else {'}'}) $b = Get-BalancedContent $text ($idx+1) $next $closeChar if ($b) { $res += '$(' + "Out-PugEnc ($($b.Content))" + ')'; $idx = $b.EndIdx + 1; continue } } if ($next -eq '[') { $b = Get-BalancedContent $text ($idx+1) '[' ']' if ($b) { $res += '$' + (ConvertTo-PugInterpolatedTag $b.Content $xmlMode); $idx = $b.EndIdx + 1; continue } } } if ($char -eq '$' -and ($idx + 1 -lt $text.Length) -and $text[$idx+1] -eq '{') { $b = Get-BalancedContent $text ($idx+1) '{' '}' if ($b) { $res += '$(' + $b.Content + ')'; $idx = $b.EndIdx + 1; continue } } if ($char -eq '"') { $res += '`"' } elseif ($char -eq '`') { $res += '``' } else { $res += $char } $idx++ } return "`"$res`"" } function Out-PSLiteral([string]$text) { if ($null -eq $text) { return '""' } $safe = $text.Replace('`', '``').Replace('"', '`"').Replace('$', '`$') return "`"$safe`"" } function Get-TabPrefix($stack) { foreach ($item in $stack) { if ($literalTags -contains $item.Tag) { return "" } } $count = 0 foreach ($item in $stack) { if ($item.Tag) { $count++ } } return "`t" * $count } function Get-PugAttributePairs([string]$attrString) { $pairs = New-Object System.Collections.Generic.List[string] if ([string]::IsNullOrWhiteSpace($attrString)) { return $pairs } $current = ""; $inQuotes = $false; $quoteChar = ""; $depth = 0 $chars = $attrString.ToCharArray() for ($j = 0; $j -lt $chars.Count; $j++) { $char = $chars[$j] if ($inQuotes) { $current += $char if ($char -eq $quoteChar) { $inQuotes = $false } } else { if ($char -eq "'" -or $char -eq '"') { $inQuotes = $true; $quoteChar = $char; $current += $char } elseif ($char -eq '(') { $depth++; $current += $char } elseif ($char -eq ')') { $depth--; $current += $char } elseif ($depth -eq 0 -and $char -eq ',') { if ($current.Trim()) { $pairs.Add($current.Trim()) } $current = "" } elseif ($depth -eq 0 -and $char -eq ' ') { $remaining = $attrString.Substring($j).Trim() if ($remaining -match '^(!?=)') { $current += $char } elseif ($current.Trim() -match '(!?=)$') { $current += $char } else { if ($current.Trim()) { $pairs.Add($current.Trim()) }; $current = "" } } else { $current += $char } } } if ($current.Trim()) { $pairs.Add($current.Trim()) } return $pairs } # Resolve Source (File vs Stream) $injection = if ($PSCmdlet.ParameterSetName -eq 'Content') { $accumulatedContent } else { $null } $lines = Resolve-SourceRecursive -CurrentPath $Path -InjectContent $injection $outputScript = New-Object System.Collections.Generic.List[string] $stack = New-Object System.Collections.Generic.Stack[PSObject] $mixinBaseStackCount = -1 $isXmlMode = $false function Get-IndentExpr { if ($mixinBaseStackCount -gt -1) { foreach ($item in $stack) { if ($literalTags -contains $item.Tag) { return "`$pug_indent" } } $relDepth = 0 foreach ($item in $stack) { if ($item.IsDefinition) { break } if ($item.Tag) { $relDepth++ } } return "(`$pug_indent + $(Out-PSLiteral ("`t" * $relDepth)))" } return Out-PSLiteral (Get-TabPrefix $stack) } function Get-TraceLine([PSObject]$l) { $safePath = $l.Path.Replace("'", "''") if ($path -ne "Stream") { $RefIncludes[$l.Path] = [System.IO.FileInfo]($l.Path) } return "`$pug_src_line=$($l.Line);`$pug_src_path='$safePath'; " } $outputScript.Add('param($data)') $outputScript.Add("`$pug_src_line = 0; `$pug_src_path = ''") $outputScript.Add("`$pug_props = $(if($Properties){'$true'}else{'$false'})") $outputScript.Add("`$pug_voidSelfClose = $(if($VoidTagsSelfClosing){'$true'}else{'$false'})") $outputScript.Add("`$pug_containerSelfClose = $(if($ContainerTagsSelfClosing){'$true'}else{'$false'})") $outputScript.Add(@' function Out-PugClass($items) { $res = New-Object System.Collections.Generic.List[string] foreach ($i in @($items)) { if ($null -eq $i -or ($i -is [bool] -and !$i)) { continue } if ($i -is [System.Collections.IDictionary]) { foreach ($entry in $i.GetEnumerator()) { if ($entry.Value) { $res.Add($entry.Key.ToString()) } } } elseif ($i -is [string]) { foreach($s in ($i -split '\s+')) { if($s.Trim()) { $res.Add($s.Trim()) } } } elseif ($i -is [System.Collections.IEnumerable]) { foreach($sub in $i) { $res.Add((Out-PugClass $sub)) } } else { $res.Add("$i") } } return (($res | Select-Object -Unique) -join " ").Trim() } function Out-PugStyle($v) { if ($null -eq $v -or ($v -is [bool] -and !$v) -or ($v -is [string] -and $v -eq "")) { return "" } if ($v -is [string]) { return $v.Trim() } if ($v -is [System.Collections.IDictionary]) { $res = New-Object System.Collections.Generic.List[string] foreach ($entry in $v.GetEnumerator()) { $val = $entry.Value if ($null -eq $val -or ($val -is [bool] -and !$val) -or ($val -is [string] -and $val -eq "")) { continue } $k = $entry.Key.ToString() $pk = $k if ($k -notmatch '^--') { $tmp = $k.Substring(0,1).ToLower() + $k.Substring(1) $pk = [regex]::Replace($tmp, '([A-Z])', '-$1').ToLower() } $res.Add("$($pk): $val") } return ($res -join "; ") } return "$v" } function Out-PugAttr($k, $v, $e) { if ($null -eq $v -or ($v -is [bool] -and !$v)) { return "" } if ($v -is [bool] -and $v) { return $(if ($pug_props) { " $k" } else { " $k=`"$k`"" }) } $s = $(if ($k -eq "class") { Out-PugClass $v } elseif ($k -eq "style") { Out-PugStyle $v } else { "$v" }) if ($null -eq $s -or $s -eq "") { return "" } if ($e) { $s = [System.Net.WebUtility]::HtmlEncode($s) } return " $k=`"$s`"" } function Out-PugEnc($v) { return [System.Net.WebUtility]::HtmlEncode("$v") } function Out-PugMergedAttrs($inline, $exploded) { if ($null -eq $exploded) { $res = New-Object System.Collections.Generic.List[string] if ($null -ne $inline) { foreach($k in $inline.Keys) { $enc = ($k -eq 'class' -or $k -eq 'style') $res.Add((Out-PugAttr $k $inline[$k] $enc)) } } return $res -join "" } if ($null -eq $inline) { $inline = @{} } $exDict = @{} if ($exploded -is [System.Collections.IDictionary]) { $exploded.Keys | ForEach-Object { $exDict[$_] = $exploded[$_] } } elseif ($null -ne $exploded) { if ($exploded.PSObject) { foreach($prop in $exploded.PSObject.Properties) { if ($prop.CanRead) { $exDict[$prop.Name] = $prop.Value } } } } if ($exDict.Contains("class")) { $existing = $(if ($inline.Contains("class")) { @($inline["class"]) } else { @() }) $new = $exDict["class"] $inline["class"] = $existing + $new $exDict.Remove("class") } if ($exDict.Contains("style")) { $iS = $(if ($inline.Contains("style")) { Out-PugStyle $inline["style"] } else { "" }) $eS = Out-PugStyle $exDict["style"] $merged = $(if ($iS -and $eS) { "$iS; $eS" } elseif ($iS) { $iS } else { $eS }) $inline["style"] = $merged $exDict.Remove("style") } foreach($k in $exDict.Keys) { $inline[$k] = $exDict[$k] } $res = New-Object System.Collections.Generic.List[string] foreach($k in $inline.Keys) { $enc = ($k -eq 'class' -or $k -eq 'style') $res.Add((Out-PugAttr $k $inline[$k] $enc)) } return $res -join "" } '@) $outputScript.Add("try {") $outputScript.Add("@(") $i = 0 while ($i -lt $lines.Count) { $lineObj = $lines[$i]; $i++ $rawLine = $lineObj.Text if ([string]::IsNullOrWhiteSpace($rawLine)) { continue } $indent = ($rawLine -split '\S', 2)[0].Length $line = $rawLine.Trim() # Peek ahead to find next content indentation (skipping empty lines) $peekI = $i while ($peekI -lt $lines.Count -and $lines[$peekI].Text.Trim().Length -eq 0) { $peekI++ } $nextContentIndent = -1 if ($peekI -lt $lines.Count) { $nextContentIndent = ($lines[$peekI].Text -split '\S', 2)[0].Length } # Calculate parentIsSwitch Context for this iteration $parentIsSwitch = $false if ($stack.Count -gt 0) { $parent = $stack.Peek(); if ($parent.IsSwitch) { $parentIsSwitch = $true } } # Handle multiline parens $parenIdx = $line.IndexOf('(') if ($parenIdx -ge 0 -and ($line -match '^[a-zA-Z0-9.#+-]')) { $balanced = Get-BalancedContent $line $parenIdx '(' ')' if ($null -eq $balanced) { while ($i -lt $lines.Count) { $nextLineObj = $lines[$i] $nextLine = $nextLineObj.Text.Trim() $line += " " + $nextLine $rawLine += " " + $nextLine $i++ if (Get-BalancedContent $line $parenIdx '(' ')') { break } } } } # Hidden Comments (//-) if ($line.StartsWith("//-")) { $currI = $indent while($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $currI)) { $i++ } continue } while ($stack.Count -gt 0 -and $stack.Peek().Indent -ge $indent) { $top = $stack.Pop() if ($top.IsDefinition) { $mixinBaseStackCount = -1 } if ($top.IsCode -or $top.IsMixin) { if (-not $top.IsExplicit) { $outputScript.Add("}") } } else { $closeIndent = if ($literalTags -contains $top.Tag) { "''" } else { Get-IndentExpr } $outputScript.Add($closeIndent + " + " + (Out-PSLiteral "</$($top.Tag)>")) } } # Recalculate parentIsSwitch after popping $parentIsSwitch = $false if ($stack.Count -gt 0) { $parent = $stack.Peek(); if ($parent.IsSwitch) { $parentIsSwitch = $true } } # Unbuffered Code Block (-) if ($line -eq "-") { $currI = $indent while($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $currI)) { if ($lines[$i].Text.Trim()) { $outputScript.Add($lines[$i].Text.Trim()) }; $i++ } continue } # Unbuffered Code Line (- ...) if ($line.StartsWith("- ")) { $code = $line.Substring(2).Trim() $hasExplicitBlock = $code.EndsWith("{") $isControlKeyword = $code -match '^\s*(if|elseif|foreach|for|while|switch|else|try|catch|finally|default)\b' $isSwitchCase = $parentIsSwitch -and -not $isControlKeyword -and ($code -notmatch '^}') $shouldAutoBlock = ($isControlKeyword -or $isSwitchCase) -and -not $hasExplicitBlock $shouldTrackExplicit = $hasExplicitBlock -and ($isControlKeyword -or $isSwitchCase) $isContinuation = $code -match '^\s*(catch|finally|else|elseif)\b' # Suppress trace if: inside switch (structural), line starts with }, or is a continuation keyword if (-not $parentIsSwitch -and $code -notmatch '^}' -and -not $isContinuation) { $outputScript.Add((Get-TraceLine $lineObj) + $code) } else { $outputScript.Add($code) } if ($shouldAutoBlock) { $isSwitchLine = $code -match '^\s*switch\b' $outputScript.Add("{") $stack.Push(@{ Indent = $indent; IsCode = $true; IsMixin = $false; IsDefinition = $false; IsSwitch = $isSwitchLine; IsExplicit = $false }) } elseif ($shouldTrackExplicit) { $isSwitchLine = $code -match '^\s*switch\b' $stack.Push(@{ Indent = $indent; IsCode = $true; IsMixin = $false; IsDefinition = $false; IsSwitch = $isSwitchLine; IsExplicit = $true }) } continue } # Filter if ($line.StartsWith(":")) { $consumedIdx = 0 $pipelineCmd = Get-PSFilterPipeline $line ([ref]$consumedIdx) $inlineContent = if ($consumedIdx -lt $line.Length) { $line.Substring($consumedIdx).Trim() } else { "" } $prefixExpr = Get-IndentExpr $blockLines = New-Object System.Collections.Generic.List[string]; $currI = $indent while ($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $currI)) { $blockLines.Add($lines[$i].Text); $i++ } $stripCount = 999 foreach($bl in $blockLines) { if ($bl.Trim()) { $val = ($bl -split '\S', 2)[0].Length; if ($val -lt $stripCount) { $stripCount = $val } } } if ($stripCount -eq 999) { $stripCount = 0 } $psContentArray = New-Object System.Collections.Generic.List[string] if ($inlineContent) { $psContentArray.Add((Out-PSEscaped $inlineContent $isXmlMode)) } foreach($bl in $blockLines) { $content = if ($bl.Length -ge $stripCount) { $bl.Substring($stripCount) } else { "" } $psContentArray.Add((Out-PSEscaped $content $isXmlMode)) } while ($psContentArray.Count -gt 1 -and ($psContentArray[-1] -eq '""')) { $psContentArray.RemoveAt($psContentArray.Count - 1) } $contentString = "(" + ($psContentArray -join "+ `"``n`" +") + ")" if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } $outputScript.Add($prefixExpr + " + ($contentString | $pipelineCmd | Out-String)") continue } # HTML Comments (//) if ($line.StartsWith("//")) { $prefixExpr = Get-IndentExpr; $inline = $line.Substring(2).Trim() $hasIndentedContent = ($nextContentIndent -gt $indent) $inSwitch = ($stack.Count -gt 0 -and $stack.Peek().IsSwitch) if ($inSwitch) { if ($inline) { $outputScript.Add("# $inline") } if ($hasIndentedContent) { $currI = $indent while($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $currI)) { if ($lines[$i].Text.Trim()) { $outputScript.Add("# " + $lines[$i].Text.Trim()) }; $i++ } } } else { if ($hasIndentedContent) { $commentBody = New-Object System.Collections.Generic.List[string] $commentBody.Add($prefixExpr + " + " + (Out-PSLiteral "<!--")) if ($inline) { $commentBody.Add((Out-PSLiteral " ") + " + " + (Out-PSLiteral $inline)) } $stripLen = $nextContentIndent $currI = $indent while($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $currI)) { $lText = $lines[$i].Text if ($lText.Trim().Length -gt 0) { $len = ($lText -split '\S', 2)[0].Length $relContent = if ($len -ge $stripLen) { $lText.Substring($stripLen) } else { $lText.TrimStart() } $commentBody.Add((Out-PSLiteral "`n") + " + " + (Get-IndentExpr) + " + " + (Out-PSLiteral "`t") + " + " + (Out-PSLiteral $relContent)) } $i++ } $commentBody.Add((Out-PSLiteral "`n") + " + " + (Get-IndentExpr) + " + " + (Out-PSLiteral "-->")) $outputScript.Add($commentBody -join " + ") } else { $outputScript.Add($prefixExpr + " + " + (Out-PSLiteral "<!-- ") + " + " + (Out-PSLiteral $inline) + " + " + (Out-PSLiteral " -->")) } } continue } # Dot Block (tag.) # Updated Regex to require tag start with letter if ($line -match '^([a-zA-Z][a-zA-Z0-9]*)?\.$') { $tagName = $matches[1]; $prefixExpr = Get-IndentExpr # OPTIMIZATION: Convert CamelCase to kebab-case if not XML if ($tagName -and -not $isXmlMode -and $KebabCaseHTML -and $tagName -cmatch '[A-Z]') { $tagName = ($tagName -creplace '([a-zA-Z0-9-])([A-Z])', '$1-$2').ToLower() } if ($tagName) { $outputScript.Add($prefixExpr + " + " + (Out-PSLiteral "<$tagName>")) } $blockLines = New-Object System.Collections.Generic.List[string]; $currI = $indent while ($i -lt $lines.Count -and (($lines[$i].Text.Trim().Length -eq 0) -or ($lines[$i].Text -split '\S', 2)[0].Length -gt $indent)) { $blockLines.Add($lines[$i].Text); $i++ } $stripCount = 999 foreach($bl in $blockLines) { if ($bl.Trim()) { $val = ($bl -split '\S', 2)[0].Length; if ($val -lt $stripCount) { $stripCount = $val } } } if ($stripCount -eq 999) { $stripCount = 0 } if ($tagName -and $voidTags -notcontains $tagName) { $stack.Push(@{ Indent = $indent; Tag = $tagName; IsCode = $false; IsMixin = $false; IsDefinition = $false; IsSwitch = $false; IsExplicit = $false }) } foreach($bl in $blockLines) { $content = if ($bl.Length -ge $stripCount) { $bl.Substring($stripCount) } else { "" } $outputScript.Add((Get-IndentExpr) + " + " + (Out-PSEscaped $content $isXmlMode)) } if ($tagName -and $voidTags -notcontains $tagName) { $top = $stack.Pop(); $closeIndent = if ($literalTags -contains $top.Tag) { "''" } else { Get-IndentExpr } $outputScript.Add($closeIndent + " + " + (Out-PSLiteral "</$($top.Tag)>")) } continue } # Mixin Def if ($line -match '^mixin\s+([a-zA-Z0-9_-]+)(?:\((.*)\))?') { $mName = $matches[1]; $rArgs = $matches[2]; $parsedArgs = New-Object System.Collections.Generic.List[string] if ($rArgs) { foreach ($rawArg in ($rArgs -split ',')) { $arg = $rawArg.Trim(); if(!$arg){continue} if ($arg -match '^(\$?[a-zA-Z0-9_-]+)\s*(?:=\s*(.*))?$') { $varName = $matches[1]; $defaultVal = $matches[2] if (-not $varName.StartsWith('$')) { $varName = "`$$varName" } if ($defaultVal) { $parsedArgs.Add("$varName = $defaultVal") } else { $parsedArgs.Add($varName) } } } } $pArgs = if ($parsedArgs.Count -gt 0) { ($parsedArgs -join ", ") + ", " } else { "" } $outputScript.Add("function mixin_$($mName) { param(`$pug_indent, $($pArgs)[scriptblock]`$mixinBlock)") $stack.Push(@{ Indent = $indent; IsCode = $false; IsMixin = $true; IsDefinition = $true; IsSwitch = $false; IsExplicit = $false }) $mixinBaseStackCount = $stack.Count; continue } # Block keyword if ($line -eq "block") { $relDepth = 0 foreach ($item in $stack) { if ($item.IsDefinition) { break }; if ($item.Tag) { $relDepth++ } } $outputScript.Add("if (`$mixinBlock) { (`& `$mixinBlock) | ForEach-Object { " + (Out-PSLiteral ("`t" * $relDepth)) + " + `$_ } }") continue } # Mixin Call (+mixin) if ($line -match '^\+([a-zA-Z0-9_-]+)(?:\((.*)\))?') { $mName = $matches[1]; $caRaw = $matches[2]; $indentToPass = Get-IndentExpr $psArgs = if ($caRaw) { (($caRaw -split ',').Trim()) -join " " } else { "" } if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } if ($nextContentIndent -gt $indent) { $outputScript.Add("mixin_$($mName) $indentToPass $psArgs {"); $stack.Push(@{ Indent = $indent; IsCode = $false; IsMixin = $true; IsDefinition = $false; IsSwitch = $false; IsExplicit = $false }) } else { $outputScript.Add("mixin_$($mName) $indentToPass $psArgs") } continue } # Buffered code (=, !=, |) if ($line -match '^(=|!=|\|)(?:\s(.*)|(.*))$') { $op = $matches[1]; $content = if($matches[2]){$matches[2]}else{$matches[3]} $prefixExpr = Get-IndentExpr if ($op -eq "|") { $outputScript.Add($prefixExpr + " + " + (Out-PSEscaped $content $isXmlMode)) } elseif ($op -eq "=") { if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } $outputScript.Add($prefixExpr + " + (Out-PugEnc ($content))") } else { if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } $outputScript.Add($prefixExpr + " + ($content)") } continue } # Doctype if ($line -match '^doctype\s+(.*)') { $dtType = $matches[1].Trim() $dtLower = $dtType.ToLower() if ($dtLower -eq 'html5') { $dtLower = 'html' } $doctypes = @{ 'html' = '<!DOCTYPE html>' '5' = '<!DOCTYPE html>' 'xml' = '<?xml version="1.0" encoding="utf-8" ?>' 'transitional' = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' 'strict' = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' 'frameset' = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">' '1.1' = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' 'basic' = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">' 'mobile' = '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">' 'plist' = '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' 'svg1.1' = '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' 'smil1' = '<!DOCTYPE smil PUBLIC "-//W3C//DTD SMIL 1.0//EN" "http://www.w3.org/TR/REC-smil/SMIL10.dtd">' 'smil2' = '<!DOCTYPE smil PUBLIC "-//W3C//DTD SMIL 2.0//EN" "http://www.w3.org/2001/SMIL20/WD/SMIL20.dtd">' } $dtString = if ($doctypes.ContainsKey($dtLower)) { $doctypes[$dtLower] } else { "<!DOCTYPE $dtType>" } if ($dtLower -eq 'xml') { $isXmlMode = $true $outputScript.Add("`$pug_voidSelfClose = `$true") $outputScript.Add("`$pug_containerSelfClose = `$true") $outputScript.Add("`$pug_props = `$false") } elseif ($dtLower -ne 'html' -and $dtLower -ne '5') { $outputScript.Add("`$pug_props = `$false") } $outputScript.Add((Out-PSLiteral $dtString)) continue } # HTML Literal if ($line.StartsWith("<")) { $prefixExpr = Get-IndentExpr; $outputScript.Add($prefixExpr + " + " + (Out-PSLiteral $line)); continue } $explodedAttrsExpr = $null $lineForTag = $line $attrMatch = [regex]::Match($line, '&attributes\s*\(') if ($attrMatch.Success) { $attrIdx = $attrMatch.Index $b = Get-BalancedContent $line ($attrIdx + $attrMatch.Length - 1) '(' ')' if ($b) { $explodedAttrsExpr = $b.Content $lineForTag = $line.Substring(0, $attrIdx) + $line.Substring($b.EndIdx + 1) } } if ($lineForTag -match $tagRegex) { $tagMatch = $matches $tagName = "div" if ($tagMatch[1] -and $tagMatch[1] -notmatch '^[#.]') { $tagName = $tagMatch[1] } # OPTIMIZATION: Convert CamelCase to kebab-case if not XML if (-not $isXmlMode -and $KebabCaseHTML -and $tagName -cmatch '[A-Z]') { $tagName = ($tagName -creplace '([a-zA-Z0-9-])([A-Z])', '$1-$2').ToLower() } $id = if ($tagMatch[1] -match '^#') { $tagMatch[1].Substring(1) } else { $tagMatch[2] } $rawClass = "" if ($tagMatch[1] -match '^\.') { $rawClass = $tagMatch[1] } if ($tagMatch[3]) { $rawClass += "." + $tagMatch[3] } $classes = if ($rawClass) { $rawClass.Split('.') | Where-Object { $_ } | ForEach-Object { Out-PSLiteral $_ } } else { @() } $attrString = $tagMatch[4] $explicitSlash = [bool]$tagMatch[5] $operator = $tagMatch[6] $inlineContent = $tagMatch[7] $hasNested = ($nextContentIndent -gt $indent) # Block Expansion (: tag) if ($inlineContent -match '^:\s*(.*)') { $remaining = $matches[1]; $prefixExpr = Get-IndentExpr $cParts = New-Object System.Collections.Generic.List[string]; $cParts.Add($prefixExpr + " + " + (Out-PSLiteral "<$tagName")) if($id){ $cParts.Add((Out-PSLiteral " id=`"$id`"")) } if($classes.Count -gt 0){ $cParts.Add("(Out-PugAttr 'class' @($($classes -join ', ')) `$false)") } $cParts.Add((Out-PSLiteral ">")); $outputScript.Add($cParts -join " + ") $stack.Push(@{ Indent = $indent; Tag = $tagName; IsCode = $false; IsMixin = $false; IsDefinition = $false; IsSwitch = $false; IsExplicit = $false }) $lines[$i-1].Text = (" " * ($indent + 2)) + $remaining $i--; continue } $isVoid = $voidTags -contains $tagName $hasContentOrChildren = (![string]::IsNullOrEmpty($inlineContent)) -or $hasNested $exprs = New-Object System.Collections.Generic.List[string]; $prefixExpr = Get-IndentExpr $exprs.Add($prefixExpr + " + " + (Out-PSLiteral "<$tagName")) if ($explodedAttrsExpr) { if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } $htEntries = New-Object System.Collections.Generic.List[string] if ($id) { $htEntries.Add("'id' = `"$id`"") } $clsList = New-Object System.Collections.Generic.List[string] foreach($c in $classes) { $clsList.Add($c) } if ($attrString) { foreach ($pair in (Get-PugAttributePairs $attrString)) { if ($pair -match '^([^!=]+)\s*(!?=)\s*(.*)$') { $ak = $matches[1].Trim(); $ao = $matches[2]; $val = $matches[3].Trim() if ($ak -eq "class") { $classVal = if($val){$val}else{'""'} $clsList.Add($classVal) } elseif ($ak -eq "style") { $styleVal = if($val){$val}else{'""'} $htEntries.Add("'style' = $styleVal") } else { $psVal = if($val){$val}else{'$true'} if ($ao -eq '=') { $psVal = "[System.Net.WebUtility]::HtmlEncode($psVal)" } $htEntries.Add("'$ak' = $psVal") } } else { $ak = $pair.Trim(); if ($ak) { $htEntries.Add("'$ak' = `$true") } } } } if ($clsList.Count -gt 0) { $htEntries.Add("'class' = @(" + ($clsList -join ", ") + ")") } $inlineHt = "@{" + ($htEntries -join "; ") + "}" $exprs.Add("(Out-PugMergedAttrs $inlineHt ($explodedAttrsExpr))") } else { if ($id) { $exprs.Add((Out-PSLiteral " id=`"$id`"" )) } $dynClasses = New-Object System.Collections.Generic.List[string] foreach($c in $classes) { $dynClasses.Add($c) } if ($attrString) { foreach ($pair in (Get-PugAttributePairs $attrString)) { if ($pair -match '^([^!=]+)\s*(!?=)\s*(.*)$') { $ak = $matches[1].Trim(); $ao = $matches[2]; $val = $matches[3].Trim(); $psVal = "" if ($val -eq "") { $psVal = '""' } elseif (($val.StartsWith("'") -and $val.EndsWith("'")) -or ($val.StartsWith('"') -and $val.EndsWith('"'))) { $inner = $val.Substring(1, $val.Length - 2); $psVal = Out-PSEscaped $inner $isXmlMode } else { if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } $psVal = $val } if ($ak -eq "class") { $dynClasses.Add($psVal) } else { $exprs.Add("(Out-PugAttr '$ak' ($psVal) " + $(if ($ao -eq '=') { '$true' } else { '$false' }) + ")") } } else { $ak = $pair.Trim(); if ($ak) { $exprs.Add("(Out-PugAttr '$ak' `$true `$false)") } } } } if ($dynClasses.Count -gt 0) { $exprs.Add("(Out-PugAttr 'class' @($($dynClasses -join ', ')) `$false)") } } if ($explicitSlash -and !$hasContentOrChildren) { $exprs.Add((Out-PSLiteral " />")) } else { if ($isVoid) { $exprs.Add("`$(if (`$pug_voidSelfClose) { ' />' } else { '>' })") } elseif (!$hasContentOrChildren) { $exprs.Add("`$(if (`$pug_containerSelfClose) { ' />' } else { '>' })") } else { $exprs.Add((Out-PSLiteral ">")) } if ($inlineContent) { if (-not $parentIsSwitch) { $outputScript.Add((Get-TraceLine $lineObj)) } if ($operator -eq '=') { $exprs.Add("(Out-PugEnc ($inlineContent))") } elseif ($operator -eq '!=') { $exprs.Add("($inlineContent)") } else { $exprs.Add((Out-PSEscaped $inlineContent $isXmlMode)) } } if (!$isVoid) { if ($hasNested) { $stack.Push(@{ Indent = $indent; Tag = $tagName; IsCode = $false; IsMixin = $false; IsDefinition = $false; IsSwitch = $false; IsExplicit = $false }) } else { if (!$hasContentOrChildren) { $exprs.Add("`$(if (-not `$pug_containerSelfClose) { " + (Out-PSLiteral "</$tagName>") + " } else { '' })") } else { $exprs.Add((Out-PSLiteral "</$tagName>")) } } } } $outputScript.Add($exprs -join " + "); continue } # If we reach here, the line was not matched by any parser rule throw (New-PugError -LineObj $lineObj -Detail "PUG parsing error: '$line'" -ContextRange $ErrorContextRange) } while ($stack.Count -gt 0) { $top = $stack.Pop(); if ($top.IsCode -or $top.IsMixin) { if (-not $top.IsExplicit) { $outputScript.Add("}") } } else { $closeIndent = if ($literalTags -contains $top.Tag) { "''" } else { Get-IndentExpr } $outputScript.Add($closeIndent + " + " + (Out-PSLiteral "</$($top.Tag)>")) } } $outputScript.Add(') -join "`n"') $outputScript.Add(@' } catch { $_.Exception.Data["PugLine"] = $pug_src_line $_.Exception.Data["PugPath"] = $pug_src_path throw } '@) return [string]::Join("`n", $outputScript) } } |