Transpilers/Core/PipeScript.Inline.psx.ps1

<#
.Synopsis
    Inline Transpiler
.Description
    The PipeScript Core Inline Transpiler. This makes Source Generators with inline PipeScript work.

    Regardless of underlying source language, a source generator works in a fairly straightforward way.

    Inline PipeScript will be embedded within the file (usually in comments).

    Anything encountered in a source generator file can be either:

    * A Literal String (written directly in the underlying source language)
    * A Script Block (written in PowerShell or PipeScript)

    This Transpiler takes a sequence of literal strings and script blocks, and constructs the source generation script.
#>

param(
# A list of source sections
[Parameter(Mandatory,ParameterSetName='SourceSections',Position=0,ValueFromPipeline)]
[PSObject[]]
$SourceSection,

# A string containing the text contents of the file
[Parameter(Mandatory,ParameterSetName='SourceTextAndPattern')]
[Parameter(Mandatory,ParameterSetName='SourceTextReplace')]
[string]
$SourceText,

# A string containing the pattern used to recognize special sections of source code.
[Parameter(Mandatory,ParameterSetName='SourceTextAndPattern')]
[regex]
$SourcePattern,


[Parameter(Mandatory,ParameterSetName='SourceTextReplace')]
[Alias('Replace')]
[ValidateScript({    
    if ($_.GetGroupNames() -notcontains 'PS' -and 
        $_.GetGroupNames() -notcontains 'PipeScript'
    ) {
        throw "Group Name PS or PipeScript required"
    }
    return $true
})]
[regex]
$ReplacePattern,

[Parameter(ParameterSetName='SourceTextReplace')]
[Alias('Replacer')]
[ScriptBlock]
$ReplacementEvaluator,

# If set, will not transpile script blocks.
[Parameter(ParameterSetName='SourceTextAndPattern')]
[Parameter(ParameterSetName='SourceSections')]
[switch]
$NoTranspile,

# The path to the source file.
[Parameter(ParameterSetName='SourceTextAndPattern')]
[Parameter(ParameterSetName='SourceSections')]
[Parameter(ParameterSetName='SourceTextReplace')]
[string]
$SourceFile,

# A Script Block that will be injected before each inline is run.
[Parameter(ParameterSetName='SourceTextAndPattern')]
[Parameter(ParameterSetName='SourceSections')]
[Parameter(ParameterSetName='SourceTextReplace')]
[ScriptBlock]
$Begin,

# A Script Block that will be piped to after each output.
[Parameter(ParameterSetName='SourceTextAndPattern')]
[Parameter(ParameterSetName='SourceSections')]
[Parameter(ParameterSetName='SourceTextReplace')]
[Alias('Process')]
[ScriptBlock]
$ForeachObject,

# A Script Block that will be injected after each inline script is run.
[Parameter(ParameterSetName='SourceTextAndPattern')]
[Parameter(ParameterSetName='SourceSections')]
[Parameter(ParameterSetName='SourceTextReplace')]
[ScriptBlock]
$End
)

begin {
    $allSections = @()
}

