summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormichael <michael@3ad0048d-3df7-0310-abae-a5850022a9f2>2021-04-20 11:39:30 +0000
committermichael <michael@3ad0048d-3df7-0310-abae-a5850022a9f2>2021-04-20 11:39:30 +0000
commit73ebe6abbe99cd7518f8dd5b9ec1106357ae8f88 (patch)
tree00d10dae5d3c9c9bd0930e72512643855bb69b97
parent4813da5171f4d11b23f06093bb346af7514f4322 (diff)
downloadfpc-73ebe6abbe99cd7518f8dd5b9ec1106357ae8f88.tar.gz
* Mustache templates implementation
git-svn-id: https://svn.freepascal.org/svn/fpc/trunk@49240 3ad0048d-3df7-0310-abae-a5850022a9f2
-rw-r--r--packages/fcl-mustache/examples/README.txt27
-rw-r--r--packages/fcl-mustache/examples/demo1.lpi57
-rw-r--r--packages/fcl-mustache/examples/demo1.lpr42
-rw-r--r--packages/fcl-mustache/examples/demo2.lpi57
-rw-r--r--packages/fcl-mustache/examples/demo2.lpr48
-rw-r--r--packages/fcl-mustache/examples/family.csv7
-rw-r--r--packages/fcl-mustache/examples/family.json10
-rw-r--r--packages/fcl-mustache/examples/family.tmpl22
-rw-r--r--packages/fcl-mustache/examples/mustache.lpi57
-rw-r--r--packages/fcl-mustache/examples/mustache.lpr207
-rw-r--r--packages/fcl-mustache/src/fpdbmustache.pp268
-rw-r--r--packages/fcl-mustache/src/fpexmustache.pp399
-rw-r--r--packages/fcl-mustache/src/fpmustache.pp1340
-rw-r--r--packages/fcl-mustache/tests/spec/comments.json1
-rw-r--r--packages/fcl-mustache/tests/spec/delimiters.json1
-rw-r--r--packages/fcl-mustache/tests/spec/interpolation.json1
-rw-r--r--packages/fcl-mustache/tests/spec/inverted.json1
-rw-r--r--packages/fcl-mustache/tests/spec/partials.json1
-rw-r--r--packages/fcl-mustache/tests/spec/sections.json1
-rw-r--r--packages/fcl-mustache/tests/tcbasemustache.pas290
-rw-r--r--packages/fcl-mustache/tests/tcdbmustache.pas149
-rw-r--r--packages/fcl-mustache/tests/tcexmustache.pas199
-rw-r--r--packages/fcl-mustache/tests/tcmustache.pas728
-rw-r--r--packages/fcl-mustache/tests/tcspecs.pas188
-rw-r--r--packages/fcl-mustache/tests/testmustache.lpi88
-rw-r--r--packages/fcl-mustache/tests/testmustache.lpr29
-rw-r--r--packages/fpmake_add.inc1
-rw-r--r--packages/fpmake_proc.inc6
28 files changed, 4225 insertions, 0 deletions
diff --git a/packages/fcl-mustache/examples/README.txt b/packages/fcl-mustache/examples/README.txt
new file mode 100644
index 0000000000..b46598b216
--- /dev/null
+++ b/packages/fcl-mustache/examples/README.txt
@@ -0,0 +1,27 @@
+demo1 sample program:
+
+Demonstrates the most basic use of the mustache parser
+
+demo2 sample program:
+
+Demonstrates the use of the mustache parser with a CSV dataset
+
+
+mustache example program:
+
+Can be used to load a template and data, and process the result.
+Output to standard output or file.
+The template and JSON value can be loaded from file (using @filename),
+or their value can be specified directly on the command-line.
+
+Example usage:
+
+Load template from family.tmpl file, data from family.json file:
+
+./mustache -d title="my family" -t @family.tmpl -j @family.json
+
+Load template from family.tmpl file, data from family.csv file:
+
+./mustache -d title="my family" -t @family.tmpl -c family.csv
+
+Use of expressions can be enabled with the -e switch.
diff --git a/packages/fcl-mustache/examples/demo1.lpi b/packages/fcl-mustache/examples/demo1.lpi
new file mode 100644
index 0000000000..6e03c0c1fd
--- /dev/null
+++ b/packages/fcl-mustache/examples/demo1.lpi
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+ <ProjectOptions>
+ <Version Value="12"/>
+ <General>
+ <Flags>
+ <MainUnitHasCreateFormStatements Value="False"/>
+ <MainUnitHasTitleStatement Value="False"/>
+ <MainUnitHasScaledStatement Value="False"/>
+ </Flags>
+ <SessionStorage Value="InProjectDir"/>
+ <Title Value="demo1"/>
+ <UseAppBundle Value="False"/>
+ <ResourceType Value="res"/>
+ </General>
+ <BuildModes>
+ <Item Name="Default" Default="True"/>
+ </BuildModes>
+ <PublishOptions>
+ <Version Value="2"/>
+ <UseFileFilters Value="True"/>
+ </PublishOptions>
+ <RunParams>
+ <FormatVersion Value="2"/>
+ </RunParams>
+ <Units>
+ <Unit>
+ <Filename Value="demo1.lpr"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ </Units>
+ </ProjectOptions>
+ <CompilerOptions>
+ <Version Value="11"/>
+ <Target>
+ <Filename Value="demo1"/>
+ </Target>
+ <SearchPaths>
+ <IncludeFiles Value="$(ProjOutDir)"/>
+ <OtherUnitFiles Value="../src"/>
+ <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+ </SearchPaths>
+ </CompilerOptions>
+ <Debugging>
+ <Exceptions>
+ <Item>
+ <Name Value="EAbort"/>
+ </Item>
+ <Item>
+ <Name Value="ECodetoolError"/>
+ </Item>
+ <Item>
+ <Name Value="EFOpenError"/>
+ </Item>
+ </Exceptions>
+ </Debugging>
+</CONFIG>
diff --git a/packages/fcl-mustache/examples/demo1.lpr b/packages/fcl-mustache/examples/demo1.lpr
new file mode 100644
index 0000000000..652ec7057c
--- /dev/null
+++ b/packages/fcl-mustache/examples/demo1.lpr
@@ -0,0 +1,42 @@
+{ Demo for mustache engine with JSON context
+
+ Copyright (C) 2021 michael Van Canneyt michael@freepascal.org
+
+ This source is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+ This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ A copy of the GNU General Public License is available on the World Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can
+ also obtain it by writing to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.
+}
+program demo1;
+
+// jsonparser includes the json parser.
+
+uses jsonparser, fpmustache;
+
+Const
+ JSON = '{ "products" : [ {"name" : "BMW" }, {"name" : "Mercedes"}, { "name" : "Audi" }] }';
+
+ // Mock markdown table
+ Template =
+ '| name |'+sLineBreak+
+ '|------|'+sLineBreak+
+ '{{#products}}| {{name}} |'+sLineBreak+
+ '{{/products}}';
+
+Var
+ M : TMustache;
+
+begin
+ M:=TMustache.Create(Nil);
+ try
+ // Json support enabled by default
+ Writeln(M.Render(Template,JSON));
+ finally
+ M.Free;
+ end;
+end.
+
diff --git a/packages/fcl-mustache/examples/demo2.lpi b/packages/fcl-mustache/examples/demo2.lpi
new file mode 100644
index 0000000000..dd4e245773
--- /dev/null
+++ b/packages/fcl-mustache/examples/demo2.lpi
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+ <ProjectOptions>
+ <Version Value="12"/>
+ <General>
+ <Flags>
+ <MainUnitHasCreateFormStatements Value="False"/>
+ <MainUnitHasTitleStatement Value="False"/>
+ <MainUnitHasScaledStatement Value="False"/>
+ </Flags>
+ <SessionStorage Value="InProjectDir"/>
+ <Title Value="demo2"/>
+ <UseAppBundle Value="False"/>
+ <ResourceType Value="res"/>
+ </General>
+ <BuildModes>
+ <Item Name="Default" Default="True"/>
+ </BuildModes>
+ <PublishOptions>
+ <Version Value="2"/>
+ <UseFileFilters Value="True"/>
+ </PublishOptions>
+ <RunParams>
+ <FormatVersion Value="2"/>
+ </RunParams>
+ <Units>
+ <Unit>
+ <Filename Value="demo2.lpr"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ </Units>
+ </ProjectOptions>
+ <CompilerOptions>
+ <Version Value="11"/>
+ <Target>
+ <Filename Value="demo2"/>
+ </Target>
+ <SearchPaths>
+ <IncludeFiles Value="$(ProjOutDir)"/>
+ <OtherUnitFiles Value="../src"/>
+ <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+ </SearchPaths>
+ </CompilerOptions>
+ <Debugging>
+ <Exceptions>
+ <Item>
+ <Name Value="EAbort"/>
+ </Item>
+ <Item>
+ <Name Value="ECodetoolError"/>
+ </Item>
+ <Item>
+ <Name Value="EFOpenError"/>
+ </Item>
+ </Exceptions>
+ </Debugging>
+</CONFIG>
diff --git a/packages/fcl-mustache/examples/demo2.lpr b/packages/fcl-mustache/examples/demo2.lpr
new file mode 100644
index 0000000000..4b5b20f885
--- /dev/null
+++ b/packages/fcl-mustache/examples/demo2.lpr
@@ -0,0 +1,48 @@
+{ Demo for mustache engine with database context
+
+ Copyright (C) 2021 michael Van Canneyt michael@freepascal.org
+
+ This source is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+ This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ A copy of the GNU General Public License is available on the World Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can
+ also obtain it by writing to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.
+}
+
+program demo2;
+
+uses csvdataset, fpmustache, fpdbmustache;
+
+Const
+ // Mock markdown table
+ Template =
+ '| name | age | '+sLineBreak+
+ '|------|------|'+sLineBreak+
+ '{{#family}}| {{name}} | {{age}} |'+sLineBreak+
+ '{{/family}}';
+
+Var
+ M : TMustache;
+ C : TMustacheDBContext;
+ D : TCSVDataset;
+
+begin
+ M:=TMustache.Create(Nil);
+ try
+ D:=TCSVDataset.Create(Nil);
+ D.CSVOptions.FirstLineAsFieldNames:=True;
+ D.LoadFromFile('family.csv');
+ C:=TMustacheDBContext.Create(Nil);
+ C.AddDataset(D,'family');
+ M.Template:=Template;
+ Writeln(M.Render(C));
+ finally
+ M.Free;
+ D.Free;
+ C.Free;
+ end;
+end.
+
diff --git a/packages/fcl-mustache/examples/family.csv b/packages/fcl-mustache/examples/family.csv
new file mode 100644
index 0000000000..628dc27a08
--- /dev/null
+++ b/packages/fcl-mustache/examples/family.csv
@@ -0,0 +1,7 @@
+name,age
+Father,30
+Mother,29
+Grandfather,62
+GrandMother,61
+Child 1,2
+Child 2,4
diff --git a/packages/fcl-mustache/examples/family.json b/packages/fcl-mustache/examples/family.json
new file mode 100644
index 0000000000..39475e98d1
--- /dev/null
+++ b/packages/fcl-mustache/examples/family.json
@@ -0,0 +1,10 @@
+{
+ "data" : [
+ { "name" : "Father", "age": 30 },
+ { "name" : "Mother", "age": 29 },
+ { "name" : "Grandfather", "age": 62 },
+ { "name" : "GrandMother", "age": 61 },
+ { "name" : "Child 1", "age": 2 },
+ { "name" : "Child 2", "age": 4 }
+ ]
+} \ No newline at end of file
diff --git a/packages/fcl-mustache/examples/family.tmpl b/packages/fcl-mustache/examples/family.tmpl
new file mode 100644
index 0000000000..b6174cfcfe
--- /dev/null
+++ b/packages/fcl-mustache/examples/family.tmpl
@@ -0,0 +1,22 @@
+<html>
+ <head>
+ <title>{{title}}</title>
+ </head>
+ <body>
+ <h1>Family members</h1>
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th><th>Age</th>
+ </tr>
+ </thead>
+ <tbody>
+{{#data}}
+ <tr>
+ <td>{{name}}</td><td>{{age}}</td>
+ </tr>
+{{/data}}
+ </tbody>
+ </table>
+ </body>
+</html>
diff --git a/packages/fcl-mustache/examples/mustache.lpi b/packages/fcl-mustache/examples/mustache.lpi
new file mode 100644
index 0000000000..24c058f4a4
--- /dev/null
+++ b/packages/fcl-mustache/examples/mustache.lpi
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+ <ProjectOptions>
+ <Version Value="12"/>
+ <General>
+ <Flags>
+ <MainUnitHasCreateFormStatements Value="False"/>
+ <MainUnitHasTitleStatement Value="False"/>
+ <MainUnitHasScaledStatement Value="False"/>
+ </Flags>
+ <SessionStorage Value="InProjectDir"/>
+ <Title Value="Mustache Templater"/>
+ <UseAppBundle Value="False"/>
+ <ResourceType Value="res"/>
+ </General>
+ <BuildModes>
+ <Item Name="Default" Default="True"/>
+ </BuildModes>
+ <PublishOptions>
+ <Version Value="2"/>
+ <UseFileFilters Value="True"/>
+ </PublishOptions>
+ <RunParams>
+ <FormatVersion Value="2"/>
+ </RunParams>
+ <Units>
+ <Unit>
+ <Filename Value="mustache.lpr"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ </Units>
+ </ProjectOptions>
+ <CompilerOptions>
+ <Version Value="11"/>
+ <Target>
+ <Filename Value="mustache"/>
+ </Target>
+ <SearchPaths>
+ <IncludeFiles Value="$(ProjOutDir)"/>
+ <OtherUnitFiles Value="../src"/>
+ <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+ </SearchPaths>
+ </CompilerOptions>
+ <Debugging>
+ <Exceptions>
+ <Item>
+ <Name Value="EAbort"/>
+ </Item>
+ <Item>
+ <Name Value="ECodetoolError"/>
+ </Item>
+ <Item>
+ <Name Value="EFOpenError"/>
+ </Item>
+ </Exceptions>
+ </Debugging>
+</CONFIG>
diff --git a/packages/fcl-mustache/examples/mustache.lpr b/packages/fcl-mustache/examples/mustache.lpr
new file mode 100644
index 0000000000..7a48df1dad
--- /dev/null
+++ b/packages/fcl-mustache/examples/mustache.lpr
@@ -0,0 +1,207 @@
+program mustache;
+
+{$mode objfpc}{$H+}
+
+uses
+ {$IFDEF UNIX}
+ cthreads,
+ {$ENDIF}
+ Classes, SysUtils, CustApp, strutils, fpjson, jsonparser, csvdataset, fpMustache, fpexmustache, fpdbmustache, iostream;
+
+type
+
+ { TMustacheApplication }
+
+ TMustacheApplication = class(TCustomApplication)
+ private
+ FTemplate : TMustacheString;
+ FJSON : TJSONStringType;
+ FCSV: TCSVDataset;
+ FPartials,
+ FDefines : TStrings;
+ FAllowExpressions : Boolean;
+ Foutput,
+ FSection,
+ FRootPath : String;
+ procedure DoGetDefine(const aName: TMustacheString; var aHandled: Boolean;
+ var aValue: TMustacheString);
+ procedure ProcessOptions;
+ Procedure Createoutput;
+ procedure Usage(ErrorMsg: String);
+ protected
+ procedure DoRun; override;
+
+ public
+ constructor Create(TheOwner: TComponent); override;
+ destructor Destroy; override;
+ end;
+
+{ TMustacheApplication }
+
+procedure TMustacheApplication.Usage(ErrorMsg : String);
+
+begin
+ If ErrorMsg<>'' then
+ Writeln('Error : ',ErrorMsg);
+ Writeln('Usage : mustache [options]');
+ Writeln('Where options is one or more of:');
+ writeln('-c --csv=FILE Use a CSV file as data source. First line must contain column names.');
+ writeln('-d --define=name=value Define fixed value.');
+ writeln('-e --expressions Allow expressions.');
+ writeln('-h --help This message.');
+ writeln('-j --json=JSON Use JSON as data source. @FILENAME will read JSON from file (UTF8).');
+ writeln('-o --output=FILE output file to write output to. If empty, stdout is assumed.');
+ writeln('-p --partial=name=PARTIAL Register partial. @FILENAME reads partial from file.');
+ writeln('-r --root=PATH Register variables at root path PATH for expression engine');
+ writeln('-s --section=SECTIOn Section name for CSV data');
+ writeln('-t --template=TEMPLATE Use TEMPLATE as data source. @FILENAME will read template from file (UTF8). Required.');
+ Halt(Ord(ErrorMsg<>''));
+end;
+
+procedure TMustacheApplication.ProcessOptions;
+
+ Function StringOrFile(S : String) : UTF8String;
+
+ begin
+ if Copy(S,1,1)<>'@' then
+ Result:=S
+ else
+ With TFileStream.Create(Copy(S,2,Length(S)-1),fmOpenRead or fmShareDenyNone) do
+ try
+ SetLength(Result,Size);
+ ReadBuffer(Result[1],Size);
+ finally
+ Free;
+ end;
+ end;
+
+Var
+ S : String;
+
+begin
+ if Not HasOption('t','template') then
+ Raise Exception.Create('Need a template');
+ if HasOption('c','csv') and HasOption('j','json') then
+ Raise Exception.Create('Cannot specify both JSON or CSV');
+ FTemplate:=StringOrFile(GetOptionValue('t','template'));
+ if HasOption('j','json') then
+ FJSON:=StringOrFile(GetOptionValue('j','json'))
+ else if HasOption('c','csv') then
+ begin
+ FCSV:=TCSVDataset.Create(Self);
+ FCSV.FileName:=GetOptionValue('c','csv');
+ FCSV.CSVOptions.FirstLineAsFieldNames:=True;
+ FCSV.Open;
+ end;
+ for S in GetOptionValues('d','define') do
+ FDefines.Add(S);
+ for S in GetOptionValues('p','partial') do
+ FPartials.Add(ExtractWord(1,S,['='])+'='+StringOrFile(ExtractWord(2,S,['='])));
+ FAllowExpressions:=HasOption('e','expressions');
+ FRootPath:=GetOptionValue('r','root');
+ FSection:=GetOptionValue('s','section');
+ if FSection='' then
+ FSection:='data';
+ Foutput:=GetOptionValue('o','output');
+end;
+
+procedure TMustacheApplication.DoGetDefine(const aName: TMustacheString;
+ var aHandled: Boolean; var aValue: TMustacheString);
+
+Var
+ Idx : Integer;
+
+begin
+ Writeln('Getting define ',aName);
+ Idx:=FDefines.IndexOfName(aName);
+ aHandled:=Idx<>-1;
+ if aHandled then
+ aValue:=FDefines.ValueFromIndex[Idx]
+ else
+ aValue:='';
+end;
+
+procedure TMustacheApplication.DoRun;
+var
+ ErrorMsg: String;
+begin
+ Terminate;
+ // quick check parameters
+ ErrorMsg:=CheckOptions('het:j:c:d:o:r:', ['help','template','json','csv','define','output','expressions','root']);
+ if (ErrorMsg<>'') or HasOption('h','help') then
+ Usage(ErrorMsg);
+ ProcessOptions;
+ CreateOutput;
+end;
+
+procedure TMustacheApplication.CreateOutput;
+
+Var
+ M : TMustache;
+ C : TMustacheContext;
+ O : TStream;
+ S : TMustacheString;
+
+begin
+ O:=Nil;
+ M:=Nil;
+ C:=Nil;
+ try
+ if FAllowExpressions then
+ M:=TMustache.Create(Self)
+ else
+ begin
+ M:=TMustacheExpr.Create(Self);
+ if (FRootPath<>'') and (FJSON<>'') then
+ TMustacheExpr(M).RegisterVariables(FJSON,FRootPath,True);
+ end;
+ M.Partials:=FPartials;
+ if Assigned(FCSV) then
+ begin
+ C:=TMustacheDBContext.Create(@DoGetDefine);
+ TMustacheDBContext(C).AddDataset(FCSV,FSection);
+ end
+ else if (FJSON<>'') then
+ C:=TMustacheJSONContext.Create(GetJSON(FJSON),@DoGetDefine)
+ else
+ C:=TMustacheContext.Create(@DoGetDefine);
+ if Foutput<>'' then
+ O:=TFileStream.Create(Foutput,fmCreate)
+ else
+ O:=TIOStream.Create(iosOutput);
+ M.Template:=FTemplate;
+ S:=M.Render(C);
+ O.WriteBuffer(S[1],Length(S));
+ finally
+ O.Free;
+ C.Free;
+ M.Free;
+ end;
+end;
+
+constructor TMustacheApplication.Create(TheOwner: TComponent);
+begin
+ inherited Create(TheOwner);
+ FPartials:=TStringList.Create;
+ FDefines:=TStringList.Create;
+ StopOnException:=True;
+end;
+
+destructor TMustacheApplication.Destroy;
+begin
+ FreeAndNil(FPartials);
+ FreeAndNil(FDefines);
+ FreeAndNil(FCSV);
+ inherited Destroy;
+end;
+
+
+var
+ Application: TMustacheApplication;
+begin
+ Application:=TMustacheApplication.Create(nil);
+ Application.Title:='Mustache Templater';
+ Application.Run;
+ Application.Free;
+end.
+
diff --git a/packages/fcl-mustache/src/fpdbmustache.pp b/packages/fcl-mustache/src/fpdbmustache.pp
new file mode 100644
index 0000000000..0f7d6d72d9
--- /dev/null
+++ b/packages/fcl-mustache/src/fpdbmustache.pp
@@ -0,0 +1,268 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ This file contains a Mustache DB context, getting data from a dataset
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpdbmustache;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, db, fpMustache;
+
+Type
+
+ { TDatasetCollectionItem }
+
+ TDatasetCollectionItem = Class(TCollectionItem)
+ private
+ FDataset: TDataSet;
+ FSection: String;
+ Public
+ Property Dataset : TDataSet Read FDataset Write FDataset;
+ Property SectionName : String Read FSection Write FSection;
+ end;
+
+ TDatasetCollection = Class(TCollection)
+ private
+ function GetDS(aIndex : Integer): TDatasetCollectionItem;
+ Public
+ Function IndexOfDataset(aDataset : TDataset) : Integer;
+ Function IndexOfSection(aSection : String) : Integer;
+ Property Datasets[aIndex : Integer] : TDatasetCollectionItem Read GetDS; default;
+ end;
+
+ { TMustacheDBContext }
+
+ TMustacheDBContext = Class(TMustacheContext)
+ Private
+ Type
+ TPair = Record
+ atStart : Boolean;
+ Value : TDataset;
+ end;
+ Private
+ FStack : Array of TPair;
+ FCount : Integer;
+ FStaticValues: TStrings;
+ FDatasets : TDatasetCollection;
+ Function FindField(Const aName : TMustacheString) : TField;
+ function GetDataset(aIndex : Integer): TDatasetCollectionItem;
+ function GetDatasetCount: INteger;
+ procedure SetStaticValues(AValue: TStrings);
+ Public
+ Constructor Create(aCallback : TGetTextValueEvent); override;
+ Destructor destroy; override;
+ Procedure Clear;
+ Function MoveNextSectionItem(Const aName : TMustacheString) : Boolean; override;
+ Function PushSection(Const aName : TMustacheString) : TMustacheSectionType; override;
+ Procedure PopSection(Const aName : TMustacheString); override;
+ Function GetTextValue(Const aName : TMustacheString) : TMustacheString; override;
+ Procedure AddDataset(aDataset : TDataset; aSectionName : String = '');
+ Procedure RemoveDataset(aDataset : TDataset);
+ Property StaticValues : TStrings Read FStaticValues Write SetStaticValues;
+ Property Datasets[aIndex : Integer] : TDatasetCollectionItem Read GetDataset;
+ Property DatasetCount : INteger Read GetDatasetCount;
+ end;
+
+implementation
+
+uses StrUtils;
+
+Resourcestring
+ SErrPopSectionNoPush = 'PopSection %s without push';
+ SErrDatasetNameEmpty = 'Dataset name and section cannot both be empty';
+ SErrDatasetEmpty = 'Dataset is Nil';
+ SErrDuplicateDataSetName = 'Duplicate dataset name: %s';
+
+{ TMustacheDBContext }
+
+function TMustacheDBContext.FindField(const aName: TMustacheString): TField;
+
+Var
+ aCount : Integer;
+
+begin
+ Result:=Nil;
+ aCount:=FCount-1;
+ While (Result=Nil) and (aCount>=0) do
+ begin
+ Result:=FStack[aCount].Value.FieldByName(aName);
+ Dec(aCount);
+ end;
+end;
+
+function TMustacheDBContext.GetDataset(aIndex : Integer
+ ): TDatasetCollectionItem;
+begin
+ Result:=FDatasets[aIndex];
+end;
+
+function TMustacheDBContext.GetDatasetCount: INteger;
+begin
+ Result:=FDatasets.Count;
+end;
+
+procedure TMustacheDBContext.SetStaticValues(AValue: TStrings);
+begin
+ if FStaticValues=AValue then Exit;
+ FStaticValues.Assign(AValue);
+end;
+
+constructor TMustacheDBContext.Create(aCallback: TGetTextValueEvent);
+begin
+ inherited Create(aCallback);
+ FDatasets:=TDatasetCollection.Create(TDatasetCollectionItem);
+ FStaticValues:=TStringList.Create;
+ SetLength(FStack,JSONListGrowCount);
+ FCount:=0;
+end;
+
+destructor TMustacheDBContext.destroy;
+begin
+ FreeAndNil(FStaticValues);
+ FreeAndNil(FDatasets);
+ inherited destroy;
+end;
+
+procedure TMustacheDBContext.Clear;
+begin
+ FStaticValues.Clear;
+ FDatasets.Clear;
+end;
+
+function TMustacheDBContext.MoveNextSectionItem(const aName: TMustacheString
+ ): Boolean;
+begin
+ if FStack[FCount-1].atStart then
+ FStack[FCount-1].atStart:=False
+ else
+ FStack[FCount-1].Value.Next;
+ Result:=Not FStack[FCount-1].Value.EOF;
+end;
+
+function TMustacheDBContext.PushSection(const aName: TMustacheString
+ ): TMustacheSectionType;
+
+Var
+ aDS : TDataset;
+ Idx : Integer;
+begin
+ Result:=mstNone;
+ Idx:=FDatasets.IndexOfSection(aName);
+ if Idx=-1 then
+ Exit;
+ aDS:=FDatasets[Idx].Dataset;
+ if aDS.IsEmpty then
+ exit;
+ if FCount=Length(FStack) then
+ SetLength(FStack,FCount+JSONListGrowCount);
+ FStack[FCount].Value:=aDS;
+ FStack[FCount].atStart:=True;
+ Inc(FCount,1);
+ Result:=mstList;
+end;
+
+procedure TMustacheDBContext.PopSection(const aName: TMustacheString);
+begin
+ if FCount<1 then
+ Raise EMustache.CreateFmt(SErrPopSectionNoPush,[aName]);
+ Dec(FCount,1);
+end;
+
+function TMustacheDBContext.GetTextValue(const aName: TMustacheString
+ ): TMustacheString;
+
+Var
+ F : TField;
+ idx : Integer;
+
+begin
+ F:=Nil;
+ if Pos('.',aName)=0 then
+ F:=FindField(aName)
+ else if WordCount(aName,['.'])=2 then
+ begin
+ Idx:=FDatasets.IndexOfSection(ExtractWord(1,aName,['.']));
+ if (Idx<>-1) then
+ F:=FDatasets[Idx].Dataset.FindField(ExtractWord(2,aName,['.']));
+ end;
+ If Assigned(F) then
+ Result:=F.AsString
+ else
+ begin
+ Idx:=FStaticValues.IndexOfName(aName);
+ if Idx<>-1 then
+ Result:=FStaticValues.ValueFromIndex[Idx]
+ else
+ Result:=Inherited GetTextValue(aName);
+ end;
+end;
+
+procedure TMustacheDBContext.AddDataset(aDataset: TDataset; aSectionName: String);
+
+Var
+ DCI : TDatasetCollectionItem;
+ aName : String;
+
+begin
+ aName:=aSectionName;
+ if aName='' then
+ aName:=aDataset.Name;
+ if aName='' then
+ raise EMustache.Create(SErrDatasetNameEmpty);
+ if aDataset=Nil then
+ raise EMustache.Create(SErrDatasetEmpty);
+ if FDatasets.IndexOfSection(aName)<>-1 then
+ raise EMustache.CreateFmt(SErrDuplicateDataSetName, [aName]);
+ DCI:=FDatasets.Add as TDatasetCollectionItem;
+ DCI.Dataset:=aDataset;
+ DCI.SectionName:=aName;
+end;
+
+procedure TMustacheDBContext.RemoveDataset(aDataset: TDataset);
+
+Var
+ Idx : Integer;
+
+begin
+ Idx:=FDatasets.IndexOfDataset(aDataset);
+ if Idx<>-1 then
+ FDatasets.Delete(Idx);
+end;
+
+{ TDatasetCollection }
+
+function TDatasetCollection.GetDS(aIndex : Integer): TDatasetCollectionItem;
+begin
+ Result:=Items[aIndex] as TDatasetCollectionItem;
+end;
+
+function TDatasetCollection.IndexOfDataset(aDataset: TDataset): Integer;
+begin
+ Result:=Count-1;
+ While (Result>=0) and (GetDS(Result).Dataset<>ADataset) do
+ Dec(Result);
+end;
+
+function TDatasetCollection.IndexOfSection(aSection: String): Integer;
+begin
+ Result:=Count-1;
+ While (Result>=0) and not SameText(GetDS(Result).SectionName,ASection) do
+ Dec(Result);
+end;
+
+end.
+
diff --git a/packages/fcl-mustache/src/fpexmustache.pp b/packages/fcl-mustache/src/fpexmustache.pp
new file mode 100644
index 0000000000..6191e6bd78
--- /dev/null
+++ b/packages/fcl-mustache/src/fpexmustache.pp
@@ -0,0 +1,399 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ This file contains a Mustache descendent with FPExpr parser expression support
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpexmustache;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+ Classes, fpexprpars, fpmustache, fpjson;
+
+Type
+
+ { TMustacheExprElement }
+
+ TMustacheExprElement = Class(TMustacheElement)
+ private
+ FNode: TFPExprNode;
+ FExpr : TMustacheString;
+ Protected
+ Procedure SetNode(aNode : TFPExprNode); virtual;
+ Function GetData : TMustacheString;override;
+ Procedure SetData(const aValue : TMustacheString) ; override;
+ Public
+ Destructor Destroy; override;
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ Property Node : TFPExprNode Read FNode;
+ end;
+
+ { TMustacheExprParser }
+
+ TMustacheExprParser = class(TMustacheParser)
+ private
+ FExprEnd: Char;
+ FExprParser: TFPExpressionParser;
+ FExprStart: Char;
+ Protected
+ function CreateDefault(aParent: TMustacheElement; aPosition: Integer; const aName: String): TMustacheElement; override;
+ Public
+ Constructor Create(aTemplate : TMustacheString = '';aStart: TMustacheString='';aStop: TMustacheString = ''); override;
+ // Default [
+ Property ExprStart : Char Read FExprStart Write FExprStart;
+ // Default ]
+ Property ExprEnd : Char Read FExprEnd Write FExprEnd;
+ // Our instance
+ Property ExprParser : TFPExpressionParser Read FExprParser Write FExprParser;
+ end;
+
+ { TMustacheExpr }
+
+ TMustacheExpr = Class(TMustache)
+ private
+ FExprEndChar: String;
+ FExpressionParser: TFPExpressionParser;
+ FExprStartChar: String;
+ FCurrentContext : TMustacheContext;
+ function GetResultType(aValue: TJSONData): TResultType;
+ procedure SetExprEndChar(AValue: String);
+ procedure SetExpressionParser(AValue: TFPExpressionParser);
+ procedure SetExprStartChar(AValue: String);
+ function DoGetExpressionParser : TFPExpressionParser;
+ Protected
+ procedure DoGetVariable(var Result: TFPExpressionResult; ConstRef AName: ShortString); virtual;
+ Procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+ Function CreateParser(aTemplate: TMustacheString): TMustacheParser; override;
+ function GetExpressionParser(aOwner : TComponent): TFPExpressionParser; virtual;
+ Public
+ Constructor Create(aOwner : TComponent); override;
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput); override; overload;
+ // Register variables from JSON in the expression engine.
+ // If UseEvent is true, the variables will be retrieved while parsing with an event.
+ // If UseEvent is false, the variables will be registered as static values.
+ Procedure RegisterVariables (aContext : TMustacheJSONContext; aPath : TJSONStringType = ''; UseEvent : Boolean = True);
+ Procedure RegisterVariables (aJSON : String; aPath : TJSONStringType = ''; UseEvent : Boolean = True);
+ Procedure RegisterVariables (aJSON : TJSONObject; aPath : TJSONStringType = ''; UseEvent : Boolean = True);
+ Published
+ // Default [
+ Property ExprStartChar : String Read FExprStartChar Write SetExprStartChar;
+ // Default ]
+ Property ExprEndChar : String Read FExprEndChar Write SetExprEndChar;
+ // An expression parser instance. If none is specified, then a default is created.
+ Property ExpressionParser : TFPExpressionParser Read DoGetExpressionParser Write SetExpressionParser;
+ end;
+
+ { TMustacheExpressionParser }
+
+ TMustacheExpressionParser = class(TFPExpressionParser)
+ end;
+
+implementation
+
+uses sysutils;
+
+Resourcestring
+ SErrLengthStartMustBe1 = 'Length expression start delimiter must be 1';
+ SErrLengthEndMustBe1 = 'Length expression end delimiter must be 1';
+
+{ TMustacheExprElement }
+
+procedure TMustacheExprElement.SetNode(aNode: TFPExprNode);
+begin
+ FNode:=aNode;
+end;
+
+function TMustacheExprElement.GetData: TMustacheString;
+begin
+ Result:=FExpr;
+end;
+
+procedure TMustacheExprElement.SetData(const aValue: TMustacheString);
+begin
+ FExpr:=aValue;
+end;
+
+procedure TMustacheExprElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix: String; aLast: Boolean);
+
+Var
+ Res : TFPExpressionResult;
+ S : TMustacheString;
+
+begin
+ Res:=Node.NodeValue;
+ case Res.ResultType of
+ rtString : S:=Res.ResString;
+ rtBoolean : S:=BoolToStr(Res.ResBoolean,True);
+ rtInteger : S:=IntToStr(Res.ResInteger);
+ rtFloat : S:=FormatFloat('0.0#######',Res.ResFloat);
+ rtCurrency : S:=CurrToStr(Res.ResCurrency);
+ rtDateTime : S:=DateTimeToStr(Res.ResDateTime);
+ end;
+ aOutput.Output(aPrefix+S);
+end;
+
+destructor TMustacheExprElement.Destroy;
+begin
+ FreeAndNil(FNode);
+ inherited Destroy;
+end;
+
+{ TMustacheExprParser }
+
+function TMustacheExprParser.CreateDefault(aParent: TMustacheElement;
+ aPosition: Integer; const aName: String): TMustacheElement;
+
+Var
+ L : Integer;
+ N : TFPExprNode;
+
+begin
+ N:=Nil;
+ L:=Length(aName);
+ If (aName[1]=FExprStart) and (aName[L]=FExprEnd) then
+ begin
+ Result:=TMustacheExprElement.Create(metVariable,aParent,aPosition);
+ Result.Data:=Copy(aName,2,L-2);
+ ExprParser.Expression:=Result.Data;
+ ExprParser.ExtractNode(N);
+ TMustacheExprElement(Result).SetNode(N);
+ aParent.AddChild(Result);
+ end
+ else
+ Result:=Inherited CreateDefault(aParent,aPosition,aName);
+end;
+
+constructor TMustacheExprParser.Create(aTemplate: TMustacheString;
+ aStart: TMustacheString; aStop: TMustacheString);
+begin
+ inherited Create(aTemplate, aStart, aStop);
+ FExprStart:='[';
+ FExprEnd:=']';
+end;
+
+{ TMustacheExpr }
+
+procedure TMustacheExpr.SetExprEndChar(AValue: String);
+begin
+ if FExprEndChar=AValue then Exit;
+ if Length(aValue)<>1 then
+ EMustache.Create(SErrLengthStartMustBe1);
+ FExprEndChar:=AValue;
+end;
+
+function TMustacheExpr.GetExpressionParser(aOwner : TComponent): TFPExpressionParser;
+begin
+ Result:=TMustacheExpressionParser.Create(AOwner);
+end;
+
+procedure TMustacheExpr.SetExpressionParser(AValue: TFPExpressionParser);
+
+begin
+ if FExpressionParser=AValue then Exit;
+ If assigned(FExpressionParser) then
+ FExpressionParser.RemoveFreeNotification(Self);
+ FExpressionParser:=AValue;
+ If assigned(FExpressionParser) then
+ FExpressionParser.FreeNotification(Self);
+end;
+
+procedure TMustacheExpr.SetExprStartChar(AValue: String);
+begin
+ if FExprStartChar=AValue then Exit;
+ if Length(aValue)<>1 then
+ EMustache.Create(SErrLengthEndMustBe1);
+ FExprStartChar:=AValue;
+end;
+
+function TMustacheExpr.DoGetExpressionParser: TFPExpressionParser;
+begin
+ if FExpressionParser=Nil then
+ begin
+ FExpressionParser:=GetExpressionParser(Self);
+ FExpressionParser.SetSubComponent(True);
+ FExpressionParser.FreeNotification(Self);
+ end;
+ Result:=FExpressionParser;
+end;
+
+procedure TMustacheExpr.Notification(AComponent: TComponent;
+ Operation: TOperation);
+begin
+ inherited Notification(AComponent, Operation);
+ if (Operation=opRemove) and (aComponent=FExpressionParser) then
+ FExpressionParser:=Nil;
+end;
+
+function TMustacheExpr.CreateParser(aTemplate: TMustacheString ): TMustacheParser;
+
+Var
+ Exp : TMustacheExprParser;
+
+begin
+ Exp:=TMustacheExprParser.Create(aTemplate);
+ Exp.ExprParser:=Self.ExpressionParser;
+ Result:=Exp;
+end;
+
+constructor TMustacheExpr.Create(aOwner: TComponent);
+begin
+ inherited Create(aOwner);
+ DoGetExpressionParser;
+end;
+
+procedure TMustacheExpr.Render(aContext: TMustacheContext; aOutput: TMustacheOutput);
+
+begin
+ FCurrentContext:=aContext;
+ try
+ inherited Render(aContext, aOutput);
+ finally
+ FCurrentContext:=nil;
+ end;
+end;
+
+procedure TMustacheExpr.DoGetVariable(var Result: TFPExpressionResult; ConstRef
+ AName: ShortString);
+
+Var
+ S : TMustacheString;
+ V : Double;
+ C : Integer;
+
+begin
+ If not Assigned(FCurrentContext) then
+ case result.ResultType of
+ rtInteger : Result.ResInteger:=0;
+ rtDateTime : Result.ResDateTime:=0.0;
+ rtString : Result.ResString:='';
+ rtFloat: Result.ResFloat:=0.0;
+ rtCurrency: Result.ResCurrency:=0.0;
+ rtBoolean: Result.ResBoolean:=False;
+ end
+ else
+ begin
+ S:=FCurrentContext.GetTextValue(aName);
+ case result.ResultType of
+ rtInteger : Result.ResInteger:=StrToInt64Def(S,0);
+ rtDateTime : if Not TryStrToDateTime(S,Result.ResDateTime) then
+ Result.ResDateTime:=0.0;
+ rtString : Result.ResString:=S;
+ rtFloat: begin
+ Val(S,V,C);
+ if C<>0 then
+ Result.ResFloat:=0.0
+ else
+ Result.ResFloat:=V;
+ end;
+ rtCurrency:
+ begin
+ Val(S,V,C);
+ if (C<>0) then
+ Result.ResCurrency:=0.0
+ else
+ Result.ResCurrency:=V;
+ end;
+ rtBoolean: Result.ResBoolean:=StrToBoolDef(S,False);
+ end;
+ end;
+end;
+
+function TMustacheExpr.GetResultType(aValue: TJSONData): TResultType;
+
+begin
+ Case aValue.JSONType of
+ jtBoolean : Result:=rtBoolean;
+ jtString,
+ jtArray,
+ jtObject,
+ jtNull : Result:=rtString;
+ jtNumber :
+ begin
+ Case TJSONNumber(aValue).NumberType of
+ ntFloat : Result:=rtFloat;
+ ntInteger,
+ ntInt64 : Result:=rtInteger;
+ ntQWord : Raise EMustache.Create('Unsupported JSON type');
+ end;
+ end;
+ end;
+end;
+
+procedure TMustacheExpr.RegisterVariables(aContext: TMustacheJSONContext;
+ aPath: TJSONStringType; UseEvent: Boolean);
+
+begin
+ RegisterVariables(aContext.RootData as TJSONObject,aPath,UseEvent);
+end;
+
+procedure TMustacheExpr.RegisterVariables(aJSON: String;
+ aPath: TJSONStringType; UseEvent: Boolean);
+
+Var
+ aData : TJSONData;
+ aObj : TJSONObject absolute aData;
+
+
+begin
+ aData:=getJSON(aJSON,True);
+ try
+ if aData is TJSONObject then
+ RegisterVariables(aObj,aPath,useEvent)
+ else
+ Raise EMustache.Create('Invalid JSON data to register variables');
+ finally
+ aData.Free;
+ end;
+end;
+
+procedure TMustacheExpr.RegisterVariables(aJSON: TJSONObject; aPath: TJSONStringType; UseEvent: Boolean);
+
+Var
+ aData,aValue : TJSONData;
+ aEnum : TJSONEnum;
+ aKey : TJSONStringType;
+ rt : TResultType;
+ aParser : TFPExpressionParser;
+
+begin
+ aParser:=ExpressionParser;
+ aData:=aJSON.FindPath(aPath);
+ if aData is TJSONObject then
+ for aEnum in aData do
+ begin
+ aKey:=aEnum.Key;
+ aValue:=aEnum.Value;
+ rt:=GetResultType(aValue);
+ if UseEvent then
+ aParser.Identifiers.AddVariable(aKey,rt,@DoGetVariable)
+ else
+ case rt of
+ rtBoolean: aParser.Identifiers.AddBooleanVariable(aKey,aValue.AsBoolean);
+ rtFloat: aParser.Identifiers.AddFloatVariable(aKey,aValue.AsFloat);
+ rtInteger: aParser.Identifiers.AddIntegerVariable(aKey,aValue.AsInteger);
+ rtString: Case aValue.JSONType of
+ jtNull: aParser.Identifiers.AddStringVariable(aKey,'');
+ jtArray,
+ jtObject: aParser.Identifiers.AddStringVariable(aKey, aValue.AsJSON);
+ else
+ aParser.Identifiers.AddStringVariable(aKey,aValue.AsString);
+ end;
+ end;
+ end;
+end;
+
+end.
+
diff --git a/packages/fcl-mustache/src/fpmustache.pp b/packages/fcl-mustache/src/fpmustache.pp
new file mode 100644
index 0000000000..37163a3ded
--- /dev/null
+++ b/packages/fcl-mustache/src/fpmustache.pp
@@ -0,0 +1,1340 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ This file contains a Mustache parser and renderer.
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpmustache;
+
+{$mode objfpc}{$H+}
+{$WARN 5024 off : Parameter "$1" not used}
+interface
+
+uses
+ Classes, SysUtils, fpjson;
+
+Type
+ EMustache = Class(Exception);
+
+ TMustacheString = UTF8String;
+ TMustacheChar = AnsiChar;
+ TMustacheContext = class;
+
+ TMustacheOutput = Class(TObject)
+ Public
+ // add atext to output
+ Procedure Output(Const aText : TMustacheString); virtual; abstract;
+ Procedure Reset; virtual; abstract;
+ end;
+
+ { TMustacheStringOutput }
+
+ TMustacheStringOutput = Class(TMustacheOutput)
+ private
+ FData: TMustacheString;
+ Public
+ // Override
+ Procedure Output(Const aText : TMustacheString); override;
+ Procedure Reset; override;
+ // The rendered TMustacheString
+ Property Data : TMustacheString Read FData;
+ end;
+
+ { TMustacheElement }
+
+ TMustacheElementType = (metRoot,metComment,metText,metVariable,metSection,metInvertedSection,metPartial);
+
+ TMustacheElement = Class(TObject)
+ private
+ FPosition: Integer;
+ FType : TMustacheElementType;
+ FParent : TMustacheElement;
+ Protected
+ function GetCount: Integer; virtual;
+ function GetElement(aIndex : Integer): TMustacheElement; virtual;
+ Function GetData : TMustacheString ; virtual; abstract;
+ Procedure SetData(Const aData : TMustacheString) ; virtual; abstract;
+ Function GetPrefix : TMustacheString; virtual;
+ Procedure SetPrefix (aValue : TMustacheString); virtual;
+ Procedure Dump(aList : Tstrings; aIndent : TMustacheString; aDumpChildren : Boolean = true); virtual;
+ Public
+ Constructor Create(aType : TMustacheElementType; aParent : TMustacheElement;aPosition : Integer); virtual;
+ // Add a child. Parent always owns child
+ Procedure AddChild(aChild : TMustacheElement); virtual;
+ // Render the text for this element
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); virtual; abstract;
+ // Position in template
+ Property Position : Integer Read FPosition;
+ // Parent element
+ Property Parent : TMustacheElement Read FParent;
+ // Access to children
+ Property Children[aIndex : Integer] : TMustacheElement Read GetElement;
+ // Child count
+ Property ChildCount : Integer Read GetCount;
+ // Element type
+ Property ElementType : TMustacheElementType Read FType;
+ // The data for this element. What this is, depends on the kind.
+ // etText : the text;
+ // etValue : the variable name
+ // etSection : the section name.
+ // etInvertedSection : the section name.
+ Property Data : TMustacheString Read GetData Write SetData;
+ // Whitespace prefix. Normally only used for partials
+ Property Prefix : TMustacheString Read GetPrefix Write SetPrefix;
+ end;
+ TMustacheElementClass = Class of TMustacheElement;
+ TMustacheElementArray = Array of TMustacheElement;
+
+ { TMustacheNamedElement }
+
+ TMustacheNamedElement = Class(TMustacheElement)
+ private
+ FName: TMustacheString;
+ Protected
+ Procedure SetData(Const aData : TMustacheString); override;
+ Function GetData : TMustacheString; override;
+ Public
+ Property Name : TMustacheString Read FName;
+ end;
+
+ { TMustacheParentElement }
+
+ TMustacheParentElement = Class(TMustacheNamedElement)
+ Private
+ FChildren : TMustacheElementArray;
+ FCount : Integer;
+ Protected
+ function GetElement(aIndex : Integer): TMustacheElement; override;
+ function GetCount : Integer; override;
+ Public
+ Destructor Destroy; override;
+ Procedure AddChild(aChild : TMustacheElement); override;
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ end;
+
+
+ { TMustacheTextElement }
+
+ TMustacheTextElement = Class(TMustacheElement)
+ Private
+ FData : TMustacheString;
+ Protected
+ Procedure SetData(Const aData : TMustacheString) ; override;
+ Function GetData : TMustacheString; override;
+ Public
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ end;
+
+ { TMustacheVariableElement }
+
+ TMustacheVariableElement = Class(TMustacheNamedElement)
+ private
+ FNoUnescape: Boolean;
+ Protected
+ Procedure SetData(Const aData : TMustacheString); override;
+ Public
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ Property NoUnescape : Boolean Read FNoUnescape;
+ end;
+
+ { TMustacheSectionElement }
+
+ TMustacheSectionElement = Class(TMustacheParentElement)
+ Public
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ end;
+
+ { TMustachePartialElement }
+
+ TMustachePartialElement = Class(TMustacheElement)
+ Private
+ FPrefix : TMustacheString;
+ FPartialName : TMustacheString;
+ FPartial : TMustacheElement;
+ Protected
+ Function GetData : TMustacheString ; override;
+ Procedure SetData(Const aData : TMustacheString) ; override;
+ Procedure Dump(aList : Tstrings; aIndent : TMustacheString; aDumpChildren : Boolean = true); override;
+ Function GetPrefix : TMustacheString; override;
+ Procedure SetPrefix (aValue : TMustacheString); override;
+ Public
+ Destructor Destroy; override;
+ Procedure AddChild(aChild: TMustacheElement); override;
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False); override;
+ Property Partial : TMustacheElement Read FPartial;
+ end;
+
+ { TMustachePartialList }
+
+ TMustachePartialList = Class(TMustacheParentElement)
+ Public
+ Function FindPartial(aName : TMustacheString) : TMustacheElement;
+ end;
+
+ { TMustacheParser }
+ TGetTextValueEvent = Procedure (Const aName : TMustacheString; var aHandled : Boolean; var aValue : TMustacheString) of Object;
+
+ TMustacheParser = Class(TObject)
+ private
+ FStopTag: TMustacheString;
+ FStartTag: TMustacheString;
+ FTemplate: TMustacheString;
+ FOnGetPartial: TGetTextValueEvent;
+ FPartials: TMustachePartialList;
+ Class var DefaultTypes : Array[TMustacheElementType] of TMustacheElementClass;
+ Protected
+ // Called to create a default element for a {{ }} tag. By default creates a variable.
+ // Override this if you want to create additional elements.
+ function CreateDefault(aParent: TMustacheElement; aPosition: Integer; const aName: String): TMustacheElement; virtual;
+ // Create element for indicated type, must add it to parent.
+ // You can override this to provide customized behaviour.
+ function CreateElement(aType: TMustacheElementType; aParent: TMustacheElement; aPosition: Integer): TMustacheElement; virtual;
+ // Parse
+ Procedure DoParse(aParent : TMustacheElement; Const aTemplate, aStart, aStop : TMustacheString); virtual;
+ // Called to get the template of a partial. The template is parsed, and the result added to the partials list.
+ Function GetPartial(const aName : TMustacheString) : TMustacheString; virtual;
+ // Auxuliary functions for the peculiar whitespace handling of Mustache specs...
+ function EndsOnWhiteSpace(aElement: TMustacheElement): Boolean; virtual;
+ function GetEndingWhiteSpace(aElement: TMustacheElement): TMustacheString; virtual;
+ procedure ExtractStartStop(const aName: TMustacheString; out aStart, aStop: TMustacheString); virtual;
+ procedure TrimEndingWhiteSpace(aElement: TMustacheElement); virtual;
+ Public
+ // Create a new parser.
+ Constructor Create(aTemplate : TMustacheString = '';aStart: TMustacheString='';aStop: TMustacheString = ''); virtual;
+ // Set the default TMustacheElements for the descendents
+ Class procedure SetDefaultTypeClass(aType : TMustacheElementType; aClass: TMustacheElementClass);
+ // Parse the template and
+ Procedure Parse(aParent : TMustacheElement);
+ Function Parse : TMustacheElement;
+ // Will be used to hold partials. You must set this before calling Parse.
+ Property Partials : TMustachePartialList Read FPartials Write FPartials;
+ // The template created on startup
+ Property Template : TMustacheString Read FTemplate write FTemplate;
+ // The initial start tag marker, by default {{
+ Property StartTag : TMustacheString Read FStartTag Write FStartTag;
+ // The initial end tag marker, by default }}
+ Property StopTag : TMustacheString Read FStopTag Write FSTopTag;
+ // Event called to get the source of a partial.
+ Property OnGetPartial : TGetTextValueEvent Read FOnGetPartial Write FOnGetPartial;
+ end;
+
+ { TMustacheContext }
+ TMustacheSectionType = (mstNone,mstSingle,mstList);
+
+ TMustacheContext = Class(TObject)
+ Private
+ FCallback : TGetTextValueEvent;
+ Public
+ Constructor Create(aCallback : TGetTextValueEvent); virtual;
+ // Helper function to quote HTML
+ Class Function QuoteHTML(aString : TMustacheString) :TMustacheString; virtual;
+ // Move to next section item. aName is section name. Returns True if move successful
+ Function MoveNextSectionItem(Const aName : TMustacheString) : Boolean; virtual;
+ // Push a new section context with name aName.
+ Function PushSection(Const aName : TMustacheString) : TMustacheSectionType; virtual;
+ // Pop current section. aName is for verification.
+ Procedure PopSection(Const aName : TMustacheString); virtual;
+ // Return the value of a variable with name aName.
+ Function GetTextValue(Const aName : TMustacheString) : TMustacheString; virtual;
+ end;
+
+ { TMustacheJSONContext }
+
+ TMustacheJSONContext = Class(TMustacheContext)
+ Private
+ Type
+ TPair = Record
+ Index : Integer; // if array, index of current element.
+ Value : TJSONData;
+ end;
+ Private
+ FCurrentData: TJSONData;
+ FStack : Array of TPair;
+ FCount : Integer;
+ Function FindValue(Const aName : TMustacheString) : TJSONData;
+ function GetRootData: TJSONData;
+ Public
+ Constructor Create(aJSON : TJSONData; aCallback : TGetTextValueEvent); reintroduce;
+ Function MoveNextSectionItem(Const aName : TMustacheString) : Boolean; override;
+ Function PushSection(Const aName : TMustacheString) : TMustacheSectionType; override;
+ Procedure PopSection(Const aName : TMustacheString); override;
+ Function GetTextValue(Const aName : TMustacheString) : TMustacheString; override;
+ Property RootData : TJSONData read GetRootData;
+ end;
+
+ TMustache = Class(TComponent)
+ private
+ FCompiled: TMustacheElement;
+ FCompiledPartials: TMustachePartialList;
+ FOnGetValue: TGetTextValueEvent;
+ FPartials: TStrings;
+ FStartTag: TMustacheString;
+ FStopTag: TMustacheString;
+ FTemplate: TMustacheString;
+ procedure SetPartials(AValue: TStrings);
+ procedure SetStartTag(AValue: TMustacheString);
+ procedure SetStopTag(AValue: TMustacheString);
+ procedure SetTemplate(AValue: TMustacheString);
+ Protected
+ Procedure DoGetPartial(Const aName : TMustacheString; var aHandled : Boolean; var aValue : TMustacheString); virtual;
+ Procedure Reset; virtual;
+ Function CreatePartials : TMustachePartialList;
+ function CreateParser(aTemplate: TMustacheString): TMustacheParser; virtual;
+ Property Compiled : TMustacheElement Read FCompiled;
+ Property CompiledPartials : TMustachePartialList Read FCompiledPartials;
+ Public
+ Constructor Create(aOwner : TComponent); override;
+ Destructor Destroy; override;
+ Procedure Compile;
+ Procedure Dump(aList : Tstrings; aindent : TMustacheString); overload; virtual;
+ Function Dump: TMustacheString;overload;
+ Procedure Render(aContext : TMustacheContext; aOutput : TMustacheOutput); virtual; overload;
+ Function Render(aContext : TMustacheContext) : TMustacheString; overload;
+ Function Render(const aJSON : TJSONData) : TMustacheString; overload;
+ Function Render(const aJSON : TJSONStringType) : TMustacheString; overload;
+ Class function CreateMustache(aOwner: TComponent; aTemplate: TMustacheString): TMustache; virtual;
+ Class Function Render(aTemplate : TMustacheString; const aJSON : TJSONStringType) : TMustacheString;
+ Published
+ Property Template : TMustacheString Read FTemplate Write SetTemplate;
+ Property OnGetValue : TGetTextValueEvent Read FOnGetValue Write FOnGetValue;
+ Property StartTag : TMustacheString Read FStartTag Write SetStartTag;
+ Property StopTag : TMustacheString Read FStopTag Write SetStopTag;
+ Property Partials : TStrings Read FPartials Write SetPartials;
+ end;
+
+
+Const
+ ListGrowCount = 10;
+ JSONListGrowCount = 10;
+
+implementation
+
+uses TypInfo;
+
+
+
+Resourcestring
+ SErrNoChildForElement = 'Class %s does not support child elements.';
+ SErrInvalidIndex= '%s: Index %d is not in valid range [0..%d].';
+ SErrUnterminatedTag = 'Tag %s opened on position %d but not closed.';
+ SErrEmptyTag = 'Tag %s on position %d is empty.';
+ SErrSectionClose = 'Structural error: Section "%s" on position %d is closed by tag "%s" on position %d.';
+ SErrNotClosedSection = 'Structural error: Section "%s" on position %d is not closed.';
+ SErrNoSectionToClose = 'Structural error: Section "%s" on position %d was never opened.';
+ SErrInvalidDelimiter = 'Invalid set delimiter: %s';
+ SErrInvalidDelimiterValue = 'Invalid set delimiter %s value: %s in "%s"';
+ SErrNoPartials = 'No partials list';
+
+ // SErrPartialNotFound = 'Partial "%s" not found.';
+ SStartTag = 'Start';
+ SStopTag = 'Stop';
+
+{ TMustachePartialList }
+
+function TMustachePartialList.FindPartial(aName: TMustacheString ): TMustacheElement;
+
+Var
+ I : Integer;
+
+begin
+ Result:=Nil;
+ I:=ChildCount-1;
+ While (Result=Nil) and (I>=0) do
+ begin
+ Result:=Children[I];
+ If (Result.Data<>aName) then
+ Result:=Nil;
+ Dec(I);
+ end;
+end;
+
+{ TMustachePartialElement }
+
+function TMustachePartialElement.GetData: TMustacheString;
+begin
+ Result:=FPartialName;
+end;
+
+procedure TMustachePartialElement.SetData(const aData: TMustacheString);
+begin
+ FPartialName:=aData;
+end;
+
+procedure TMustachePartialElement.AddChild(aChild: TMustacheElement);
+begin
+ If (FPartial<>Nil) and (aChild<>Nil) then
+ Raise EMustache.Create('Cannot set partial twice');
+ FPartial:=aChild;
+end;
+
+procedure TMustachePartialElement.Dump(aList: Tstrings;
+ aIndent: TMustacheString; aDumpChildren: Boolean);
+begin
+ inherited Dump(aList, aIndent, aDumpChildren);
+ if Prefix<>'' then
+ aList[aList.Count-1]:=aList[aList.Count-1]+' Prefix: "'+Prefix+'"';
+end;
+
+function TMustachePartialElement.GetPrefix: TMustacheString;
+begin
+ Result:=FPrefix;
+end;
+
+procedure TMustachePartialElement.SetPrefix(aValue: TMustacheString);
+begin
+ FPrefix:=aValue;
+end;
+
+procedure TMustachePartialElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix : String = ''; aLast : Boolean = False);
+
+begin
+ FPartial.Render(aContext,aOutput,Prefix);
+end;
+
+destructor TMustachePartialElement.Destroy;
+begin
+ inherited Destroy;
+end;
+
+
+{ TMustache }
+
+function TMustache.CreateParser(aTemplate : TMustacheString): TMustacheParser;
+begin
+ Result:=TMustacheParser.Create(aTemplate);
+end;
+
+constructor TMustache.Create(aOwner: TComponent);
+begin
+ Inherited;
+ FPartials:=TStringList.Create;
+ FCompiledPartials:=CreatePartials;
+end;
+
+destructor TMustache.Destroy;
+begin
+ Reset;
+ FreeAndNil(FPartials);
+ FreeAndNil(FCompiledPartials);
+ inherited Destroy;
+end;
+
+procedure TMustache.Compile;
+
+Var
+ Parser : TMustacheParser;
+
+begin
+ Parser:=CreateParser(Self.Template);
+ try
+ Parser.OnGetPartial:=@DoGetPartial;
+ //Parser.Template:=Self.Template;
+ Parser.Partials:=Self.FCompiledPartials;
+ if Self.StartTag<>'' then
+ Parser.StartTag:=Self.StartTag;
+ if Self.StopTag<>'' then
+ Parser.StopTag:=Self.StopTag;
+ FCompiled:=Parser.Parse;
+ finally
+ Parser.Free;
+ end;
+end;
+
+procedure TMustache.Dump(aList: Tstrings; aindent: TMustacheString);
+begin
+ if Assigned(Compiled) then
+ Compiled.Dump(aList,aIndent);
+end;
+
+function TMustache.Dump: TMustacheString;
+
+Var
+ I : integer;
+ L : TStrings;
+
+begin
+ L:=TStringList.Create;
+ try
+ Dump(L,'');
+ if Partials.Count>0 then
+ begin
+ L.Add('Partials:');
+ for I:=0 to Partials.Count-1 do
+ L.Add('Partial '+IntToStr(I)+': '+Partials[i]);
+ L.Add('End of partials');
+ end;
+ Result:=L.Text;
+ finally
+ L.Free;
+ end;
+end;
+
+procedure TMustache.Render(aContext: TMustacheContext; aOutput: TMustacheOutput);
+
+begin
+ if not Assigned(Compiled) then
+ Compile;
+ Compiled.Render(aContext,aOutput);
+end;
+
+function TMustache.Render(aContext: TMustacheContext): TMustacheString;
+
+Var
+ S : TMustacheStringOutput;
+
+begin
+ S:=TMustacheStringOutput.Create;
+ try
+ Render(aContext,S);
+ Result:=S.Data;
+ finally
+ S.Free;
+ end;
+end;
+
+function TMustache.Render(const aJSON: TJSONData): TMustacheString;
+
+Var
+ C : TMustacheJSONContext;
+
+begin
+ C:=TMustacheJSONContext.Create(aJSON,FOnGetValue);
+ try
+ Result:=Render(C);
+ finally
+ C.Free;
+ end;
+end;
+
+function TMustache.Render(const aJSON: TJSONStringType): TMustacheString;
+
+Var
+ JSONData : TJSONData;
+
+begin
+ JSONData:=GetJSON(aJSON);
+ try
+ Result:=Render(JSONData);
+ finally
+ JSONData.Free;
+ end;
+end;
+
+class function TMustache.CreateMustache(aOwner : TComponent; aTemplate : TMustacheString) : TMustache;
+
+begin
+ Result:=TMustache.Create(aOwner);
+ Result.Template:=aTemplate;
+end;
+
+procedure TMustache.SetStartTag(AValue: TMustacheString);
+begin
+ if FStartTag=AValue then Exit;
+ FStartTag:=AValue;
+ Reset;
+end;
+
+procedure TMustache.SetPartials(AValue: TStrings);
+begin
+ if FPartials=AValue then Exit;
+ FPartials.Assign(AValue);
+end;
+
+procedure TMustache.SetStopTag(AValue: TMustacheString);
+begin
+ if FStopTag=AValue then Exit;
+ FStopTag:=AValue;
+ Reset;
+end;
+
+procedure TMustache.SetTemplate(AValue: TMustacheString);
+begin
+ if FTemplate=AValue then Exit;
+ FTemplate:=AValue;
+ Reset;
+end;
+
+procedure TMustache.DoGetPartial(const aName: TMustacheString;
+ var aHandled: Boolean; var aValue: TMustacheString);
+begin
+ aValue:=FPartials.Values[aName];
+ aHandled:=aValue<>'';
+ if Not aHandled then
+ aHandled:=FPartials.IndexOfName(aName)<>-1;
+end;
+
+procedure TMustache.Reset;
+begin
+ FreeAndNil(FCompiled);
+ FreeAndNil(FCompiledPartials);
+ FCompiledPartials:=CreatePartials;
+end;
+
+function TMustache.CreatePartials: TMustachePartialList;
+begin
+ Result:=TMustachePartialList.Create(metRoot,Nil,0);
+end;
+
+class function TMustache.Render(aTemplate: TMustacheString;
+ const aJSON: TJSONStringType): TMustacheString;
+
+begin
+ With CreateMustache(Nil,aTemplate) do
+ try
+ Result:=Render(aJSON);
+ finally
+ Free;
+ end;
+end;
+
+{ TMustacheJSONContext }
+
+function TMustacheJSONContext.FindValue(const aName: TMustacheString
+ ): TJSONData;
+Var
+ aCount : Integer;
+ N : TMustacheString;
+
+begin
+ Result:=Nil;
+ aCount:=FCount-1;
+ While (Result=Nil) and (aCount>=0) do
+ begin
+ N:=aName;
+ if N='.' then
+ N:='';
+ With FStack[aCount] do
+ if (Index>=0) and (Index<Value.Count) then
+ Result:=Value.Items[Index].FindPath(N)
+ else
+ Result:=Value.FindPath(N);
+ Dec(aCount);
+ end;
+end;
+
+function TMustacheJSONContext.GetRootData: TJSONData;
+begin
+ Result:=FStack[0].Value;
+end;
+
+
+constructor TMustacheJSONContext.Create(aJSON: TJSONData;
+ aCallback: TGetTextValueEvent);
+begin
+ Inherited Create(aCallBack);
+ SetLength(FStack,JSONListGrowCount);
+ FStack[0].Value:=aJSON;
+ FStack[0].Index:=-1;
+ FCount:=1;
+end;
+
+function TMustacheJSONContext.MoveNextSectionItem(const aName: TMustacheString
+ ): Boolean;
+
+begin
+ With FStack[FCount-1] do
+ begin
+ Inc(Index);
+ Result:=Index<Value.Count;
+ end;
+end;
+
+function TMustacheJSONContext.PushSection(const aName: TMustacheString
+ ): TMustacheSectionType;
+
+Var
+ S : TJSONData;
+
+begin
+ Result:=mstNone;
+ S:=FindValue(aName);
+ if S=Nil then
+ Exit;
+ if (S.JSONType=jtArray) then
+ begin
+ if (S.Count>0) then
+ Result:=mstList
+ end
+ else if Not ((S.JSONType=jtNull) or ((S.JSONType=jtBoolean) and Not S.AsBoolean)) then
+ Result:=mstSingle;
+ if Result<>mstNone then
+ begin
+ if FCount=Length(FStack) then
+ SetLength(FStack,FCount+JSONListGrowCount);
+ FStack[FCount].Value:=S;
+ FStack[FCount].Index:=-1;
+ Inc(FCount,1);
+ end;
+end;
+
+procedure TMustacheJSONContext.PopSection(const aName: TMustacheString);
+begin
+ if FCount<1 then
+ Raise EMustache.CreateFmt('PopSection %s without push',[aName]);
+ Dec(FCount,1);
+end;
+
+function TMustacheJSONContext.GetTextValue(const aName: TMustacheString): TMustacheString;
+
+Var
+ aJSON : TJSONData;
+
+begin
+ Result:='';
+ aJSON:=FindValue(aName);
+ if not Assigned(aJSON) then
+ Result:=Inherited GetTextValue(aName)
+ else
+ if (AJSON.JSONType=jtNumber) and (TJSONNumber(aJSON).NumberType=ntFloat) then
+ Result:=FormatFloat('0.0###########',aJSON.AsFloat)
+ else
+ Result:=aJSON.AsString;
+end;
+
+{ TMustacheSectionElement }
+
+procedure TMustacheSectionElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix: String; aLast : Boolean = False);
+
+Var
+ L : TMustacheSectionType;
+
+begin
+ L:=aContext.PushSection(Name);
+ if ElementType=metInvertedSection then
+ begin
+ if L=mstNone then
+ inherited Render(aContext, aOutput,aPrefix);
+ end
+ else
+ Case L of
+ mstSingle :
+ inherited Render(aContext, aOutput);
+ mstList :
+ while aContext.MoveNextSectionItem(Name) do
+ inherited Render(aContext, aOutput,aPrefix);
+ end;
+ if L<>mstNone then
+ aContext.PopSection(Name);
+end;
+
+{ TMustacheContext }
+
+constructor TMustacheContext.Create(aCallback: TGetTextValueEvent);
+begin
+ FCallback:=aCallback;
+end;
+
+class function TMustacheContext.QuoteHTML(aString: TMustacheString
+ ): TMustacheString;
+
+Const
+ QuoteChars = ['<','>','&','"'];
+
+Var
+ I,Last,Len : Integer;
+ Res : TMustacheString;
+
+ Procedure AddToResult; overload;
+
+ begin
+ Res:=Res+Copy(aString,Last,I-Last);
+ Last:=I;
+ end;
+
+ Procedure AddToResult(aTerm : TMustacheString); overload;
+
+ begin
+ Res:=Res+aTerm;
+ Last:=Last+1;
+ end;
+
+begin
+ Res:='';
+ Last:=1;
+ Len:=Length(Astring);
+ I:=1;
+ While (I<=Len) do
+ begin
+ While (I<=Len) and not (aString[i] in QuoteChars) do
+ Inc(I);
+ AddToResult;
+ if I<=Len then
+ Case aString[i] of
+ '<' : AddToResult('&lt;');
+ '>' : AddToResult('&gt;');
+ '&' : AddToResult('&amp;');
+ '"' : AddToResult('&quot;');
+ end;
+ Inc(i);
+ end;
+ AddToResult;
+ Result:=Res;
+end;
+
+function TMustacheContext.MoveNextSectionItem(const aName: TMustacheString): Boolean;
+begin
+ Result:=False
+end;
+
+function TMustacheContext.PushSection(const aName: TMustacheString): TMustacheSectionType;
+begin
+ Result:=mstNone;
+end;
+
+procedure TMustacheContext.PopSection(const aName: TMustacheString);
+begin
+ //
+end;
+
+function TMustacheContext.GetTextValue(const aName: TMustacheString): TMustacheString;
+
+var
+ aHandled : Boolean;
+
+begin
+ aHandled:=False;
+ Result:='';
+ if Assigned(FCallBack) then
+ FCallBack(aName,aHandled,Result);
+end;
+
+{ TMustacheTextElement }
+
+procedure TMustacheTextElement.SetData(const aData: TMustacheString);
+begin
+ FData:=aData;
+end;
+
+function TMustacheTextElement.GetData: TMustacheString;
+begin
+ Result:=FData;
+end;
+
+procedure TMustacheTextElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix: String; aLast : Boolean = False);
+
+Var
+ S : String;
+ L : Integer;
+
+begin
+ if (ElementType=metText) then
+ begin
+ S:=FData;
+ L:=Length(S);
+ if (aPrefix<>'') then
+ begin
+ if (S[L]=#10) and aLast then
+ S:=StringReplace(Copy(S,1,L-1),#10,#10+aPrefix,[rfReplaceAll])+#10
+ else
+ S:=StringReplace(S,#10,#10+aPrefix,[rfReplaceAll]);
+{$IFDEF DEBUGMUSTACHE}
+ Writeln('Adding prefix =]',aPrefix,'[= to =]',FData, '[= ---> =]',S,'["');
+{$ENDIF}
+ end;
+ aOutput.Output(S);
+ end;
+end;
+
+{ TMustacheVariableElement }
+
+procedure TMustacheVariableElement.SetData(const aData: TMustacheString);
+
+Var
+ L : Integer;
+ N : TMustacheString;
+begin
+ N:=aData;
+ L:=Length(N);
+ FNoUnescape:=(L>1) and (N[1]='{') and (N[L]='}');
+ if NoUnescape then
+ N:=Copy(N,2,L-2)
+ else
+ begin
+ FNoUnescape:=(L>0) and (N[1]='&');
+ if NoUnescape then
+ N:=Copy(N,2,L-1);
+ end;
+ inherited SetData(N);
+end;
+
+procedure TMustacheVariableElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix: String; aLast : Boolean = False);
+
+Var
+ aValue : TMustacheString;
+
+begin
+ aValue:='';
+ if Assigned(aContext) then
+ begin
+ aValue:=aContext.GetTextValue(Name);
+ if Not NoUnescape then
+ aValue:=aContext.QuoteHTML(aValue);
+ end;
+ aOutput.Output(aValue);
+end;
+
+{ TMustacheParser }
+
+function TMustacheParser.CreateElement(aType: TMustacheElementType; aParent : TMustacheElement; aPosition : Integer): TMustacheElement;
+
+begin
+ Result:=DefaultTypes[aType].Create(aType,aParent,aPosition);
+ if Assigned(aParent) then
+ aParent.AddChild(Result);
+end;
+
+constructor TMustacheParser.Create(aTemplate: TMustacheString; aStart: TMustacheString;
+ aStop: TMustacheString);
+begin
+ FStartTag:=aStart;
+ FStopTag:=aStop;
+ FTemplate:=aTemplate;
+ if FStartTag='' then
+ FStartTag:='{{';
+ if FStopTag='' then
+ FStopTag:='}}';
+end;
+
+class procedure TMustacheParser.SetDefaultTypeClass(aType: TMustacheElementType;
+ aClass: TMustacheElementClass);
+
+begin
+ DefaultTypes[aType]:=aClass;
+end;
+
+function TMustacheParser.GetPartial(const aName: TMustacheString): TMustacheString;
+
+Var
+ Handled : Boolean;
+begin
+ Result:='';
+ Handled:=False;
+ if Assigned(FOnGetPartial) then
+ FOnGetPartial(aName,Handled,Result);
+// If not Handled then
+// Raise EMustache.CreateFmt(SErrPartialNotFound,[aName]);
+end;
+
+procedure TMustacheParser.ExtractStartStop(const aName: TMustacheString; out aStart,
+ aStop: TMustacheString);
+
+ Function Invalid(S : TMustacheString) : Boolean;
+ begin
+ Invalid:=(Length(S)=0) or (Pos('=',S)<>0);
+ end;
+
+Var
+ DLen,NLen : Integer;
+ N : TMustacheString;
+
+begin
+ NLen:=Length(aName);
+ if aName[NLen]<>'=' then
+ Raise EMustache.CreateFmt(SErrInvalidDelimiter,[aName]);
+ N:=Copy(aName,1,NLen-1);
+ DLen:=(NLen-1) div 2;
+ aStart:=Trim(Copy(N,1,DLen));
+ aStop:=Trim(Copy(N,NLen-DLen,DLen));
+ // Writeln('New: "',aStart,'" - "',aStop,'" - ',DLEn);
+ if Invalid(aStop) then
+ Raise EMustache.CreateFmt(SErrInvalidDelimiterValue,[SStopTag,aStop,N]);
+ if Invalid(aStart) then
+ Raise EMustache.CreateFmt(SErrInvalidDelimiterValue,[SStartTag,aStart,N]);
+end;
+
+procedure TMustacheParser.Parse(aParent: TMustacheElement);
+
+begin
+ DoParse(aParent,FTemplate,StartTag, StopTag);
+end;
+
+function TMustacheParser.EndsOnWhiteSpace(aElement: TMustacheElement): Boolean;
+
+Var
+ I : Integer;
+ S : TMustacheString;
+
+begin
+ // if on standalone line, the entire line must be removed, see specs comments.standalone
+ Result:=(aElement.ElementType=metText);
+ s:=aElement.Data;
+ I:=Length(S);
+ While Result and (I>0) do
+ begin
+ if S[i] in [#13,#10] then
+ Break;
+ Result:=(S[I]=' ');
+ Dec(i);
+ end;
+ Result:=Result and ((I>0) or (aElement.Position=1));
+end;
+
+function TMustacheParser.GetEndingWhiteSpace(aElement: TMustacheElement): TMustacheString;
+
+Var
+ S : TMustacheString;
+ I : Integer;
+
+begin
+ s:=aElement.Data;
+ I:=Length(S);
+ While (I>0) and (S[I]=' ') do
+ Dec(i);
+ Result:=Copy(S,I+1);
+end;
+
+procedure TMustacheParser.TrimEndingWhiteSpace(aElement: TMustacheElement);
+
+Var
+ I : Integer;
+ S : TMustacheString;
+
+begin
+ s:=aElement.Data;
+ I:=Length(S);
+ While (I>0) and (S[I]=' ') do
+ Dec(i);
+ aElement.Data:=Copy(S,1,I);
+end;
+
+Function TMustacheParser.CreateDefault(aParent : TMustacheElement; aPosition : Integer;Const aName : String) : TMustacheElement;
+
+begin
+ Result:=CreateElement(metVariable,aParent,aPosition);
+ Result.SetData(aName);
+end;
+
+procedure TMustacheParser.DoParse(aParent: TMustacheElement; const aTemplate,
+ aStart, aStop: TMustacheString);
+
+Var
+ currParent : TMustacheElement;
+ aLen,clStop, lStart,lStop, NewPos, Current, Total : Integer;
+ aName,cStart,cStop,R : TMustacheString;
+ C: TMustacheChar;
+ IsWhiteSpace : Boolean;
+ Partial,WhiteSpaceEl : TMustacheELement;
+
+ Function CheckWhiteSpace : Boolean;
+
+ begin
+ WhiteSpaceEl:=Nil;
+ With CurrParent do
+ begin
+ Result:=(ChildCount=0) or EndsOnWhiteSpace(Children[ChildCount-1]);
+ if Result and (ChildCount>0) then
+ WhiteSpaceEl:=Children[ChildCount-1];
+ end;
+ end;
+
+ Procedure FinishWhiteSpace(Full : Boolean = true);
+ Var
+ I : Integer;
+ begin
+ I:=NewPos;
+ While IsWhiteSpace and (I+clStop<=Total) do
+ begin
+ C:=aTemplate[I+clStop];
+ if (C in [#13,#10]) then
+ Break;
+ isWhiteSpace:=aTemplate[I+clStop]=' ';
+ I:=I+1;
+ end;
+ if isWhiteSpace then
+ begin
+ While (I<=Total) and (aTemplate[I+clStop] in [#13,#10]) do
+ Inc(I);
+ NewPos:=I;
+ if Assigned(WhiteSpaceEl) and full then
+ TrimEndingWhiteSpace(WhiteSpaceEl);
+ end;
+ end;
+
+begin
+ currParent:=aParent;
+ cStart:=aStart;
+ cStop:=aStop;
+ lStart:=Length(cStart);
+ lStop:=Length(cStop);
+ Current:=1;
+ Total:=Length(aTemplate);
+ While (Current<=Total) do
+ begin
+ C:=Template[Current];
+ NewPos:=Pos(cStart,aTemplate,Current);
+ if NewPos=0 then
+ NewPos:=Total+1;
+ // Stash what we have till now.
+ if NewPos>Current then
+ begin
+ R:=Copy(aTemplate,Current,NewPos-Current);
+ CreateElement(metText,currParent,Current).SetData(R);
+ Current:=NewPos;
+ end;
+ if Current<Total then
+ begin
+ NewPos:=Pos(cStop,aTemplate,Current+lStart);
+ if (NewPos=0) then
+ Raise EMustache.CreateFmt(SErrUnterminatedTag,[cStart,Current]);
+ aLen:=NewPos-Current-LStart;
+ aName:=Copy(aTemplate,Current+LStart,ALen);
+ if (aName='') then
+ Raise EMustache.CreateFmt(SErrEmptyTag,[cStart,Current]);
+ C:=aName[1];
+ if C in ['=','#','^','/','!','>'] then
+ aName:=Copy(aName,2,Length(aName)-1);
+ clStop:=Lstop; // Can change.
+ case C of
+ '=' :
+ begin
+ IsWhiteSpace:=CheckWhiteSpace;
+ if IsWhiteSpace then
+ FinishWhiteSpace;
+ ExtractStartStop(aName,cStart,cStop);
+ lStart:=Length(cStart);
+ lStop:=Length(cStop);
+ //R:=Copy(aTemplate,newPos+clStop);
+ //Writeln(R);
+ end;
+ '{' :
+ begin
+ if (cStop='}}') then
+ begin
+ if (FTemplate[NewPos+lStop]<>'}') then
+ Raise EMustache.CreateFmt(SErrUnterminatedTag,[cStart,Current]);
+ inc(NewPos);
+ aName:=aName+'}';
+ end;
+ CreateElement(metVariable,currParent,Current).SetData(aName);
+ end;
+ '#' :
+ begin
+ IsWhiteSpace:=CheckWhiteSpace;
+ CurrParent:=CreateElement(metSection,currParent,Current);
+ CurrParent.SetData(aName);
+ if IsWhiteSpace then
+ FinishWhiteSpace;
+ end;
+ '!' :
+ begin
+ IsWhiteSpace:=CheckWhiteSpace;
+ CreateElement(metComment,currParent,Current).SetData(aName);
+ if IsWhiteSpace then
+ FinishWhiteSpace;
+ end;
+ '^' :
+ begin
+ IsWhiteSpace:=CheckWhiteSpace;
+ CurrParent:=CreateElement(metInvertedSection,currParent,Current);
+ CurrParent.SetData(aName);
+ if IsWhiteSpace then
+ FinishWhiteSpace;
+ end;
+ '>' :
+ begin
+ // Find or create compiled partial;
+ IsWhiteSpace:=CheckWhiteSpace;
+ aName:=Trim(aName);
+ if not Assigned(Partials) then
+ Raise EMustache.Create(SErrNoPartials);
+ Partial:=Partials.FindPartial(aName);
+ if Partial=Nil then
+ begin
+ Partial:=CreateElement(metRoot,Partials,Current);
+ Partial.Data:=aName;
+ DoParse(Partial,GetPartial(aName),FStartTag,FStopTag);
+ end;
+ // Create reference and insert into current tree
+ With CreateElement(metPartial,currParent,Current) do
+ begin
+ AddChild(Partial);
+ Data:=aName;
+ if isWhitespace and assigned(WhiteSpaceEl) then
+ Prefix:=GetEndingWhiteSpace(WhiteSpaceEl);
+ end;
+ if IsWhiteSpace then
+ FinishWhiteSpace(False);
+ end;
+ '/' :
+ begin
+ IsWhiteSpace:=CheckWhiteSpace;
+ if Not (CurrParent.ElementType in [metSection,metInvertedSection]) then
+ Raise EMustache.CreateFmt(SErrNoSectionToClose,[aName,Current])
+ else if (CurrParent.Data<>Trim(aName)) then
+ Raise EMustache.CreateFmt(SErrSectionClose,[currParent.Data,CurrParent.Position,aName,Current])
+ else
+ currParent:=currParent.Parent;
+ if IsWhiteSpace then
+ FinishWhiteSpace;
+ end
+ else
+ CreateDefault(CurrParent,Current,aName);
+ end;
+ Current:=NewPos+clStop;
+ end;
+ end;
+ if CurrParent<>aParent then
+ Raise EMustache.CreateFmt(SErrNotClosedSection,[currParent.Data,CurrParent.Position])
+
+end;
+
+function TMustacheParser.Parse: TMustacheElement;
+
+begin
+ Result:=TMustacheParentElement.Create(metRoot,Nil,1);
+ try
+ Parse(Result);
+ except
+ Result.Free;
+ Raise;
+ end;
+end;
+
+{ TMustacheNamedElement }
+
+procedure TMustacheNamedElement.SetData(Const aData: TMustacheString);
+
+begin
+ FName:=Trim(aData);
+end;
+
+function TMustacheNamedElement.GetData: TMustacheString;
+begin
+ Result:=FName;
+end;
+
+{ TMustacheParentElement }
+
+function TMustacheParentElement.GetElement(aIndex : Integer): TMustacheElement;
+begin
+ If (aIndex<0) or (aIndex>=FCount) then
+ Raise EMustache.CreateFmt(SErrInvalidIndex,[ClassName,aIndex,FCount-1]);
+ Result:=FChildren[aIndex];
+end;
+
+function TMustacheParentElement.GetCount: Integer;
+begin
+ Result:=FCount;
+end;
+
+destructor TMustacheParentElement.Destroy;
+begin
+ While FCount>0 do
+ begin
+ Dec(FCount);
+ FreeAndNil(FChildren[FCount]);
+ end;
+ inherited Destroy;
+end;
+
+
+procedure TMustacheParentElement.AddChild(aChild: TMustacheElement);
+
+Var
+ Len : Integer;
+
+begin
+ Len:=Length(FChildren);
+ if (FCount>=Len) then
+ SetLength(FChildren,Len+ListGrowCount);
+ FChildren[FCount]:=aChild;
+ Inc(FCount);
+end;
+
+procedure TMustacheParentElement.Render(aContext: TMustacheContext;
+ aOutput: TMustacheOutput; const aPrefix: String; aLast : Boolean = False);
+
+Var
+ I : integer;
+
+begin
+ For I:=0 to ChildCount-1 do
+ Children[I].Render(aContext,aOutPut,aPrefix,I=ChildCount-1);
+end;
+
+{ TMustacheElement }
+
+function TMustacheElement.GetCount: Integer;
+begin
+ Result:=0;
+end;
+
+function TMustacheElement.GetElement(aIndex : Integer): TMustacheElement;
+begin
+ Result:=Nil;
+end;
+
+function TMustacheElement.GetPrefix: TMustacheString;
+begin
+ Result:='';
+end;
+
+procedure TMustacheElement.SetPrefix(aValue: TMustacheString);
+begin
+ //
+end;
+
+procedure TMustacheElement.Dump(aList: Tstrings; aIndent: TMustacheString; aDumpChildren : Boolean = true);
+
+Var
+ I : Integer;
+
+begin
+ aList.Add(aIndent+Format('%s (%s, %d) : "%s"',[ClassName,GetEnumName(TypeInfo(TMustacheElementType),Ord(ElementType)),Position,Data]));
+ if aDumpChildren then
+ For I:=0 to ChildCount-1 do
+ Children[I].Dump(aList,' '+aIndent);
+end;
+
+constructor TMustacheElement.Create(aType : TMustacheElementType; aParent : TMustacheElement;aPosition: Integer);
+begin
+ FType:=aType;
+ FParent:=aParent;
+ FPosition:=aPosition;
+end;
+
+procedure TMustacheElement.AddChild(aChild: TMustacheElement);
+begin
+ Raise EMustache.CreateFmt(SErrNoChildForElement,[ClassName])
+end;
+
+{ TMustacheStringOutput }
+
+procedure TMustacheStringOutput.Output(const aText: TMustacheString);
+begin
+ FData:=FData+aText;
+{$IFDEF DEBUGMUSTACHE}
+ Writeln('--');
+ Writeln('Output -]',aText,'[-');
+ Writeln('--');
+{$ENDIF}
+end;
+
+procedure TMustacheStringOutput.Reset;
+begin
+ FData:='';
+end;
+
+begin
+ TMustacheParser.SetDefaultTypeClass(metRoot,TMustacheParentElement);
+ TMustacheParser.SetDefaultTypeClass(metComment,TMustacheTextElement);
+ TMustacheParser.SetDefaultTypeClass(metText,TMustacheTextElement);
+ TMustacheParser.SetDefaultTypeClass(metVariable,TMustacheVariableElement);
+ TMustacheParser.SetDefaultTypeClass(metSection,TMustacheSectionElement);
+ TMustacheParser.SetDefaultTypeClass(metInvertedSection,TMustacheSectionElement);
+ TMustacheParser.SetDefaultTypeClass(metPartial,TMustachePartialElement);
+end.
+
diff --git a/packages/fcl-mustache/tests/spec/comments.json b/packages/fcl-mustache/tests/spec/comments.json
new file mode 100644
index 0000000000..30cb927e62
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/comments.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/spec/delimiters.json b/packages/fcl-mustache/tests/spec/delimiters.json
new file mode 100644
index 0000000000..fcf95888db
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/delimiters.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes. .yes. ]\n[ .yes. .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"| |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":" {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @ @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/spec/interpolation.json b/packages/fcl-mustache/tests/spec/interpolation.json
new file mode 100644
index 0000000000..d1a1a32897
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/interpolation.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag. A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object, the data is the value returned by the\n method with the given name.\n 5) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: &amp; &quot; &lt; &gt;\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/spec/inverted.json b/packages/fcl-mustache/tests/spec/inverted.json
new file mode 100644
index 0000000000..c9b550b964
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/inverted.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/spec/partials.json b/packages/fcl-mustache/tests/spec/partials.json
new file mode 100644
index 0000000000..e5f21a2a48
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/partials.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X<Y<>>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/spec/sections.json b/packages/fcl-mustache/tests/spec/sections.json
new file mode 100644
index 0000000000..795f6acd74
--- /dev/null
+++ b/packages/fcl-mustache/tests/spec/sections.json
@@ -0,0 +1 @@
+{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} \ No newline at end of file
diff --git a/packages/fcl-mustache/tests/tcbasemustache.pas b/packages/fcl-mustache/tests/tcbasemustache.pas
new file mode 100644
index 0000000000..80abdf9e37
--- /dev/null
+++ b/packages/fcl-mustache/tests/tcbasemustache.pas
@@ -0,0 +1,290 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ Helper classes for Mustache test cases
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+unit tcbasemustache;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, fpcunit, fpmustache;
+
+type
+
+ { TTestContext }
+
+ (* StringList with following encoding
+ // Null value
+ aName=<null>
+ // false value
+ aName=<null>
+ // plain value
+ aName=AValue
+ // Object value & member. Object value must be present
+ SubObj={}
+ SubObj.aName=aValue
+ // Array and members. Array value must be present
+ SubObj.SubArr=[]
+ SubObj.SubArr[0]={}
+ SubObj.SubArr[0].aName=aValue
+ SubObj.SubArr[1]={}
+ Subobj.SubArr[1].aName=aValue
+ *)
+
+ TTestContext = class (TMustacheContext)
+ Private
+ FValues : TStringList;
+ FPath : String;
+ public
+ Constructor Create(aCallback: TGetTextValueEvent); override;
+ Destructor destroy; override;
+ Function GetTextValue(Const aName : TMustacheString) : TMustacheString; override;
+ Function MoveNextSectionItem(Const aName : TMustacheString) : Boolean; override;
+ Function PushSection(Const aName : TMustacheString) : TMustacheSectionType; override;
+ Procedure PopSection(Const aName : TMustacheString); override;
+ Procedure SetValue(const aPath,aValue : string);
+ Property Values : TStringList read FValues;
+ end;
+
+ TBaseMustacheTest = class(TTestCase)
+ Private
+ FPartials: TStrings;
+ FTemplate: String;
+ FResult: TMustacheElement;
+ FParser: TMustacheParser;
+ Protected
+ Function CreateParser : TMustacheParser; virtual; abstract;
+ Procedure DoGetPartial(const aName: TMustacheString; var aHandled: Boolean; var aValue: TMustacheString);
+ Public
+ Class Procedure AssertEquals(Msg : String; aExpected,aActual : TMustacheElementType); overload;
+ Class Function AssertElement(aParent : TMustacheElement; aIndex: Integer; aType: TMustacheElementType; aData: String; aClass : TMustacheElementClass = Nil) : TMustacheElement; overload;
+ Function AssertElement(aIndex: Integer; aType: TMustacheElementType; aData: String; aClass : TMustacheElementClass = Nil) : TMustacheElement; overload;
+ Procedure AssertResultCount(aCount : Integer);
+ procedure SetUp; override;
+ procedure TearDown; override;
+ Procedure CallParser;
+ Procedure AddPartial(Const aName,aText: TMustacheString);
+ Property Partials : TStrings Read FPartials;
+ Property Template : String Read FTemplate Write FTemplate;
+ property ParseResult : TMustacheElement Read FResult;
+ property Parser : TMustacheParser Read FParser;
+ end;
+
+
+implementation
+
+uses strutils, typinfo;
+
+{ TTestContext }
+
+constructor TTestContext.Create(aCallback: TGetTextValueEvent);
+begin
+ inherited Create(aCallback);
+ FValues:=TStringList.Create;
+ FValues.OwnsObjects:=True;
+end;
+
+destructor TTestContext.destroy;
+begin
+ FreeAndNil(FValues);
+ inherited destroy;
+end;
+
+function TTestContext.GetTextValue(const aName: TMustacheString
+ ): TMustacheString;
+
+Var
+ aPath,N : String;
+ Done : Boolean;
+begin
+ Result:='';
+ aPath:=FPath;
+ Done:=False;
+ Repeat
+ if aPath<>'' then
+ N:=aPath+'.'+aName
+ else
+ begin
+ N:=aName;
+ Done:=True;
+ end;
+ Result:=FValues.Values[N];
+ if not Done then
+ aPath:=Copy(aPath,1,RPos('.',aPath)-1);
+ until (Result<>'') or Done;
+end;
+
+function TTestContext.MoveNextSectionItem(const aName: TMustacheString
+ ): Boolean;
+
+Var
+ L,P,Idx : Integer;
+ N : String;
+
+begin
+ L:=Length(FPath);
+ if (L>0) and (FPath[L]=']') then
+ begin
+ P:=RPos('[',FPath)+1;
+ Idx:=StrToIntDef(Copy(FPath,P,L-P),-1);
+ N:=Copy(FPath,1,P-1)+IntToStr(Idx+1)+']';
+ Result:=FValues.Values[N]<>''; // We could check for {}
+ if Result then
+ FPath:=N;
+ end;
+
+end;
+
+function TTestContext.PushSection(const aName: TMustacheString): TMustacheSectionType;
+
+Var
+ aPath,S : String;
+
+begin
+ if FPath<>'' then
+ FPath:=FPath+'.';
+ aPath:=FPath+aName;
+ S:=Values.Values[aPath];
+ if S='{}' then
+ begin
+ FPath:=aPath;
+ result:=mstSingle;
+ end;
+ if S='[]' then
+ begin
+ if Values.Values[aPath+'[0]']='' then
+ Result:=mstNone
+ else
+ begin
+ FPath:=aPath+'[-1]';
+ result:=mstList;
+ end;
+ end
+ else if (s='<null>') or (s='<false>') or (s='') then
+ begin
+ Result:=mstNone;
+ end
+ else
+ begin
+ FPath:=aPath;
+ result:=mstSingle;
+ end;
+
+end;
+
+procedure TTestContext.PopSection(const aName: TMustacheString);
+begin
+ FPath:=Copy(FPath,1,RPos('.',FPath)-1);
+end;
+
+procedure TTestContext.SetValue(const aPath, aValue: string);
+begin
+ Values.Values[aPath]:=aValue;
+end;
+
+
+{ TBaseMustacheTest }
+
+procedure TBaseMustacheTest.SetUp;
+
+begin
+ Inherited;
+ FParser:=CreateParser;
+ FParser.Partials:=TMustachePartialList.Create(metRoot,Nil,0);
+ FParser.OnGetPartial:=@DoGetPartial;
+ FPartials:=TStringList.Create;
+ TStringList(FPartials).OwnsObjects:=True;
+end;
+
+procedure TBaseMustacheTest.TearDown;
+
+begin
+ FreeAndNil(FPartials);
+ FreeAndNil(FResult);
+ FParser.Partials.Free;
+ FreeAndNil(FParser);
+ Inherited;
+end;
+
+procedure TBaseMustacheTest.DoGetPartial(const aName: TMustacheString;
+ var aHandled: Boolean; var aValue: TMustacheString);
+begin
+ aValue:=FPartials.Values[aName];
+ aHandled:=FPartials.IndexOfName(aName)<>-1;
+end;
+
+class function TBaseMustacheTest.AssertElement(aParent: TMustacheElement;
+ aIndex: Integer; aType: TMustacheElementType; aData: String;
+ aClass: TMustacheElementClass): TMustacheElement;
+Var
+ El : TMustacheElement;
+ aChild : String;
+begin
+ AssertNotNull('Have parent',aParent);
+ AssertTrue(Format('Index %d in range 0..%d',[aIndex,aParent.ChildCount-1]),(aIndex>=0) and (aIndex<aParent.ChildCount));
+ EL:=aParent.Children[aIndex];
+ aChild:=Format('Child %d',[aIndex]);
+ AssertNotNull('Have result '+aChild,El);
+ AssertEquals(aChild+' has correct type',aType,El.ElementType);
+ AssertEquals(aChild+' has correct data',aData,El.Data);
+ if (aClass<>Nil) then
+ AssertEquals(aChild+' has correct class',aClass,el.Classtype);
+ Result:=El;
+end;
+
+function TBaseMustacheTest.AssertElement(aIndex: Integer;
+ aType: TMustacheElementType; aData: String; aClass : TMustacheElementClass = Nil): TMustacheElement;
+
+begin
+ AssertNotNull('Have result',FResult);
+ Result:=AssertElement(FResult,aIndex,aType,aData,aClass);
+end;
+
+procedure TBaseMustacheTest.AssertResultCount(aCount: Integer);
+begin
+ AssertNotNull('Have result',FResult);
+ AssertEquals('Result count',aCount,FResult.ChildCount);
+end;
+
+
+procedure TBaseMustacheTest.CallParser;
+
+begin
+ Parser.Template:=Template;
+ FResult:=Parser.Parse;
+end;
+
+procedure TBaseMustacheTest.AddPartial(const aName, aText: TMustacheString);
+
+//Var
+// T : TMustacheTextElement;
+
+begin
+// T:=TMustacheTextElement.Create(metText,Nil,0);
+// T.Data:=aText;
+ FPartials.Add(aName+'='+atext);
+end;
+
+class procedure TBaseMustacheTest.AssertEquals(Msg: String; aExpected,
+ aActual: TMustacheElementType);
+
+begin
+ AssertEquals(Msg,GetEnumName(typeInfo(TMustacheElementType),Ord(aExpected)),
+ GetEnumName(typeInfo(TMustacheElementType),Ord(aActual)));
+end;
+
+
+end.
+
diff --git a/packages/fcl-mustache/tests/tcdbmustache.pas b/packages/fcl-mustache/tests/tcdbmustache.pas
new file mode 100644
index 0000000000..07710c9493
--- /dev/null
+++ b/packages/fcl-mustache/tests/tcdbmustache.pas
@@ -0,0 +1,149 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ Test cases for DB Context for Mustache
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+unit tcdbmustache;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, fpcunit, testregistry, fpmustache, db, bufdataset, fpdbmustache;
+
+Type
+
+ { TTestMustacheDBContext }
+
+ TTestMustacheDBContext = Class(TTestCase)
+ private
+ FContext: TMustacheDBContext;
+ FDataset1: TBufDataset;
+ FDataset2: TBufDataset;
+ FMustache: TMustache;
+ Public
+ Procedure Setup; override;
+ Procedure TearDown; override;
+ Procedure CreateDataset1;
+ Procedure CreateDataset2;
+ Property Dataset1 : TBufDataset Read FDataset1;
+ Property Dataset2 : TBufDataset Read FDataset2;
+ Property Context : TMustacheDBContext Read FContext;
+ Property Mustache : TMustache Read FMustache;
+ Published
+ Procedure TestEmpty;
+ Procedure TestSingleSection;
+ Procedure TestTwoSections;
+ end;
+
+implementation
+
+Const
+ Template1 = '{{title}}! {{#Parents}}{{name}} {{age}} - {{/Parents}}';
+ Template2 = '{{title}}! {{#Parents}}{{name}}({{age}}) : {{#Children}}{{name}} {{age}},{{/Children}} - {{/Parents}}';
+
+{ TTestMustacheDBContext }
+
+procedure TTestMustacheDBContext.Setup;
+begin
+ Inherited;
+ FDataset1:=TBufDataset.Create(Nil);
+ FDataset1.Name:='Parents';
+ FDataset2:=TBufDataset.Create(Nil);
+ FDataset2.Name:='Children';
+ FContext:=TMustacheDBContext.Create(Nil);
+ FContext.StaticValues.Values['title']:='Family';
+ FMustache:=TMustache.Create(Nil);
+end;
+
+procedure TTestMustacheDBContext.TearDown;
+begin
+ FreeAndNil(FDataset1);
+ FreeAndNil(FDataset2);
+ FreeAndNil(FContext);
+ FreeAndNil(FMustache);
+end;
+
+procedure TTestMustacheDBContext.CreateDataset1;
+begin
+ FDataset1.FieldDefs.Add('name',ftString,20);
+ FDataset1.FieldDefs.Add('age',ftInteger);
+ FDataset1.CreateDataset;
+ FDataset1.Append;
+ FDataset1.FieldByName('name').AsString:='Father';
+ FDataset1.FieldByName('age').AsInteger:=40;
+ FDataset1.Post;
+ FDataset1.Append;
+ FDataset1.FieldByName('name').AsString:='Mother';
+ FDataset1.FieldByName('age').AsInteger:=39;
+ FDataset1.Post;
+ FDataset1.First;
+end;
+
+procedure TTestMustacheDBContext.CreateDataset2;
+begin
+ FDataset2.FieldDefs.Add('name',ftString,20);
+ FDataset2.FieldDefs.Add('age',ftInteger);
+ FDataset2.CreateDataset;
+ FDataset2.Append;
+ FDataset2.FieldByName('name').AsString:='Child1';
+ FDataset2.FieldByName('age').AsInteger:=4;
+ FDataset2.Post;
+ FDataset2.Append;
+ FDataset2.FieldByName('name').AsString:='Child2';
+ FDataset2.FieldByName('age').AsInteger:=2;
+ FDataset2.Post;
+ FDataset2.First;
+end;
+
+procedure TTestMustacheDBContext.TestEmpty;
+begin
+ AssertNotNull('Mustache',Mustache);
+ AssertNotNull('Dataset1',Dataset1);
+ AssertNotNull('Dataset2',Dataset2);
+ AssertNotNull('Context',Context);
+ AssertEquals('Context static','Family',Context.StaticValues.Values['title']);
+end;
+
+procedure TTestMustacheDBContext.TestSingleSection;
+
+Var
+ S : String;
+
+begin
+ Mustache.Template:=Template1;
+ CreateDataset1;
+ Context.AddDataset(FDataset1);
+ S:=Mustache.Render(Context);
+ AssertEquals('Correct result','Family! Father 40 - Mother 39 - ',S);
+end;
+
+procedure TTestMustacheDBContext.TestTwoSections;
+
+Var
+ S : String;
+
+begin
+ Mustache.Template:=Template2;
+ CreateDataset1;
+ CreateDataset2;
+ Context.AddDataset(FDataset1);
+ Context.AddDataset(FDataset2);
+ S:=Mustache.Render(Context);
+ AssertEquals('Correct result','Family! Father(40) : Child1 4,Child2 2, - Mother(39) : - ',S);
+end;
+
+initialization
+ RegisterTest(TTestMustacheDBContext);
+end.
+
diff --git a/packages/fcl-mustache/tests/tcexmustache.pas b/packages/fcl-mustache/tests/tcexmustache.pas
new file mode 100644
index 0000000000..b88ed9fba0
--- /dev/null
+++ b/packages/fcl-mustache/tests/tcexmustache.pas
@@ -0,0 +1,199 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ Test cases for expression parser support
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit tcexmustache;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, fpcunit, fpjson, testregistry, fpmustache, tcbasemustache, fpexmustache, fpexprpars;
+
+Type
+
+ { TTestExMustacheParser }
+
+ TTestExMustacheParser = Class(TBaseMustacheTest)
+ private
+ FExpr: TFPExpressionParser;
+ FOutput: TMustacheStringOutput;
+ FContext : TMustacheJSONContext;
+ FData : TJSONData;
+ procedure GetVar(var Result: TFPExpressionResult; ConstRef
+ AName: ShortString);
+ Public
+ Procedure SetUp; override;
+ Procedure TearDown; override;
+ Function CreateParser: TMustacheParser; override;
+ Property Expr : TFPExpressionParser Read FExpr;
+ Property Output : TMustacheStringOutput Read FOutput;
+ Published
+ Procedure TestSimple;
+ Procedure TestRenderSimple;
+ Procedure TestRenderSection;
+ end;
+
+ { TTestMustacheExpr }
+
+ TTestMustacheExpr = Class(TTestCase)
+ private
+ FJSON: TJSONObject;
+ FMustache: TMustacheExpr;
+ public
+ Procedure SetUp; override;
+ Procedure TearDown; override;
+ Property Mustache : TMustacheExpr Read FMustache;
+ Property JSON : TJSONObject Read FJSON;
+ Published
+ Procedure TestEmpty;
+ Procedure TestRegisterVariables;
+ Procedure TestRenderSection;
+ procedure TestRenderSectionStaticVariables;
+ end;
+
+
+implementation
+
+Const
+ STestJSON = '{ "data" : [ { "name": "me", "age" : 10}, { "name": "you", "age" : 12 }, { "name": "he", "age" : 13 } ] }';
+
+{ TTestMustacheExpr }
+
+procedure TTestMustacheExpr.SetUp;
+begin
+ inherited SetUp;
+ FMustache:=TMustacheExpr.Create(Nil);
+ FJSON:=GetJSON(STestJSON) as TJSONObject;
+end;
+
+procedure TTestMustacheExpr.TearDown;
+begin
+ FreeAndNil(FJSON);
+ FreeAndNil(FMustache);
+ inherited TearDown;
+end;
+
+procedure TTestMustacheExpr.TestEmpty;
+begin
+ AssertNotNull('Have mustache instance',Mustache);
+ AssertNotNull('Have mustache expression engine instance',Mustache.ExpressionParser);
+end;
+
+procedure TTestMustacheExpr.TestRegisterVariables;
+begin
+ Mustache.RegisterVariables(JSON,'data[0]',True);
+ AssertEquals('Variable count',2,Mustache.ExpressionParser.Identifiers.Count);
+ AssertEquals('Variable 0','name',Mustache.ExpressionParser.Identifiers[0].Name);
+ AssertEquals('Variable 1','age',Mustache.ExpressionParser.Identifiers[1].Name);
+ AssertTrue('Variable 0 type',rtString=Mustache.ExpressionParser.Identifiers[0].ResultType);
+ AssertTrue('Variable 1 type',rtInteger=Mustache.ExpressionParser.Identifiers[1].ResultType);
+end;
+
+procedure TTestMustacheExpr.TestRenderSection;
+
+Var
+ S : String;
+
+Const
+ Template = '{{#data}}{{[name]}}:{{[age>11]}} {{/data}}';
+
+begin
+ Mustache.Template:=Template;
+ Mustache.RegisterVariables(JSON,'data[0]',True);
+ S:=Mustache.Render(JSON);
+ AssertEquals('Correct result','me:False you:True he:True ',S);
+end;
+
+procedure TTestMustacheExpr.TestRenderSectionStaticVariables;
+Var
+ S : String;
+
+Const
+ Template = '{{#data}}{{[name]}}:{{[age>11]}} {{/data}}';
+
+begin
+ Mustache.Template:=Template;
+ Mustache.RegisterVariables(JSON,'data[0]',False);
+ S:=Mustache.Render(JSON);
+ AssertEquals('Correct result','me:False me:False me:False ',S);
+end;
+
+
+{ TTestExMustacheParser }
+
+procedure TTestExMustacheParser.SetUp;
+begin
+ FExpr:=TFPExpressionParser.Create(Nil);
+ Foutput:=TMustacheStringOutput.Create;
+ inherited SetUp;
+end;
+
+procedure TTestExMustacheParser.TearDown;
+begin
+ inherited TearDown;
+ FreeAndNil(FExpr);
+ FreeAndNil(Foutput);
+ FreeAndNil(FContext);
+ FreeAndNil(FData);
+end;
+
+function TTestExMustacheParser.CreateParser: TMustacheParser;
+
+Var
+ P : TMustacheExprParser;
+
+begin
+ P:=TMustacheExprParser.Create;
+ P.ExprParser:=FExpr;
+ Result:=P;
+end;
+
+procedure TTestExMustacheParser.TestSimple;
+begin
+ Template:='{{[1+2]}}';
+ CallParser;
+ AssertElement(0,metVariable,'1+2',TMustacheExprElement);
+end;
+
+procedure TTestExMustacheParser.TestRenderSimple;
+begin
+ TestSimple;
+ ParseResult.Children[0].Render(Nil,Output,'',False);
+ AssertEquals('Correct result','3',Output.Data);
+end;
+
+procedure TTestExMustacheParser.GetVar(Var Result : TFPExpressionResult; ConstRef AName : ShortString);
+
+begin
+ Result.ResultType:=rtInteger;
+ Result.ResInteger:=StrToINt(FContext.GetTextValue('age'));
+end;
+
+procedure TTestExMustacheParser.TestRenderSection;
+begin
+ FData:=GetJSON(STestJSON);
+ FContext:=TMustacheJSONContext.Create(FData,Nil);
+ FExpr.Identifiers.AddVariable('age',rtInteger,@GetVar);
+ Template:='{{#data}}{{{name}}}:{{[age>11]}} {{/data}}';
+ CallParser;
+ ParseResult.Render(FContext,Output,'',False);
+ AssertEquals('Correct result','me:False you:True he:True ',Output.Data);
+end;
+
+initialization
+ RegisterTests([TTestExMustacheParser,TTestMustacheExpr]);
+end.
+
diff --git a/packages/fcl-mustache/tests/tcmustache.pas b/packages/fcl-mustache/tests/tcmustache.pas
new file mode 100644
index 0000000000..6175044b0b
--- /dev/null
+++ b/packages/fcl-mustache/tests/tcmustache.pas
@@ -0,0 +1,728 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ Test cases for basic mustache parser support
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit tcmustache;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, fpcunit, testregistry, fpmustache, tcbasemustache;
+
+type
+
+ { TTestMustacheParser }
+
+
+ TTestMustacheParser= class(TBaseMustacheTest)
+ private
+ protected
+ Function CreateParser : TMustacheParser; override;
+ Public
+ procedure SetUp; override;
+ procedure TearDown; override;
+ published
+ procedure TestEmpty;
+ procedure TestText;
+ procedure TestVariable;
+ procedure TestVariableErrNonClosed;
+ procedure TestVariableAlternateStartStop;
+ procedure TestDottedVariable;
+ procedure TestVariableNoUnescape;
+ procedure TestVariableNoUnescapeErrNonClosed;
+ procedure TestVariableNoUnescapeAlternateStartStop;
+ procedure TestComment;
+ procedure TestCommentSurround;
+ procedure TestCommentStandalone;
+ procedure TestCommentStandaloneSpaced;
+ procedure TestSetDelimiter;
+ procedure TestSetDelimiterErrInvalid;
+ procedure TestSection;
+ procedure TestSectionNested;
+ procedure TestSectionErrNotClosed;
+ procedure TestSectionErrWrongClosed;
+ procedure TestSectionErrNotStarted;
+ procedure TestTextSection;
+ procedure TestPartial;
+ end;
+
+ { TTestMustacheOutput }
+
+ TTestMustacheOutput = class(TTestCase)
+ Published
+ Procedure TestStringOutput;
+ end;
+
+ { TTestMustacheElement }
+
+ TTestMustacheElement = class(TTestCase)
+ private
+ FContext: TTestContext;
+ FEl: TMustacheElement;
+ Foutput: TMustacheStringOutput;
+ procedure DoCallBack(const aName: TMustacheString; var aHandled: Boolean;
+ var aValue: TMustacheString);
+ Public
+ Procedure SetUp; override;
+ Procedure TearDown; override;
+ Property Context : TTestContext Read FContext;
+ Property Output : TMustacheStringOutput Read Foutput;
+ Property El : TMustacheElement Read FEl;
+ Published
+ Procedure TestEmpty;
+ Procedure TestTextElement;
+ Procedure TestTextElementNoEscape;
+ Procedure TestTextElementComment;
+ Procedure TestTextElementPrefix;
+ procedure TestTextElementPrefixNotLast;
+ procedure TestTextElementPrefixLast;
+ Procedure TestVariableElement;
+ Procedure TestVariableElementNoEscape;
+ Procedure TestVariableElementEscape;
+ Procedure TestSectionEmpty;
+ Procedure TestSectionValue;
+ Procedure TestSectionValueFalse;
+ Procedure TestSectionValueNull;
+ Procedure TestSectionValueEmptyArray;
+ Procedure TestSectionValueArray1El;
+ Procedure TestSectionValueArray2El;
+ Procedure TestSectionValueArray2ElValue;
+ Procedure TestSectionValueArray1ElValueSuper;
+ Procedure TestSectionValueArray2ElValueSuper;
+ Procedure TestParentElement;
+ Procedure TestParentElementRender;
+ Procedure TestParentElementRenderPrefix;
+ end;
+
+implementation
+
+uses Typinfo;
+
+Const
+ SNeedsQuoting = '< > & "';
+ SQuotedResult = '&lt; &gt; &amp; &quot;';
+
+
+{ TTestMustacheElement }
+
+procedure TTestMustacheElement.DoCallBack(const aName: TMustacheString;
+ var aHandled: Boolean; var aValue: TMustacheString);
+begin
+ aValue:='';
+end;
+
+procedure TTestMustacheElement.SetUp;
+begin
+ inherited SetUp;
+ FOutput:=TMustacheStringOutput.Create;
+ FContext:=TTestContext.Create(@DoCallBack);
+end;
+
+procedure TTestMustacheElement.TearDown;
+begin
+ FreeAndNil(FContext);
+ FreeAndNil(FOutput);
+ FreeAndNil(FEl);
+ inherited TearDown;
+end;
+
+procedure TTestMustacheElement.TestEmpty;
+begin
+ AssertNotNull('Have output',Output);
+end;
+
+procedure TTestMustacheElement.TestTextElement;
+
+begin
+ Fel:=TMustacheTextElement.Create(metText,Nil,0);
+ El.Render(Nil,Output);
+ AssertEquals('No output','',Output.Data);
+ El.Data:='me';
+ El.Render(Nil,Output);
+ AssertEquals('Correct output','me',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestTextElementNoEscape;
+begin
+ Fel:=TMustacheTextElement.Create(metText,Nil,0);
+ El.Data:=SNeedsQuoting;
+ El.Render(Nil,Output);
+ AssertEquals('Correct output',SNeedsQuoting,Output.Data);
+end;
+
+procedure TTestMustacheElement.TestTextElementComment;
+begin
+ Fel:=TMustacheTextElement.Create(metComment,Nil,0);
+ El.Data:='Something';
+ El.Render(Nil,Output);
+ AssertEquals('Correct output','',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestTextElementPrefix;
+begin
+ Fel:=TMustacheTextElement.Create(metText,Nil,0);
+ El.Data:='me'#10'you';
+ El.Render(Nil,Output,' ');
+ AssertEquals('Correct output 1','me'#10' you',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestTextElementPrefixNotLast;
+begin
+ Fel:=TMustacheTextElement.Create(metText,Nil,0);
+ El.Data:='me'#10'you'#10;
+ El.Render(Nil,Output,' ');
+ AssertEquals('Correct output 2','me'#10' you'#10' ',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestTextElementPrefixLast;
+
+begin
+ Fel:=TMustacheTextElement.Create(metText,Nil,0);
+ El.Data:='me'#10'you'#10;
+ El.Render(Nil,Output,' ',True);
+ AssertEquals('Correct output 2','me'#10' you'#10,Output.Data);
+end;
+
+
+procedure TTestMustacheElement.TestVariableElement;
+begin
+ Fel:=TMustacheVariableElement.Create(metText,Nil,0);
+ Context.Values.Values['name']:='abc';
+ El.Data:='name';
+ El.Render(Context,Output);
+ AssertEquals('Correct output','abc',Output.Data);
+end;
+
+
+procedure TTestMustacheElement.TestVariableElementNoEscape;
+begin
+ Fel:=TMustacheVariableElement.Create(metText,Nil,0);
+ Context.Values.Values['name']:=SNeedsQuoting;
+ El.Data:='{name}';
+ El.Render(Context,Output);
+ AssertEquals('Correct output',SNeedsQuoting,Output.Data);
+end;
+
+procedure TTestMustacheElement.TestVariableElementEscape;
+begin
+ Fel:=TMustacheVariableElement.Create(metText,Nil,0);
+ Context.Values.Values['name']:=SNeedsQuoting;
+ El.Data:='name';
+ El.Render(Context,Output);
+ AssertEquals('Correct output',SQuotedResult,Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionEmpty;
+
+Var
+ T : TMustacheTextElement;
+
+begin
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('No output','',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValue;
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','b');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('Single pass','a',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueFalse;
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','<false>');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('no pass','',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueNull;
+
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','<null>');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('no pass','',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueEmptyArray;
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','[]');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('no pass','',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueArray1El;
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','[]');
+ Context.SetValue('s[0]','toto');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('Single pass','a',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueArray2El;
+
+Var
+ T : TMustacheTextElement;
+
+begin
+ Context.SetValue('s','[]');
+ Context.SetValue('s[0]','toto');
+ Context.SetValue('s[1]','tata');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheTextElement.Create(metText,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('Double pass','aa',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueArray2ElValue;
+
+Var
+ T : TMustacheElement;
+
+begin
+ Context.SetValue('s','[]');
+ Context.SetValue('s[0]','{}');
+ Context.SetValue('s[0].a','1');
+ Context.SetValue('s[1]','{}');
+ Context.SetValue('s[1].a','2');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheVariableElement.Create(metVariable,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('Double pass','12',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueArray1ElValueSuper;
+
+Var
+ T : TMustacheElement;
+
+begin
+ Context.SetValue('s','[]');
+ Context.SetValue('s[0]','{}');
+ Context.SetValue('s[0].b','1');
+ Context.SetValue('a','2');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheVariableElement.Create(metVariable,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ Fel.Render(Context,Output);
+ AssertEquals('Single pass','2',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestSectionValueArray2ElValueSuper;
+Var
+ T : TMustacheElement;
+
+begin
+ Context.SetValue('s','[]');
+ Context.SetValue('s[0]','{}');
+ Context.SetValue('s[0].b','1');
+ Context.SetValue('s[1]','{}');
+ Context.SetValue('s[1].b','2');
+ Context.SetValue('a','.a.');
+ Fel:=TMustacheSectionElement.Create(metSection,Nil,0);
+ Fel.Data:='s';
+ T:=TMustacheVariableElement.Create(metVariable,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='a';
+ T:=TMustacheVariableElement.Create(metVariable,Nil,0);
+ Fel.AddChild(T);
+ T.Data:='b';
+ Fel.Render(Context,Output);
+ AssertEquals('Single pass','.a.1.a.2',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestParentElement;
+
+Var
+ SEl : TMustacheElement;
+
+begin
+ Fel:=TMustacheParentElement.Create(metSection,Nil,0);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ AssertSame('Parent stored',Fel,Sel.Parent);
+ AssertEquals('Not added to parent',0,FEl.ChildCount);
+ Fel.AddChild(Sel);
+ AssertEquals('added to parent - count',1,FEl.ChildCount);
+ AssertSame('added to parent - stored',Sel,FEl.Children[0]);
+end;
+
+procedure TTestMustacheElement.TestParentElementRender;
+
+Var
+ SEl : TMustacheElement;
+
+begin
+ Fel:=TMustacheParentElement.Create(metSection,Nil,0);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='a';
+ Fel.AddChild(Sel);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='b';
+ Fel.AddChild(Sel);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='c';
+ Fel.AddChild(Sel);
+ Fel.Render(Context,Output);
+ AssertEquals('Correct output','abc',Output.Data);
+end;
+
+procedure TTestMustacheElement.TestParentElementRenderPrefix;
+Var
+ SEl : TMustacheElement;
+
+begin
+ Fel:=TMustacheParentElement.Create(metSection,Nil,0);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='a'#10'b';
+ Fel.AddChild(Sel);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='d'#10'e';
+ Fel.AddChild(Sel);
+ Sel:=TMustacheTextElement.Create(metText,Fel,0);
+ Sel.Data:='f'#10;
+ Fel.AddChild(Sel);
+ Fel.Render(Context,Output,' ');
+ AssertEquals('Correct output','a'#10' bd'#10' ef'#10,Output.Data);
+end;
+
+{ TTestMustacheOutput }
+
+procedure TTestMustacheOutput.TestStringOutput;
+
+Var
+ SO : TMustacheStringOutput;
+
+begin
+ SO:=TMustacheStringOutput.Create;
+ try
+ AssertEquals('Empty start','',SO.Data);
+ SO.Output('abc');
+ AssertEquals('Output 1','abc',SO.Data);
+ SO.Output('def');
+ AssertEquals('Output 2','abcdef',SO.Data);
+ finally
+ SO.Free;
+ end;
+
+end;
+
+function TTestMustacheParser.CreateParser: TMustacheParser;
+begin
+ Result:=TMustacheParser.Create;
+end;
+
+procedure TTestMustacheParser.SetUp;
+begin
+ inherited SetUp;
+end;
+
+procedure TTestMustacheParser.TearDown;
+begin
+ inherited TearDown;
+end;
+
+procedure TTestMustacheParser.TestEmpty;
+
+begin
+ AssertNotNull('Have parser',Parser);
+ AssertNull('Have no result',ParseResult);
+ AssertEquals('Have no template','',Template);
+end;
+
+
+procedure TTestMustacheParser.TestText;
+
+begin
+ Template:='a simple text';
+ CallParser;
+ AssertResultCount(1);
+ AssertElement(0,metText,'a simple text');
+end;
+
+procedure TTestMustacheParser.TestVariable;
+
+Var
+ el : TMustacheVariableElement;
+
+begin
+ Template:='{{a}}';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metVariable,'a',TMustacheVariableElement) as TMustacheVariableElement;
+ AssertFalse('unescape',El.NoUnescape);
+end;
+
+procedure TTestMustacheParser.TestVariableErrNonClosed;
+
+begin
+ Template:='{{a';
+ AssertException('Have error',EMustache,@CallParser,'Tag {{ opened on position 1 but not closed.');
+ Template:='{{a}';
+ AssertException('Have error',EMustache,@CallParser,'Tag {{ opened on position 1 but not closed.');
+end;
+
+procedure TTestMustacheParser.TestVariableAlternateStartStop;
+Var
+ el : TMustacheVariableElement;
+
+begin
+ Parser.StartTag:='<<';
+ Parser.StopTag:='>>';
+ Template:='<<a>>';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metVariable,'a',TMustacheVariableElement) as TMustacheVariableElement;
+ AssertFalse('unescape',El.NoUnescape);
+end;
+
+procedure TTestMustacheParser.TestDottedVariable;
+begin
+ Template:='{{a.b}}';
+ CallParser;
+ AssertResultCount(1);
+ AssertElement(0,metVariable,'a.b');
+end;
+
+procedure TTestMustacheParser.TestVariableNoUnescape;
+Var
+ el : TMustacheVariableElement;
+
+begin
+ Template:='{{{a}}}';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metVariable,'a',TMustacheVariableElement) as TMustacheVariableElement;
+ AssertTrue('unescape',El.NoUnescape);
+end;
+
+procedure TTestMustacheParser.TestVariableNoUnescapeErrNonClosed;
+begin
+ Template:='{{{a';
+ AssertException('Have error',EMustache,@CallParser,'Tag {{ opened on position 1 but not closed.');
+ Template:='{{{a}';
+ AssertException('Have error',EMustache,@CallParser,'Tag {{ opened on position 1 but not closed.');
+ Template:='{{{a}}';
+ AssertException('Have error',EMustache,@CallParser,'Tag {{ opened on position 1 but not closed.');
+end;
+
+procedure TTestMustacheParser.TestVariableNoUnescapeAlternateStartStop;
+
+Var
+ el : TMustacheVariableElement;
+
+begin
+ Parser.StartTag:='<<';
+ Parser.StopTag:='>>';
+ Template:='<<{a}>>';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metVariable,'a',TMustacheVariableElement) as TMustacheVariableElement;
+ AssertTrue('unescape',El.NoUnescape);
+end;
+
+procedure TTestMustacheParser.TestComment;
+
+begin
+ Parser.StartTag:='<<';
+ Parser.StopTag:='>>';
+ Template:='<<! a comment>>';
+ CallParser;
+ AssertResultCount(1);
+ AssertElement(0,metComment,' a comment',TMustacheTextElement);
+end;
+
+procedure TTestMustacheParser.TestCommentSurround;
+begin
+ Template:='ab{{! a comment}}cd';
+ CallParser;
+ AssertResultCount(3);
+ AssertElement(0,metText,'ab',TMustacheTextElement);
+ AssertElement(1,metComment,' a comment',TMustacheTextElement);
+ AssertElement(2,metText,'cd',TMustacheTextElement);
+end;
+
+procedure TTestMustacheParser.TestCommentStandalone;
+begin
+ Template:='a'+sLineBreak+'{{! a comment}}'+sLineBreak+'b';
+ CallParser;
+ AssertResultCount(3);
+ AssertElement(0,metText,'a'+sLineBreak,TMustacheTextElement);
+ AssertElement(1,metComment,' a comment',TMustacheTextElement);
+ AssertElement(2,metText,'b',TMustacheTextElement);
+end;
+
+procedure TTestMustacheParser.TestCommentStandaloneSpaced;
+begin
+ Template:='a'+sLineBreak+' {{! a comment}} '+sLineBreak+'b';
+ CallParser;
+ AssertResultCount(3);
+ AssertElement(0,metText,'a'+sLineBreak,TMustacheTextElement);
+ AssertElement(1,metComment,' a comment',TMustacheTextElement);
+ AssertElement(2,metText,'b',TMustacheTextElement);
+end;
+
+procedure TTestMustacheParser.TestSetDelimiter;
+
+begin
+ Template:='{{=<< >>=}}<<! a comment>>';
+ CallParser;
+ AssertResultCount(1);
+ AssertElement(0,metComment,' a comment',TMustacheTextElement);
+end;
+
+procedure TTestMustacheParser.TestSetDelimiterErrInvalid;
+begin
+ Template:='{{=== ===}}';
+ AssertException('Have error',EMustache,@CallParser,'Invalid set delimiter Stop value: == in "== =="');
+end;
+
+procedure TTestMustacheParser.TestSection;
+
+Var
+ el : TMustacheSectionElement;
+
+begin
+ Template:='{{#a}}{{/a}}';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metSection,'a',TMustacheSectionElement) as TMustacheSectionElement;
+ AssertEquals('No elements in section',0,el.ChildCount);
+end;
+
+procedure TTestMustacheParser.TestSectionNested;
+
+Var
+ el : TMustacheSectionElement;
+
+begin
+ Template:='{{#a}}{{#b}}{{/b}}{{/a}}';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metSection,'a',TMustacheSectionElement) as TMustacheSectionElement;
+ AssertEquals('elements in section',1,el.ChildCount);
+ el:=AssertElement(el,0,metSection,'b',TMustacheSectionElement) as TMustacheSectionElement;
+ AssertEquals('elements in section sub',0,el.ChildCount);
+end;
+
+procedure TTestMustacheParser.TestSectionErrNotClosed;
+
+begin
+ Template:='{{#a}}';
+ AssertException('Have error',EMustache,@CallParser,'Structural error: Section "a" on position 1 is not closed.');
+end;
+
+procedure TTestMustacheParser.TestSectionErrWrongClosed;
+begin
+ Template:='{{#a}}{{#b}}{{/a}}{{/b}}';
+ AssertException('Have error',EMustache,@CallParser,'Structural error: Section "b" on position 7 is closed by tag "a" on position 13.');
+end;
+
+procedure TTestMustacheParser.TestSectionErrNotStarted;
+begin
+ Template:='{{/a}}';
+ AssertException('Have error',EMustache,@CallParser,'Structural error: Section "a" on position 1 was never opened.');
+end;
+
+procedure TTestMustacheParser.TestTextSection;
+
+Var
+ el : TMustacheSectionElement;
+
+begin
+ Template:='{{#a}}bbb{{/a}}';
+ CallParser;
+ AssertResultCount(1);
+ el:=AssertElement(0,metSection,'a',TMustacheSectionElement) as TMustacheSectionElement;
+ AssertEquals('No elements in section',1,el.ChildCount);
+ AssertElement(el,0,metText,'bbb');
+end;
+
+procedure TTestMustacheParser.TestPartial;
+
+Var
+ el : TMustachePartialElement;
+
+begin
+ AddPartial('part','bcd');
+ Template:='a{{>part}}e';
+ CallParser;
+ AssertResultCount(3);
+ AssertElement(0,metText,'a',TMustacheTextElement);
+ el:=AssertElement(1,metPartial,'part',TMustachePartialElement) as TMustachePartialElement;
+ AssertElement(2,metText,'e',TMustacheTextElement);
+ AssertEquals('Correct partial','part',El.Partial.Data);
+ AssertEquals('Correct partial',1,El.Partial.ChildCount);
+ AssertElement(el.Partial,0,metText,'bcd',TMustacheTextElement);
+end;
+
+
+initialization
+ RegisterTests([TTestMustacheParser,TTestMustacheOutput,TTestMustacheElement]);
+end.
+
diff --git a/packages/fcl-mustache/tests/tcspecs.pas b/packages/fcl-mustache/tests/tcspecs.pas
new file mode 100644
index 0000000000..5892733e37
--- /dev/null
+++ b/packages/fcl-mustache/tests/tcspecs.pas
@@ -0,0 +1,188 @@
+{
+ This file is part of the Free Pascal Run time library.
+ Copyright (c) 2021 by Michael Van Canneyt (michael@freepascal.org)
+
+ testcase for official Mustache tests
+
+ See the File COPYING.FPC, included in this distribution,
+ for details about the copyright.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+unit tcspecs;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ Classes, SysUtils, fpcunit, testregistry, fpmustache, fpjson, jsonparser;
+
+Type
+
+ { TTestMustacheSpecs }
+
+ TTestMustacheSpecs = class(TTestCase)
+ private
+ FTests: TJSONArray;
+ procedure RunMustacheTest(aIndex: Integer; aTest: TJSONObject);
+ Public
+ class var BaseDir : string;
+ Public
+ Procedure Setup; override;
+ Procedure TearDown; override;
+ Procedure DoTest(aFileName : string);
+ Property Tests : TJSONArray Read FTests;
+ Published
+ Procedure TestComments;
+ Procedure TestDelimiters;
+ Procedure TestInterpolation;
+ Procedure TestInverted;
+ Procedure TestPartials;
+ Procedure TestSections;
+ end;
+
+
+implementation
+
+{ TTestMustacheSpecs }
+
+procedure TTestMustacheSpecs.RunMustacheTest(aIndex : Integer; aTest : TJSONObject);
+
+Var
+ M : TMustache;
+ aTempl,aErr,aRes,aName : TMustacheString;
+ Parts : TJSONObject;
+ I : Integer;
+ Ok : Boolean;
+
+ Procedure TreeDump;
+
+ begin
+ if not OK then
+ begin
+ Writeln('Tree dump:');
+ Writeln(M.Dump);
+ end;
+ end;
+
+ Procedure InputDump;
+
+ begin
+ Writeln('Test : ',aIndex);
+ writeln(aTempl);
+ writeln(StringReplace(StringReplace(aTempl,#10,' ',[rfReplaceAll]),#13,' ',[rfReplaceAll]));
+ aName:='';
+ While Length(aName)<Length(aTempl) do
+ aName:=AName+'1234567890';
+ Writeln(aName);
+ end;
+
+begin
+ OK:=False;
+ aTempl:=aTest.Get('template','');
+ // InputDump;
+ M:=TMustache.CreateMustache(Nil,aTempl);
+ try
+ // Load partials
+ Parts:=aTest.Get('partials',TJSONObject(Nil));
+ if Assigned(Parts) then
+ for I:=0 to Parts.Count-1 do
+ M.Partials.Add(Parts.Names[i]+'='+Parts.Items[i].AsString);
+ // Set test name and run tests
+ aName:='Test '+IntToStr(aIndex)+': '+aTest.Get('name','');
+ Try
+ aErr:='';
+ aRes:=m.Render(aTest.Get('data',TJSONObject(Nil)));
+ except
+ on e : exception do
+ aErr:=E.ClassName+' '+E.message;
+ end;
+ if aErr<>'' then
+ Fail(aName+': Unexpected error: '+aErr);
+ AssertEquals(aName,aTest.Get('expected',''),aRes);
+ OK:=true;
+ finally
+ // TreeDump;
+ M.Free;
+ end;
+end;
+
+procedure TTestMustacheSpecs.Setup;
+begin
+ inherited Setup;
+end;
+
+procedure TTestMustacheSpecs.TearDown;
+begin
+ inherited TearDown;
+end;
+
+procedure TTestMustacheSpecs.DoTest(aFileName: string);
+
+Var
+ I : Integer;
+ F : TFileStream;
+ D : TJSONData;
+ FN : String;
+
+begin
+ D:=Nil;
+ FN:=IncludeTrailingPathDelimiter(BaseDir)+aFileName+'.json';
+ F:=TFileStream.Create(FN,fmOpenRead or fmShareDenyWrite);
+ try
+ D:=GetJSON(F);
+ if D is TJSONObject then
+ begin
+ Ftests:=(D as TJSONObject).Get('tests',TJSONArray(Nil));
+ if (FTests=Nil) then
+ Fail('Invalid mustache tests in '+FN);
+ end
+ else
+ Fail('Invalid JSON object in '+FN);
+ For I:=0 to Tests.Count-1 do
+ RunMustacheTest(I,Tests.Items[i] as TJSONObject);
+ finally
+ D.Free;
+ F.Free;
+ end;
+end;
+
+procedure TTestMustacheSpecs.TestComments;
+begin
+ DoTest('comments');
+end;
+
+procedure TTestMustacheSpecs.TestDelimiters;
+begin
+ DoTest('delimiters');
+end;
+
+procedure TTestMustacheSpecs.TestInterpolation;
+begin
+ DoTest('interpolation');
+end;
+
+procedure TTestMustacheSpecs.TestInverted;
+begin
+ DoTest('inverted');
+end;
+
+procedure TTestMustacheSpecs.TestPartials;
+begin
+ DoTest('partials');
+end;
+
+procedure TTestMustacheSpecs.TestSections;
+begin
+ DoTest('sections');
+end;
+
+begin
+ TTestMustacheSpecs.BaseDir:='spec/';
+ RegisterTest(TTestMustacheSpecs);
+end.
+
diff --git a/packages/fcl-mustache/tests/testmustache.lpi b/packages/fcl-mustache/tests/testmustache.lpi
new file mode 100644
index 0000000000..6a8b3c1be6
--- /dev/null
+++ b/packages/fcl-mustache/tests/testmustache.lpi
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+ <ProjectOptions>
+ <Version Value="12"/>
+ <General>
+ <Flags>
+ <SaveOnlyProjectUnits Value="True"/>
+ <MainUnitHasCreateFormStatements Value="False"/>
+ <MainUnitHasTitleStatement Value="False"/>
+ <MainUnitHasScaledStatement Value="False"/>
+ </Flags>
+ <SessionStorage Value="InProjectDir"/>
+ <Title Value="testmustache"/>
+ <UseAppBundle Value="False"/>
+ <ResourceType Value="res"/>
+ </General>
+ <BuildModes>
+ <Item Name="Default" Default="True"/>
+ </BuildModes>
+ <PublishOptions>
+ <Version Value="2"/>
+ <UseFileFilters Value="True"/>
+ </PublishOptions>
+ <RunParams>
+ <FormatVersion Value="2"/>
+ </RunParams>
+ <RequiredPackages>
+ <Item>
+ <PackageName Value="FCL"/>
+ </Item>
+ </RequiredPackages>
+ <Units>
+ <Unit>
+ <Filename Value="testmustache.lpr"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ <Unit>
+ <Filename Value="tcmustache.pas"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ <Unit>
+ <Filename Value="tcspecs.pas"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ <Unit>
+ <Filename Value="tcexmustache.pas"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ <Unit>
+ <Filename Value="tcbasemustache.pas"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ <Unit>
+ <Filename Value="tcdbmustache.pas"/>
+ <IsPartOfProject Value="True"/>
+ </Unit>
+ </Units>
+ </ProjectOptions>
+ <CompilerOptions>
+ <Version Value="11"/>
+ <Target>
+ <Filename Value="testmustache"/>
+ </Target>
+ <SearchPaths>
+ <IncludeFiles Value="$(ProjOutDir)"/>
+ <OtherUnitFiles Value="../src"/>
+ <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+ </SearchPaths>
+ <Linking>
+ <Debugging>
+ <UseHeaptrc Value="True"/>
+ </Debugging>
+ </Linking>
+ </CompilerOptions>
+ <Debugging>
+ <Exceptions>
+ <Item>
+ <Name Value="EAbort"/>
+ </Item>
+ <Item>
+ <Name Value="ECodetoolError"/>
+ </Item>
+ <Item>
+ <Name Value="EFOpenError"/>
+ </Item>
+ </Exceptions>
+ </Debugging>
+</CONFIG>
diff --git a/packages/fcl-mustache/tests/testmustache.lpr b/packages/fcl-mustache/tests/testmustache.lpr
new file mode 100644
index 0000000000..421c0a679c
--- /dev/null
+++ b/packages/fcl-mustache/tests/testmustache.lpr
@@ -0,0 +1,29 @@
+program testmustache;
+
+{$mode objfpc}{$H+}
+
+uses
+ Classes, consoletestrunner, tcmustache, tcspecs,
+ tcexmustache, tcbasemustache, tcdbmustache;
+
+type
+
+ { TMyTestRunner }
+
+ TMyTestRunner = class(TTestRunner)
+ protected
+ // override the protected methods of TTestRunner to customize its behavior
+ end;
+
+var
+ Application: TMyTestRunner;
+
+begin
+ DefaultFormat:=fPlain;
+ DefaultRunAllTests:=True;
+ Application := TMyTestRunner.Create(nil);
+ Application.Initialize;
+ Application.Title := 'FPCUnit Console test runner';
+ Application.Run;
+ Application.Free;
+end.
diff --git a/packages/fpmake_add.inc b/packages/fpmake_add.inc
index 7cdfb19c13..63d4724bfd 100644
--- a/packages/fpmake_add.inc
+++ b/packages/fpmake_add.inc
@@ -145,4 +145,5 @@
add_ide(ADirectory+IncludeTrailingPathDelimiter('ide'));
add_vclcompat(ADirectory+IncludeTrailingPathDelimiter('vcl-compat'));
add_qlunits(ADirectory+IncludeTrailingPathDelimiter('qlunits'));
+ add_mustache(ADirectory+IncludeTrailingPathDelimiter('fcl-mustache'));
diff --git a/packages/fpmake_proc.inc b/packages/fpmake_proc.inc
index 169e4ad060..799737deb7 100644
--- a/packages/fpmake_proc.inc
+++ b/packages/fpmake_proc.inc
@@ -821,4 +821,10 @@ begin
{$include qlunits/fpmake.pp}
end;
+procedure add_mustache(const ADirectory: string);
+begin
+ with Installer do
+{$include fcl-mustache/fpmake.pp}
+end;
+
{$include ide/fpmake.pp}