EPS.psm1

#######################################################
##
## EPS - Embedded PowerShell
## Dave Wu, June 2014
##
## Templating tool for PowerShell
## For detailed usage please refer to:
## http://straightdave.github.io/eps
##
#######################################################

$execPath   = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
$thisfile   = "$execPath\eps.psm1"
#$sysLibFile = "$execPath\sys_lib.ps1" # import built-in resources to eps file

## Expand-Template:
##
## Key entrance of EPS
## Safe mode: start a new/isolated PowerShell instance to compile the templates
## to prevent result from being polluted by variables in current context
## With Safe mode: you can pass a hashtable containing all variables to this function.
## Compiling process will inject values recorded in hashtable to template
##
## Usage:
##
## Expand-Template [[-template] <text>]|[-file <file name>] [-safe] [-binding <hashtable>]
##
## Examples:
## - Expand-Template -template $text
## will use current context to fill variables in template. If no '$name' exists in current context, it will produce blanks.
## - Expand-Template -template $text -safe -binding @{ name = "dave" }
## will use "dave" to render the placeholder "<%= $name %>" in template
##
## Other example:
## $result = Expand-Template -file $a_file -safe -binding @{ name = "dave" }
## *Note*: here using safe mode
##
## or
##
## $text = @'
## Dave is a <% if($true){ %>man<% }else{ %>lady<% } %>.
## Davie is <%= $age %>.
## '@
##
## $age = 26
## $result = Expand-Template -template $text
##
function Expand-Template {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
  Param(
    [string]$Template,
    [string]$File,

    [Parameter(ValueFromPipeline=$True, ValueFromPipelinebyPropertyName=$True)]
    [Hashtable]$binding = @{},
    
    [switch]$safe
  )
  
  if (!$Template -and !$File) {
    Throw New-Object System.ArgumentException "Either Template or File must be provided" 
  }
  
  if($file -and (test-path $file)){
    $temp1 = Get-Content $file
    $template = $temp1 -join "`n"
  }
  
  if($sysLibFile -and (test-path $sysLibFile)){
    $template = "<% . $sysLibFile %>`n" + $template  
  }
  
  if($safe){
    $p = [powershell]::create()
    
    $block = {
      param(
      $temp,
      $lib,
      $binding = @{}    # variable binding
      )
      
      . $lib   # load Compile-Raw

      $binding.keys | ForEach-Object { New-Variable -Name $_ -Value $binding[$_] }     
      
      $script = Compile-Raw $temp      
      Invoke-Expression $script      
    }
    
    [void]$p.addscript($block)
    [void]$p.addparameter("temp",$template)
    [void]$p.addparameter("lib",$thisfile)
    [void]$p.addparameter("binding",$binding)
    $p.invoke()
  }
  else{
    $binding.keys | ForEach-Object { New-Variable -Name $_ -Value $binding[$_] }     

    $script = Compile-Raw $template
    Invoke-Expression $script
  }
}

## Compile-Raw:
##
## Used internally. To comiple templates into text
## Input parameter '$raw' should be a [string] type.
## So if reading from a file via 'gc/get-content' cmdlet,
## you should join all lines together with new-line ("`n") as delimiters
##
function Compile-Raw{
  param(
  [string]$raw,
  [switch]$debug = $false
  )

  #========================
  # constants
  #========================
  $pre_cmd = @('$_temp = ""')
  $post_cmd = @('$_temp')
  $put_cmd = '$_temp += '
  $insert_cmd = '$_temp += ' 
  $p = [regex]'(?si)(?<content>.*?)(?<token><%%|%%>|<%=|<%#|<%|%>|\n)'
  
  #========================
  # 'global' variables
  #========================
  $content = ''
  $stag = ''  # start tag
  $line = @()
  $w = $false # whether last tag-pair is <% %>
  
  #========================
  # start!
  #========================
  $pre_cmd | ForEach-Object { $line += $_ }
  $raw += "`n"
  
  $m = $p.match($raw)
  while($m.success){
    $content = $m.groups["content"].value
    $token = $m.groups["token"].value
    
    if($stag -eq ''){
      
      # escaping characters
      $content = $content -replace '([`"$])', '`$1'
    
      switch($token){
        { '<%', '<%=', '<%#' -contains $_ } {
          $stag = $token          
        }
        
        "`n" {
          if( -not $w ) { 
            $content += '`n'
          }
        }
        
        '<%%' {
          $content += '<%'
        }
        
        '%%>' {
          $content += '%>'
        }
        
        default {
          $content += $token
        }
      }
      
      $w = $false
    } 
    else{
      switch($token){
        '%>' {          
          switch($stag){
            '<%' {
              $line += $content
              $w = $true
            }
            
            '<%=' {
              $line += ($insert_cmd + '"$(' + $content.trim() + ')"')
            }
            
            '<%#' { }
          }
          
          $stag = ''
          $content = ''
        }
        
        "`n" {
          if($stag -eq '<%' -and $content -ne ''){            
            $line += $content
          }
          $content = ''
        }
        
        default {
          $content += $token
        }
      }
    }
    
    if( $content -ne '') { $line += ($put_cmd + '"' + $content + '"') }
    $m = $m.nextMatch()
  }
  
  $post_cmd | ForEach-Object { $line += $_ }
  $script = ($line -join ';')
  
  if($debug) {
    return $line
  }
  
  $line = $null
  $script
}