process {
    if ($psCmdlet.ParameterSetName -eq 'SourceTextReplace') {
        $fileText      = $SourceText
        if (-not $PSBoundParameters["ReplacementEvaluator"]) {
            $ReplacementEvaluator = {
                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
                }

                $InlineScriptBlock = [scriptblock]::Create($pipeScriptText)
                if (-not $InlineScriptBlock) {                    
                    return
                }

                if (-not $NoTranspile) {
                    $TranspiledOutput = $InlineScriptBlock | .>Pipescript
                    if ($TranspiledOutput -is [ScriptBlock]) {
                        $InlineScriptBlock = $TranspiledOutput
                    }
                }
                
                $inlineAstString = $InlineScriptBlock.Ast.Extent.ToString()
                if ($InlineScriptBlock.ParamBlock) {
                    $inlineAstString = $inlineAstString.Replace($InlineScriptBlock.ParamBlock.Extent.ToString(), '')
                }
                $inlineAstString = $inlineAstString
                $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[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)

                "$(& $codeToRun)"
            }
        }

        return $ReplacePattern.Replace($fileText, $ReplacementEvaluator)
    }

    if ($psCmdlet.ParameterSetName -eq 'SourceTextAndPattern') {

        $fileText      = $SourceText        
        $foundSpots    = @($SourcePattern.Matches($fileText))

        $SourceGeneratorInput = @(
            $index = 0                                    
            for ($spotIndex = 0; $spotIndex -lt $foundSpots.Count; $spotIndex++) {

                # If there's any distance between the last token and here, output it as a string.
                if ($foundSpots[$spotIndex].Index -gt $index) {
                    $captureLength    = $foundSpots[$spotIndex].Index - $index
                    if ($captureLength -ge 0) {
                        $fileText.Substring($index, $captureLength)                
                    }
                    $index = $foundSpots[$spotIndex].Index + $foundSpots[$spotIndex].Length
                }

                $isLastSpot = $spotIndex -ge ($foundSpots.Length - 1)

                if ($foundSpots[$spotIndex].Groups["PSStart"].Length) {
                    $absoluteStart = $foundSpots[$spotIndex].Groups["PSStart"].Index + 
                        $foundSpots[$spotIndex].Groups["PSStart"].Length 

                    $index = $foundSpots[$spotIndex + 1].Index + 
                        $foundSpots[$spotIndex + 1].Length
                    if (-not $isLastSpot -and $foundSpots[$spotIndex + 1].Groups["PSEnd"].Length) {
                        # If we find an end block, the next section becomes code
                                                
                        $scriptToCreate = @(
                            if ($Begin) { $Begin }
                            $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[0].CommandElements -and
                                                $foreachStatement.Ast.EndBlock.Statements[0].PipelineElements[0].CommandElements.Value -in 'Foreach-Object', '%') {
                                                "$ForeachStatement"
                                            } else {
                                                "Foreach-Object {$ForeachStatement}"
                                            }
                                        }) -join (' |' + [Environment]::NewLine)
                                    }
                                )
                            $Statement = $fileText.Substring($absoluteStart, 
                                $index - $absoluteStart - $foundSpots[$spotIndex + 1].Groups["PSEnd"].Length
                            )
                            if ($AddForeach) {
                                "@($Statement)" + $AddForeach.Trim()
                            } else {
                                $Statement
                            }
                            
                            if ($End) { $end}
                            ) -join [Environment]::Newline
                        [scriptblock]::Create($scriptToCreate)
                        
                        $spotIndex++                
                    } else {
                        Write-Error "Start Not Followed By End' $($foundSpots[$spotIndex].Index)'"    
                    }
                }            
            }

            if ($index -lt $fileText.Length) {
                $fileText.Substring($index)            
            }
        )

        $null = $PSBoundParameters.Remove("SourceText")
        $null = $PSBoundParameters.Remove("SourcePattern")
        
        & $myInvocation.MyCommand.ScriptBlock @PSBoundParameters -SourceSection $SourceGeneratorInput
        return
    }

    $allSections += @(
    foreach ($section in $SourceSection) {
        
        if ($section -is [string]) {
            if ($section -match '[\r\n]') {
                "@'" + [Environment]::NewLine + $section + [Environment]::newLine + "'@"
            } else {
                "'" + $section.Replace("'", "''") + "'"
            }
        }

        if ($section -is [ScriptBlock]) {
            if (-not $NoTranspile) {
                $section | 
                    .>Pipescript
            } else {
                $section
            }
        }

    })
}

end {
    if ($allSections) {
        
        $combinedSections = @(for ($sectionIndex = 0 ; $sectionIndex -lt $allSections.Length; $sectionIndex++) {
            $section = $allSections[$sectionIndex]
            $isLastSection = $sectionIndex -eq $allSections.Length - 1
            if ($section -is [ScriptBlock]) {
                "`$($section)"
            } else {
                $section
            }
            if (-not $isLastSection) {
                '+'
            }
        })
        $combinedFile = $combinedSections -join ' '

        if ($SourceFile -and $SourceFile -match '\.ps1{0,1}\.(?<ext>[^.]+$)') {
            $sourceTempFilePath = $SourceFile -replace '\.ps1{0,1}\.(?<ext>[^.]+$)', '.${ext}.source.ps1'
            $combinedFile | Set-Content $sourceTempFilePath -Force
        }

        
        try {
            [scriptblock]::Create($combinedFile)
        } catch {
            $ex = $_       
            Write-Error -ErrorRecord $ex
        }
    }
}