Transpilers/Core/PipeScript.Template.psx.ps1
<# .Synopsis Template Transpiler .Description The PipeScript Core Template Transpiler. This allows PipeScript to generate many other languages. Regardless of the underlying language, the core template transpiler works in a fairly straightforward way. A language will contain PipeScript within the file (usually in comments). If a Regular Expression can match each section, then the content in each section can be replaced. When a file that can be transpiled is encountered, the template transpiler for that file type will call the core template transpiler. When templates are used as a keyword, the template transpiler will produce an object that can evaluate the template on demand. #> [ValidateScript({ $validating = $_ if ($validating -isnot [Management.Automation.Language.CommandAst]) { # Leave non-AST commands alone for the moment. } else { # For CommandASTs, make a list of the barewords $barewords = @(foreach ($cmdElement in $validating.CommandElements) { if ($cmdElement.StringConstantType -ne 'Bareword') { break } $cmdElement.Value }) # If one of the first two barewords was 'template if ($barewords[0..1] -eq 'template') { return $true # return true (and run this transpiler) } # Otherwise, return false. return $false } })] [Reflection.AssemblyMetaData('Order', -1)] param( # A string containing the text contents of the file [Parameter()] [Alias('TemplateText')] [string] $SourceText, [Alias('Replace')] [ValidateScript({ if ($_.GetGroupNames() -notcontains 'PS' -and $_.GetGroupNames() -notcontains 'PipeScript' ) { throw "Group Name PS or PipeScript required" } return $true })] [regex] $ReplacePattern, # The name of the template. This can be implied by the pattern. [Alias('Name')] $TemplateName, # The Start Pattern. # This indicates the beginning of what should be considered PipeScript. # An expression will match everything until -EndPattern [Alias('StartRegex')] [Regex] $StartPattern, # The End Pattern # This indicates the end of what should be considered PipeScript. [Alias('EndRegex')] [Regex] $EndPattern, # A custom replacement evaluator. # If not provided, will run any embedded scripts encountered. # The output of these scripts will be the replacement text. [Alias('Replacer')] [ScriptBlock] $ReplacementEvaluator, # If set, will not transpile script blocks. [switch] $NoTranspile, # The path to the source file. [string] $SourceFile, # A Script Block that will be injected before each inline is run. [ScriptBlock] $Begin, # A Script Block that will be piped to after each output. [Alias('Process')] [ScriptBlock] $ForeachObject, # A Script Block that will be injected after each inline script is run. [ScriptBlock] $End, # A collection of parameters [Collections.IDictionary] $Parameter = @{}, # An argument list. [Alias('Args')] [PSObject[]] $ArgumentList = @(), # Some languages only allow single-line comments. # To work with these languages, provide a -LinePattern indicating what makes a comment # Only lines beginning with this pattern within -StartPattern and -EndPattern will be considered a script. [Regex] $LinePattern, # The Command Abstract Syntax Tree. If this is provided, we are transpiling a template keyword. [Parameter(ValueFromPipeline)] [Management.Automation.Language.CommandAst] $CommandAst ) begin { $GetInlineScript = { param($match) $pipeScriptText = if ($Match.Groups["PipeScript"].Value) { $Match.Groups["PipeScript"].Value } elseif ($match.Groups["PS"].Value) { $Match.Groups["PS"].Value } if (-not $pipeScriptText) { return } if ($LinePattern -and $match.Groups["IsSingleLine"].Value) { $pipeScriptLines = @($pipeScriptText -split '(?>\r\n|\n)') $pipeScriptText = $pipeScriptLines -match $LinePattern -replace $LinePattern -join [Environment]::Newline } $InlineScriptBlock = [scriptblock]::Create($pipeScriptText) if (-not $InlineScriptBlock) { return } if ((-not $NoTranspile) -and $ExecutionContext.SessionState.InvokeCommand.GetCommand('.>PipeScript','Alias') ) { $TranspiledOutput = $InlineScriptBlock | .>Pipescript if ($TranspiledOutput -is [ScriptBlock]) { $InlineScriptBlock = $TranspiledOutput } } $InlineScriptBlock } } process { #region Finding Template Transpiler if ($CommandAst) { # Get command transpilers $commandInfoTranspilers = Get-Transpiler -CouldPipe $MyInvocation.MyCommand # Collect all of the bareword arguments $barewords = @(foreach ($cmdElement in $CommandAst.CommandElements) { if ( $cmdElement -isnot [Management.Automation.Language.StringConstantExpressionAst] -or $cmdElement.StringConstantType -ne 'Bareword' ) { break } $cmdElement.Value }) # Find a matching template transpiler. $foundTemplateTranspiler = :nextTranspiler foreach ($cmdTranspiler in $commandInfoTranspilers) { $langName = $cmdTranspiler.ExtensionCommand.DisplayName -replace '^(?>Inline|Template)\.' if ($barewords -contains $langName) { $cmdTranspiler continue } if ($CommandAst.CommandElements[1] -is [Management.Automation.Language.MemberExpressionAst] -and $CommandAst.CommandElements[1].Member.Value -eq $langName) { $cmdTranspiler continue } foreach ($attr in $cmdTranspiler.ExtensionCommand.Attributes) { if ($attr -isnot [Management.Automation.ValidatePatternAttribute]) { continue } $regexPattern = [Regex]::new($attr.RegexPattern, $attr.Options, '00:00:05') if ($regexPattern.Match($barewords[0]).Success) { $TemplateName = $barewords[0] $cmdTranspiler continue nextTranspiler } elseif ($barewords[1] -and $regexPattern.Match($barewords[1]).Success) { $TemplateName = $barewords[1] $cmdTranspiler continue nextTranspiler } if ($CommandAst.CommandElements[1] -is [Management.Automation.Language.MemberExpressionAst] -and $regexPattern.Match(('.' + $CommandAst.CommandElements[1].Member)).Success) { $cmdTranspiler continue nextTranspiler } } } # If we found a template transpiler # we'll want to effectively pack the transpilation engine into an object if ($foundTemplateTranspiler) { if (-not $foundTemplateTranspiler.ExtensionCommand.Parameters.AsTemplateObject) { Write-Error "$($foundTemplateTranspiler) does not support dynamic use" return } $Splat = & $foundTemplateTranspiler.ExtensionCommand -AsTemplateObject foreach ($kv in $splat.GetEnumerator()) { $ExecutionContext.SessionState.PSVariable.Set($kv.Key, $kv.Value) $PSBoundParameters[$kv.Key] = $kv.Value } } } #endregion Finding Template Transpiler if ($StartPattern -and $EndPattern) { # If the Source Start and End were provided, # create a replacepattern that matches all content until the end pattern. $ReplacePattern = [Regex]::New(" # Match the PipeScript Start $StartPattern # Match until the PipeScript end. This will be PipeScript (?<PipeScript> (?:.|\s){0,}?(?=\z|$endPattern) ) # Then Match the PipeScript End $EndPattern ", 'IgnoreCase, IgnorePatternWhitespace', '00:00:10') # Now switch the parameter set to SourceTextReplace $psParameterSet = 'SourceTextReplace' } $newModuleSplat = @{ScriptBlock={}} if ($SourceFile) { $newModuleSplat.Name = $SourceFile } # See if we have a replacement evaluator. if (-not $PSBoundParameters["ReplacementEvaluator"]) { # If we don't, create one. $ReplacementEvaluator = { param($match) $InlineScriptBlock = if ($this.GetInlineScript) { $this.GetInlineScript($match) } else { & $GetInlineScript $match } if (-not $InlineScriptBlock) { return } $inlineAstString = $InlineScriptBlock.Ast.Extent.ToString() if ($InlineScriptBlock.Ast.ParamBlock) { $inlineAstString = $inlineAstString.Replace($InlineScriptBlock.Ast.ParamBlock.Extent.ToString(), '') } $inlineAstString = $inlineAstString $ForeachObject = if ("$($this.ForeachObject)" -notmatch "(?>^\s{0,}$|^\s{0,}\{\s{0,}\}\s{0,}$)") { $this.ForeachObject } else { $ForeachObject } $begin = if ($this.Begin) { $this.Begin } else { $begin } $end = if ($this.End) { $this.End } else { $end } $AddForeach = $( if ($ForeachObject) { '|' + [Environment]::NewLine @(foreach ($foreachStatement in $ForeachObject) { if ($foreachStatement.Ast.ProcessBlock -or $foreachStatement.Ast.BeginBlock) { ". {$ForeachStatement}" } elseif ($foreachStatement.Ast.EndBlock.Statements -and $foreachStatement.Ast.EndBlock.Statements[0].PipelineElements -and $foreachStatement.Ast.EndBlock.Statements[0].PipelineElements[0].CommandElements -and $foreachStatement.Ast.EndBlock.Statements[0].PipelineElements[0].CommandElements.Value -in 'Foreach-Object', '%') { "$ForeachStatement" } else { "Foreach-Object {$ForeachStatement}" } }) -join (' |' + [Environment]::NewLine) } ) $statements = @( if ($begin) { "$begin" } if ($AddForeach) { "@($inlineAstString)" + $AddForeach.Trim() } else { $inlineAstString } if ($end) { "$end" } ) $codeToRun = [ScriptBlock]::Create($statements -join [Environment]::Newline) $context = if ($this.Context) { $this.Context } elseif ($FileModuleContext) { $FileModuleContext } . $context { $match = $($args)} $match "$(. $context $codeToRun)" } } #region Template Keyword if ($CommandAst) { # This object will need to be able to evaluate itself. $EvaluateTemplate = { param() $fileText = $this.Template # Collect arguments for our template $ArgumentList = @() $Parameter = [Ordered]@{} foreach ($arg in $args) { if ($arg -is [Collections.IDictionary]) { foreach ($kv in $arg.GetEnumerator()) { $Parameter[$kv.Key] = $kv.Value } } else { $ArgumentList += $arg } } $ReplacePattern = [Regex]::new($this.Pattern,'IgnoreCase,IgnorePatternwhitespace','00:00:05') # Walk thru each match before we replace it foreach ($match in $ReplacePattern.Matches($fileText)) { # get the inline script block $inlineScriptBlock = $this.GetInlineScript($match) if (-not $inlineScriptBlock -or # If there was no block or # there were no parameters, -not $inlineScriptBlock.Ast.ParamBlock.Parameters ) { continue # skip. } # Create a script block out of just the parameter block $paramScriptBlock = [ScriptBlock]::Create( $inlineScriptBlock.Ast.ParamBlock.Extent.ToString() ) # Dot that script into the file's context. # This is some wonderful PowerShell magic. # By doing this, the variables are defined with strong types and values. . $this.Context $paramScriptBlock @Parameter @ArgumentList } $ReplacementEvaluator = if ($this.Evaluator.Script) { $this.Evaluator.Script } elseif ($ReplacementEvaluator) { $ReplacementEvaluator } else { {} } return $ReplacePattern.Replace($fileText, $ReplacementEvaluator) } $templateToString = { param() if ($args) { $this.Evaluate($args) } else { $this.Evaluate() } } $SaveTemplate = { param() if ($this.Name -eq $this.Language) { $name, $evalArgs = $args } else { if ($args -and $args[0] -is [string] -and $args[0] -match '[\\/\.]') { $name, $evalArgs = $args } else { $name = $this.Name $evalArgs = $args } } if (-not $name) { throw "Must provide a .Name or the first argument must be a name" } $evaluated = if ($evalArgs) { $this.Evaluate($evalArgs) } else { $this.Evaluate() } if (-not (Test-Path $Name)) { $null = New-Item -ItemType File -Path $name -Force } $evaluated | Set-Content -Path $name Get-Item -Path $name } $filePattern = foreach ($attr in $foundTemplateTranspiler.ExtensionCommand.ScriptBlock.Attributes) { if ($attr -is [ValidatePattern]) { $attr.RegexPattern break } } $mySentence = $CommandAst.AsSentence($MyInvocation.MyCommand) if ($mySentence.Parameter.Count) { foreach ($clause in $mySentence.Clauses) { if ($clause.ParameterName) { $ExecutionContext.SessionState.PSVariable.Set($clause.ParameterName, $mySentence.Parameter[$clause.Name]) } } } $null, $templateElements = foreach ($sentenceArg in $mySentence.ArgumentList) { if ($sentenceArg.StringConstantType -eq 'Bareword' -and $sentenceArg.Value -eq 'template') { continue } $convertedAst = if ($sentenceArg.ConvertFromAst) { $sentenceArg.ConvertFromAst() } else { $sentenceArg } if ($convertedAst -is [string]) { $convertedAst = "'$($convertedAst -replace "'","''")'" } if ($convertedAst -is [ScriptBlock]) { $convertedAst = "{$ConvertedAst}" } $convertedAst } if (-not $templateElements) { $templateElements = "''"} $languageString = $($foundTemplateTranspiler.ExtensionCommand.DisplayName -replace '(?>Template|Inline)\.' -replace '^\.') if (-not $TemplateName) { $TemplateName = $languageString } $createdSb = [scriptblock]::Create(@" `$( `$templateObject = [PSCustomObject][Ordered]@{ PSTypeName = 'PipeScript.Template' Name = '$($TemplateName -replace "'", "''")' Language = '$languageString' SourceFile = '' FilePattern = '$($filePattern -replace "'", "''")' Pattern = [regex]::new(@' $replacePattern '@,'IgnoreCase,IgnorePatternWhitespace', '00:00:05') Context = New-Module -ScriptBlock {} ForeachObject = $(if ($ForeachObject) { "{ $foreachObject }"} else {'{}'}) Begin = $(if ($Begin) { "{ $begin }"} else {'{}'}) End = $(if ($End) { "{ $end }"} else {'{}'}) Template = $templateElements } `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'Evaluator', { $replacementEvaluator } ), `$true) `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'Evaluate', { $evaluateTemplate } ), `$true) `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'GetInlineScript', { $GetInlineScript } ), `$true) `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'EvaluateTemplate', { $EvaluateTemplate } ), `$true) `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'Save', { $SaveTemplate } ), `$true) `$templateObject.psobject.members.Add([PSScriptMethod]::new( 'ToString', { $TemplateToString } ), `$true) `$templateObject ) "@ ) $createdSb return } #endregion Template Keyword $FileModuleContext = New-Module @newModuleSplat # If the parameter set was SourceTextReplace if ($ReplacePattern) { $fileText = $SourceText # Walk thru each match before we replace it foreach ($match in $ReplacePattern.Matches($fileText)) { # get the inline script block $inlineScriptBlock = & $GetInlineScript $match if (-not $inlineScriptBlock -or # If there was no block or # there were no parameters, -not $inlineScriptBlock.Ast.ParamBlock.Parameters ) { continue # skip. } # Create a script block out of just the parameter block $paramScriptBlock = [ScriptBlock]::Create( $inlineScriptBlock.Ast.ParamBlock.Extent.ToString() ) # Dot that script into the file's context. # This is some wonderful PowerShell magic. # By doing this, the variables are defined with strong types and values. . $FileModuleContext $paramScriptBlock @Parameter @ArgumentList } # Now, we run the replacer. # This should run each inline script and replace the text. $replacement = try { $ReplacePattern.Replace($fileText, $ReplacementEvaluator) } catch { $ex = $_ Write-Error -ErrorRecord $ex # $PSCmdlet.WriteError($ex) } return $replacement } } |