PowerShell: рендеринг HTML представлений

в 14:32, , рубрики: powershell, метки:
Вступление

Не так давно передо мной возникла задача рендерить из PowerShell скрипта различные HTML отчеты для дальнейшей отсылки по e-mail. Поиск готовых решений дал не очень много. Кто-то подключает Razor, кто-то свои самописные сложноватые велосипеды движки.
Скромный список требований был такой:

  1. Код вьюх должен быть в отдельных файлах.
  2. Внутри вьюх должна быть поддержка вложенности, и вставок кода на PowerShell.
  3. Должен работать на любых хостах с PowerShell 2.0 без дополнительных настроек.

Так как ничего подобного не удалось найти, то был реализован простой (и одновременно мощный) движок рендеринга вьюх в стиле классического Asp.

PowerShell: рендеринг HTML представлений

Подробности реализации

Изучая вопрос (как и сам PowerShell) я обратил внимание на синтаксис вычисления PowerShell выражений внутри строк. Например выражение "<div> $($env:COMPUTERNAME)</div>" будет во время выполнения интерпретировано и на выходе мы получим что-то вроде <div>MYCOMPUTER</div>.

Это уже собственно и есть темплейтинг в простейшем виде. Он позволяет рендерить достаточно сложные вьюхи:

$Model = @{}
$Model.Title = 'Hello, this is a test'
$Model.Clients = @('Ivan', 'Sergiy', 'John')
	
$html = "<h1>
	$($Model.Title)
</h1>
<div class=""test"">
	<ul>
		 $( foreach($client in $Model.Clients) {"
			<li>
				$( $client )
			</li>
		"})
	</ul>
</div>"

$html

Как видим, парсер PowerShell позволяет использовать вложенные вставки кода заключенного в $() в строки, это очень удобно для реализации ветвлений и циклов.

Данный метод уже можно использовать для небольших задач, хотя есть и недостатки:

  1. Код вьюхи содержится в коде скрипта, а не в отдельном файле.
  2. Нет возможности использовать вложенные вьюхи.
  3. Синтаксис немного не совсем нагляден, и часто из-за пропущенной скобки или кавычки приходится напряженно все проверять.
  4. Необходимо в текстовых вставках кодировать двойную кавычку " как "".

Первые два недостатка решаются довольно просто – темплейт перемещается в отдельный файл в подпапку Views, и пишется функция для рендера модели:

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 + '"')
}

После чего ее можно вызывать так:

RenderViewNativePowerShell 'Test_ps.html' $Model

При этом поддерживаются вложенные вьюхи. Вот так выглядит код test_ps.html:

$( RenderViewNativePowerShell 'header_ps.html' $Model )

<div class=""test"">
	<ul>
		 $( foreach($client in $Model.Clients) {"
			<li>
				$( $client )
			</li>
		"})
	</ul>
</div>

Кому-то этого может показаться достаточно, но я решил побороть оставшиеся недостатки – перейти на использование скобок ASP <%...%>, так как этот синтаксис поддерживается во многих текстовых редакторах, и верстка страницы выглядит намного читабельнее.
Итак, основная идея реализации довольно проста: взять и заменить все скобки <%...%> на их PowerShell эквиваленты $(…). Некоторая сложность состояла в том, что замена должна быть неоднозначной, чтобы учитывать вложенные вьюхи, так как они должны быть в “…” блоках.

После некоторых мучений возникла такая функция:

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 + '"')
}

Кроме требуемой замены <%%> на их PowerShell эквиваленты также выполняется замена “ на “” в текстовых блоках.
В итоге наша вьюха выглядит довольно неплохо в Visual Studio:

PowerShell: рендеринг HTML представлений

В заключение остается отметить, что исходный код с некоторыми тестами и примерами выложен на GitHub.

Автор: megaboich

* - обязательные к заполнению поля