PowerShell: rendering HTML views

    Introduction

    Not so long ago, I had the task of rendering various HTML reports from a PowerShell script for further sending by e-mail. The search for ready-made solutions did not give much. Someone connects Razor, someone their own proprietary complex bikes engines.
    A modest list of requirements was as follows:
    1. View code must be in separate files.
    2. Inside the view, there should be support for nesting, and code insertions in PowerShell.
    3. It should work on any hosts with PowerShell 2.0 without additional settings.

    Since nothing of the kind could be found, a simple (and at the same time powerful) rendering engine was implemented in the style of the classic Asp.




    Implementation Details

    Studying the issue (like PowerShell itself), I noticed the syntax for calculating PowerShell expressions inside strings. For example, the expression " $($env:COMPUTERNAME)"will be interpreted at runtime, and at the output we will get something like MYCOMPUTER.

    This, in fact, is templating in its simplest form. It allows you to render quite complex views:

    $Model = @{}
    $Model.Title = 'Hello, this is a test'
    $Model.Clients = @('Ivan', 'Sergiy', 'John')
    $html = "

    $($Model.Title)

      $( foreach($client in $Model.Clients) {"
    • $( $client )
    • "})
    " $html


    As you can see, the PowerShell parser allows you to use nested inserts of the code enclosed in $ () in strings, which is very convenient for implementing branches and loops.

    This method can already be used for small tasks, although there are disadvantages:
    1. The view code is contained in the script code, and not in a separate file.
    2. There is no way to use nested views.
    3. The syntax is a bit obscure, and often because of a missing bracket or quotation mark, you have to check everything hard.
    4. It is necessary to encode the double quotation mark "as in text inserts "".


    The first two drawbacks are solved quite simply - the template is moved to a separate file in the Views subfolder, and a function is written to render the model:

    function RenderViewNativePowerShell(
    	[Parameter(Mandatory=$true)][string] $viewName,
    	[Parameter(Mandatory=$true)][Object] $model
    )
    {
    	$viewFileName = Resolve-Path ('Views\' + $viewName)
    	$templateContent = Get-Content $viewFileName | Out-String
    	return $ExecutionContext.InvokeCommand.ExpandString('"' + $templateContent + '"')
    }
    


    After which it can be called like this:

    RenderViewNativePowerShell 'Test_ps.html' $Model
    


    Nested views are supported. This is what test_ps.html looks like:
    $( RenderViewNativePowerShell 'header_ps.html' $Model )
    
      $( foreach($client in $Model.Clients) {"
    • $( $client )
    • "})


    This may seem enough for some, but I decided to overcome the remaining shortcomings - switch to using ASP brackets <% ...%>, since this syntax is supported in many text editors, and the layout of the page looks much more readable.
    So, the main idea of ​​the implementation is quite simple: take and replace all the brackets <% ...%> with their PowerShell equivalents $ (...). Some difficulty was that the replacement must be ambiguous in order to take into account the nested views, since they must be in “...” blocks.

    After some torment, such a function arose:

    function RenderView(
    	[Parameter(Mandatory=$true)][string] $viewName,
    	[Parameter(Mandatory=$true)][Object] $model
    )
    {
    	$viewFileName = Resolve-Path ("Views\" + $viewName)
    	$templateContent = Get-Content $viewFileName | Out-String
    	$rx = New-Object System.Text.RegularExpressions.Regex('(<%.*?%>)', [System.Text.RegularExpressions.RegexOptions]::Singleline)
    	$res = @()
    	$splitted = $rx.split($templateContent);
    	foreach($part in $splitted)
    	{
    		if ($part.StartsWith('<%') -and $part.EndsWith('%>')) #transform <%...%> blocks
    		{	
    			$expr = $part.Substring(2, $part.Length-4) #remove <%%> quotes
    			$normExpr = $expr.Replace('`n','').Replace('`r','').Trim();
    			$startClosure = '$('
    			$endClosure = ')'
    			if ($normExpr.endswith('{')) {
    				$endClosure = '"'
    			}
    			if ($normExpr.startsWith('}')) {
    				$startClosure = '"'
    			}
    			$res += @($startClosure + $expr + $endClosure)
    		}
    		else #encode text blocks
    		{	
    			$expr = $part.Replace('"', '""');
    			$res += @($expr)
    		}
    	}
    	$viewExpr = $res -join ''
    	return $ExecutionContext.InvokeCommand.ExpandString('"' + $viewExpr + '"')
    }
    


    In addition to the required replacement of <%%> with their PowerShell equivalents, the replacement of “by“ ”in text blocks is also performed.
    As a result, our view looks pretty good in Visual Studio:



    In conclusion, it remains to note that the source code with some tests and examples is uploaded to GitHub .

    Also popular now: