├── .config └── dotnet-tools.json ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── .octopus ├── channels.ocl ├── deployment_process.ocl ├── deployment_settings.ocl └── schema_version.ocl ├── GitVersion.yml ├── LICENSE.txt ├── NuGet.Config ├── README.md ├── build.cake ├── build.cmd ├── build.ps1 ├── build.sh ├── global.json ├── source ├── Octostache.Tests │ ├── BaseFixture.cs │ ├── CalculationFixture.cs │ ├── ConditionalsFixture.cs │ ├── ExtensionFixture.cs │ ├── FiltersFixture.cs │ ├── ItemCacheFixture.cs │ ├── IterationFixture.cs │ ├── JsonFixture.cs │ ├── Octostache.Tests.csproj │ ├── ParserFixture.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── UsageFixture.cs │ └── VersionFixture.cs ├── Octostache.sln ├── Octostache.sln.DotSettings └── Octostache │ ├── CustomStringParsers │ └── JsonParser.cs │ ├── NullableReferenceTypeAttributes.cs │ ├── Octostache.csproj │ ├── Octostache.nuspec │ ├── Properties │ └── AssemblyInfo.cs │ ├── Templates │ ├── AnalysisContext.cs │ ├── Binding.cs │ ├── BuiltInFunctions.cs │ ├── CalculationToken.cs │ ├── ConditionalToken.cs │ ├── Constants.cs │ ├── ContentExpression.cs │ ├── DependencyWildcard.cs │ ├── EvaluationContext.cs │ ├── FunctionCallExpression.cs │ ├── Functions │ │ ├── DateFunction.cs │ │ ├── FormatFunction.cs │ │ ├── HashFunction.cs │ │ ├── NullFunction.cs │ │ ├── TextCaseFunction.cs │ │ ├── TextComparisonFunctions.cs │ │ ├── TextEscapeFunctions.cs │ │ ├── TextManipulationFunction.cs │ │ ├── TextReplaceFunction.cs │ │ ├── TextSubstringFunction.cs │ │ └── VersionParseFunction.cs │ ├── IInputToken.cs │ ├── Identifier.cs │ ├── Indexer.cs │ ├── ItemCache.cs │ ├── PropertyListBinder.cs │ ├── RecursiveDefinitionException.cs │ ├── RepetitionToken.cs │ ├── SubstitutionToken.cs │ ├── SymbolExpression.cs │ ├── SymbolExpressionStep.cs │ ├── Template.cs │ ├── TemplateAnalyzer.cs │ ├── TemplateEvaluator.cs │ ├── TemplateParser.cs │ ├── TemplateToken.cs │ └── TextToken.cs │ ├── VariableDictionary.cs │ └── VariablesFileFormatter.cs └── tools └── packages.config /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "gitversion.tool": { 6 | "version": "6.0.4", 7 | "commands": [ 8 | "dotnet-gitversion" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/source/" 5 | schedule: 6 | interval: "daily" 7 | time: "09:00" 8 | timezone: "Pacific/Auckland" 9 | open-pull-requests-limit: 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | project.lock.json 198 | tools/ 199 | 200 | # Include the cake packages.config 201 | !tools/packages.config 202 | 203 | # Rider 204 | .idea/ 205 | -------------------------------------------------------------------------------- /.octopus/channels.ocl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OctopusDeploy/Octostache/f906f440a154818402b98fe85711d6d787581ea4/.octopus/channels.ocl -------------------------------------------------------------------------------- /.octopus/deployment_process.ocl: -------------------------------------------------------------------------------- 1 | step "Approval Required" { 2 | 3 | action { 4 | action_type = "Octopus.Manual" 5 | environments = ["Components - External"] 6 | properties = { 7 | Octopus.Action.Manual.BlockConcurrentDeployments = "False" 8 | Octopus.Action.Manual.Instructions = "Please approve release before it is promoted to NuGet" 9 | } 10 | worker_pool_variable = "" 11 | } 12 | } 13 | 14 | step "NuGet Push" { 15 | 16 | action { 17 | properties = { 18 | NuGetPush.ApiKey = "#{NugetApiKey}" 19 | NuGetPush.Source.Package = "{\"PackageId\":\"Octostache\",\"FeedId\":\"Octopus Server (built-in)\"}" 20 | NuGetPush.Target.Url = "#{NugetUrl}" 21 | Octopus.Action.Template.Id = "ActionTemplates-1241" 22 | Octopus.Action.Template.Version = "0" 23 | } 24 | worker_pool = "Hosted Ubuntu" 25 | 26 | packages "NuGetPush.Source.Package" { 27 | acquisition_location = "Server" 28 | feed = "Octopus Server (built-in)" 29 | package_id = "Octostache" 30 | properties = { 31 | Extract = "False" 32 | PackageParameterName = "NuGetPush.Source.Package" 33 | SelectionMode = "deferred" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /.octopus/deployment_settings.ocl: -------------------------------------------------------------------------------- 1 | connectivity_policy { 2 | } 3 | 4 | versioning_strategy { 5 | 6 | donor_package { 7 | package = "NuGetPush.Source.Package" 8 | step = "NuGet Push" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.octopus/schema_version.ocl: -------------------------------------------------------------------------------- 1 | version = 3 -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDeployment 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Octopus Deploy and contributors. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Octostache is the variable substitution syntax for Octopus Deploy. 2 | 3 | Octopus allows you to [define variables](http://g.octopushq.com/DocumentationVariables), which can then be referenced from deployment steps and in scripts using the following syntax: 4 | 5 | ``` 6 | #{MyVariable} 7 | ``` 8 | 9 | This library contains the code for parsing and evaluating these variable expressions. 10 | 11 | Usage is simple: install **Octostache** from NuGet.org, then create a `VariableDictionary`: 12 | 13 | ```csharp 14 | var variables = new VariableDictionary(); 15 | variables.Set("Server", "Web01"); 16 | variables.Set("Port", "10933"); 17 | variables.Set("Url", "http://#{Server | ToLower}:#{Port}"); 18 | 19 | var url = variables.Get("Url"); // http://web01:10933 20 | var raw = variables.GetRaw("Url"); // http://#{Server | ToLower}:#{Port} 21 | var eval = variables.Evaluate("#{Url}/foo"); // http://web01:10933/foo 22 | ``` 23 | 24 | More examples can be found in [UsageFixture](https://github.com/OctopusDeploy/Octostache/blob/master/source/Octostache.Tests/UsageFixture.cs). 25 | 26 | ## Contributing 27 | 🐙 We welcome Pull Requests ❤️🧑‍💻 28 | 29 | ### Code Cleanup 30 | The first stage of our CI/CD pipeline for Octostache runs a ReSharper code cleanup, to keep everything neat and tidy. 31 | 32 | Your PR won't be able to pass without ensuring the code is clean. You can do this locally via the [ReSharper CLI tools](https://www.jetbrains.com/help/rider/ReSharper_Command_Line_Tools.html), which is how we enforce it during our builds. 33 | 34 | All the formatting settings are committed to `Octostache.sln.DotSettings`, so as long as you don't override these with an `Octostache.sln.DotSettings.User` file, you should be all good. 35 | 36 | To get started with code cleanup the easiest way (via `dotnet tool`), get the CodeCleanup tool installed globally (one-time): 37 | ``` 38 | dotnet tool install -g JetBrains.ReSharper.GlobalTools 39 | ``` 40 | then execute the cleanup: 41 | ``` 42 | jb cleanupcode ./source/Octostache.sln 43 | ``` 44 | 45 | We don't try to enforce this through build scripts or pre-commit hooks, it's up to you to run when you need to. If you use the Rider IDE, it seems to apply another opinion or two when running the code cleanup, and might get different results to the CLI approach; we don't recommend cleaning up this way. 46 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // TOOLS 3 | ////////////////////////////////////////////////////////////////////// 4 | #module nuget:?package=Cake.DotNetTool.Module&version=0.4.0 5 | #tool "dotnet:?package=GitVersion.Tool&version=5.12.0" 6 | #tool "nuget:?package=OctopusTools&version=9.0.0" 7 | #addin nuget:?package=Cake.Git&version=1.1.0 8 | 9 | ////////////////////////////////////////////////////////////////////// 10 | // ARGUMENTS 11 | ////////////////////////////////////////////////////////////////////// 12 | var target = Argument("target", "Default"); 13 | var configuration = Argument("configuration", "Release"); 14 | 15 | /////////////////////////////////////////////////////////////////////////////// 16 | // GLOBAL VARIABLES 17 | /////////////////////////////////////////////////////////////////////////////// 18 | var artifactsDir = "./artifacts/"; 19 | var localPackagesDir = "../LocalPackages"; 20 | 21 | GitVersion gitVersionInfo; 22 | string nugetVersion; 23 | 24 | 25 | /////////////////////////////////////////////////////////////////////////////// 26 | // SETUP / TEARDOWN 27 | /////////////////////////////////////////////////////////////////////////////// 28 | Setup(context => 29 | { 30 | gitVersionInfo = GitVersion(new GitVersionSettings { 31 | OutputType = GitVersionOutput.Json 32 | }); 33 | 34 | nugetVersion = gitVersionInfo.NuGetVersion; 35 | 36 | Information("Building Octostache v{0}", nugetVersion); 37 | Information("Informational Version {0}", gitVersionInfo.InformationalVersion); 38 | }); 39 | 40 | Teardown(context => 41 | { 42 | Information("Finished running tasks."); 43 | }); 44 | 45 | ////////////////////////////////////////////////////////////////////// 46 | // PRIVATE TASKS 47 | ////////////////////////////////////////////////////////////////////// 48 | 49 | Task("Clean") 50 | .Does(() => 51 | { 52 | CleanDirectory(artifactsDir); 53 | CleanDirectories("./source/**/bin"); 54 | CleanDirectories("./source/**/obj"); 55 | CleanDirectories("./source/**/TestResults"); 56 | }); 57 | 58 | Task("Restore") 59 | .IsDependentOn("Clean") 60 | .Does(() => { 61 | DotNetCoreRestore("source"); 62 | }); 63 | 64 | 65 | Task("Build") 66 | .IsDependentOn("Restore") 67 | .IsDependentOn("Clean") 68 | .Does(() => 69 | { 70 | DotNetCoreBuild("./source", new DotNetCoreBuildSettings 71 | { 72 | Configuration = configuration, 73 | ArgumentCustomization = args => args.Append($"/p:Version={nugetVersion}") 74 | }); 75 | }); 76 | 77 | Task("Test") 78 | .IsDependentOn("Build") 79 | .Does(() => 80 | { 81 | DotNetCoreTest("./source/Octostache.Tests/Octostache.Tests.csproj", new DotNetCoreTestSettings 82 | { 83 | Configuration = configuration, 84 | NoBuild = true, 85 | ArgumentCustomization = args => args.Append("-l trx") 86 | }); 87 | }); 88 | 89 | Task("Pack") 90 | .IsDependentOn("Test") 91 | .Does(() => 92 | { 93 | DotNetCorePack("./source/Octostache", new DotNetCorePackSettings 94 | { 95 | Configuration = configuration, 96 | OutputDirectory = artifactsDir, 97 | NoBuild = true, 98 | ArgumentCustomization = args => args.Append($"/p:Version={nugetVersion}") 99 | }); 100 | 101 | DeleteFiles(artifactsDir + "*symbols*"); 102 | }); 103 | 104 | Task("CopyToLocalPackages") 105 | .IsDependentOn("Pack") 106 | .WithCriteria(BuildSystem.IsLocalBuild) 107 | .Does(() => 108 | { 109 | CreateDirectory(localPackagesDir); 110 | CopyFileToDirectory($"{artifactsDir}/Octostache.{nugetVersion}.nupkg", localPackagesDir); 111 | }); 112 | 113 | Task("Default") 114 | .IsDependentOn("Pack") 115 | .IsDependentOn("CopyToLocalPackages"); 116 | 117 | ////////////////////////////////////////////////////////////////////// 118 | // EXECUTION 119 | ////////////////////////////////////////////////////////////////////// 120 | RunTarget(target); 121 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | REM see http://joshua.poehls.me/powershell-batch-file-wrapper/ 3 | 4 | SET SCRIPTNAME=%~d0%~p0%~n0.ps1 5 | SET ARGS=%* 6 | IF [%ARGS%] NEQ [] GOTO ESCAPE_ARGS 7 | 8 | :POWERSHELL 9 | PowerShell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Unrestricted -Command "& { $ErrorActionPreference = 'Stop'; & '%SCRIPTNAME%' @args; EXIT $LASTEXITCODE }" %ARGS% 10 | EXIT /B %ERRORLEVEL% 11 | 12 | :ESCAPE_ARGS 13 | SET ARGS=%ARGS:"=\"% 14 | SET ARGS=%ARGS:`=``% 15 | SET ARGS=%ARGS:'=`'% 16 | SET ARGS=%ARGS:$=`$% 17 | SET ARGS=%ARGS:{=`}% 18 | SET ARGS=%ARGS:}=`}% 19 | SET ARGS=%ARGS:(=`(% 20 | SET ARGS=%ARGS:)=`)% 21 | SET ARGS=%ARGS:,=`,% 22 | SET ARGS=%ARGS:^%=% 23 | 24 | GOTO POWERSHELL -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER Experimental 25 | Tells Cake to use the latest Roslyn release. 26 | .PARAMETER WhatIf 27 | Performs a dry run of the build script. 28 | No tasks will be executed. 29 | .PARAMETER Mono 30 | Tells Cake to use the Mono scripting engine. 31 | .PARAMETER SkipToolPackageRestore 32 | Skips restoring of packages. 33 | .PARAMETER ScriptArgs 34 | Remaining arguments are added here. 35 | 36 | .LINK 37 | http://cakebuild.net 38 | 39 | #> 40 | 41 | [CmdletBinding()] 42 | Param( 43 | [string]$Script = "build.cake", 44 | [string]$Target = "Default", 45 | [ValidateSet("Release", "Debug")] 46 | [string]$Configuration = "Release", 47 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 48 | [string]$Verbosity = "Verbose", 49 | [switch]$Experimental = $true, 50 | [Alias("DryRun","Noop")] 51 | [switch]$WhatIf, 52 | [switch]$Mono, 53 | [switch]$SkipToolPackageRestore, 54 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 55 | [string[]]$ScriptArgs 56 | ) 57 | 58 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 59 | function MD5HashFile([string] $filePath) 60 | { 61 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 62 | { 63 | return $null 64 | } 65 | 66 | [System.IO.Stream] $file = $null; 67 | [System.Security.Cryptography.MD5] $md5 = $null; 68 | try 69 | { 70 | $md5 = [System.Security.Cryptography.MD5]::Create() 71 | $file = [System.IO.File]::OpenRead($filePath) 72 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 73 | } 74 | finally 75 | { 76 | if ($file -ne $null) 77 | { 78 | $file.Dispose() 79 | } 80 | } 81 | } 82 | 83 | Write-Host "Preparing to run build script..." 84 | 85 | if(!$PSScriptRoot){ 86 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 87 | } 88 | 89 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 90 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 91 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 92 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 93 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 94 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 95 | 96 | # Should we use mono? 97 | $UseMono = ""; 98 | if($Mono.IsPresent) { 99 | Write-Verbose -Message "Using the Mono based scripting engine." 100 | $UseMono = "-mono" 101 | } 102 | 103 | # Should we use the new Roslyn? 104 | $UseExperimental = ""; 105 | if($Experimental.IsPresent -and !($Mono.IsPresent)) { 106 | Write-Verbose -Message "Using experimental version of Roslyn." 107 | $UseExperimental = "-experimental" 108 | } 109 | 110 | # Is this a dry run? 111 | $UseDryRun = ""; 112 | if($WhatIf.IsPresent) { 113 | $UseDryRun = "-dryrun" 114 | } 115 | 116 | # Make sure tools folder exists 117 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 118 | Write-Verbose -Message "Creating tools directory..." 119 | New-Item -Path $TOOLS_DIR -Type directory | out-null 120 | } 121 | 122 | # Make sure that packages.config exist. 123 | if (!(Test-Path $PACKAGES_CONFIG)) { 124 | Write-Verbose -Message "Downloading packages.config..." 125 | try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { 126 | Throw "Could not download packages.config." 127 | } 128 | } 129 | 130 | # Try find NuGet.exe in path if not exists 131 | if (!(Test-Path $NUGET_EXE)) { 132 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 133 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } 134 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 135 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 136 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 137 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 138 | } 139 | } 140 | 141 | # Try download NuGet.exe if not exists 142 | if (!(Test-Path $NUGET_EXE)) { 143 | Write-Verbose -Message "Downloading NuGet.exe..." 144 | try { 145 | (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) 146 | } catch { 147 | Throw "Could not download NuGet.exe." 148 | } 149 | } 150 | 151 | # Save nuget.exe path to environment to be available to child processed 152 | $ENV:NUGET_EXE = $NUGET_EXE 153 | 154 | # Restore tools from NuGet? 155 | if(-Not $SkipToolPackageRestore.IsPresent) { 156 | Push-Location 157 | Set-Location $TOOLS_DIR 158 | 159 | # Check for changes in packages.config and remove installed tools if true. 160 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 161 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 162 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 163 | Write-Verbose -Message "Missing or changed package.config hash..." 164 | Remove-Item * -Recurse -Exclude packages.config,nuget.exe 165 | } 166 | 167 | Write-Verbose -Message "Restoring tools from NuGet..." 168 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 169 | 170 | if ($LASTEXITCODE -ne 0) { 171 | Throw "An error occured while restoring NuGet tools." 172 | } 173 | else 174 | { 175 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 176 | } 177 | Write-Verbose -Message ($NuGetOutput | out-string) 178 | Pop-Location 179 | } 180 | 181 | # Make sure that Cake has been installed. 182 | if (!(Test-Path $CAKE_EXE)) { 183 | Throw "Could not find Cake.exe at $CAKE_EXE" 184 | } 185 | 186 | # We added this so we can use dotnet tools 187 | # See https://www.gep13.co.uk/blog/introducing-cake.dotnettool.module 188 | Write-Host "Installing cake modules using the --bootstrap argument" 189 | &$CAKE_EXE --bootstrap 190 | 191 | # Start Cake 192 | Write-Host "Running build script..." 193 | Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" 194 | exit $LASTEXITCODE -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ########################################################################## 4 | # This is the Cake bootstrapper script for Linux and OS X. 5 | # This file was downloaded from https://github.com/cake-build/resources 6 | # Feel free to change this file to fit your needs. 7 | ########################################################################## 8 | 9 | # Define directories. 10 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 11 | TOOLS_DIR=$SCRIPT_DIR/tools 12 | NUGET_EXE=$TOOLS_DIR/nuget.exe 13 | CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe 14 | PACKAGES_CONFIG=$TOOLS_DIR/packages.config 15 | PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum 16 | 17 | # Define md5sum or md5 depending on Linux/OSX 18 | MD5_EXE= 19 | if [[ "$(uname -s)" == "Darwin" ]]; then 20 | MD5_EXE="md5 -r" 21 | else 22 | MD5_EXE="md5sum" 23 | fi 24 | 25 | # Define default arguments. 26 | SCRIPT="build.cake" 27 | TARGET="Default" 28 | CONFIGURATION="Release" 29 | VERBOSITY="verbose" 30 | DRYRUN= 31 | SHOW_VERSION=false 32 | SCRIPT_ARGUMENTS=() 33 | 34 | # Parse arguments. 35 | for i in "$@"; do 36 | case $1 in 37 | -s|--script) SCRIPT="$2"; shift ;; 38 | -t|--target) TARGET="$2"; shift ;; 39 | -c|--configuration) CONFIGURATION="$2"; shift ;; 40 | -v|--verbosity) VERBOSITY="$2"; shift ;; 41 | -d|--dryrun) DRYRUN="-dryrun" ;; 42 | --version) SHOW_VERSION=true ;; 43 | --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; 44 | *) SCRIPT_ARGUMENTS+=("$1") ;; 45 | esac 46 | shift 47 | done 48 | 49 | # Make sure the tools folder exist. 50 | if [ ! -d "$TOOLS_DIR" ]; then 51 | mkdir "$TOOLS_DIR" 52 | fi 53 | 54 | # Make sure that packages.config exist. 55 | if [ ! -f "$TOOLS_DIR/packages.config" ]; then 56 | echo "Downloading packages.config..." 57 | curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages 58 | if [ $? -ne 0 ]; then 59 | echo "An error occured while downloading packages.config." 60 | exit 1 61 | fi 62 | fi 63 | 64 | # Download NuGet if it does not exist. 65 | if [ ! -f "$NUGET_EXE" ]; then 66 | echo "Downloading NuGet..." 67 | curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe 68 | if [ $? -ne 0 ]; then 69 | echo "An error occured while downloading nuget.exe." 70 | exit 1 71 | fi 72 | fi 73 | 74 | # Restore tools from NuGet. 75 | pushd "$TOOLS_DIR" >/dev/null 76 | if [ ! -f $PACKAGES_CONFIG_MD5 ] || [ "$( cat $PACKAGES_CONFIG_MD5 | sed 's/\r$//' )" != "$( $MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' )" ]; then 77 | find . -type d ! -name . | xargs rm -rf 78 | fi 79 | 80 | mono "$NUGET_EXE" install -ExcludeVersion 81 | if [ $? -ne 0 ]; then 82 | echo "Could not restore NuGet packages." 83 | exit 1 84 | fi 85 | 86 | $MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' >| $PACKAGES_CONFIG_MD5 87 | 88 | popd >/dev/null 89 | 90 | # Make sure that Cake has been installed. 91 | if [ ! -f "$CAKE_EXE" ]; then 92 | echo "Could not find Cake.exe at '$CAKE_EXE'." 93 | exit 1 94 | fi 95 | 96 | # Start Cake 97 | if $SHOW_VERSION; then 98 | exec mono "$CAKE_EXE" -version 99 | else 100 | exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" 101 | fi -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.203", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /source/Octostache.Tests/BaseFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public abstract class BaseFixture 9 | { 10 | protected BaseFixture() 11 | { 12 | // The TemplateParser Cache is retained between tests. A little reflection to clear it. 13 | var parser = typeof(VariableDictionary).Assembly.GetType("Octostache.Templates.TemplateParser"); 14 | var clearMethod = parser?.GetMethod("ClearCache", BindingFlags.NonPublic | BindingFlags.Static); 15 | clearMethod?.Invoke(null, new object[] { }); 16 | } 17 | 18 | protected string Evaluate(string template, IDictionary variables, bool haltOnError = true) 19 | { 20 | var dictionary = new VariableDictionary(); 21 | foreach (var pair in variables) 22 | { 23 | dictionary[pair.Key] = pair.Value; 24 | } 25 | 26 | return dictionary.Evaluate(template, out _, haltOnError); 27 | } 28 | 29 | protected bool EvaluateTruthy(string template, IDictionary variables) 30 | { 31 | var dictionary = new VariableDictionary(); 32 | foreach (var pair in variables) 33 | { 34 | dictionary[pair.Key] = pair.Value; 35 | } 36 | 37 | return dictionary.EvaluateTruthy(template); 38 | } 39 | 40 | protected VariableDictionary ParseVariables(string variableDefinitions) 41 | { 42 | var variables = new VariableDictionary(); 43 | 44 | var items = variableDefinitions.Split(';'); 45 | foreach (var item in items) 46 | { 47 | var pair = item.Split('='); 48 | var key = pair.First(); 49 | var value = pair.Last(); 50 | variables[key] = value; 51 | } 52 | 53 | return variables; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/Octostache.Tests/CalculationFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace Octostache.Tests 8 | { 9 | public class CalculationFixture : BaseFixture 10 | { 11 | [Theory] 12 | [MemberData(nameof(ConditionalIsSupportedData))] 13 | public void ConditionalIsSupported(string expression, string expectedResult) 14 | { 15 | var result = Evaluate($"#{{calc {expression}}}", 16 | new Dictionary 17 | { 18 | { "A", "5" }, 19 | { "B", "7" }, 20 | { "The/Var", "9" }, 21 | { "Another-Var", "11" }, 22 | }); 23 | 24 | result.Should().Be(expectedResult); 25 | } 26 | 27 | public static IEnumerable ConditionalIsSupportedData() 28 | { 29 | yield return new object[] { "3+2", "5" }; 30 | yield return new object[] { "3-2", "1" }; 31 | yield return new object[] { "3*2", "6" }; 32 | yield return new object[] { "3/2", (3d / 2).ToString(CultureInfo.CurrentCulture) }; 33 | yield return new object[] { "3*2+2*4", "14" }; 34 | yield return new object[] { "3*(2+2)*4", "48" }; 35 | yield return new object[] { "A+2", "7" }; 36 | yield return new object[] { "A+B", "12" }; 37 | yield return new object[] { "C+2", "#{C+2}" }; 38 | yield return new object[] { "{B}-2", "5" }; 39 | yield return new object[] { "2-B", "-5" }; 40 | yield return new object[] { "2-4", "-2" }; 41 | yield return new object[] { "(B*2)-2", "12" }; 42 | yield return new object[] { "2-(B*2)", "-12" }; 43 | yield return new object[] { "{B}/2", (7d / 2).ToString(CultureInfo.CurrentCulture) }; 44 | yield return new object[] { "2/B", (2d / 7).ToString(CultureInfo.CurrentCulture) }; 45 | yield return new object[] { "2/{B}", (2d / 7).ToString(CultureInfo.CurrentCulture) }; 46 | yield return new object[] { "2/4", "0.5" }; 47 | yield return new object[] { "0.2*B", (7d * 0.2).ToString(CultureInfo.CurrentCulture) }; 48 | yield return new object[] { "B*0.2", (7d * 0.2).ToString(CultureInfo.CurrentCulture) }; 49 | yield return new object[] { "{B}*0.2", (7d * 0.2).ToString(CultureInfo.CurrentCulture) }; 50 | yield return new object[] { "{Another-Var}-2", "9" }; 51 | yield return new object[] { "{The/Var}-2", "7" }; 52 | 53 | //erroneous parsing - variables as rhs operands must be surrounded with "{ ... }" to calc correctly. 54 | yield return new object[] { "B-2", "#{B-2}" }; 55 | yield return new object[] { "B/2", "#{B/2}" }; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/Octostache.Tests/ConditionalsFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public class ConditionalsFixture : BaseFixture 9 | { 10 | [Fact] 11 | public void ConditionalIsSupported() 12 | { 13 | var result = Evaluate("#{if Truthy}#{Result}#{/if}", 14 | new Dictionary 15 | { 16 | { "Result", "result" }, 17 | { "Truthy", "true" }, 18 | }); 19 | 20 | result.Should().Be("result"); 21 | } 22 | 23 | [Theory] 24 | [InlineData("#{ if Truthy}#{Result}#{/if}")] 25 | [InlineData("#{ if Truthy}#{Result}#{/if}")] 26 | [InlineData("#{if Truthy }#{Result}#{/if}")] 27 | [InlineData("#{ if Truthy }#{Result}#{/if}")] 28 | [InlineData("#{if Truthy}#{Result}#{/if}")] 29 | [InlineData("#{if Truthy}#{ Result}#{/if}")] 30 | [InlineData("#{if Truthy}#{ Result}#{/if}")] 31 | [InlineData("#{if Truthy}#{Result }#{/if}")] 32 | [InlineData("#{if Truthy}#{Result }#{/if}")] 33 | [InlineData("#{if Truthy == \"true\"}#{Result}#{/if}")] 34 | [InlineData("#{if Truthy != \"false\"}#{Result}#{/if}")] 35 | [InlineData("#{if Truthy | ToLower != \"false\"}#{Result}#{/if}")] 36 | public void ConditionalIgnoresWhitespacesCorrectly(string input) 37 | { 38 | var result = Evaluate(input, 39 | new Dictionary 40 | { 41 | { "Result", "result" }, 42 | { "Truthy", "true" }, 43 | }); 44 | 45 | result.Should().Be("result"); 46 | 47 | result = Evaluate("#{ if Truthy}#{Result}#{/if}", 48 | new Dictionary 49 | { 50 | { "Result", "result" }, 51 | { "Truthy", "true" }, 52 | }); 53 | 54 | result.Should().Be("result"); 55 | } 56 | 57 | [Fact] 58 | public void ConditionalUnlessIsSupported() 59 | { 60 | var result = Evaluate("#{unless Truthy}#{Result}#{/unless}", 61 | new Dictionary 62 | { 63 | { "Result", "result" }, 64 | { "Truthy", "false" }, 65 | }); 66 | 67 | result.Should().Be("result"); 68 | } 69 | 70 | [Theory] 71 | [InlineData("#{if MyVar}True#{/if}", "")] 72 | [InlineData("#{unless MyVar}False#{/unless}", "False")] 73 | public void UnknownVariablesAreTreatedAsFalsy(string template, string expected) 74 | { 75 | var result = Evaluate(template, new Dictionary()); 76 | result.Should().Be(expected); 77 | } 78 | 79 | [Fact] 80 | public void UnknownVariablesOnBothSidesAreTreatedAsEqual() 81 | { 82 | var result = Evaluate("#{if Unknown1 == Unknown2}Equal#{/if}", new Dictionary()); 83 | result.Should().Be("Equal"); 84 | } 85 | 86 | [Fact] 87 | public void ConditionalToOtherDictValueIsSupported() 88 | { 89 | var result = Evaluate("#{if Octopus == Compare}#{Result}#{/if}", 90 | new Dictionary 91 | { 92 | { "Result", "result" }, 93 | { "Octopus", "octopus" }, 94 | { "Compare", "octopus" }, 95 | }); 96 | 97 | result.Should().Be("result"); 98 | } 99 | 100 | [Fact] 101 | public void ConditionalToStringIsSupportedWhenStringIsOnTheRightHandSide() 102 | { 103 | var result = Evaluate("#{if Octopus == \"octopus\"}#{Result}#{/if}", 104 | new Dictionary 105 | { 106 | { "Result", "result" }, 107 | { "Octopus", "octopus" }, 108 | }); 109 | 110 | result.Should().Be("result"); 111 | } 112 | 113 | [Fact] 114 | public void ConditionalToStringIsSupportedWhenStringIsOnTheLeftHandSide() 115 | { 116 | var result = Evaluate("#{if \"octopus\" == Octopus }#{Result}#{/if}", 117 | new Dictionary 118 | { 119 | { "Result", "result" }, 120 | { "Octopus", "octopus" }, 121 | }); 122 | 123 | result.Should().Be("result"); 124 | } 125 | 126 | [Fact] 127 | public void ConditionalNegationIsSupportedWhenStringIsOnTheLeftHandSide() 128 | { 129 | var result = Evaluate("#{if \"software\" != Octopus}#{Result}#{/if}", 130 | new Dictionary 131 | { 132 | { "Result", "result" }, 133 | { "Octopus", "something else" }, 134 | }); 135 | 136 | result.Should().Be("result"); 137 | } 138 | 139 | [Fact] 140 | public void ConditionalNegationIsSupportedWhenStringIsOnTheRightHandSide() 141 | { 142 | var result = Evaluate("#{if Octopus != \"software\"}#{Result}#{/if}", 143 | new Dictionary 144 | { 145 | { "Result", "result" }, 146 | { "Octopus", "something else" }, 147 | }); 148 | 149 | result.Should().Be("result"); 150 | } 151 | 152 | [Fact] 153 | public void NestedConditionalsAreSupported() 154 | { 155 | var result = Evaluate("#{if Truthy}#{if Fooey==\"foo\"}#{Result}#{/if}#{/if}", 156 | new Dictionary 157 | { 158 | { "Result", "result" }, 159 | { "Truthy", "true" }, 160 | { "Fooey", "foo" }, 161 | }); 162 | 163 | result.Should().Be("result"); 164 | } 165 | 166 | [Theory] 167 | [InlineData("#{if Truthy}#{Result}#{else}#{ | null }#{/if}", "true", "result")] 168 | [InlineData("#{if Truthy}#{Result}#{else}#{ | null }#{/if}", "false", null)] 169 | [InlineData("#{if Truthy}#{ | null }#{else}#{ElseResult}#{/if}", "true", null)] 170 | [InlineData("#{if Truthy}#{ | null }#{else}#{ElseResult}#{/if}", "false", "elseresult")] 171 | public void ConditionalsWithNestedNullShouldReturnCorrect(string template, string truthyValue, string expectedValue) 172 | { 173 | var result = Evaluate(template, 174 | new Dictionary 175 | { 176 | { "Result", "result" }, 177 | { "ElseResult", "elseresult" }, 178 | { "Truthy", truthyValue }, 179 | }); 180 | 181 | result.Should().Be(expectedValue); 182 | } 183 | 184 | [Fact] 185 | public void ElseIsSupportedTrue() 186 | { 187 | var result = Evaluate("#{if Truthy}#{Result}#{else}#{ElseResult}#{/if}", 188 | new Dictionary 189 | { 190 | { "Result", "result" }, 191 | { "ElseResult", "elseresult" }, 192 | { "Truthy", "true" }, 193 | }); 194 | 195 | result.Should().Be("result"); 196 | } 197 | 198 | [Fact] 199 | public void ElseIsSupportedFalse() 200 | { 201 | var result = Evaluate("#{if Truthy}#{Result}#{else}#{ElseResult}#{/if}", 202 | new Dictionary 203 | { 204 | { "Result", "result" }, 205 | { "ElseResult", "elseresult" }, 206 | { "Truthy", "false" }, 207 | }); 208 | 209 | result.Should().Be("elseresult"); 210 | } 211 | 212 | [Fact] 213 | public void NestIfInElse() 214 | { 215 | var result = Evaluate("#{if Truthy}#{Result}#{else}#{if Fooey==\"foo\"}#{ElseResult}#{/if}#{/if}", 216 | new Dictionary 217 | { 218 | { "Result", "result" }, 219 | { "ElseResult", "elseresult" }, 220 | { "Fooey", "foo" }, 221 | { "Truthy", "false" }, 222 | }); 223 | 224 | result.Should().Be("elseresult"); 225 | } 226 | 227 | [Fact] 228 | public void FunctionCallIsSupported() 229 | { 230 | var result = Evaluate("#{if Hello | Contains \"O\" }#{Result}#{/if}", 231 | new Dictionary 232 | { 233 | { "Hello", "HELLO" }, 234 | { "Result", "result" }, 235 | }); 236 | 237 | result.Should().Be("result"); 238 | } 239 | 240 | [Fact] 241 | public void FunctionCallIsSupportedWithStringOnTheRightHandSide() 242 | { 243 | var result = Evaluate("#{if Hello | ToLower == \"hello\"}#{Result}#{/if}", 244 | new Dictionary 245 | { 246 | { "Hello", "HELLO" }, 247 | { "Result", "result" }, 248 | }); 249 | 250 | result.Should().Be("result"); 251 | } 252 | 253 | [Fact] 254 | public void FunctionCallIsSupportedWithStringOnTheLeftHandSide() 255 | { 256 | var result = Evaluate("#{if \"hello\" == Hello | ToLower }#{Result}#{/if}", 257 | new Dictionary 258 | { 259 | { "Hello", "HELLO" }, 260 | { "Result", "result" }, 261 | }); 262 | 263 | result.Should().Be("result"); 264 | } 265 | 266 | [Fact] 267 | public void FunctionCallIsSupportedOnBothSide() 268 | { 269 | var result = Evaluate("#{if Greeting | ToLower == Hello | ToLower }#{Result}#{/if}", 270 | new Dictionary 271 | { 272 | { "Greeting", "Hello" }, 273 | { "Hello", "HELLO" }, 274 | { "Result", "result" }, 275 | }); 276 | 277 | result.Should().Be("result"); 278 | } 279 | 280 | [Fact] 281 | public void ChainedFunctionCallIsSupported() 282 | { 283 | var result = Evaluate("#{if Greeting | Trim | ToUpper | ToLower == Hello | ToBase64 | FromBase64 | Trim | ToLower }#{Result}#{/if}", 284 | new Dictionary 285 | { 286 | { "Greeting", " Hello " }, 287 | { "Hello", " HELLO " }, 288 | { "Result", "result" }, 289 | }); 290 | 291 | result.Should().Be("result"); 292 | } 293 | 294 | [Fact] 295 | public void UnknownFunctionsAreEchoed() 296 | { 297 | const string template = "#{if Greeting | NonExistingFunction}#{Result}#{/if}"; 298 | var result = Evaluate(template, 299 | new Dictionary 300 | { 301 | { "Greeting", "Hello world" }, 302 | { "Result", "result" }, 303 | }); 304 | 305 | result.Should().Be(template); 306 | } 307 | 308 | [Fact] 309 | public void UnknownVariablesAsFunctionArgumentsAreEchoed() 310 | { 311 | const string template = "#{if Greeting | TpUpper}#{Result}#{/if}"; 312 | var result = Evaluate(template, 313 | new Dictionary 314 | { 315 | { "Result", "result" }, 316 | { "MyVar", "Value" }, 317 | }); 318 | 319 | result.Should().Be(template); 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /source/Octostache.Tests/ExtensionFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public class ExtensionFixture : BaseFixture 9 | { 10 | [Fact] 11 | public void TestCustomBuiltInExtension() 12 | { 13 | var variables = new VariableDictionary(); 14 | variables.Add("Foo", "Bar"); 15 | 16 | variables.AddExtension("testFunc", ToLower); 17 | 18 | var result = variables.Evaluate("#{Foo|testFunc}"); 19 | 20 | result.Should().Be("bar"); 21 | } 22 | 23 | public static string ToLower(string argument, string[] options) => options.Any() ? null : argument?.ToLower(); 24 | 25 | [Fact] 26 | public void TestCustomExtension() 27 | { 28 | var variables = new VariableDictionary(); 29 | variables.Add("Foo", "Bar"); 30 | 31 | variables.AddExtension("supercoolfunc", SuperCoolFunc); 32 | 33 | var result = variables.Evaluate("#{Foo|supercoolfunc}"); 34 | 35 | result.Should().Be("2-1-18"); 36 | } 37 | 38 | public static string SuperCoolFunc(string argument, string[] options) 39 | { 40 | return string.Join("-", argument.Select(c => (char.ToUpper(c) - 64).ToString())); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/Octostache.Tests/ItemCacheFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Octostache.Templates; 4 | using Xunit; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public class ItemCacheFixture : BaseFixture 9 | { 10 | [Fact] 11 | protected void GetWithNullReturnsNull() 12 | { 13 | var item = new TestItem { Value = nameof(GetWithNullReturnsNull) }; 14 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 15 | cache.Add("item", null); 16 | 17 | cache.Get("item").Should().BeNull(); 18 | } 19 | 20 | [Fact] 21 | protected void GetOrAddWithNullReturnsNull() 22 | { 23 | var item = new TestItem { Value = nameof(GetOrAddWithNullReturnsNull) }; 24 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 25 | cache.Add("item", null); 26 | 27 | cache.GetOrAdd("item", () => throw new Exception("Should not re-create null")).Should().BeNull(); 28 | } 29 | 30 | [Fact] 31 | protected void GetOrAddWillAddNullOnce() 32 | { 33 | var item = new TestItem { Value = nameof(GetOrAddWillAddNullOnce) }; 34 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 35 | cache.GetOrAdd("item", () => null); 36 | 37 | cache.GetOrAdd("item", () => throw new Exception("Should not re-create item")).Should().BeNull(); 38 | } 39 | 40 | [Fact] 41 | protected void GetWithItemReturnsItem() 42 | { 43 | var item = new TestItem { Value = nameof(GetWithItemReturnsItem) }; 44 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 45 | cache.Add("item", item); 46 | 47 | cache.Get("item").Should().Be(item); 48 | } 49 | 50 | [Fact] 51 | protected void GetOrAddWithItemReturnsItem() 52 | { 53 | var item = new TestItem { Value = nameof(GetOrAddWithItemReturnsItem) }; 54 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 55 | cache.Add("item", item); 56 | 57 | cache.GetOrAdd("item", () => throw new Exception("Should not re-create item")).Should().Be(item); 58 | } 59 | 60 | [Fact] 61 | protected void GetOrAddWillAddItemOnce() 62 | { 63 | var item = new TestItem { Value = nameof(GetOrAddWillAddItemOnce) }; 64 | var cache = new ItemCache("temp", 1, TimeSpan.FromMinutes(2)); 65 | cache.GetOrAdd("item", () => item); 66 | 67 | cache.GetOrAdd("item", () => throw new Exception("Should not re-create item")).Should().Be(item); 68 | } 69 | 70 | class TestItem 71 | { 72 | public string Value { get; set; } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /source/Octostache.Tests/IterationFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public class IterationFixture : BaseFixture 9 | { 10 | [Fact] 11 | public void IterationOverAnEmptyCollectionIsFine() 12 | { 13 | var result = Evaluate("Ok#{each nothing in missing}#{nothing}#{/each}", new Dictionary()); 14 | 15 | result.Should().Be("Ok"); 16 | } 17 | 18 | [Fact] 19 | public void SimpleIterationIsSupported() 20 | { 21 | var result = Evaluate( 22 | "#{each a in Octopus.Action}#{a}-#{a.Name}#{/each}", 23 | new Dictionary 24 | { 25 | { "Octopus.Action[Package A].Name", "A" }, 26 | { "Octopus.Action[Package B].Name", "B" }, 27 | { "Octopus.Action[].Name", "Blank" }, 28 | }); 29 | 30 | result.Should().Be("Package A-APackage B-B-Blank"); 31 | } 32 | 33 | [Theory] 34 | [InlineData("#{ each a in Octopus.Action}#{a}-#{a.Name}#{/each}")] 35 | [InlineData("#{ each a in Octopus.Action}#{a}-#{a.Name}#{/each}")] 36 | [InlineData("#{each a in Octopus.Action}#{a}-#{a.Name}#{/each}")] 37 | [InlineData("#{each a in Octopus.Action}#{a}-#{a.Name}#{/each}")] 38 | [InlineData("#{each a in Octopus.Action}#{a}-#{a.Name}#{/each}")] 39 | [InlineData("#{each a in Octopus.Action }#{a}-#{a.Name}#{/each}")] 40 | [InlineData("#{each a in Octopus.Action }#{a}-#{a.Name}#{/each}")] 41 | [InlineData("#{each a in Octopus.Action}#{ a}-#{a.Name}#{/each}")] 42 | [InlineData("#{each a in Octopus.Action}#{ a}-#{a.Name}#{/each}")] 43 | [InlineData("#{each a in Octopus.Action}#{a }-#{a.Name}#{/each}")] 44 | [InlineData("#{each a in Octopus.Action}#{a }-#{a.Name}#{/each}")] 45 | public void IterationIgnoresWhitespacesCorrectly(string input) 46 | { 47 | var result = Evaluate( 48 | input, 49 | new Dictionary 50 | { 51 | { "Octopus.Action[Package A].Name", "A" }, 52 | { "Octopus.Action[Package B].Name", "B" }, 53 | { "Octopus.Action[].Name", "Blank" }, 54 | }); 55 | 56 | result.Should().Be("Package A-APackage B-B-Blank"); 57 | } 58 | 59 | [Fact] 60 | public void NestedIterationIsSupported() 61 | { 62 | var result = Evaluate( 63 | "#{each a in Octopus.Action}#{each tr in a.TargetRoles}#{a.Name}#{tr}#{/each}#{/each}", 64 | new Dictionary 65 | { 66 | { "Octopus.Action[Package A].Name", "A" }, 67 | { "Octopus.Action[Package A].TargetRoles", "a,b" }, 68 | { "Octopus.Action[Package B].Name", "B" }, 69 | { "Octopus.Action[Package B].TargetRoles", "c" }, 70 | { "Octopus.Action[].Name", "Z" }, 71 | { "Octopus.Action[].TargetRoles", "y" }, 72 | }); 73 | 74 | result.Should().Be("AaAbBcZy"); 75 | } 76 | 77 | [Fact] 78 | public void RecursiveIterationIsSupported() 79 | { 80 | var result = Evaluate("#{each a in Octopus.Action}#{a.Name}#{/each}", 81 | new Dictionary 82 | { 83 | { "PackageA_Name", "A" }, 84 | { "PackageB_Name", "B" }, 85 | { "PackageC_Name", "C" }, 86 | { "Octopus.Action[Package A].Name", "#{PackageA_Name}" }, 87 | { "Octopus.Action[Package B].Name", "#{PackageB_Name}" }, 88 | { "Octopus.Action[].Name", "#{PackageC_Name}" }, 89 | }); 90 | 91 | result.Should().Be("ABC"); 92 | } 93 | 94 | [Fact] 95 | public void ScopedSymbolIndexerInIterationIsSupported() 96 | { 97 | var result = 98 | Evaluate( 99 | "#{each action in Octopus.Action}#{if Octopus.Step[#{action.StepName}].Status != \"Skipped\"}#{Octopus.Step[#{action.StepName}].Details}#{/if}#{/each}", 100 | new Dictionary 101 | { 102 | { "Octopus.Action[Action 1].StepName", "Step 1" }, 103 | { "Octopus.Action[Action 2].StepName", "Step 2" }, 104 | { "Octopus.Step[Step 1].Details", "Step 1 Details" }, 105 | { "Octopus.Step[Step 2].Details", "Step 2 Details" }, 106 | { "Octopus.Step[Step 1].Status", "Skipped" }, 107 | { "Octopus.Step[Step 2].Status", "Running" }, 108 | }); 109 | 110 | result.Should().Be("Step 2 Details"); 111 | } 112 | 113 | [Fact] 114 | public void IterationSpecialVariablesAreSupported() 115 | { 116 | var result = Evaluate(@"#{each a in Numbers}#{a} First:#{Octopus.Template.Each.First} Last:#{Octopus.Template.Each.Last} Index:#{Octopus.Template.Each.Index}, #{/each}", 117 | new Dictionary 118 | { 119 | { "Numbers", "A,B,C" }, 120 | }); 121 | 122 | result.Should().Be("A First:True Last:False Index:0, B First:False Last:False Index:1, C First:False Last:True Index:2, "); 123 | } 124 | 125 | [Fact] 126 | public void NestedIndexIterationIsSupported() 127 | { 128 | var result = Evaluate("#{each a in Octopus.Action.Package}#{a}: #{a.Name} #{/each}", 129 | new Dictionary 130 | { 131 | { "Octopus.Action.Package[container[0]].Name", "A" }, 132 | { "Octopus.Action.Package[container[1]].Name", "B" }, 133 | { "Octopus.Action.Package[container[2]].Name", "C" }, 134 | }); 135 | 136 | result.Should().Be("container[0]: A container[1]: B container[2]: C "); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /source/Octostache.Tests/JsonFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace Octostache.Tests 6 | { 7 | public class JsonFixture : BaseFixture 8 | { 9 | [Fact] 10 | public void JsonDoesNotOverrideExisting() 11 | { 12 | var variables = new VariableDictionary 13 | { 14 | ["Test.Hello"] = "Go Away", 15 | ["Test"] = "{\"Hello\": \"World\", \"Foo\": \"Bar\", \"Donkey\" : {\"Kong\": 12}}", 16 | ["Test[Foo]"] = "Nope", 17 | ["Test.Donkey.Kong"] = "MARIO", 18 | }; 19 | 20 | variables.Evaluate("#{Test.Hello}").Should().Be("Go Away"); 21 | variables.Evaluate("#{Test[Foo]}").Should().Be("Nope"); 22 | variables.Evaluate("#{Test.Donkey.Kong}").Should().Be("MARIO"); 23 | } 24 | 25 | [Fact] 26 | public void JsonSupportsVariableInVariable() 27 | { 28 | var variables = new VariableDictionary 29 | { 30 | ["Prop"] = "Foo", 31 | ["Val"] = "Bar", 32 | ["Test"] = "{#{Prop}: \"#{Val}\"}", 33 | }; 34 | 35 | variables.Evaluate("#{Test[Foo]}").Should().Be("Bar"); 36 | variables.Evaluate("#{Test.Foo}").Should().Be("Bar"); 37 | variables.Evaluate("#{Test[#{Prop}]}").Should().Be("Bar"); 38 | } 39 | 40 | [Theory] 41 | [InlineData("{\"Hello\": \"World\"}", "#{Test[Hello]}", "World", "Simple Indexing")] 42 | [InlineData("{\"Hello\": \"World\"}", "#{Test.Hello}", "World", "Simple Dot Notation")] 43 | [InlineData("{\"Hello\": {\"World\": {\"Foo\": {\"Bar\": 12 }}}}", "#{Test[Hello][World][Foo][Bar]}", "12", "Deep")] 44 | [InlineData("{\"Items\": [{\"Name\": \"Toast\"}, {\"Name\": \"Bread\"}]}", "#{Test.Items[1].Name}", "Bread", "Arrays")] 45 | [InlineData("{\"Foo\": {\"Bar\":\"11\"}}", "#{Test.Foo}", "{\"Bar\":\"11\"}", "Raw JSON returned")] 46 | [InlineData("{Name: \"#{Test.Value}\", Desc: \"Monkey\", Value: 12}", "#{Test.Name}", "12", "Non-Direct inner JSON")] 47 | public void SuccessfulJsonParsing(string json, string pattern, string expectedResult, string testName) 48 | { 49 | var variables = new VariableDictionary 50 | { 51 | ["Test"] = json, 52 | }; 53 | 54 | variables.Evaluate(pattern).Should().Be(expectedResult); 55 | } 56 | 57 | [Fact] 58 | public void JsonInvalidDoesNotReplace() 59 | { 60 | var variables = new VariableDictionary 61 | { 62 | ["Test"] = "{Name: NoComma}", 63 | }; 64 | 65 | variables.Evaluate("#{Test.Name}").Should().Be("#{Test.Name}"); 66 | } 67 | 68 | [Fact] 69 | public void JsonArraySupportsIterator() 70 | { 71 | var variables = new VariableDictionary 72 | { 73 | ["Test"] = "[2,3,5,8]", 74 | }; 75 | 76 | var pattern = "#{each number in Test}#{number}#{if Octopus.Template.Each.Last == \"False\"}-#{/if}#{/each}"; 77 | 78 | variables.Evaluate(pattern).Should().Be("2-3-5-8"); 79 | } 80 | 81 | [Fact] 82 | public void JsonArraySafeguardedFromNullValues() 83 | { 84 | var variables = new VariableDictionary 85 | { 86 | ["Test"] = "{Blah: null}", 87 | }; 88 | 89 | var pattern = "Before:#{each number in Test.Blah}#{number}#{/each}:After"; 90 | 91 | variables.Evaluate(pattern).Should().Be("Before::After"); 92 | } 93 | 94 | [Fact] 95 | public void JsonObjectSupportsIterator() 96 | { 97 | var variables = new VariableDictionary 98 | { 99 | ["Octopus.Sizes"] = "{\"Small\": \"11.5\", Large: 15.21}", 100 | }; 101 | 102 | var pattern = @"#{each size in Octopus.Sizes}#{size}:#{size.Value},#{/each}"; 103 | 104 | variables.Evaluate(pattern).Should().Be("Small:11.5,Large:15.21,"); 105 | } 106 | 107 | [Fact] 108 | public void JsonEvaluatesConditionalsWithEscapes() 109 | { 110 | var variables = new VariableDictionary 111 | { 112 | ["Foo"] = "test text", 113 | }; 114 | 115 | var pattern = "{\"Bar\":\"#{if Foo == \\\"test text\\\"}Blaa#{/if}\"}"; 116 | 117 | variables.Evaluate(pattern).Should().Be("{\"Bar\":\"Blaa\"}"); 118 | } 119 | 120 | [Fact] 121 | public void JsonObjectSupportsIteratorWithInnerSelection() 122 | { 123 | var variables = new VariableDictionary 124 | { 125 | ["Octopus.Sizes"] = "{\"X-Large\": {\"Error\": \"Not Stocked\"}}", 126 | }; 127 | 128 | var pattern = @"#{each size in Octopus.Sizes}#{size.Key} - #{size.Value.Error}#{/each}"; 129 | 130 | variables.Evaluate(pattern).Should().Be("X-Large - Not Stocked"); 131 | } 132 | 133 | [Fact] 134 | public void NullJsonPropertyTreatedAsEmptyString() 135 | { 136 | var variables = new VariableDictionary 137 | { 138 | ["Foo"] = "{Bar: null}", 139 | }; 140 | 141 | var pattern = @"Alpha#{Foo.Bar}bet"; 142 | 143 | variables.Evaluate(pattern).Should().Be("Alphabet"); 144 | } 145 | 146 | [Fact] 147 | public void MissingJsonPropertyTreatedAsEmptyString() 148 | { 149 | var variables = new VariableDictionary 150 | { 151 | ["Foo"] = "{Bar: \"ABC\"}", 152 | }; 153 | 154 | var pattern = @"Alpha#{Foo.NotBar}bet"; 155 | 156 | variables.Evaluate(pattern).Should().Be("Alphabet"); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /source/Octostache.Tests/Octostache.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 0.0.0 5 | net8.0;net48 6 | Octostache.Tests 7 | Octostache.Tests 8 | true 9 | false 10 | false 11 | false 12 | false 13 | false 14 | false 15 | false 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /source/Octostache.Tests/ParserFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using FluentAssertions; 4 | using Octostache.Templates; 5 | using Xunit; 6 | 7 | namespace Octostache.Tests 8 | { 9 | public class ParserFixture : BaseFixture 10 | { 11 | [Fact] 12 | public void AListOfVariablesCanBeExtracted() 13 | { 14 | var template = @" 15 | #{var} 16 | #{filter | Format #{option}} 17 | #{nestedParent[#{nestedChild}]} 18 | #{if ifvar}#{unless ifnested}debug#{/unless}#{/if} 19 | #{if comparison != ""value""}true#{/if} 20 | #{each item in List} 21 | #{item.Address} 22 | #{/each} 23 | ##{ignored} 24 | "; 25 | 26 | var result = TemplateParser.ParseTemplateAndGetArgumentNames(template); 27 | result.Should().Contain(new[] 28 | { 29 | "var", 30 | "filter", 31 | "option", 32 | "nestedParent", 33 | "nestedChild", 34 | "ifvar", 35 | "ifnested", 36 | "comparison", 37 | "List", 38 | }); 39 | 40 | result.Should().NotContain("ignored"); 41 | result.Should().NotContain("item"); 42 | result.Should().NotContain("item.Address"); 43 | } 44 | 45 | [Fact] 46 | public void ParsedTemplateIsConvertibleBackToString() 47 | { 48 | // This test verifies that it is possible to convert a parsed template to a string 49 | // and then find individual tokens within the template by calling ToString on each token individually 50 | 51 | var template = @" 52 | #{var} 53 | #{filter | Format #{option}} 54 | #{nestedParent[#{nestedChild}]} 55 | #{if ifvar}#{if ifnested}#{else}debug#{/if}#{/if} 56 | #{if comparison != ""value"" }true#{/if} 57 | #{each item in List} 58 | #{item.Address} 59 | #{/each} 60 | ##{ignored} 61 | #{MyVar | Match ""a b""} 62 | #{MyVar | StartsWith Ab} 63 | #{MyVar | Match ""ab[0-9]+""} 64 | #{MyVar | Match #{pattern}} 65 | #{MyVar | Contains "" b(""} 66 | #{ | NowDateUtc} 67 | #{MyVar | UriPart IsFile} 68 | "; 69 | 70 | TemplateParser.TryParseTemplate(template, out var parsedTemplate, out _); 71 | parsedTemplate.Should().NotBeNull(); 72 | // ReSharper disable once PossibleNullReferenceException - Asserted above 73 | parsedTemplate.ToString().Should().NotBeEmpty(); 74 | 75 | // We convert the template back to the string representation and then remove individual parsed expressions until there is nothing left 76 | // The purpose is to verify that both methods (on template and tokens) are deterministic and produce equivalent string representations 77 | var templateConvertedBackToString = parsedTemplate.ToString(); 78 | foreach (var templateToken in parsedTemplate.Tokens) 79 | { 80 | if (!string.IsNullOrWhiteSpace(templateToken.ToString())) 81 | templateConvertedBackToString = templateConvertedBackToString.Replace(templateToken.ToString(), ""); 82 | } 83 | 84 | templateConvertedBackToString = templateConvertedBackToString.Replace("\r\n", ""); 85 | templateConvertedBackToString.Trim().Should().BeEmpty(); 86 | } 87 | 88 | [Theory] 89 | [InlineData(true)] 90 | [InlineData(false)] 91 | protected void TryParseWithHaltOnError(bool haltOnError) 92 | { 93 | var template = @" 94 | #{var} 95 | #{if ifvar}#{if ifnested}#{else}debug#{/if}#{/if} 96 | missing start tag#{/if} 97 | ##{ignored} 98 | #{MyVar | Match ""a b""} 99 | "; 100 | 101 | TemplateParser.TryParseTemplate(template, out var parsedTemplate1, out var error1, haltOnError); 102 | TemplateParser.TryParseTemplate(template, out var parsedTemplate2, out var error2, !haltOnError); 103 | 104 | if (haltOnError) 105 | { 106 | error1.Should().NotBeNullOrEmpty(); 107 | parsedTemplate1.Should().BeNull(); 108 | error2.Should().BeNullOrEmpty(); 109 | parsedTemplate2.Should().NotBeNull(); 110 | } 111 | else 112 | { 113 | error1.Should().BeNullOrEmpty(); 114 | parsedTemplate1.Should().NotBeNull(); 115 | error2.Should().NotBeNullOrEmpty(); 116 | parsedTemplate2.Should().BeNull(); 117 | } 118 | } 119 | 120 | [Fact] 121 | protected void EvaluateLotsOfTimesWithSet() 122 | { 123 | var sw = new Stopwatch(); 124 | var dictionary = new VariableDictionary(); 125 | for (var x = 0; x < 5_000; x++) 126 | dictionary.Add($"Octopus.Step[{x.ToString("")}].Action[{x.ToString("")}]", "Value"); 127 | 128 | sw.Start(); 129 | for (var x = 0; x < 100; x++) 130 | { 131 | // The set effectively re-sets the binding, so we can test the cache of the path parsing. 132 | dictionary.Set("a", "a"); 133 | dictionary.Evaluate("#{foo}"); 134 | } 135 | 136 | sw.Stop(); 137 | sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(12)); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /source/Octostache.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Octostache.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Octostache.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3820d077-6782-49ad-8fea-459f5cab31df")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("2.0.0.0")] 36 | [assembly: AssemblyFileVersion("2.0.0.0")] 37 | -------------------------------------------------------------------------------- /source/Octostache.Tests/VersionFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace Octostache.Tests 7 | { 8 | public class VersionFixture : BaseFixture 9 | { 10 | // @formatter:off 11 | [Theory] 12 | [InlineData("1", "1", "0", "0", "0", "", "", "", "")] 13 | [InlineData("1.2", "1", "2", "0", "0", "", "", "", "")] 14 | [InlineData("1.2.3", "1", "2", "3", "0", "", "", "", "")] 15 | [InlineData("1.2.3.4", "1", "2", "3", "4", "", "", "", "")] 16 | [InlineData("1.2.3.4-branch.1", "1", "2", "3", "4", "branch.1", "branch", "1", "")] 17 | [InlineData("1.2.3.4-branch.1+meta", "1", "2", "3", "4", "branch.1", "branch", "1", "meta")] 18 | [InlineData("v1.2.3.4-branch.1+meta", "1", "2", "3", "4", "branch.1", "branch", "1", "meta")] 19 | [InlineData("V1.2.3.4-branch.1+meta", "1", "2", "3", "4", "branch.1", "branch", "1", "meta")] 20 | [InlineData("V1.2.3.4-branch.hithere+meta", "1", "2", "3", "4", "branch.hithere", "branch", "hithere", "meta")] 21 | [InlineData("V1.2.3.4-branch-hithere+meta", "1", "2", "3", "4", "branch-hithere", "branch", "hithere", "meta")] 22 | [InlineData("V1.2.3.4-branch_hithere+meta", "1", "2", "3", "4", "branch_hithere", "branch", "hithere", "meta")] 23 | [InlineData("19.0.0.Final", "19", "0", "0", "0", "Final", "Final", "", "")] 24 | [InlineData("284.0.0-debian_component_based", "284", "0", "0", "0", "debian_component_based", "debian", "component_based", "")] 25 | [InlineData("latest", "0", "0", "0", "0", "latest", "latest", "", "")] 26 | [InlineData("v1", "1", "0", "0", "0", "", "", "", "")] 27 | [InlineData("v1.2.3", "1", "2", "3", "0", "", "", "", "")] 28 | public void TestVersionMajor(string version, string major, string minor, string patch, string revision, string release, string releasePrefix, string releaseCounter, string metadata) 29 | { 30 | var variables = new Dictionary 31 | { 32 | { "Version", version }, 33 | }; 34 | 35 | var majorResult = Evaluate("#{Version | VersionMajor}", variables); 36 | var minorResult = Evaluate("#{Version | VersionMinor}", variables); 37 | var patchResult = Evaluate("#{Version | VersionPatch}", variables); 38 | var revisionResult = Evaluate("#{Version | VersionRevision}", variables); 39 | var releaseResult = Evaluate("#{Version | VersionPreRelease}", variables); 40 | var releasePrefixResult = Evaluate("#{Version | VersionPreReleasePrefix}", variables); 41 | var releaseCounterResult = Evaluate("#{Version | VersionPreReleaseCounter}", variables); 42 | var metadataResult = Evaluate("#{Version | VersionMetadata}", variables); 43 | 44 | majorResult.Should().Be(major); 45 | minorResult.Should().Be(minor); 46 | patchResult.Should().Be(patch); 47 | revisionResult.Should().Be(revision); 48 | releaseResult.Should().Be(release); 49 | releasePrefixResult.Should().Be(releasePrefix); 50 | releaseCounterResult.Should().Be(releaseCounter); 51 | metadataResult.Should().Be(metadata); 52 | } 53 | // @formatter:on 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /source/Octostache.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26228.4 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octostache", "Octostache\Octostache.csproj", "{84654EEE-89BE-4507-A253-98F674417CF9}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octostache.Tests", "Octostache.Tests\Octostache.Tests.csproj", "{31E68EAE-742C-4FA5-9C63-FE50F7D269D3}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{8BE71604-F713-42B2-B067-E9E2FA42E261}" 10 | ProjectSection(SolutionItems) = preProject 11 | ..\global.json = ..\global.json 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {84654EEE-89BE-4507-A253-98F674417CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {84654EEE-89BE-4507-A253-98F674417CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {84654EEE-89BE-4507-A253-98F674417CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {84654EEE-89BE-4507-A253-98F674417CF9}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {31E68EAE-742C-4FA5-9C63-FE50F7D269D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {31E68EAE-742C-4FA5-9C63-FE50F7D269D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {31E68EAE-742C-4FA5-9C63-FE50F7D269D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {31E68EAE-742C-4FA5-9C63-FE50F7D269D3}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /source/Octostache.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | DO_NOT_SHOW 3 | DO_NOT_SHOW 4 | DO_NOT_SHOW 5 | True 6 | DO_NOT_SHOW 7 | RequiredForMultiline 8 | NotRequiredForBoth 9 | False 10 | Implicit 11 | Implicit 12 | ExpressionBody 13 | BlockScoped 14 | True 15 | False 16 | False 17 | False 18 | False 19 | False 20 | False 21 | False 22 | True 23 | False 24 | False 25 | False 26 | True 27 | True 28 | True 29 | True 30 | 31 | NEXT_LINE 32 | False 33 | False 34 | SEPARATE 35 | ALWAYS_ADD 36 | ALWAYS_ADD 37 | ALWAYS_ADD 38 | ALWAYS_ADD 39 | ALWAYS_ADD 40 | ALWAYS_ADD 41 | False 42 | False 43 | INSIDE 44 | 45 | NEXT_LINE 46 | 1 47 | 1 48 | False 49 | True 50 | True 51 | True 52 | True 53 | 1 54 | 4 55 | 4 56 | NEVER 57 | True 58 | IF_OWNER_IS_SINGLE_LINE 59 | False 60 | NEVER 61 | True 62 | False 63 | False 64 | True 65 | 66 | CHOP_IF_LONG 67 | True 68 | True 69 | CHOP_IF_LONG 70 | WRAP_IF_LONG 71 | WRAP_IF_LONG 72 | 700 73 | False 74 | CHOP_ALWAYS 75 | CHOP_IF_LONG 76 | False 77 | DoNotTouch 78 | False 79 | False 80 | 1 81 | 82 | False 83 | False 84 | ByFirstAttr 85 | True 86 | 300 87 | 300 88 | False 89 | False 90 | False 91 | False 92 | <?xml version="1.0" encoding="utf-16"?> 93 | <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> 94 | <TypePattern DisplayName="COM interfaces or structs"> 95 | <TypePattern.Match> 96 | <Or> 97 | <And> 98 | <Kind Is="Interface" /> 99 | <Or> 100 | <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> 101 | <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> 102 | </Or> 103 | </And> 104 | <Kind Is="Struct" /> 105 | </Or> 106 | </TypePattern.Match> 107 | </TypePattern> 108 | <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> 109 | <TypePattern.Match> 110 | <And> 111 | <Kind Is="Class" /> 112 | <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> 113 | <HasAttribute Name="NUnit.Framework.TestCaseFixtureAttribute" Inherited="True" /> 114 | </And> 115 | </TypePattern.Match> 116 | <Entry DisplayName="Setup/Teardown Methods"> 117 | <Entry.Match> 118 | <And> 119 | <Kind Is="Method" /> 120 | <Or> 121 | <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> 122 | <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> 123 | <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> 124 | <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> 125 | </Or> 126 | </And> 127 | </Entry.Match> 128 | </Entry> 129 | <Entry DisplayName="All other members" /> 130 | <Entry Priority="100" DisplayName="Test Methods"> 131 | <Entry.Match> 132 | <And> 133 | <Kind Is="Method" /> 134 | <HasAttribute Name="NUnit.Framework.TestAttribute" /> 135 | </And> 136 | </Entry.Match> 137 | <Entry.SortBy> 138 | <Name /> 139 | </Entry.SortBy> 140 | </Entry> 141 | </TypePattern> 142 | <TypePattern DisplayName="Default Pattern"> 143 | <Entry Priority="100" DisplayName="Public Delegates"> 144 | <Entry.Match> 145 | <And> 146 | <Access Is="Public" /> 147 | <Kind Is="Delegate" /> 148 | </And> 149 | </Entry.Match> 150 | <Entry.SortBy> 151 | <Name /> 152 | </Entry.SortBy> 153 | </Entry> 154 | <Entry Priority="100" DisplayName="Public Enums"> 155 | <Entry.Match> 156 | <And> 157 | <Access Is="Public" /> 158 | <Kind Is="Enum" /> 159 | </And> 160 | </Entry.Match> 161 | <Entry.SortBy> 162 | <Name /> 163 | </Entry.SortBy> 164 | </Entry> 165 | <Entry DisplayName="Public Properties"> 166 | <Entry.Match> 167 | <And> 168 | <Access Is="Public" /> 169 | <Kind Is="Property" /> 170 | </And> 171 | </Entry.Match> 172 | </Entry> 173 | <Entry DisplayName="Static Fields and Constants"> 174 | <Entry.Match> 175 | <Or> 176 | <Kind Is="Constant" /> 177 | <And> 178 | <Kind Is="Field" /> 179 | <Static /> 180 | </And> 181 | </Or> 182 | </Entry.Match> 183 | <Entry.SortBy> 184 | <Kind Order="Constant Field" /> 185 | </Entry.SortBy> 186 | </Entry> 187 | <Entry DisplayName="Fields"> 188 | <Entry.Match> 189 | <And> 190 | <Kind Is="Field" /> 191 | <Not> 192 | <Static /> 193 | </Not> 194 | </And> 195 | </Entry.Match> 196 | <Entry.SortBy> 197 | <Readonly /> 198 | </Entry.SortBy> 199 | </Entry> 200 | <Entry DisplayName="Constructors"> 201 | <Entry.Match> 202 | <Kind Is="Constructor" /> 203 | </Entry.Match> 204 | <Entry.SortBy> 205 | <Static /> 206 | </Entry.SortBy> 207 | </Entry> 208 | <Entry DisplayName="Properties, Indexers"> 209 | <Entry.Match> 210 | <Or> 211 | <Kind Is="Property" /> 212 | <Kind Is="Indexer" /> 213 | </Or> 214 | </Entry.Match> 215 | </Entry> 216 | <Entry DisplayName="All other members" /> 217 | <Entry DisplayName="Nested Types"> 218 | <Entry.Match> 219 | <Kind Is="Type" /> 220 | </Entry.Match> 221 | </Entry> 222 | </TypePattern> 223 | </Patterns> 224 | System 225 | System 226 | True 227 | False 228 | False 229 | True 230 | False 231 | True 232 | AD 233 | CSS 234 | ID 235 | SSL 236 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> 237 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> 238 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> 239 | True 240 | True 241 | True 242 | True 243 | True 244 | True 245 | True 246 | True 247 | -------------------------------------------------------------------------------- /source/Octostache/CustomStringParsers/JsonParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using Octostache.Templates; 6 | #if HAS_NULLABLE_REF_TYPES 7 | using System.Diagnostics.CodeAnalysis; 8 | #endif 9 | 10 | namespace Octostache.CustomStringParsers 11 | { 12 | static class JsonParser 13 | { 14 | internal static bool TryParse(Binding parentBinding, string property, [NotNullWhen(true)] out Binding? subBinding) 15 | { 16 | subBinding = null; 17 | 18 | try 19 | { 20 | var obj = JsonConvert.DeserializeObject(parentBinding.Item!); 21 | 22 | if (obj is JValue jvalue) 23 | { 24 | return TryParseJValue(jvalue, out subBinding); 25 | } 26 | 27 | var jarray = obj as JArray; 28 | if (jarray != null) 29 | { 30 | return TryParseJArray(jarray, property, out subBinding); 31 | } 32 | 33 | var jobj = obj as JObject; 34 | if (jobj != null) 35 | { 36 | return TryParseJObject(jobj, property, out subBinding); 37 | } 38 | } 39 | catch (JsonException) 40 | { 41 | return false; 42 | } 43 | 44 | return false; 45 | } 46 | 47 | internal static bool TryParse(Binding binding, out Binding[] subBindings) 48 | { 49 | subBindings = new Binding[0]; 50 | 51 | try 52 | { 53 | var obj = JsonConvert.DeserializeObject(binding.Item!); 54 | 55 | var jarray = obj as JArray; 56 | if (jarray != null) 57 | { 58 | return TryParseJArray(jarray, out subBindings); 59 | } 60 | 61 | var jobj = obj as JObject; 62 | if (jobj != null) 63 | { 64 | return TryParseJObject(jobj, out subBindings); 65 | } 66 | } 67 | catch (JsonException) 68 | { 69 | return false; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | static bool TryParseJObject(JObject jObject, out Binding[] subBindings) 76 | { 77 | subBindings = jObject.Properties().Select(p => 78 | { 79 | var b = new Binding(p.Name) 80 | { 81 | { "Key", new Binding(p.Name) }, 82 | { "Value", ConvertJTokenToBinding(p.Value) }, 83 | }; 84 | return b; 85 | }).ToArray(); 86 | return true; 87 | } 88 | 89 | static bool TryParseJArray(JArray jArray, out Binding[] subBindings) 90 | { 91 | subBindings = jArray.Select(ConvertJTokenToBinding).ToArray(); 92 | return true; 93 | } 94 | 95 | static bool TryParseJValue(JValue jvalue, out Binding subBinding) 96 | { 97 | subBinding = new Binding(jvalue.Value()); 98 | return true; 99 | } 100 | 101 | static bool TryParseJObject(JObject jObject, string property, out Binding subBinding) 102 | { 103 | subBinding = ConvertJTokenToBinding(jObject[property]!); 104 | return true; 105 | } 106 | 107 | static bool TryParseJArray(JArray jarray, string property, [NotNullWhen(true)] out Binding? subBinding) 108 | { 109 | int index; 110 | subBinding = null; 111 | 112 | if (!int.TryParse(property, out index)) 113 | return false; 114 | 115 | subBinding = ConvertJTokenToBinding(jarray[index]); 116 | return true; 117 | } 118 | 119 | static Binding ConvertJTokenToBinding(JToken? token) 120 | { 121 | if (token == null) 122 | { 123 | return new Binding(string.Empty); 124 | } 125 | 126 | if (token is JValue) 127 | { 128 | return new Binding(token.Value() ?? string.Empty); 129 | } 130 | 131 | return new Binding(JsonConvert.SerializeObject(token)); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /source/Octostache/NullableReferenceTypeAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if !HAS_NULLABLE_REF_TYPES 4 | // These attributes replicate the ones from System.Diagnostics.CodeAnalysis, and are here so we can still compile against the older frameworks. 5 | 6 | namespace Octostache 7 | { 8 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true)] 9 | public sealed class NotNullIfNotNullAttribute : Attribute 10 | { 11 | public string ParameterName { get; } 12 | 13 | public NotNullIfNotNullAttribute(string parameterName) 14 | { 15 | ParameterName = parameterName; 16 | } 17 | } 18 | 19 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] 20 | public sealed class DisallowNullAttribute : Attribute 21 | { 22 | } 23 | 24 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue)] 25 | public sealed class MaybeNullAttribute : Attribute 26 | { 27 | } 28 | 29 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue)] 30 | public sealed class NotNullAttribute : Attribute 31 | { 32 | } 33 | 34 | /// 35 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 36 | /// 37 | [AttributeUsage(AttributeTargets.Parameter)] 38 | public sealed class NotNullWhenAttribute : Attribute 39 | { 40 | /// Gets the return value condition. 41 | public bool ReturnValue { get; } 42 | 43 | /// Initializes the attribute with the specified return value condition. 44 | /// 45 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 46 | /// 47 | public NotNullWhenAttribute(bool returnValue) 48 | { 49 | ReturnValue = returnValue; 50 | } 51 | } 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /source/Octostache/Octostache.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Variable substitution syntax used in Octopus Deploy. 5 | en-US 6 | 0.0.0 7 | Octopus Deploy 8 | netstandard2.1;net462 9 | Octostache 10 | Octostache 11 | http://i.octopusdeploy.com/resources/Avatar3-32x32.png 12 | https://github.com/OctopusDeploy/Octostache/ 13 | https://github.com/OctopusDeploy/Octostache/blob/master/LICENSE.txt 14 | false 15 | false 16 | false 17 | false 18 | false 19 | false 20 | false 21 | false 22 | Enable 23 | 8 24 | 25 | 26 | 27 | $(DefineConstants);HAS_NULLABLE_REF_TYPES 28 | 29 | 30 | CS8600;CS8601;CS8602;CS8603;CS8604 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /source/Octostache/Octostache.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Octostache 6 | 1.0.0.0 7 | Octopus Deploy 8 | Variable substitution syntax used in Octopus Deploy. 9 | 10 | en-US 11 | https://i.octopusdeploy.com/resources/Avatar3-32x32.png 12 | https://github.com/OctopusDeploy/Octostache/ 13 | https://github.com/OctopusDeploy/Octostache/blob/master/LICENSE 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /source/Octostache/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Octostache")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Octostache")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("2.0.0.0")] 33 | [assembly: AssemblyFileVersion("2.0.0.0")] 34 | -------------------------------------------------------------------------------- /source/Octostache/Templates/AnalysisContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | class AnalysisContext 8 | { 9 | readonly AnalysisContext parent; 10 | readonly Identifier identifier; 11 | readonly SymbolExpression expansion; 12 | 13 | AnalysisContext(AnalysisContext parent, Identifier identifier, SymbolExpression expansion) 14 | { 15 | this.parent = parent; 16 | this.identifier = identifier; 17 | this.expansion = expansion; 18 | } 19 | 20 | public string Expand(SymbolExpression expression) => new SymbolExpression(Expand(expression.Steps)).ToString(); 21 | 22 | public IEnumerable Expand(IEnumerable expression) 23 | { 24 | var nodes = expression.ToArray(); 25 | if (nodes.FirstOrDefault() is Identifier first 26 | && string.Equals(first.Text, identifier.Text, StringComparison.OrdinalIgnoreCase)) 27 | { 28 | nodes = expansion.Steps.Concat(new[] { new DependencyWildcard() }).Concat(nodes.Skip(1)).ToArray(); 29 | } 30 | 31 | nodes = parent.Expand(nodes).ToArray(); 32 | 33 | return nodes; 34 | } 35 | 36 | public AnalysisContext BeginChild(Identifier ident, SymbolExpression expan) => new AnalysisContext(this, ident, expan); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Binding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | class Binding : Dictionary 7 | { 8 | public string? Item { get; set; } 9 | public Dictionary Indexable { get; } 10 | 11 | public Binding(string? item = null) 12 | : base(StringComparer.OrdinalIgnoreCase) 13 | { 14 | Item = item; 15 | Indexable = new Dictionary(StringComparer.OrdinalIgnoreCase); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/Octostache/Templates/BuiltInFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Octostache.Templates.Functions; 4 | 5 | namespace Octostache.Templates 6 | { 7 | static class BuiltInFunctions 8 | { 9 | static readonly IDictionary> Extensions = new Dictionary>(StringComparer.OrdinalIgnoreCase) 10 | { 11 | { "tolower", TextCaseFunction.ToLower }, 12 | { "toupper", TextCaseFunction.ToUpper }, 13 | { "tobase64", TextManipulationFunction.ToBase64 }, 14 | { "frombase64", TextManipulationFunction.FromBase64 }, 15 | { "htmlescape", TextEscapeFunctions.HtmlEscape }, 16 | { "uriescape", TextEscapeFunctions.UriStringEscape }, 17 | { "uridataescape", TextEscapeFunctions.UriDataStringEscape }, 18 | { "xmlescape", TextEscapeFunctions.XmlEscape }, 19 | { "jsonescape", TextEscapeFunctions.JsonEscape }, 20 | { "yamlsinglequoteescape", TextEscapeFunctions.YamlSingleQuoteEscape }, 21 | { "yamldoublequoteescape", TextEscapeFunctions.YamlDoubleQuoteEscape }, 22 | { "propertieskeyescape", TextEscapeFunctions.PropertiesKeyEscape }, 23 | { "propertiesvalueescape", TextEscapeFunctions.PropertiesValueEscape }, 24 | { "markdown", TextEscapeFunctions.MarkdownToHtml }, 25 | { "markdowntohtml", TextEscapeFunctions.MarkdownToHtml }, 26 | { "nowdate", DateFunction.NowDate }, 27 | { "nowdateutc", DateFunction.NowDateUtc }, 28 | { "format", FormatFunction.Format }, 29 | { "match", TextComparisonFunctions.Match }, 30 | { "replace", TextReplaceFunction.Replace }, 31 | { "startswith", TextComparisonFunctions.StartsWith }, 32 | { "endswith", TextComparisonFunctions.EndsWith }, 33 | { "contains", TextComparisonFunctions.Contains }, 34 | { "substring", TextSubstringFunction.Substring }, 35 | { "truncate", TextManipulationFunction.Truncate }, 36 | { "trim", TextManipulationFunction.Trim }, 37 | { "indent", TextManipulationFunction.Indent }, 38 | { "uripart", TextManipulationFunction.UriPart }, 39 | { "versionmajor", VersionParseFunction.VersionMajor }, 40 | { "versionminor", VersionParseFunction.VersionMinor }, 41 | { "versionpatch", VersionParseFunction.VersionPatch }, 42 | { "versionprerelease", VersionParseFunction.VersionRelease }, 43 | { "versionprereleaseprefix", VersionParseFunction.VersionReleasePrefix }, 44 | { "versionprereleasecounter", VersionParseFunction.VersionReleaseCounter }, 45 | { "versionrevision", VersionParseFunction.VersionRevision }, 46 | { "versionmetadata", VersionParseFunction.VersionMetadata }, 47 | { "append", TextManipulationFunction.Append }, 48 | { "prepend", TextManipulationFunction.Prepend }, 49 | { "md5", HashFunction.Md5 }, 50 | { "sha1", HashFunction.Sha1 }, 51 | { "sha256", HashFunction.Sha256 }, 52 | { "sha384", HashFunction.Sha384 }, 53 | { "sha512", HashFunction.Sha512 }, 54 | { "null", NullFunction.Null }, 55 | }; 56 | 57 | public static string? InvokeOrNull(string function, string? argument, string[] options) 58 | { 59 | var functionName = function.ToLowerInvariant(); 60 | 61 | if (Extensions.TryGetValue(functionName, out var ext)) 62 | return ext(argument, options); 63 | 64 | return null; // Undefined, will cause source text to print 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/Octostache/Templates/CalculationToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Octostache.Templates 7 | { 8 | class CalculationToken : TemplateToken 9 | { 10 | public ICalculationComponent Expression { get; } 11 | 12 | public CalculationToken(ICalculationComponent expression) 13 | { 14 | Expression = expression; 15 | } 16 | 17 | public override string ToString() => "#{" + Expression + "}"; 18 | 19 | public override IEnumerable GetArguments() => Expression.GetArguments(); 20 | } 21 | 22 | class CalculationConstant : ICalculationComponent 23 | { 24 | public double Value { get; } 25 | 26 | public CalculationConstant(double value) 27 | { 28 | Value = value; 29 | } 30 | 31 | public double? Evaluate(Func resolve) => Value; 32 | 33 | public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); 34 | 35 | public IEnumerable GetArguments() 36 | { 37 | yield break; 38 | } 39 | } 40 | 41 | class CalculationVariable : ICalculationComponent 42 | { 43 | public SymbolExpression Variable { get; } 44 | 45 | public CalculationVariable(SymbolExpression variable) 46 | { 47 | Variable = variable; 48 | } 49 | 50 | public double? Evaluate(Func resolve) 51 | { 52 | var stringValue = resolve(Variable); 53 | 54 | if (stringValue == null) 55 | return null; 56 | 57 | if (!double.TryParse(stringValue, out var value)) 58 | return null; 59 | 60 | return value; 61 | } 62 | 63 | public override string ToString() => Variable.ToString(); 64 | 65 | public IEnumerable GetArguments() => Variable.GetArguments(); 66 | } 67 | 68 | class CalculationOperation : ICalculationComponent 69 | { 70 | public ICalculationComponent Left { get; } 71 | public CalculationOperator Op { get; } 72 | public ICalculationComponent Right { get; } 73 | 74 | public CalculationOperation(ICalculationComponent left, CalculationOperator op, ICalculationComponent right) 75 | { 76 | Left = left; 77 | Op = op; 78 | Right = right; 79 | } 80 | 81 | string OperatorAsString => 82 | Op switch 83 | { 84 | CalculationOperator.Add => "+", 85 | CalculationOperator.Subtract => "-", 86 | CalculationOperator.Multiply => "*", 87 | CalculationOperator.Divide => "/", 88 | _ => throw new ArgumentOutOfRangeException(), 89 | }; 90 | 91 | public double? Evaluate(Func resolve) 92 | { 93 | var leftValue = Left.Evaluate(resolve); 94 | if (leftValue == null) 95 | return null; 96 | 97 | var rightValue = Right.Evaluate(resolve); 98 | if (rightValue == null) 99 | return null; 100 | 101 | return Op switch 102 | { 103 | CalculationOperator.Add => leftValue + rightValue, 104 | CalculationOperator.Subtract => leftValue - rightValue, 105 | CalculationOperator.Multiply => leftValue * rightValue, 106 | CalculationOperator.Divide => leftValue / rightValue, 107 | _ => throw new ArgumentOutOfRangeException(), 108 | }; 109 | } 110 | 111 | public IEnumerable GetArguments() => Left.GetArguments().Concat(Right.GetArguments()); 112 | 113 | public override string ToString() => $"{Left}{OperatorAsString}{Right}"; 114 | } 115 | 116 | interface ICalculationComponent 117 | { 118 | double? Evaluate(Func resolve); 119 | IEnumerable GetArguments(); 120 | } 121 | 122 | enum CalculationOperator 123 | { 124 | Add, 125 | Subtract, 126 | Multiply, 127 | Divide, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /source/Octostache/Templates/ConditionalToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | /// 8 | /// Example: #{if Octopus.IsCool}...#{/if} 9 | /// Example: #{if Octopus.CoolStatus != "Uncool"}...#{/if} 10 | /// Example: #{if Octopus.IsCool == Octostache.IsCool}...#{/if} 11 | /// Example: #{if "Cool" == Octostache.CoolStatus}...#{/if} 12 | /// Example: #{if Octopus.CoolStatus | ToLower | StartsWith "cool"}...#{/if} 13 | /// 14 | class ConditionalToken : TemplateToken 15 | { 16 | public ConditionalExpressionToken Token { get; } 17 | public TemplateToken[] TruthyTemplate { get; } 18 | public TemplateToken[] FalsyTemplate { get; } 19 | 20 | public ConditionalToken(ConditionalExpressionToken token, IEnumerable truthyBranch, IEnumerable falsyBranch) 21 | { 22 | Token = token; 23 | TruthyTemplate = truthyBranch.ToArray(); 24 | FalsyTemplate = falsyBranch.ToArray(); 25 | } 26 | 27 | public override string ToString() => 28 | "#{if " + Token.LeftSide + Token.EqualityText + "}" + string.Join("", TruthyTemplate.Cast()) + (FalsyTemplate.Length == 0 ? "" : "#{else}" + string.Join("", FalsyTemplate.Cast())) + "#{/if}"; 29 | 30 | public override IEnumerable GetArguments() 31 | { 32 | return Token.GetArguments() 33 | .Concat(TruthyTemplate.SelectMany(t => t.GetArguments())) 34 | .Concat(FalsyTemplate.SelectMany(t => t.GetArguments())); 35 | } 36 | } 37 | 38 | class ConditionalExpressionToken : TemplateToken 39 | { 40 | public ContentExpression LeftSide { get; } 41 | public virtual string EqualityText => ""; 42 | 43 | public ConditionalExpressionToken(ContentExpression leftSide) 44 | { 45 | LeftSide = leftSide; 46 | } 47 | 48 | public override IEnumerable GetArguments() => LeftSide.GetArguments(); 49 | } 50 | 51 | class ConditionalStringExpressionToken : ConditionalExpressionToken 52 | { 53 | public string RightSide { get; } 54 | public bool Equality { get; } 55 | public override string EqualityText => " " + (Equality ? "==" : "!=") + " \"" + RightSide + "\" "; 56 | 57 | public ConditionalStringExpressionToken(ContentExpression leftSide, bool eq, string rightSide) : base(leftSide) 58 | { 59 | Equality = eq; 60 | RightSide = rightSide; 61 | } 62 | 63 | public override IEnumerable GetArguments() 64 | { 65 | return base.GetArguments().Concat(new[] { RightSide }); 66 | } 67 | } 68 | 69 | class ConditionalSymbolExpressionToken : ConditionalExpressionToken 70 | { 71 | public ContentExpression RightSide { get; } 72 | public bool Equality { get; } 73 | public override string EqualityText => " " + (Equality ? "==" : "!=") + " " + RightSide + " "; 74 | 75 | public ConditionalSymbolExpressionToken(ContentExpression leftSide, bool eq, ContentExpression rightSide) : base(leftSide) 76 | { 77 | Equality = eq; 78 | RightSide = rightSide; 79 | } 80 | 81 | public override IEnumerable GetArguments() => base.GetArguments().Concat(RightSide.GetArguments()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octostache.Templates 4 | { 5 | class Constants 6 | { 7 | public static class Each 8 | { 9 | public static readonly string First = "Octopus.Template.Each.First"; 10 | public static readonly string Last = "Octopus.Template.Each.Last"; 11 | public static readonly string Index = "Octopus.Template.Each.Index"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /source/Octostache/Templates/ContentExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Sprache; 4 | 5 | namespace Octostache.Templates 6 | { 7 | /// 8 | /// The top-level "thing that has a textual value" that 9 | /// can be manipulated or inserted into the output. 10 | /// 11 | abstract class ContentExpression : IInputToken 12 | { 13 | public Position? InputPosition { get; set; } 14 | public abstract IEnumerable GetArguments(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/Octostache/Templates/DependencyWildcard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | class DependencyWildcard : SymbolExpressionStep 7 | { 8 | public override string ToString() => "*"; 9 | 10 | public override IEnumerable GetArguments() => new string[0]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/Octostache/Templates/EvaluationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Octostache.CustomStringParsers; 6 | #if HAS_NULLABLE_REF_TYPES 7 | using System.Diagnostics.CodeAnalysis; 8 | #endif 9 | 10 | namespace Octostache.Templates 11 | { 12 | class EvaluationContext 13 | { 14 | public TextWriter Output { get; } 15 | 16 | readonly Binding binding; 17 | readonly EvaluationContext? parent; 18 | readonly Stack symbolStack = new Stack(); 19 | public Dictionary> Extensions; 20 | 21 | public EvaluationContext(Binding binding, TextWriter output, EvaluationContext? parent = null, Dictionary>? extensions = null) 22 | { 23 | this.binding = binding; 24 | Output = output; 25 | this.parent = parent; 26 | Extensions = extensions ?? new Dictionary>(); 27 | } 28 | 29 | public string Resolve(SymbolExpression expression, out string[] missingTokens, out string[] nullTokens) 30 | { 31 | var val = WalkTo(expression, out missingTokens, out nullTokens); 32 | if (val == null) return ""; 33 | return val.Item ?? ""; 34 | } 35 | 36 | void ValidateThatRecursionIsNotOccuring(SymbolExpression expression) 37 | { 38 | var ancestor = this; 39 | while (ancestor != null) 40 | { 41 | if (ancestor.symbolStack.Contains(expression, SymbolExpression.StepsComparer)) 42 | { 43 | throw new RecursiveDefinitionException(expression, ancestor.symbolStack); 44 | } 45 | 46 | ancestor = ancestor.parent; 47 | } 48 | } 49 | 50 | public string? ResolveOptional(SymbolExpression expression, out string[] missingTokens, out string[] nullTokens) 51 | { 52 | var val = WalkTo(expression, out missingTokens, out nullTokens); 53 | return val?.Item; 54 | } 55 | 56 | public Binding? Walker(TemplateToken token, out string[]? missingTokens, out string[]? nullTokens) 57 | { 58 | missingTokens = null; 59 | nullTokens = null; 60 | return null; 61 | } 62 | 63 | Binding? WalkTo(SymbolExpression expression, out string[] missingTokens, out string[] nullTokens) 64 | { 65 | ValidateThatRecursionIsNotOccuring(expression); 66 | symbolStack.Push(expression); 67 | 68 | try 69 | { 70 | var val = binding; 71 | missingTokens = new string[0]; 72 | nullTokens = new string[0]; 73 | 74 | expression = CopyExpression(expression); 75 | 76 | foreach (var step in expression.Steps) 77 | { 78 | var iss = step as Identifier; 79 | if (iss != null) 80 | { 81 | Binding? newVal; 82 | if (val.TryGetValue(iss.Text, out newVal)) 83 | { 84 | val = newVal; 85 | continue; 86 | } 87 | 88 | if (TryCustomParsers(val, iss.Text, out newVal)) 89 | { 90 | val = newVal; 91 | continue; 92 | } 93 | } 94 | else 95 | { 96 | if (step is Indexer ix) 97 | { 98 | if (ix.IsSymbol || ix.Index == null) 99 | { 100 | // Substitution should have taken place in previous CopyExpression above. 101 | // If not then it must not be found. 102 | return null; 103 | } 104 | 105 | if (ix.Index == "*" && val?.Indexable.Count > 0) 106 | { 107 | val = val.Indexable.First().Value; 108 | continue; 109 | } 110 | 111 | if (val != null && val.Indexable.TryGetValue(ix.Index, out var newVal)) 112 | { 113 | val = newVal; 114 | continue; 115 | } 116 | 117 | if (val != null && TryCustomParsers(val, ix.Index, out newVal)) 118 | { 119 | val = newVal; 120 | continue; 121 | } 122 | } 123 | else 124 | { 125 | throw new NotImplementedException("Unknown step type: " + step); 126 | } 127 | } 128 | 129 | if (parent == null) 130 | return null; 131 | 132 | return parent.WalkTo(expression, out missingTokens, out nullTokens); 133 | } 134 | 135 | return ParseTemplate(val, out missingTokens, out nullTokens); 136 | } 137 | finally 138 | { 139 | symbolStack.Pop(); 140 | } 141 | } 142 | 143 | Binding ParseTemplate(Binding b, out string[] missingTokens, out string[] nullTokens) 144 | { 145 | if (b.Item != null) 146 | { 147 | if (TemplateParser.TryParseTemplate(b.Item, out var template, out _)) 148 | { 149 | using (var x = new StringWriter()) 150 | { 151 | var context = new EvaluationContext(new Binding(), x, this); 152 | 153 | TemplateEvaluator.Evaluate(template, context, out missingTokens, out nullTokens); 154 | x.Flush(); 155 | return new Binding(x.ToString()); 156 | } 157 | } 158 | } 159 | 160 | missingTokens = new string[0]; 161 | nullTokens = new string[0]; 162 | return b; 163 | } 164 | 165 | bool TryCustomParsers(Binding parentBinding, string property, [NotNullWhen(true)] out Binding? subBinding) 166 | { 167 | subBinding = null; 168 | if (string.IsNullOrEmpty(parentBinding.Item) || string.IsNullOrEmpty(property)) 169 | return false; 170 | 171 | if (parentBinding.Item.Contains("#{")) //Shortcut the inner variable replacement if no templates are detected 172 | { 173 | try 174 | { 175 | parentBinding = ParseTemplate(parentBinding, out _, out _); 176 | } 177 | catch (InvalidOperationException ex) 178 | { 179 | if (ex.Message.Contains("self referencing loop")) 180 | return false; 181 | } 182 | } 183 | 184 | return JsonParser.TryParse(parentBinding, property, out subBinding); 185 | } 186 | 187 | SymbolExpression CopyExpression(SymbolExpression expression) 188 | { 189 | //any indexers that are lookups, do them now so we are in the right context 190 | //take a copy so the lookup version remains for later use 191 | return new SymbolExpression(expression.Steps.Select(s => 192 | { 193 | if (s is Indexer indexer && indexer.IsSymbol) 194 | { 195 | var index = WalkTo(indexer.Symbol!, out _, out _); 196 | 197 | return index == null 198 | ? new Indexer(CopyExpression(indexer.Symbol!)) 199 | : new Indexer(index.Item); 200 | } 201 | 202 | return s; 203 | })); 204 | } 205 | 206 | public IEnumerable ResolveAll(SymbolExpression collection, out string[] missingTokens, out string[] nullTokens) 207 | { 208 | var val = WalkTo(collection, out missingTokens, out nullTokens); 209 | if (val == null) 210 | return Enumerable.Empty(); 211 | 212 | if (val.Indexable.Count != 0) 213 | return val.Indexable.Select(c => c.Value); 214 | 215 | if (val.Item == null) 216 | return Enumerable.Empty(); 217 | 218 | if (JsonParser.TryParse(new Binding(val.Item), out var bindings)) 219 | { 220 | return bindings; 221 | } 222 | 223 | return val.Item.Split(',').Select(s => new Binding(s)); 224 | } 225 | 226 | public EvaluationContext BeginChild(Binding locals) => new EvaluationContext(locals, Output, this); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /source/Octostache/Templates/FunctionCallExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | /// 8 | /// Syntactically this appears as the | FilterName construct, where 9 | /// the (single) argument is specified to the left of the bar. Under the hood this 10 | /// same AST node will also represent classic Function(Foo,Bar) expressions. 11 | /// 12 | class FunctionCallExpression : ContentExpression 13 | { 14 | public TemplateToken[] Options { get; } 15 | public string Function { get; } 16 | public ContentExpression Argument { get; } 17 | 18 | readonly bool filterSyntax; 19 | 20 | public FunctionCallExpression(bool filterSyntax, string function, ContentExpression argument, params TemplateToken[] options) 21 | { 22 | Options = options; 23 | this.filterSyntax = filterSyntax; 24 | Function = function; 25 | Argument = argument; 26 | } 27 | 28 | IInputToken[] GetAllArguments() 29 | { 30 | var tokens = new List(); 31 | if (Argument.InputPosition != null) 32 | tokens.Add(Argument); 33 | 34 | tokens.AddRange(Options); 35 | return tokens.ToArray(); 36 | } 37 | 38 | public override string ToString() 39 | { 40 | if (filterSyntax) 41 | return $"{Argument} | {Function}{(Options.Any() ? " " : "")}{string.Join(" ", Options.Select(t => t is TextToken ? $"\"{t.ToString()}\"" : t.ToString()))}"; 42 | 43 | return $"{Function} ({string.Join(", ", GetAllArguments().Select(t => t.ToString()))})"; 44 | } 45 | 46 | public override IEnumerable GetArguments() 47 | { 48 | return Argument.GetArguments() 49 | .Concat(Options.SelectMany(o => o.GetArguments())); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/DateFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | static class DateFunction 7 | { 8 | public static string? NowDate(string? argument, string[] options) 9 | { 10 | if (argument != null || options.Length > 1) 11 | return null; 12 | 13 | return DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified).ToString(options.Any() ? options[0] : "O"); 14 | } 15 | 16 | public static string? NowDateUtc(string? argument, string[] options) 17 | { 18 | if (argument != null || options.Length > 1) 19 | return null; 20 | 21 | return DateTime.UtcNow.ToString(options.Any() ? options[0] : "O"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/FormatFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octostache.Templates.Functions 4 | { 5 | class FormatFunction 6 | { 7 | public static string? Format(string? argument, string[] options) 8 | { 9 | if (argument == null) 10 | return null; 11 | 12 | if (options.Length == 0) 13 | return null; 14 | 15 | var formatString = options[0]; 16 | 17 | try 18 | { 19 | if (options.Length == 1) 20 | { 21 | decimal dbl; 22 | if (decimal.TryParse(argument, out dbl)) 23 | { 24 | return dbl.ToString(formatString); 25 | } 26 | 27 | DateTimeOffset dto; 28 | if (DateTimeOffset.TryParse(argument, out dto)) 29 | { 30 | return dto.ToString(formatString); 31 | } 32 | 33 | return null; 34 | } 35 | 36 | if (options.Length != 2) 37 | return null; 38 | 39 | formatString = options[1]; 40 | switch (options[0].ToLower()) 41 | { 42 | case "int32": 43 | case "int": 44 | int result; 45 | if (int.TryParse(argument, out result)) 46 | { 47 | return result.ToString(formatString); 48 | } 49 | 50 | break; 51 | case "double": 52 | double dbl; 53 | if (double.TryParse(argument, out dbl)) 54 | { 55 | return dbl.ToString(formatString); 56 | } 57 | 58 | break; 59 | case "decimal": 60 | decimal dcml; 61 | if (decimal.TryParse(argument, out dcml)) 62 | { 63 | return dcml.ToString(formatString); 64 | } 65 | 66 | break; 67 | case "date": 68 | case "datetime": 69 | DateTime date; 70 | if (DateTime.TryParse(argument, out date)) 71 | { 72 | return date.ToString(formatString); 73 | } 74 | 75 | break; 76 | case "datetimeoffset": 77 | DateTimeOffset dateOffset; 78 | if (DateTimeOffset.TryParse(argument, out dateOffset)) 79 | { 80 | return dateOffset.ToString(formatString); 81 | } 82 | 83 | break; 84 | } 85 | } 86 | catch (FormatException) { } 87 | 88 | return null; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/HashFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace Octostache.Templates.Functions 6 | { 7 | static class HashFunction 8 | { 9 | public static string? Md5(string? argument, string[] options) => CalculateHash(MD5.Create, argument, options); 10 | 11 | public static string? Sha1(string? argument, string[] options) => CalculateHash(SHA1.Create, argument, options); 12 | 13 | public static string? Sha256(string? argument, string[] options) => CalculateHash(SHA256.Create, argument, options); 14 | 15 | public static string? Sha384(string? argument, string[] options) => CalculateHash(SHA384.Create, argument, options); 16 | 17 | public static string? Sha512(string? argument, string[] options) => CalculateHash(SHA512.Create, argument, options); 18 | 19 | static string? CalculateHash(Func algorithm, string? argument, string[] options) 20 | { 21 | if (argument == null) 22 | { 23 | return null; 24 | } 25 | 26 | var hashOptions = new HashOptions(options); 27 | if (!hashOptions.IsValid) 28 | { 29 | return null; 30 | } 31 | 32 | try 33 | { 34 | var argumentBytes = hashOptions.GetBytes(argument); 35 | 36 | var hsh = algorithm(); 37 | return HexDigest(hsh.ComputeHash(argumentBytes), hashOptions); 38 | } 39 | catch 40 | { 41 | // Likely invalid input 42 | return null; 43 | } 44 | } 45 | 46 | static string HexDigest(byte[] bytes, HashOptions options) 47 | { 48 | var size = options.DigestSize.GetValueOrDefault(bytes.Length); 49 | if (size > bytes.Length) 50 | { 51 | size = bytes.Length; 52 | } 53 | 54 | var sb = new StringBuilder(size * 2); 55 | 56 | for (var i = 0; i < size; i++) 57 | { 58 | sb.Append(bytes[i].ToString("x2")); 59 | } 60 | 61 | return sb.ToString(); 62 | } 63 | 64 | class HashOptions 65 | { 66 | public Func GetBytes { get; } = Encoding.UTF8.GetBytes; 67 | public int? DigestSize { get; } 68 | 69 | public bool IsValid { get; } = true; 70 | 71 | public HashOptions(string[] options) 72 | { 73 | if (options.Length == 0) 74 | { 75 | return; 76 | } 77 | 78 | if (options.Length > 2) 79 | { 80 | IsValid = false; 81 | return; 82 | } 83 | 84 | if (int.TryParse(options[0], out var size) && size > 0) 85 | { 86 | DigestSize = size; 87 | if (options.Length > 1) 88 | { 89 | var encoding = GetEncoding(options[1]); 90 | if (encoding == null) 91 | { 92 | IsValid = false; 93 | } 94 | else 95 | { 96 | GetBytes = encoding; 97 | } 98 | } 99 | } 100 | else 101 | { 102 | var encoding = GetEncoding(options[0]); 103 | if (encoding == null) 104 | { 105 | IsValid = false; 106 | } 107 | else 108 | { 109 | GetBytes = encoding; 110 | } 111 | 112 | if (IsValid && options.Length > 1) 113 | { 114 | if (int.TryParse(options[1], out size) && size > 0) 115 | { 116 | DigestSize = size; 117 | } 118 | else 119 | { 120 | IsValid = false; 121 | } 122 | } 123 | } 124 | } 125 | 126 | static Func? GetEncoding(string encoding) 127 | { 128 | switch (encoding.ToLowerInvariant()) 129 | { 130 | case "base64": 131 | return Convert.FromBase64String; 132 | case "utf8": 133 | case "utf-8": 134 | return Encoding.UTF8.GetBytes; 135 | case "unicode": 136 | return Encoding.Unicode.GetBytes; 137 | } 138 | 139 | return null; 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/NullFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Octostache.Templates.Functions 4 | { 5 | static class NullFunction 6 | { 7 | public static string? Null(string? argument, string[] options) => null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextCaseFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | class TextCaseFunction 7 | { 8 | public static string? ToUpper(string? argument, string[] options) => options.Any() ? null : argument?.ToUpper(); 9 | 10 | public static string? ToLower(string? argument, string[] options) => options.Any() ? null : argument?.ToLower(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextComparisonFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | static class TextComparisonFunctions 7 | { 8 | public static string? Match(string? argument, string[] options) 9 | { 10 | if (argument == null || options.Length != 1) 11 | return null; 12 | 13 | return Regex.Match(argument, options[0]).Success.ToString().ToLowerInvariant(); 14 | } 15 | 16 | public static string? StartsWith(string? argument, string[] options) 17 | { 18 | if (argument == null || options.Length != 1) 19 | return null; 20 | 21 | return argument.StartsWith(options[0], StringComparison.Ordinal).ToString().ToLowerInvariant(); 22 | } 23 | 24 | public static string? EndsWith(string? argument, string[] options) 25 | { 26 | if (argument == null || options.Length != 1) 27 | return null; 28 | 29 | return argument.EndsWith(options[0], StringComparison.Ordinal).ToString().ToLowerInvariant(); 30 | } 31 | 32 | public static string? Contains(string? argument, string[] options) 33 | { 34 | if (argument == null || options.Length != 1) 35 | return null; 36 | 37 | return argument.Contains(options[0]).ToString().ToLowerInvariant(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextEscapeFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Markdig; 6 | #if HAS_NULLABLE_REF_TYPES 7 | using System.Diagnostics.CodeAnalysis; 8 | #endif 9 | 10 | namespace Octostache.Templates.Functions 11 | { 12 | class TextEscapeFunctions 13 | { 14 | static readonly Regex NewLineRegex = new Regex(@"(?:\r?\n)+", RegexOptions.Compiled); 15 | 16 | static readonly IDictionary HtmlEntityMap = new Dictionary 17 | { 18 | { '&', "&" }, 19 | { '<', "<" }, 20 | { '>', ">" }, 21 | { '"', """ }, 22 | { '\'', "'" }, 23 | { '/', "/" }, 24 | }; 25 | 26 | static readonly IDictionary XmlEntityMap = new Dictionary 27 | { 28 | { '&', "&" }, 29 | { '<', "<" }, 30 | { '>', ">" }, 31 | { '"', """ }, 32 | { '\'', "'" }, 33 | }; 34 | 35 | // This is overly simplistic since Unicode chars also need escaping. 36 | static readonly IDictionary JsonEntityMap = new Dictionary 37 | { 38 | { '\"', @"\""" }, 39 | { '\r', @"\r" }, 40 | { '\t', @"\t" }, 41 | { '\n', @"\n" }, 42 | { '\\', @"\\" }, 43 | }; 44 | 45 | static readonly IDictionary YamlSingleQuoteMap = new Dictionary 46 | { 47 | { '\'', "''" }, 48 | }; 49 | 50 | public static string? HtmlEscape(string? argument, string[] options) => options.Any() ? null : Escape(argument, HtmlEntityMap); 51 | 52 | public static string? XmlEscape(string? argument, string[] options) => options.Any() ? null : Escape(argument, XmlEntityMap); 53 | 54 | public static string? JsonEscape(string? argument, string[] options) => options.Any() ? null : Escape(argument, JsonEntityMap); 55 | 56 | public static string? YamlSingleQuoteEscape(string? argument, string[] options) 57 | { 58 | // https://yaml.org/spec/history/2002-10-31.html#syntax-single 59 | 60 | if (argument == null || options.Any()) 61 | return null; 62 | 63 | argument = HandleSingleQuoteYamlNewLines(argument); 64 | 65 | return Escape(argument, YamlSingleQuoteMap); 66 | } 67 | 68 | public static string? YamlDoubleQuoteEscape(string? argument, string[] options) 69 | { 70 | if (options.Any()) 71 | return null; 72 | 73 | return Escape(argument, YamlDoubleQuoteMap); 74 | } 75 | 76 | static string HandleSingleQuoteYamlNewLines(string input) 77 | { 78 | // A single newline is parsed by YAML as a space 79 | // A double newline is parsed by YAML as a single newline 80 | // A triple newline is parsed by YAML as a double newline 81 | // ...etc 82 | 83 | var output = NewLineRegex.Replace(input, 84 | m => 85 | { 86 | var newlineToInsert = m.Value.StartsWith("\r") 87 | ? "\r\n" 88 | : "\n"; 89 | 90 | return newlineToInsert + m.Value; 91 | }); 92 | 93 | return output; 94 | } 95 | 96 | public static string? PropertiesKeyEscape(string? argument, string[] options) 97 | { 98 | if (options.Any()) 99 | return null; 100 | 101 | return Escape(argument, PropertiesKeyMap); 102 | } 103 | 104 | public static string? PropertiesValueEscape(string? argument, string[] options) 105 | { 106 | if (options.Any()) 107 | return null; 108 | 109 | return Escape(argument, PropertiesValueMap); 110 | } 111 | 112 | public static string? MarkdownToHtml(string? argument, string[] options) 113 | { 114 | if (argument == null || options.Any()) 115 | return null; 116 | 117 | var pipeline = new MarkdownPipelineBuilder() 118 | .UsePipeTables() 119 | .UseEmphasisExtras() //strike through, subscript, superscript 120 | .UseAutoLinks() //make links for http:// etc 121 | .Build(); 122 | return Markdown.ToHtml(argument.Trim(), pipeline) + '\n'; 123 | } 124 | 125 | public static string? UriStringEscape(string? argument, string[] options) 126 | { 127 | if (options.Any()) 128 | return null; 129 | 130 | if (argument == null) 131 | return null; 132 | 133 | return Uri.EscapeUriString(argument); 134 | } 135 | 136 | public static string? UriDataStringEscape(string? argument, string[] options) 137 | { 138 | if (options.Any()) 139 | return null; 140 | 141 | if (argument == null) 142 | return null; 143 | 144 | return Uri.EscapeDataString(argument); 145 | } 146 | 147 | [return: NotNullIfNotNull("raw")] 148 | static string? Escape(string? raw, IDictionary entities) 149 | { 150 | if (raw == null) 151 | return null; 152 | 153 | return string.Join("", 154 | raw.Select(c => 155 | { 156 | string entity; 157 | if (entities.TryGetValue(c, out entity)) 158 | return entity; 159 | return c.ToString(); 160 | })); 161 | } 162 | 163 | [return: NotNullIfNotNull("raw")] 164 | static string? Escape(string? raw, Func mapping) => raw == null ? null : string.Join("", raw.Select(mapping)); 165 | 166 | [return: NotNullIfNotNull("raw")] 167 | static string? Escape(string? raw, Func mapping) => raw == null ? null : string.Join("", raw.Select(mapping)); 168 | 169 | static bool IsAsciiPrintable(char ch) => ch >= 0x20 && ch <= 0x7E; 170 | 171 | static bool IsIso88591Compatible(char ch) => ch >= 0x00 && ch < 0xFF; 172 | 173 | static string EscapeUnicodeCharForYamlOrProperties(char ch) 174 | { 175 | var hex = ((int) ch).ToString("x4"); 176 | return $"\\u{hex}"; 177 | } 178 | 179 | static string YamlDoubleQuoteMap(char ch) 180 | { 181 | // Yaml supports multiple ways to encode newlines. One method we tried 182 | // (doubling newlines) doesn't work consistently across all libraries/ 183 | // validators, so we've gone with escaping newlines (\\r, \\n) instead. 184 | 185 | switch (ch) 186 | { 187 | case '\r': 188 | return "\\r"; 189 | case '\n': 190 | return "\\n"; 191 | case '\t': 192 | return "\\t"; 193 | case '\\': 194 | return "\\\\"; 195 | case '"': 196 | return "\\\""; 197 | default: 198 | return IsAsciiPrintable(ch) ? ch.ToString() : EscapeUnicodeCharForYamlOrProperties(ch); 199 | } 200 | } 201 | 202 | static string CommonPropertiesMap(char ch) 203 | { 204 | switch (ch) 205 | { 206 | case '\\': 207 | return "\\\\"; 208 | case '\r': 209 | return "\\r"; 210 | case '\n': 211 | return "\\n"; 212 | case '\t': 213 | // In some contexts a tab can get treated as non-semantic whitespace, 214 | // or as part of the separator between keys and values. It's safer to 215 | // always encode tabs. 216 | return "\\t"; 217 | default: 218 | return IsIso88591Compatible(ch) 219 | ? ch.ToString() 220 | : EscapeUnicodeCharForYamlOrProperties(ch); 221 | } 222 | } 223 | 224 | static string PropertiesKeyMap(char ch) 225 | { 226 | switch (ch) 227 | { 228 | case ' ': 229 | return "\\ "; 230 | case ':': 231 | return "\\:"; 232 | case '=': 233 | return "\\="; 234 | default: 235 | return CommonPropertiesMap(ch); 236 | } 237 | } 238 | 239 | static string PropertiesValueMap(char ch, int index) 240 | { 241 | switch (ch) 242 | { 243 | case ' ' when index == 0: 244 | return "\\ "; 245 | default: 246 | return CommonPropertiesMap(ch); 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextManipulationFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | #if HAS_NULLABLE_REF_TYPES 7 | using System.Diagnostics.CodeAnalysis; 8 | #endif 9 | 10 | namespace Octostache.Templates.Functions 11 | { 12 | class TextManipulationFunction 13 | { 14 | public static string? ToBase64(string? argument, string[] options) 15 | { 16 | if (options.Length > 1 || argument == null) 17 | { 18 | return null; 19 | } 20 | 21 | var encoding = !options.Any() ? "utf8" : options[0].ToLower(); 22 | 23 | byte[] argumentBytes; 24 | switch (encoding) 25 | { 26 | case "utf8": 27 | case "utf-8": 28 | { 29 | argumentBytes = Encoding.UTF8.GetBytes(argument); 30 | break; 31 | } 32 | case "unicode": 33 | { 34 | argumentBytes = Encoding.Unicode.GetBytes(argument); 35 | break; 36 | } 37 | default: 38 | { 39 | return null; 40 | } 41 | } 42 | 43 | return Convert.ToBase64String(argumentBytes); 44 | } 45 | 46 | public static string? FromBase64(string? argument, string[] options) 47 | { 48 | if (options.Length > 1 || argument == null) 49 | { 50 | return null; 51 | } 52 | 53 | var encoding = !options.Any() ? "utf8" : options[0].ToLower(); 54 | var argumentBytes = Convert.FromBase64String(argument); 55 | switch (encoding) 56 | { 57 | case "utf8": 58 | case "utf-8": 59 | { 60 | return Encoding.UTF8.GetString(argumentBytes); 61 | } 62 | case "unicode": 63 | { 64 | return Encoding.Unicode.GetString(argumentBytes); 65 | } 66 | default: 67 | { 68 | return null; 69 | } 70 | } 71 | } 72 | 73 | public static string? Append(string? argument, string[] options) 74 | { 75 | if (argument == null) 76 | { 77 | if (!options.Any()) 78 | { 79 | return null; 80 | } 81 | 82 | return string.Concat(options); 83 | } 84 | 85 | if (!options.Any()) 86 | { 87 | return null; 88 | } 89 | 90 | return argument + string.Concat(options); 91 | } 92 | 93 | public static string? Prepend(string? argument, string[] options) 94 | { 95 | if (argument == null) 96 | { 97 | if (!options.Any()) 98 | { 99 | return null; 100 | } 101 | 102 | return string.Concat(options); 103 | } 104 | 105 | if (!options.Any()) 106 | { 107 | return null; 108 | } 109 | 110 | return string.Concat(options) + argument; 111 | } 112 | 113 | public static string? Truncate(string? argument, string[] options) 114 | { 115 | if (argument == null || !options.Any() || !int.TryParse(options[0], out _) || int.Parse(options[0]) < 0) 116 | { 117 | return null; 118 | } 119 | 120 | var length = int.Parse(options[0]); 121 | return length < argument.Length 122 | ? $"{argument.Substring(0, length)}..." 123 | : argument; 124 | } 125 | 126 | [return: NotNullIfNotNull("argument")] 127 | public static string? Trim(string? argument, string[] options) 128 | { 129 | if (argument == null) 130 | return null; 131 | 132 | if (!options.Any()) return argument.Trim(); 133 | 134 | switch (options[0].ToLower()) 135 | { 136 | case "start": 137 | return argument.TrimStart(); 138 | case "end": 139 | return argument.TrimEnd(); 140 | default: 141 | return null; 142 | } 143 | } 144 | 145 | public static string? Indent(string? argument, string[] options) 146 | { 147 | if (argument == null) 148 | { 149 | return null; 150 | } 151 | 152 | if (argument.Length == 0) 153 | { 154 | // No content, no indenting 155 | return string.Empty; 156 | } 157 | 158 | var indentOptions = new IndentOptions(options); 159 | 160 | if (!indentOptions.IsValid) 161 | { 162 | return null; 163 | } 164 | 165 | return indentOptions.InitialIndent + argument.Replace("\n", "\n" + indentOptions.SubsequentIndent); 166 | } 167 | 168 | [return: NotNullIfNotNull("argument")] 169 | public static string? UriPart(string? argument, string[] options) 170 | { 171 | if (argument == null) 172 | return null; 173 | 174 | if (!options.Any()) 175 | return $"[{nameof(UriPart)} error: no argument given]"; 176 | 177 | if (!Uri.TryCreate(argument, UriKind.RelativeOrAbsolute, out var uri)) 178 | return argument; 179 | 180 | // NOTE: IdnHost property not available in .NET Framework target 181 | 182 | try 183 | { 184 | switch (options[0].ToLowerInvariant()) 185 | { 186 | case "absolutepath": 187 | return uri.AbsolutePath; 188 | case "absoluteuri": 189 | return uri.AbsoluteUri; 190 | case "authority": 191 | return uri.Authority; 192 | case "dnssafehost": 193 | return uri.DnsSafeHost; 194 | case "fragment": 195 | return uri.Fragment; 196 | case "host": 197 | return uri.Host; 198 | case "hostandport": 199 | return uri.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped); 200 | case "hostnametype": 201 | return Enum.GetName(typeof(UriHostNameType), uri.HostNameType); 202 | case "isabsoluteuri": 203 | return uri.IsAbsoluteUri.ToString().ToLowerInvariant(); 204 | case "isdefaultport": 205 | return uri.IsDefaultPort.ToString().ToLowerInvariant(); 206 | case "isfile": 207 | return uri.IsFile.ToString().ToLowerInvariant(); 208 | case "isloopback": 209 | return uri.IsLoopback.ToString().ToLowerInvariant(); 210 | case "isunc": 211 | return uri.IsUnc.ToString().ToLowerInvariant(); 212 | case "path": 213 | return uri.LocalPath; 214 | case "pathandquery": 215 | return uri.PathAndQuery; 216 | case "port": 217 | return uri.Port.ToString(CultureInfo.InvariantCulture); 218 | case "query": 219 | return uri.Query; 220 | case "scheme": 221 | return uri.Scheme; 222 | case "schemeandserver": 223 | return uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped); 224 | case "userinfo": 225 | return uri.UserInfo; 226 | default: 227 | return $"[{nameof(UriPart)} {options[0]} error: argument '{options[0]}' not supported]"; 228 | } 229 | } 230 | catch (Exception e) 231 | { 232 | return $"[{nameof(UriPart)} {options[0]} error: {e.Message}]"; 233 | } 234 | } 235 | 236 | class IndentOptions 237 | { 238 | public string InitialIndent { get; } 239 | public string SubsequentIndent { get; } 240 | public bool IsValid { get; } = true; 241 | 242 | static readonly Regex DualSizeEx = new Regex(@"^((\d{1,3})?/)?(\d{1,3})$", RegexOptions.Compiled); 243 | 244 | public IndentOptions(string[] options) 245 | { 246 | if (options.Length == 0) 247 | { 248 | SubsequentIndent = InitialIndent = " "; 249 | return; 250 | } 251 | 252 | if (options.Length == 1) 253 | { 254 | var dualSize = DualSizeEx.Match(options[0]); 255 | if (dualSize.Success) 256 | { 257 | var separator = dualSize.Groups[1]; 258 | var initial = dualSize.Groups[2]; 259 | var subsequent = dualSize.Groups[3]; 260 | if (separator.Success) 261 | { 262 | // Different sized indents 263 | if (initial.Success) 264 | { 265 | if (byte.TryParse(initial.Value, out var initialIndent) && byte.TryParse(subsequent.Value, out var subsequentIndent)) 266 | { 267 | InitialIndent = new string(' ', initialIndent); 268 | SubsequentIndent = new string(' ', subsequentIndent); 269 | return; 270 | } 271 | } 272 | else 273 | { 274 | if (byte.TryParse(subsequent.Value, out var subsequentIndent)) 275 | { 276 | InitialIndent = string.Empty; 277 | SubsequentIndent = new string(' ', subsequentIndent); 278 | return; 279 | } 280 | } 281 | } 282 | else if (byte.TryParse(subsequent.Value, out var overallIndent)) 283 | { 284 | InitialIndent = SubsequentIndent = new string(' ', overallIndent); 285 | return; 286 | } 287 | } 288 | 289 | InitialIndent = SubsequentIndent = options[0]; 290 | } 291 | else if (options.Length == 2) 292 | { 293 | InitialIndent = options[0]; 294 | SubsequentIndent = options[1]; 295 | } 296 | else 297 | { 298 | InitialIndent = SubsequentIndent = string.Empty; 299 | IsValid = false; 300 | } 301 | } 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextReplaceFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | static class TextReplaceFunction 7 | { 8 | public static string? Replace(string? argument, string[] options) 9 | { 10 | if (argument == null || options.Length == 0) 11 | return null; 12 | 13 | return Regex.Replace(argument, options[0], options.Length == 1 ? "" : options[1]); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/TextSubstringFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | static class TextSubstringFunction 7 | { 8 | public static string? Substring(string? argument, string[] options) 9 | { 10 | if (argument == null || options.Length == 0 || options.Length > 2) 11 | return null; 12 | 13 | if (options.Any(o => !int.TryParse(o, out _)) || options.Any(o => int.Parse(o) < 0)) 14 | return null; 15 | 16 | if (options.Length == 1 && int.Parse(options[0]) > argument.Length) 17 | return null; 18 | 19 | if (options.Length == 2 && int.Parse(options[0]) > argument.Length) 20 | return null; 21 | 22 | var startIndex = options.Length == 1 ? 0 : int.Parse(options[0]); 23 | var length = options.Length == 1 ? int.Parse(options[0]) : int.Parse(options[1]); 24 | 25 | // If starting position is valid but the length would exceed the string, use the remaining length of the string 26 | if (startIndex < argument.Length && startIndex + length > argument.Length) 27 | length = argument.Length - startIndex; 28 | 29 | return argument.Substring(startIndex, length); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Functions/VersionParseFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Octopus.Versioning.Octopus; 3 | 4 | namespace Octostache.Templates.Functions 5 | { 6 | class VersionParseFunction 7 | { 8 | static readonly OctopusVersionParser OctopusVersionParser = new OctopusVersionParser(); 9 | 10 | public static string? VersionMajor(string? argument, string[] options) 11 | { 12 | if (argument == null) 13 | { 14 | return null; 15 | } 16 | 17 | return OctopusVersionParser.Parse(argument).Major.ToString(); 18 | } 19 | 20 | public static string? VersionMinor(string? argument, string[] options) 21 | { 22 | if (argument == null) 23 | { 24 | return null; 25 | } 26 | 27 | return OctopusVersionParser.Parse(argument).Minor.ToString(); 28 | } 29 | 30 | public static string? VersionPatch(string? argument, string[] options) 31 | { 32 | if (argument == null) 33 | { 34 | return null; 35 | } 36 | 37 | return OctopusVersionParser.Parse(argument).Patch.ToString(); 38 | } 39 | 40 | public static string? VersionRevision(string? argument, string[] options) 41 | { 42 | if (argument == null) 43 | { 44 | return null; 45 | } 46 | 47 | return OctopusVersionParser.Parse(argument).Revision.ToString(); 48 | } 49 | 50 | public static string? VersionRelease(string? argument, string[] options) 51 | { 52 | if (argument == null) 53 | { 54 | return null; 55 | } 56 | 57 | return OctopusVersionParser.Parse(argument).Release; 58 | } 59 | 60 | public static string? VersionReleasePrefix(string? argument, string[] options) 61 | { 62 | if (argument == null) 63 | { 64 | return null; 65 | } 66 | 67 | return OctopusVersionParser.Parse(argument).ReleasePrefix; 68 | } 69 | 70 | public static string? VersionReleaseCounter(string? argument, string[] options) 71 | { 72 | if (argument == null) 73 | { 74 | return null; 75 | } 76 | 77 | return OctopusVersionParser.Parse(argument).ReleaseCounter; 78 | } 79 | 80 | public static string? VersionMetadata(string? argument, string[] options) 81 | { 82 | if (argument == null) 83 | { 84 | return null; 85 | } 86 | 87 | return OctopusVersionParser.Parse(argument).Metadata; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /source/Octostache/Templates/IInputToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Sprache; 4 | 5 | namespace Octostache.Templates 6 | { 7 | interface IInputToken 8 | { 9 | Position? InputPosition { get; set; } 10 | 11 | IEnumerable GetArguments(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Identifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | class Identifier : SymbolExpressionStep 7 | { 8 | public string Text { get; } 9 | 10 | public Identifier(string text) 11 | { 12 | Text = text; 13 | } 14 | 15 | public override string ToString() => Text; 16 | 17 | public override IEnumerable GetArguments() 18 | { 19 | return new[] { Text }; 20 | } 21 | 22 | public override bool Equals(SymbolExpressionStep? other) => Equals(other as Identifier); 23 | 24 | protected bool Equals(Identifier? other) => base.Equals(other) && string.Equals(Text, other?.Text); 25 | 26 | public override bool Equals(object? obj) 27 | { 28 | if (ReferenceEquals(null, obj)) return false; 29 | if (ReferenceEquals(this, obj)) return true; 30 | if (obj.GetType() != GetType()) return false; 31 | return Equals((Identifier) obj); 32 | } 33 | 34 | public override int GetHashCode() 35 | { 36 | unchecked 37 | { 38 | return (base.GetHashCode() * 397) ^ (Text != null ? Text.GetHashCode() : 0); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Indexer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | class Indexer : SymbolExpressionStep 7 | { 8 | public string? Index { get; } 9 | public SymbolExpression? Symbol { get; } 10 | public bool IsSymbol => Symbol != null; 11 | 12 | public Indexer(string? index) 13 | { 14 | Index = index; 15 | } 16 | 17 | public Indexer(SymbolExpression expression) 18 | { 19 | Symbol = expression; 20 | } 21 | 22 | public override string ToString() => "[" + (IsSymbol ? "#{" + Symbol + "}" : Index) + "]"; 23 | 24 | public override IEnumerable GetArguments() => Symbol?.GetArguments() ?? new string[0]; 25 | 26 | public override bool Equals(SymbolExpressionStep? other) => other != null && Equals((other as Indexer)!); 27 | 28 | protected bool Equals(Indexer other) => base.Equals(other) && string.Equals(Index, other.Index) && Equals(Symbol, other.Symbol); 29 | 30 | public override bool Equals(object? obj) 31 | { 32 | if (ReferenceEquals(null, obj)) return false; 33 | if (ReferenceEquals(this, obj)) return true; 34 | if (obj.GetType() != GetType()) return false; 35 | return Equals((Indexer) obj); 36 | } 37 | 38 | public override int GetHashCode() 39 | { 40 | unchecked 41 | { 42 | // ReSharper disable once SuggestVarOrType_BuiltInTypes 43 | int hashCode = base.GetHashCode(); 44 | hashCode = (hashCode * 397) ^ (Index != null ? Index.GetHashCode() : 0); 45 | hashCode = (hashCode * 397) ^ (Symbol != null ? Symbol.GetHashCode() : 0); 46 | return hashCode; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/Octostache/Templates/ItemCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #if NET462 3 | using System.Runtime.Caching; 4 | using System.Collections.Specialized; 5 | 6 | #else 7 | using Microsoft.Extensions.Caching.Memory; 8 | #endif 9 | 10 | namespace Octostache.Templates 11 | { 12 | public class ItemCache where T : class 13 | { 14 | // ReSharper disable once MemberCanBePrivate.Global 15 | // ReSharper disable once UnusedAutoPropertyAccessor.Global 16 | public string Name { get; } 17 | 18 | // ReSharper disable once MemberCanBePrivate.Global 19 | public int MegabyteLimit { get; } 20 | 21 | // ReSharper disable once MemberCanBePrivate.Global 22 | public TimeSpan SlidingExpiration { get; } 23 | 24 | readonly object nullItem = new object(); 25 | MemoryCache cache; 26 | 27 | public ItemCache(string name, int megabyteLimit, TimeSpan slidingExpiration) 28 | { 29 | Name = name; 30 | MegabyteLimit = megabyteLimit; 31 | SlidingExpiration = slidingExpiration; 32 | 33 | #if NET462 34 | cache = new MemoryCache(Name, 35 | new NameValueCollection 36 | { { "CacheMemoryLimitMegabytes", MegabyteLimit.ToString() } }); 37 | #else 38 | cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = MegabyteLimit * 1024 * 1024 }); 39 | #endif 40 | } 41 | 42 | // ReSharper disable once MemberCanBePrivate.Global 43 | public void Add(string key, T? item) 44 | { 45 | #if NET462 46 | cache.Set(key, item ?? nullItem, new CacheItemPolicy { SlidingExpiration = SlidingExpiration }); 47 | #else 48 | // NOTE: Setting the size to the string length, is not quite right, but close enough for our purposes. 49 | cache.Set(key, item ?? nullItem, new MemoryCacheEntryOptions { SlidingExpiration = SlidingExpiration, Size = item?.ToString().Length ?? 0 }); 50 | #endif 51 | } 52 | 53 | // ReSharper disable once MemberCanBePrivate.Global 54 | public T? Get(string key) => cache.Get(key) as T; 55 | 56 | public T? GetOrAdd(string key, Func getItem) 57 | { 58 | var obj = cache.Get(key); 59 | 60 | if (obj != null) 61 | { 62 | return obj as T; 63 | } 64 | 65 | var item = getItem(); 66 | Add(key, item); 67 | 68 | return item; 69 | } 70 | 71 | public void Clear() 72 | { 73 | #if NET462 74 | cache = new MemoryCache(Name, 75 | new NameValueCollection 76 | { { "CacheMemoryLimitMegabytes", MegabyteLimit.ToString() } }); 77 | #else 78 | cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = MegabyteLimit * 1024 * 1024 }); 79 | #endif 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /source/Octostache/Templates/PropertyListBinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | static class PropertyListBinder 8 | { 9 | public static Binding CreateFrom(IDictionary properties) 10 | { 11 | var result = new Binding(); 12 | foreach (var property in properties) 13 | { 14 | if (TemplateParser.TryParseIdentifierPath(property.Key, out var pathExpression) && pathExpression != null) 15 | { 16 | Add(result, pathExpression.Steps, property.Value ?? ""); 17 | } 18 | } 19 | 20 | return result; 21 | } 22 | 23 | static void Add(Binding result, IList steps, string value) 24 | { 25 | var first = steps.FirstOrDefault(); 26 | 27 | if (first == null) 28 | { 29 | result.Item = value; 30 | return; 31 | } 32 | 33 | Binding next; 34 | 35 | switch (first) 36 | { 37 | case Identifier iss: 38 | { 39 | if (!result.TryGetValue(iss.Text, out next)) 40 | { 41 | result[iss.Text] = next = new Binding(); 42 | } 43 | 44 | break; 45 | } 46 | // ReSharper disable once MergeIntoPattern 47 | case Indexer ix when ix.Index != null: 48 | { 49 | if (!result.Indexable.TryGetValue(ix.Index, out next)) 50 | { 51 | result.Indexable[ix.Index] = next = new Binding(ix.Index); 52 | } 53 | 54 | break; 55 | } 56 | default: 57 | throw new NotImplementedException("Unknown step type: " + first); 58 | } 59 | 60 | Add(next, steps.Skip(1).ToList(), value); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /source/Octostache/Templates/RecursiveDefinitionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | public class RecursiveDefinitionException : InvalidOperationException 8 | { 9 | const string MessageTemplate = "An attempt to parse the variable symbol \"{0}\" appears to have resulted in a self referencing loop ({1} -> {0}). Ensure that recursive loops do not exist in the variable values."; 10 | 11 | internal RecursiveDefinitionException(SymbolExpression symbol, Stack ancestorSymbolStack) 12 | : base(string.Format(MessageTemplate, symbol, string.Join(" -> ", ancestorSymbolStack.Reverse().Select(x => x.ToString())))) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/Octostache/Templates/RepetitionToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | class RepetitionToken : TemplateToken 8 | { 9 | public SymbolExpression Collection { get; } 10 | public Identifier Enumerator { get; } 11 | public TemplateToken[] Template { get; } 12 | 13 | public RepetitionToken(SymbolExpression collection, Identifier enumerator, IEnumerable template) 14 | { 15 | Collection = collection; 16 | Enumerator = enumerator; 17 | Template = template.ToArray(); 18 | } 19 | 20 | public override string ToString() 21 | => "#{each " + Enumerator + " in " + Collection + "}" + string.Join("", Template.Cast()) + "#{/each}"; 22 | 23 | public override IEnumerable GetArguments() => Collection.GetArguments(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/Octostache/Templates/SubstitutionToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | /// 7 | /// Example: #{Octopus.Action[Foo].Name. 8 | /// 9 | class SubstitutionToken : TemplateToken 10 | { 11 | public ContentExpression Expression { get; } 12 | 13 | public SubstitutionToken(ContentExpression expression) 14 | { 15 | Expression = expression; 16 | } 17 | 18 | public override string ToString() => "#{" + Expression + "}"; 19 | 20 | public override IEnumerable GetArguments() => Expression.GetArguments(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/Octostache/Templates/SymbolExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Octostache.Templates 7 | { 8 | /// 9 | /// A value, identified using dotted/bracketed notation, e.g.: 10 | /// Octopus.Action[Name].Foo. This would classically 11 | /// be represented using nesting "property expressions" rather than a path, but in the 12 | /// current very simple language a path is more convenient to work with. 13 | /// 14 | class SymbolExpression : ContentExpression 15 | { 16 | public SymbolExpressionStep[] Steps { get; } 17 | public static IEqualityComparer StepsComparer { get; } = new StepsEqualityComparer(); 18 | 19 | public SymbolExpression(IEnumerable steps) 20 | { 21 | Steps = steps.ToArray(); 22 | } 23 | 24 | public override string ToString() 25 | { 26 | var result = new StringBuilder(); 27 | var identifierJoin = ""; 28 | foreach (var step in Steps) 29 | { 30 | if (step is Identifier) 31 | result.Append(identifierJoin); 32 | 33 | result.Append(step); 34 | 35 | identifierJoin = "."; 36 | } 37 | 38 | return result.ToString(); 39 | } 40 | 41 | public override IEnumerable GetArguments() 42 | { 43 | return Steps.SelectMany(s => s.GetArguments()); 44 | } 45 | 46 | sealed class StepsEqualityComparer : IEqualityComparer 47 | { 48 | public bool Equals(SymbolExpression x, SymbolExpression y) 49 | { 50 | if (ReferenceEquals(x, y)) return true; 51 | if (ReferenceEquals(x, null)) return false; 52 | if (ReferenceEquals(y, null)) return false; 53 | if (x.GetType() != y.GetType()) return false; 54 | return x.Steps.SequenceEqual(y.Steps); 55 | } 56 | 57 | public int GetHashCode(SymbolExpression obj) => obj.Steps?.GetHashCode() ?? 0; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/Octostache/Templates/SymbolExpressionStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Sprache; 4 | 5 | namespace Octostache.Templates 6 | { 7 | /// 8 | /// A segment of a , 9 | /// e.g. Octopus, [Foo]. 10 | /// 11 | abstract class SymbolExpressionStep : IInputToken 12 | { 13 | public Position? InputPosition { get; set; } 14 | public abstract IEnumerable GetArguments(); 15 | 16 | public virtual bool Equals(SymbolExpressionStep? other) 17 | { 18 | if (ReferenceEquals(null, other)) return false; 19 | if (ReferenceEquals(this, other)) return true; 20 | return Equals(InputPosition, other.InputPosition); 21 | } 22 | 23 | public override bool Equals(object? obj) 24 | { 25 | if (ReferenceEquals(null, obj)) return false; 26 | if (ReferenceEquals(this, obj)) return true; 27 | if (obj.GetType() != GetType()) return false; 28 | return Equals((SymbolExpressionStep) obj); 29 | } 30 | 31 | public override int GetHashCode() => InputPosition != null ? InputPosition.GetHashCode() : 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/Octostache/Templates/Template.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | public class Template 8 | { 9 | public TemplateToken[] Tokens { get; } 10 | 11 | public Template(IEnumerable tokens) 12 | { 13 | Tokens = tokens.ToArray(); 14 | } 15 | 16 | public override string ToString() => string.Concat(Tokens.Cast()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/Octostache/Templates/TemplateAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Octostache.Templates 6 | { 7 | static class TemplateAnalyzer 8 | { 9 | static IEnumerable GetDependencies(IEnumerable tokens, AnalysisContext context) 10 | { 11 | foreach (var token in tokens) 12 | { 13 | foreach (var dependency in GetDependencies(token, context)) 14 | { 15 | yield return dependency; 16 | } 17 | } 18 | } 19 | 20 | static IEnumerable GetDependencies(TemplateToken token, AnalysisContext context) 21 | { 22 | if (token is TextToken) 23 | yield break; 24 | 25 | var st = token as SubstitutionToken; 26 | if (st != null) 27 | { 28 | foreach (var symbol in GetSymbols(st.Expression)) 29 | yield return context.Expand(symbol); 30 | yield break; 31 | } 32 | 33 | var ct = token as ConditionalToken; 34 | if (ct != null) 35 | { 36 | foreach (var symbol in GetSymbols(ct.Token.LeftSide)) 37 | { 38 | yield return context.Expand(symbol); 39 | } 40 | 41 | var exp = ct.Token as ConditionalSymbolExpressionToken; 42 | if (exp != null) 43 | { 44 | foreach (var symbol in GetSymbols(exp.RightSide)) 45 | { 46 | yield return context.Expand(symbol); 47 | } 48 | } 49 | 50 | foreach (var templateDependency in GetDependencies(ct.TruthyTemplate.Concat(ct.FalsyTemplate), context)) 51 | { 52 | yield return templateDependency; 53 | } 54 | 55 | yield break; 56 | } 57 | 58 | var rt = token as RepetitionToken; 59 | if (rt != null) 60 | { 61 | var ctx = context.BeginChild(rt.Enumerator, rt.Collection); 62 | foreach (var dependency in GetDependencies(rt.Template, ctx)) 63 | yield return dependency; 64 | 65 | yield break; 66 | } 67 | 68 | throw new NotImplementedException("Unknown token type: " + token); 69 | } 70 | 71 | static IEnumerable GetSymbols(ContentExpression expression) 72 | { 73 | var sx = expression as SymbolExpression; 74 | if (sx != null) 75 | { 76 | yield return sx; 77 | } 78 | else 79 | { 80 | var fx = expression as FunctionCallExpression; 81 | if (fx != null) 82 | { 83 | foreach (var symbol in GetSymbols(fx.Argument)) 84 | { 85 | yield return symbol; 86 | } 87 | } 88 | else 89 | { 90 | throw new NotImplementedException("Unknown expression type: " + expression); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /source/Octostache/Templates/TemplateEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Octostache.Templates 7 | { 8 | class TemplateEvaluator 9 | { 10 | readonly List missingTokens = new List(); 11 | readonly List nullTokens = new List(); 12 | 13 | public static void Evaluate(Template template, EvaluationContext context, out string[] missingTokens, out string[] nullTokens) 14 | { 15 | var evaluator = new TemplateEvaluator(); 16 | evaluator.Evaluate(template.Tokens, context); 17 | missingTokens = evaluator.missingTokens.Distinct().ToArray(); 18 | nullTokens = evaluator.nullTokens.Distinct().ToArray(); 19 | } 20 | 21 | public static void Evaluate(Template template, 22 | Binding properties, 23 | TextWriter output, 24 | out string[] missingTokens, 25 | out string[] nullTokens) 26 | { 27 | var context = new EvaluationContext(properties, output); 28 | Evaluate(template, context, out missingTokens, out nullTokens); 29 | } 30 | 31 | public static void Evaluate(Template template, 32 | Binding properties, 33 | TextWriter output, 34 | Dictionary> extensions, 35 | out string[] missingTokens, 36 | out string[] nullTokens) 37 | { 38 | var context = new EvaluationContext(properties, output, extensions: extensions); 39 | Evaluate(template, context, out missingTokens, out nullTokens); 40 | } 41 | 42 | void Evaluate(IEnumerable tokens, EvaluationContext context) 43 | { 44 | foreach (var token in tokens) 45 | { 46 | Evaluate(token, context); 47 | } 48 | } 49 | 50 | void Evaluate(TemplateToken token, EvaluationContext context) 51 | { 52 | var tt = token as TextToken; 53 | if (tt != null) 54 | { 55 | EvaluateTextToken(context, tt); 56 | return; 57 | } 58 | 59 | var st = token as SubstitutionToken; 60 | 61 | if (st != null) 62 | { 63 | EvaluateSubstitutionToken(context, st); 64 | return; 65 | } 66 | 67 | var ct = token as ConditionalToken; 68 | if (ct != null) 69 | { 70 | EvaluateConditionalToken(context, ct); 71 | return; 72 | } 73 | 74 | var rt = token as RepetitionToken; 75 | if (rt != null) 76 | { 77 | EvaluateRepititionToken(context, rt); 78 | return; 79 | } 80 | 81 | var cat = token as CalculationToken; 82 | if (cat != null) 83 | { 84 | var value = cat.Expression.Evaluate(s => 85 | { 86 | var value = context.ResolveOptional(s, out var innerTokens, out _); 87 | missingTokens.AddRange(innerTokens); 88 | return value; 89 | }); 90 | 91 | if (value != null) 92 | context.Output.Write(value); 93 | else 94 | context.Output.Write(cat.ToString()); 95 | 96 | return; 97 | } 98 | 99 | throw new NotImplementedException("Unknown token type: " + token); 100 | } 101 | 102 | void EvaluateRepititionToken(EvaluationContext context, RepetitionToken rt) 103 | { 104 | string[] innerTokens; 105 | var items = context.ResolveAll(rt.Collection, out innerTokens, out _).ToArray(); 106 | missingTokens.AddRange(innerTokens); 107 | 108 | for (var i = 0; i < items.Length; ++i) 109 | { 110 | var item = items[i]; 111 | 112 | var specials = new Dictionary 113 | { 114 | { Constants.Each.Index, i.ToString() }, 115 | { Constants.Each.First, i == 0 ? "True" : "False" }, 116 | { Constants.Each.Last, i == items.Length - 1 ? "True" : "False" }, 117 | }; 118 | 119 | var locals = PropertyListBinder.CreateFrom(specials); 120 | 121 | locals.Add(rt.Enumerator.Text, item); 122 | 123 | var newContext = context.BeginChild(locals); 124 | Evaluate(rt.Template, newContext); 125 | } 126 | } 127 | 128 | void EvaluateConditionalToken(EvaluationContext context, ConditionalToken ct) 129 | { 130 | string? leftSide; 131 | var leftToken = ct.Token.LeftSide as SymbolExpression; 132 | if (leftToken != null) 133 | { 134 | leftSide = context.Resolve(leftToken, out var innerTokens, out _); 135 | missingTokens.AddRange(innerTokens); 136 | } 137 | else 138 | { 139 | leftSide = Calculate(ct.Token.LeftSide, context); 140 | if (leftSide == null) 141 | { 142 | context.Output.Write(ct.ToString()); 143 | missingTokens.Add(ct.Token.LeftSide.ToString()); 144 | return; 145 | } 146 | } 147 | 148 | var eqToken = ct.Token as ConditionalStringExpressionToken; 149 | if (eqToken != null) 150 | { 151 | var comparer = eqToken.Equality ? new Func((x, y) => x == y) : (x, y) => x != y; 152 | 153 | if (comparer(leftSide, eqToken.RightSide)) 154 | Evaluate(ct.TruthyTemplate, context); 155 | else 156 | Evaluate(ct.FalsyTemplate, context); 157 | 158 | return; 159 | } 160 | 161 | var symToken = ct.Token as ConditionalSymbolExpressionToken; 162 | if (symToken != null) 163 | { 164 | var comparer = symToken.Equality ? new Func((x, y) => x == y) : (x, y) => x != y; 165 | string? rightSide; 166 | 167 | var rightToken = symToken.RightSide as SymbolExpression; 168 | if (rightToken != null) 169 | { 170 | rightSide = context.Resolve(rightToken, out var innerTokens, out _); 171 | missingTokens.AddRange(innerTokens); 172 | } 173 | else 174 | { 175 | rightSide = Calculate(symToken.RightSide, context); 176 | if (rightSide == null) 177 | { 178 | context.Output.Write(ct.ToString()); 179 | missingTokens.Add(symToken.RightSide.ToString()); 180 | return; 181 | } 182 | } 183 | 184 | if (comparer(leftSide, rightSide)) 185 | Evaluate(ct.TruthyTemplate, context); 186 | else 187 | Evaluate(ct.FalsyTemplate, context); 188 | 189 | return; 190 | } 191 | 192 | if (IsTruthy(leftSide)) 193 | Evaluate(ct.TruthyTemplate, context); 194 | else 195 | Evaluate(ct.FalsyTemplate, context); 196 | } 197 | 198 | void EvaluateSubstitutionToken(EvaluationContext context, SubstitutionToken st) 199 | { 200 | var value = Calculate(st.Expression, context); 201 | if (value == null) 202 | { 203 | if (st.Expression is FunctionCallExpression { Function: "null" }) 204 | { 205 | nullTokens.Add(st.ToString()); 206 | } 207 | else 208 | { 209 | missingTokens.Add(st.ToString()); 210 | } 211 | } 212 | 213 | context.Output.Write(value ?? st.ToString()); 214 | } 215 | 216 | static void EvaluateTextToken(EvaluationContext context, TextToken tt) 217 | { 218 | foreach (var text in tt.Text) 219 | { 220 | context.Output.Write(text); 221 | } 222 | } 223 | 224 | string? Calculate(ContentExpression expression, EvaluationContext context) 225 | { 226 | if (expression is SymbolExpression sx) 227 | { 228 | var resolvedSymbol = context.ResolveOptional(sx, out var innerTokens, out _); 229 | missingTokens.AddRange(innerTokens); 230 | return resolvedSymbol; 231 | } 232 | 233 | var fx = expression as FunctionCallExpression; 234 | if (fx == null) 235 | { 236 | throw new NotImplementedException("Unknown expression type: " + expression); 237 | } 238 | 239 | var argument = Calculate(fx.Argument, context); 240 | 241 | var args = fx.Options.Select(opt => Resolve(opt, context)).ToArray(); 242 | 243 | var funcOut = BuiltInFunctions.InvokeOrNull(fx.Function, argument, args); 244 | if (funcOut != null) 245 | { 246 | return funcOut; 247 | } 248 | 249 | return InvokeOrNullExtension(context.Extensions, fx.Function, argument, args); 250 | } 251 | 252 | string? InvokeOrNullExtension(Dictionary> extensions, string function, string? argument, string[] args) 253 | { 254 | var functionName = function.ToLowerInvariant(); 255 | 256 | if (extensions.TryGetValue(functionName, out var ext)) 257 | return ext(argument, args); 258 | 259 | return null; 260 | } 261 | 262 | string Resolve(TemplateToken token, EvaluationContext context) 263 | { 264 | using (var x = new StringWriter()) 265 | { 266 | var c2 = new EvaluationContext(new Binding(), x, context); 267 | Evaluate(token, c2); 268 | x.Flush(); 269 | return x.ToString(); 270 | } 271 | } 272 | 273 | internal static bool IsTruthy(string value) => value != "0" && value != "" && !StringEqual(value.Trim(), "no") && !StringEqual(value.Trim(), "false"); 274 | static bool StringEqual(string a, string b) => string.Compare(a, b, StringComparison.OrdinalIgnoreCase) == 0; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /source/Octostache/Templates/TemplateToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Sprache; 4 | 5 | namespace Octostache.Templates 6 | { 7 | public abstract class TemplateToken : IInputToken 8 | { 9 | public Position? InputPosition { get; set; } 10 | public abstract IEnumerable GetArguments(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/Octostache/Templates/TextToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Octostache.Templates 5 | { 6 | class TextToken : TemplateToken 7 | { 8 | public IEnumerable Text { get; } 9 | 10 | public TextToken(params string[] text) 11 | { 12 | Text = text; 13 | } 14 | 15 | public override string ToString() => string.Concat(Text).Replace("#{", "##{"); 16 | 17 | public override IEnumerable GetArguments() => new string[0]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /source/Octostache/VariableDictionary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using Octostache.Templates; 7 | #if HAS_NULLABLE_REF_TYPES 8 | using System.Diagnostics.CodeAnalysis; 9 | #endif 10 | 11 | namespace Octostache 12 | { 13 | public class VariableDictionary : IEnumerable> 14 | { 15 | readonly Dictionary variables = new Dictionary(StringComparer.OrdinalIgnoreCase); 16 | readonly Dictionary> extensions = new Dictionary>(); 17 | string? storageFilePath; 18 | Binding? binding; 19 | 20 | public VariableDictionary() : this(null) 21 | { 22 | } 23 | 24 | public VariableDictionary(string? storageFilePath) 25 | { 26 | if (string.IsNullOrWhiteSpace(storageFilePath)) return; 27 | this.storageFilePath = Path.GetFullPath(storageFilePath); 28 | Reload(); 29 | } 30 | 31 | Binding Binding => binding ?? (binding = PropertyListBinder.CreateFrom(variables)); 32 | 33 | /// 34 | /// Gets or sets a variable by name. 35 | /// 36 | /// The name of the variable to set. 37 | /// The current (evaluated) value of the variable. 38 | public string? this[string name] 39 | { 40 | get => Get(name); 41 | set => Set(name, value); 42 | } 43 | 44 | /// 45 | /// Sets a variable value. 46 | /// 47 | /// The name of the variable. 48 | /// The value of the variable. 49 | public void Set(string name, string? value) 50 | { 51 | if (name == null) return; 52 | variables[name] = value; 53 | binding = null; 54 | Save(); 55 | } 56 | 57 | /// 58 | /// Sets a variable to a list of strings, by joining each value with a separator. 59 | /// 60 | /// The name of the variable to set. 61 | /// The list of values. 62 | /// The separator character to join by. 63 | public void SetStrings(string variableName, IEnumerable values, string separator = ",") 64 | { 65 | var value = string.Join(separator, values.Where(v => !string.IsNullOrWhiteSpace(v))); 66 | Set(variableName, value); 67 | } 68 | 69 | /// 70 | /// Sets a variable to a list of values, by putting each value on a newline. Mostly used for file paths. 71 | /// 72 | /// The name of the variable to set. 73 | /// The list of values. 74 | public void SetPaths(string variableName, IEnumerable values) => SetStrings(variableName, values, Environment.NewLine); 75 | 76 | /// 77 | /// If this variable dictionary was read from a file, reloads all variables from the file. 78 | /// 79 | public void Reload() 80 | { 81 | if (!string.IsNullOrWhiteSpace(storageFilePath)) 82 | { 83 | VariablesFileFormatter.Populate(variables, storageFilePath); 84 | binding = null; 85 | } 86 | } 87 | 88 | public void Save(string path) 89 | { 90 | storageFilePath = Path.GetFullPath(path); 91 | Save(); 92 | } 93 | 94 | public void Save() 95 | { 96 | if (!string.IsNullOrWhiteSpace(storageFilePath)) 97 | { 98 | VariablesFileFormatter.Persist(variables, storageFilePath); 99 | } 100 | } 101 | 102 | public string SaveAsString() 103 | { 104 | using (var writer = new StringWriter()) 105 | { 106 | VariablesFileFormatter.Persist(variables, writer); 107 | return writer.ToString(); 108 | } 109 | } 110 | 111 | /// 112 | /// Performs the raw (not evaluated) value of a variable. 113 | /// 114 | /// Name of the variable. 115 | /// The value of the variable, or null if one is not defined. 116 | public string? GetRaw(string variableName) 117 | { 118 | if (variables.TryGetValue(variableName, out var variable) && variable != null) 119 | return variable; 120 | 121 | return null; 122 | } 123 | 124 | /// 125 | /// Gets the value of a variable, or returns a default value if the variable is not defined. If the variable contains an expression, it will be evaluated first. 126 | /// 127 | /// The name of the variable. 128 | /// The default value to return. 129 | /// The value of the variable, or the default value if the variable is not defined. 130 | [return: NotNullIfNotNull("defaultValue")] 131 | public string? Get(string variableName, string? defaultValue = null) => Get(variableName, out _, defaultValue); 132 | 133 | /// 134 | /// Gets the value of a variable, or returns a default value if the variable is not defined. If the variable contains an expression, it will be evaluated first. 135 | /// 136 | /// The name of the variable. 137 | /// Any parsing errors silently found. 138 | /// The default value to return. 139 | /// The value of the variable, or the default value if the variable is not defined. 140 | [return: NotNullIfNotNull("defaultValue")] 141 | public string? Get(string variableName, out string? error, string? defaultValue = null) 142 | { 143 | error = null; 144 | if (!variables.TryGetValue(variableName, out var variable) || variable == null) 145 | return defaultValue; 146 | 147 | return Evaluate(variable, out error); 148 | } 149 | 150 | /// 151 | /// Evaluates a given expression as if it were the value of a variable. 152 | /// 153 | /// The value or expression to evaluate. 154 | /// Any parsing errors silently found. 155 | /// Stop parsing if an error is found. 156 | /// The result of the expression. 157 | public string? Evaluate(string? expressionOrVariableOrText, out string? error, bool haltOnError = true) 158 | { 159 | error = null; 160 | if (expressionOrVariableOrText == null) return null; 161 | 162 | if (CanEvaluationBeSkippedForExpression(expressionOrVariableOrText)) 163 | return expressionOrVariableOrText; 164 | 165 | if (!TemplateParser.TryParseTemplate(expressionOrVariableOrText, out var template, out error, haltOnError)) 166 | return expressionOrVariableOrText; 167 | 168 | using (var writer = new StringWriter()) 169 | { 170 | TemplateEvaluator.Evaluate(template, 171 | Binding, 172 | writer, 173 | extensions, 174 | out var missingTokens, 175 | out var nullTokens); 176 | if (missingTokens.Any()) 177 | { 178 | var tokenList = string.Join(", ", missingTokens.Select(token => "'" + token + "'")); 179 | error = string.Format("The following tokens were unable to be evaluated: {0}", tokenList); 180 | } 181 | 182 | if (nullTokens.Any()) 183 | { 184 | return null; 185 | } 186 | 187 | return writer.ToString(); 188 | } 189 | } 190 | 191 | /// 192 | /// Evaluates a given expression for truthiness. 193 | /// 194 | /// The value or expression to evaluate. 195 | /// Whether the expression evaluates with no errors and the result is truthy (Not empty, 0 or false). 196 | public bool EvaluateTruthy(string? expressionOrVariableOrText) 197 | { 198 | var result = Evaluate(expressionOrVariableOrText, out var error); 199 | return string.IsNullOrWhiteSpace(error) && result != null && TemplateEvaluator.IsTruthy(result); 200 | } 201 | 202 | /// 203 | /// Evaluates a given expression as if it were the value of a variable. 204 | /// 205 | /// The value or expression to evaluate. 206 | /// The result of the expression. 207 | [return: NotNullIfNotNull("expressionOrVariableOrText")] 208 | public string? Evaluate(string? expressionOrVariableOrText) => Evaluate(expressionOrVariableOrText, out _); 209 | 210 | /// 211 | /// Gets a list of strings, assuming each path is separated by commas or some other separator character. If the variable contains an expression, it will be evaluated first. 212 | /// 213 | /// The name of the variable to find. 214 | /// The separators to split the list by. Defaults to a comma if no other separators are passed. 215 | /// The list of strings, or an empty list if the value is null or empty. 216 | public List GetStrings(string variableName, params char[] separators) 217 | { 218 | separators = separators ?? new char[0]; 219 | if (separators.Length == 0) separators = new[] { ',' }; 220 | 221 | var value = Get(variableName); 222 | if (string.IsNullOrWhiteSpace(value)) 223 | return new List(); 224 | 225 | var values = value.Split(separators) 226 | .Select(v => v.Trim()) 227 | .Where(v => v != ""); 228 | 229 | return values.ToList(); 230 | } 231 | 232 | /// 233 | /// Gets a list of paths, assuming each path is separated by newlines. If the variable contains an expression, it will be evaluated first. 234 | /// 235 | /// The name of the variable to find. 236 | /// The list of strings, or an empty list if the value is null or empty. 237 | public List GetPaths(string variableName) => GetStrings(variableName, '\r', '\n'); 238 | 239 | /// 240 | /// Gets a given variable by name. If the variable contains an expression, it will be evaluated. Converts the variable to a boolean using bool.TryParse(). Returns a given 241 | /// default value if the variable is not defined, is empty, or isn't a valid boolean value. 242 | /// 243 | /// The name of the variable to find. 244 | /// The default value to return if the variable is not defined. 245 | /// The boolean value of the variable, or the default value. 246 | public bool GetFlag(string variableName, bool defaultValueIfUnset = false) 247 | { 248 | bool value; 249 | var text = Get(variableName); 250 | if (string.IsNullOrWhiteSpace(text) || !bool.TryParse(text, out value)) 251 | { 252 | value = defaultValueIfUnset; 253 | } 254 | 255 | return value; 256 | } 257 | 258 | /// 259 | /// Gets a given variable by name. If the variable contains an expression, it will be evaluated. Converts the variable to an integer using int.TryParse(). Returns null 260 | /// if the variable is not defined. 261 | /// 262 | /// The name of the variable to find. 263 | /// The integer value of the variable, or null if not defined. 264 | public int? GetInt32(string variableName) 265 | { 266 | int value; 267 | var text = Get(variableName); 268 | if (string.IsNullOrWhiteSpace(text) || !int.TryParse(text, out value)) 269 | { 270 | return null; 271 | } 272 | 273 | return value; 274 | } 275 | 276 | /// 277 | /// Gets a given variable by name. If the variable contains an expression, it will be evaluated. Throws an if the variable is not defined. 278 | /// 279 | /// The name of the variable to find. 280 | /// The value 281 | public string Require(string name) 282 | { 283 | if (name == null) throw new ArgumentNullException("name"); 284 | var value = Get(name); 285 | if (string.IsNullOrEmpty(value)) 286 | throw new ArgumentOutOfRangeException("name", "The variable '" + name + "' is required but no value is set."); 287 | return value; 288 | } 289 | 290 | /// 291 | /// Gets the names of all variables in this dictionary. 292 | /// 293 | /// A list of variable names. 294 | public List GetNames() => variables.Keys.ToList(); 295 | 296 | /// 297 | /// Returns any index values for a collection. 298 | /// For example, given keys: Package[A].Name, Package[B].Name 299 | /// GetIndexes("Package") would return {A, B} 300 | /// 301 | /// 302 | /// A list of index values for the specified collection name. 303 | public List GetIndexes(string variableCollectionName) 304 | { 305 | if (string.IsNullOrWhiteSpace(variableCollectionName)) 306 | throw new ArgumentOutOfRangeException(nameof(variableCollectionName), 307 | $"{nameof(variableCollectionName)} must not be null or empty"); 308 | 309 | if (!TemplateParser.TryParseIdentifierPath(variableCollectionName, out var symbolExpression)) 310 | throw new Exception($"Could not evaluate indexes for path {variableCollectionName}"); 311 | 312 | var context = new EvaluationContext(Binding, TextWriter.Null); 313 | var bindings = context.ResolveAll(symbolExpression, out _, out _); 314 | // ReSharper disable once RedundantEnumerableCastCall 315 | return bindings.Select(b => b.Item).Where(x => x != null).Cast().ToList(); 316 | } 317 | 318 | /// 319 | /// Determines whether an expression/variable value/text needs to be evaluated before being used. 320 | /// If true is returned from this method, the raw value of 321 | /// can be used without running it through a VariableDictionary. Even if false is returned, the value 322 | /// may not contain subsitution tokens, and may be unchanged after evaluation. 323 | /// 324 | /// The variable to evaluate 325 | /// False if the variable contains something that looks like a substitution tokens, otherwise true 326 | public static bool CanEvaluationBeSkippedForExpression(string expressionOrVariableOrText) => expressionOrVariableOrText == null || !expressionOrVariableOrText.Contains("#{"); 327 | 328 | public IEnumerator> GetEnumerator() => variables.GetEnumerator(); 329 | 330 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 331 | 332 | public void Add(string key, string? value) => Set(key, value); 333 | 334 | /// 335 | /// Adds a custom extension function 336 | /// 337 | /// 338 | /// 339 | public void AddExtension(string name, Func func) 340 | { 341 | extensions[name.ToLowerInvariant()] = func; 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /source/Octostache/VariablesFileFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | 7 | namespace Octostache 8 | { 9 | static class VariablesFileFormatter 10 | { 11 | static readonly JsonSerializer Serializer = new JsonSerializer { Formatting = Formatting.Indented }; 12 | static readonly Encoding FileEncoding = Encoding.UTF8; 13 | 14 | public static void Populate(Dictionary variables, string variablesFilePath) 15 | { 16 | var fullPath = Path.GetFullPath(variablesFilePath); 17 | if (!File.Exists(fullPath)) 18 | return; 19 | 20 | using (var sourceStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read)) 21 | { 22 | using (var reader = new JsonTextReader(new StreamReader(sourceStream, FileEncoding))) 23 | { 24 | Serializer.Populate(reader, variables); 25 | } 26 | } 27 | } 28 | 29 | public static void Persist(Dictionary variables, TextWriter output) 30 | { 31 | Serializer.Serialize(new JsonTextWriter(output), variables); 32 | output.Flush(); 33 | } 34 | 35 | public static void Persist(Dictionary variables, string variablesFilePath) 36 | { 37 | var fullPath = Path.GetFullPath(variablesFilePath); 38 | var parentDirectory = Path.GetDirectoryName(fullPath); 39 | if (parentDirectory != null && !Directory.Exists(parentDirectory)) 40 | { 41 | Directory.CreateDirectory(parentDirectory); 42 | } 43 | 44 | using (var targetStream = new FileStream(variablesFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) 45 | { 46 | using (var writer = new StreamWriter(targetStream, FileEncoding)) 47 | { 48 | Persist(variables, writer); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------