delphimvcframework/lib/dmustache/mormot.core.mustache.pas
2024-04-29 15:40:45 +02:00

2564 lines
84 KiB
ObjectPascal

/// Framework Core {{mustache}} Templates Renderer
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.core.mustache;
{
*****************************************************************************
Logic-Less Mustache Templates Rendering
- Mustache Execution Data Context Types
- TSynMustache Template Processing
*****************************************************************************
}
interface
{$I mormot.defines.inc}
uses
classes,
sysutils,
variants,
mormot.core.base,
mormot.core.os,
mormot.core.unicode,
mormot.core.text,
mormot.core.search, // for TSynMustache.Match helper
mormot.core.buffers,
mormot.core.datetime,
mormot.core.rtti,
mormot.core.json,
mormot.core.data,
mormot.core.variants;
{ ************ Mustache Execution Data Context Types }
type
/// exception raised during process of a {{mustache}} template
ESynMustache = class(ESynException);
/// identify the {{mustache}} tag kind
// - mtVariable if the tag is a variable - e.g. {{myValue}} - or an Expression
// Helper - e.g. {{helperName valueName}}
// - mtVariableUnescape, mtVariableUnescapeAmp to unescape the variable HTML - e.g.
// {{{myRawValue}}} or {{& name}}
// - mtSection and mtInvertedSection for sections beginning - e.g.
// {{#person}} or {{^person}}
// - mtSectionEnd for sections ending - e.g. {{/person}}
// - mtComment for comments - e.g. {{! ignore me}}
// - mtPartial for partials - e.g. {{> next_more}}
// - mtSetPartial for setting an internal partial - e.g.
// {{<foo}}This is the foo partial {{myValue}} template{{/foo}}
// - mtSetDelimiter for setting custom delimeter symbols - e.g. {{=<% %>=}} -
// Warning: current implementation only supports two character delimiters
// - mtTranslate for content i18n via a callback - e.g. {{"English text}}
// - mtText for all text that appears outside a symbol
TSynMustacheTagKind = (
mtVariable,
mtVariableUnescape,
mtVariableUnescapeAmp,
mtSection,
mtInvertedSection,
mtSectionEnd,
mtComment,
mtPartial,
mtSetPartial,
mtSetDelimiter,
mtTranslate,
mtText);
/// store a {{mustache}} tag
TSynMustacheTag = record
/// points to the mtText buffer start
// - main template's text is not allocated as a separate string during
// parsing, but will rather be copied directly from the template memory
TextStart: PUtf8Char;
/// stores the mtText buffer length
TextLen: integer;
/// the index in Tags[] of the other end of this section (16-bit)
// - either the index of mtSectionEnd for mtSection/mtInvertedSection
// - or the index of mtSection/mtInvertedSection for mtSectionEnd
SectionOppositeIndex: SmallInt;
/// the kind of the tag
Kind: TSynMustacheTagKind;
/// if the Value has an included ' ' within, i.e. could be an helper
// - equals PosExChar(' ', Value)
ValueSpace: byte;
/// the tag content, excluding trailing {{ }} and corresponding symbol
// - is not set for mtText nor mtSetDelimiter
Value: RawUtf8;
end;
/// store all {{mustache}} tags of a given template
TSynMustacheTagDynArray = array of TSynMustacheTag;
/// states the section content according to a given value
// - msNothing for false values or empty lists
// - msSingle for non-false values but not a list
// - msSinglePseudo is for *-first *-last *-odd and helper values
// - msList for non-empty lists
TSynMustacheSectionType = (
msNothing,
msSingle,
msSinglePseudo,
msList);
TSynMustache = class;
/// callback signature used to process an Expression Helper variable
// - i.e. {{helperName value}} tags
// - returned value will be used to process as replacement of a single {{tag}}
TSynMustacheHelperEvent =
procedure(const Value: variant; out Result: variant) of object;
/// used to store a registered Expression Helper implementation
TSynMustacheHelper = record
/// the Expression Helper name
Name: RawUtf8;
/// the corresponding callback to process the tag
Event: TSynMustacheHelperEvent;
end;
/// used to store all registered Expression Helpers
// - i.e. {{helperName value}} tags
// - use TSynMustache.HelperAdd/HelperDelete class methods to manage the list
// or retrieve standard helpers via TSynMustache.HelpersGetStandardList
TSynMustacheHelpers = array of TSynMustacheHelper;
TSynMustachePartials = class;
/// handle {{mustache}} template rendering context, i.e. all values
// - this abstract class should not be used directly, but rather any
// other overridden class
TSynMustacheContext = class
protected
fContextCount: integer;
fEscapeInvert: boolean;
fOwnWriter: boolean;
fGetVarDataFromContextNeedsFree: boolean;
fPathDelim: AnsiChar;
fWriter: TJsonWriter;
fHelpers: TSynMustacheHelpers;
fPartials: TSynMustachePartials;
fTempProcessHelper: TVariantDynArray;
fOnStringTranslate: TOnStringTranslate;
fOwner: TSynMustache;
fReuse: TLightLock;
// some variant support is needed for the helpers
function ProcessHelper(const ValueName: RawUtf8; space, helper: PtrInt;
var Value: TVarData; OwnValue: PPVarData): TSynMustacheSectionType; virtual;
function GetHelperFromContext(ValueSpace: integer; const ValueName: RawUtf8;
var Value: TVarData; OwnValue: PPVarData): TSynMustacheSectionType;
procedure TranslateBlock(Text: PUtf8Char; TextLen: Integer); virtual;
function GetVariantFromContext(const ValueName: RawUtf8): variant;
procedure PopContext;
procedure AppendVariant(const Value: variant; UnEscape: boolean);
// inherited class should override those methods
function GotoNextListItem: boolean;
virtual; abstract;
function GetVarDataFromContext(ValueSpace: integer; const ValueName: RawUtf8;
var Value: TVarData): TSynMustacheSectionType; virtual; abstract;
procedure AppendValue(ValueSpace: integer; const ValueName: RawUtf8;
UnEscape: boolean); virtual; abstract;
function AppendSection(ValueSpace: integer;
const ValueName: RawUtf8): TSynMustacheSectionType; virtual; abstract;
public
/// initialize the rendering context for the given text writer
constructor Create(Owner: TSynMustache; WR: TJsonWriter; OwnWR: boolean);
/// release this rendering context instance
destructor Destroy; override;
/// allow to reuse this Mustache template rendering context
procedure CancelAll;
/// the registered Expression Helpers, to handle {{helperName value}} tags
// - use TSynMustache.HelperAdd/HelperDelete class methods to manage the list
// or retrieve standard helpers via TSynMustache.HelpersGetStandardList
property Helpers: TSynMustacheHelpers
read fHelpers write fHelpers;
/// access to the custom Partials associated with this execution context
property Partials: TSynMustachePartials
read fPartials write fPartials;
/// access to the {{"English text}} translation callback
property OnStringTranslate: TOnStringTranslate
read fOnStringTranslate write fOnStringTranslate;
/// read-only access to the associated text writer instance
property Writer: TJsonWriter
read fWriter;
/// invert the HTML characters escaping process
// - by default, {{value}} will escape value chars, and {{{value}} won't
// - set this property to true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
property EscapeInvert: boolean
read fEscapeInvert write fEscapeInvert;
/// the path delimited for getting a value
// - equals '.' by default
property PathDelim: AnsiChar
read fPathDelim write fPathDelim;
end;
/// handle {{mustache}} template rendering context from a custom variant
// - the context is given via a custom variant type implementing
// TSynInvokeableVariantType.Lookup, e.g. TDocVariant or TSMVariant
TSynMustacheContextVariant = class(TSynMustacheContext)
protected
fContext: array of record
Document: TVarData;
DocumentType: TSynInvokeableVariantType;
ListCount: integer;
ListCurrent: integer;
ListCurrentDocument: TVarData;
ListCurrentDocumentType: TSynInvokeableVariantType;
end;
procedure PushContext(const aDoc: TVarData);
function GotoNextListItem: boolean; override;
function GetVarDataFromContext(ValueSpace: integer; const ValueName: RawUtf8;
var Value: TVarData): TSynMustacheSectionType; override;
procedure AppendValue(ValueSpace: integer; const ValueName: RawUtf8;
UnEscape: boolean); override;
function AppendSection(ValueSpace: integer;
const ValueName: RawUtf8): TSynMustacheSectionType; override;
public
/// initialize the context from a custom variant document
// - note that the aDocument instance shall be available during all
// lifetime of this TSynMustacheContextVariant instance
// - you should not use this constructor directly, but the
// corresponding TSynMustache.Render*() methods
constructor Create(Owner: TSynMustache; WR: TJsonWriter;
SectionMaxCount: integer; const aDocument: variant; OwnWriter: boolean);
/// render this reusable rendering context
// - wrap PushContext + Owner.RenderContext + Writer.SetText + CancelAll
function Render(const aDoc: variant): RawUtf8;
end;
TSynMustacheContextData = class;
/// TSynMustacheContextData.OnGetGlobalData callback signature
// - Data and Rtti are filled with the {{.}} current context at call
// - implementation should lookup ValueName and set Data/Rtti with result=true
// - warning: the returned Data pointer should remain active until the
// Mustache rendering task is completed
TOnGetGlobalData = function(Sender: TSynMustacheContextData;
const ValueName: RawUtf8; var Data: pointer; var Rtti: TRttiCustom): boolean;
/// handle {{mustache}} template rendering context from RTTI and variables
// - the context is given via our RTTI information
// - performance is somewhat higher than TSynMustacheContextVariant because
// less computation is needed for filling transient TDocVariant instances
TSynMustacheContextData = class(TSynMustacheContext)
protected
fContext: array of record
Data: pointer;
Info: TRttiCustom;
ListCount: integer;
ListCurrent: integer;
Temp: TRttiVarData;
end;
fOnGetGlobalData: TOnGetGlobalData;
procedure PushContext(Value: pointer; Rtti: TRttiCustom);
function GotoNextListItem: boolean; override;
function GetDataFromContext(const ValueName: RawUtf8;
out rc: TRttiCustom; out d: pointer): boolean;
function GetVarDataFromContext(ValueSpace: integer; const ValueName: RawUtf8;
var Value: TVarData): TSynMustacheSectionType; override;
procedure AppendValue(ValueSpace: integer; const ValueName: RawUtf8;
UnEscape: boolean); override;
function AppendSection(ValueSpace: integer;
const ValueName: RawUtf8): TSynMustacheSectionType; override;
public
/// initialize the context from a document stored in a local variable
// - note that the variable instance shall be available during all
// lifetime of this TSynMustacheContextData instance
// - you should not use this constructor directly, but the
// corresponding TSynMustache.RenderData() methods
constructor Create(Owner: TSynMustache; WR: TJsonWriter;
SectionMaxCount: integer; Value: pointer; ValueRtti: TRttiCustom;
OwnWriter: boolean);
/// render this reusable rendering context
// - wrap PushContext + Owner.RenderContext + Writer.SetText + CancelAll
function RenderArray(const arr: TDynArray): RawUtf8;
/// render this reusable rendering context
// - wrap PushContext + Owner.RenderContext + Writer.SetText + CancelAll
function RenderRtti(Value: pointer; Rtti: TRttiCustom): RawUtf8;
/// callback to get data at runtime from a global name
// - when the Value variable provided to TSynMustache.RenderData is not enough
property OnGetGlobalData: TOnGetGlobalData
read fOnGetGlobalData write fOnGetGlobalData;
end;
/// maintain a list of {{mustache}} partials
// - this list of partials template could be supplied to TSynMustache.Render()
// method, to render {{>partials}} as expected
// - using a dedicated class allows to share the partials between execution
// context, without recurring to non SOLID global variables
// - you may also define "internal" partials, e.g. {{<foo}}This is foo{{/foo}}
TSynMustachePartials = class
protected
fList: TRawUtf8List;
fOwned: boolean;
function GetPartial(const PartialName: RawUtf8): TSynMustache;
public
/// initialize the template partials storage
// - after creation, the partials should be registered via the Add() method
// - you shall manage this instance life time with a try..finally Free block
constructor Create; overload;
/// initialize a template partials storage with the supplied templates
// - partials list is expected to be supplied in Name / Template pairs
// - this instance can be supplied as parameter to the TSynMustache.Render()
// method, which will free the instances as soon as it finishes
constructor CreateOwned(
const NameTemplatePairs: array of RawUtf8); overload;
/// initialize a template partials storage with the supplied templates
// - partials list is expected to be supplied as a dvObject TDocVariant,
// each member being the name/template string pairs
// - if the supplied variant is not a matching TDocVariant, will return nil
// - this instance can be supplied as parameter to the TSynMustache.Render()
// method, which will free the instances as soon as it finishes
class function CreateOwned(
const Partials: variant): TSynMustachePartials; overload;
/// register a {{>partialName}} template
// - returns the parsed template
function Add(const aName,aTemplate: RawUtf8): TSynMustache; overload;
/// register a {{>partialName}} template
// - returns the parsed template
function Add(const aName: RawUtf8;
aTemplateStart, aTemplateEnd: PUtf8Char): TSynMustache; overload;
/// search some text withing the {{mustache}} partial
function FoundInTemplate(const text: RawUtf8): PtrInt;
/// delete the partials
destructor Destroy; override;
/// low-level access to the internal partials list
property List: TRawUtf8List
read fList;
end;
{ ************ TSynMustache Template Processing }
/// handles one {{mustache}} pre-rendered template
// - once parsed, a template will be stored in this class instance, to be
// rendered lated via the Render() method
// - you can use the Parse() class function to maintain a shared cache of
// parsed templates
// - implements all official mustache specifications, and some extensions
// - handles {{.}} pseudo-variable for the current context object (very
// handy when looping through a simple list, for instance)
// - handles {{-index}} pseudo-variable for the current context array index
// (1-based value) so that e.g.
// "My favorite things:\n{{#things}}{{-index}}. {{.}}\n{{/things}}"
// over {things:["Peanut butter", "Pen spinning", "Handstands"]} renders as
// "My favorite things:\n1. Peanut butter\n2. Pen spinning\n3. Handstands\n"
// - you could use {{-index0}} for 0-based index value
// - handles -first -last and -odd pseudo-section keys, e.g.
// "{{#things}}{{^-first}}, {{/-first}}{{.}}{{/things}}"
// over {things:["one", "two", "three"]} renders as 'one, two, three'
// - allows inlined partial templates , to be defined e.g. as
// {{<foo}}This is the foo partial {{myValue}} template{{/foo}}
// - features {{"English text}} translation, via a custom callback
// - this implementation is thread-safe and re-entrant (i.e. the same
// TSynMustache instance can be used by several threads at once)
TSynMustache = class
protected
fTemplate: RawUtf8;
fTags: TSynMustacheTagDynArray;
fInternalPartials: TSynMustachePartials;
fSectionMaxCount: integer;
fCachedContextVariant: TSynMustacheContextVariant;
fCachedContextData: TSynMustacheContextData;
// standard helpers implementation
class procedure DateTimeToText(const Value: variant; out Result: variant);
class procedure DateToText(const Value: variant; out Result: variant);
class procedure DateFmt(const Value: variant; out Result: variant);
class procedure TimeLogToText(const Value: variant; out Result: variant);
class procedure BlobToBase64(const Value: variant; out Result: variant);
class procedure ToJson(const Value: variant; out Result: variant);
class procedure JsonQuote(const Value: variant; out Result: variant);
class procedure JsonQuoteUri(const Value: variant; out Result: variant);
class procedure WikiToHtml(const Value: variant; out Result: variant);
class procedure MarkdownToHtml(const Value: variant; out Result: variant);
class procedure SimpleToHtml(const Value: variant; out Result: variant);
class procedure Match(const Value: variant; out Result: variant);
class procedure MatchI(const Value: variant; out Result: variant);
class procedure Lower(const Value: variant; out Result: variant);
class procedure Upper(const Value: variant; out Result: variant);
class procedure EnumTrim(const Value: variant; out Result: variant);
class procedure EnumTrimRight(const Value: variant; out Result: variant);
class procedure PowerOfTwo(const Value: variant; out Result: variant);
class procedure Equals_(const Value: variant; out Result: variant);
class procedure If_(const Value: variant; out Result: variant);
class procedure NewGuid(const Value: variant; out Result: variant);
class procedure ExtractFileName(const Value: variant; out Result: variant);
class procedure HumanBytes(const Value: variant; out Result: variant);
class procedure Sub(const Value: variant; out Result: variant);
class procedure Values(const Value: variant; out Result: variant);
class procedure Keys(const Value: variant; out Result: variant);
public
/// parse a {{mustache}} template, and returns the corresponding
// TSynMustache instance
// - an internal cache is maintained by this class function
// - don't free the returned instance: it is owned by the cache
// - this implementation is thread-safe and re-entrant: i.e. the same
// TSynMustache returned instance can be used by several threads at once
// - will raise an ESynMustache exception on error
class function Parse(const aTemplate: RawUtf8): TSynMustache;
/// remove the specified {{mustache}} template from the internal cache
// - returns TRUE on success, or FALSE if the template was not cached
// by a previous call to Parse() class function
class function UnParse(const aTemplate: RawUtf8): boolean;
/// parse and render a {{mustache}} template over the supplied JSON
// - an internal templates cache is maintained by this class function
// - returns TRUE and set aContent the rendered content on success
// - returns FALSE if the template is not correct
class function TryRenderJson(const aTemplate, aJson: RawUtf8;
out aContent: RawUtf8): boolean;
public
/// initialize and parse a pre-rendered {{mustache}} template
// - you should better use the Parse() class function instead, which
// features an internal thread-safe cache
constructor Create(const aTemplate: RawUtf8); overload;
/// initialize and parse a pre-rendered {{mustache}} template
// - you should better use the Parse() class function instead, which
// features an internal thread-safe cache
constructor Create(
aTemplate: PUtf8Char; aTemplateLen: integer); overload; virtual;
/// finalize internal memory
destructor Destroy; override;
/// internal factory calling TSynMustacheContextVariant.Create()
// - to call e.g. result.Render() several times
function NewMustacheContextVariant(
aBufSize: integer = 16384): TSynMustacheContextVariant;
/// internal factory calling TSynMustacheContextData.Create()
// - to call e.g. result.RenderArray() or result.RenderRtti() several times
function NewMustacheContextData(
aBufSize: integer = 16384): TSynMustacheContextData;
/// search some text within the {{mustache}} template text
function FoundInTemplate(const text: RawUtf8): boolean;
/// register one Expression Helper callback for a given list of helpers
// - i.e. to let aEvent process {{aName value}} tags
// - the supplied name will be checked against the current list, and replace
// any existing entry
class procedure HelperAdd(var Helpers: TSynMustacheHelpers;
const aName: RawUtf8; aEvent: TSynMustacheHelperEvent); overload;
/// register several Expression Helper callbacks for a given list of helpers
// - the supplied names will be checked against the current list, and replace
// any existing entry
class procedure HelperAdd(var Helpers: TSynMustacheHelpers;
const aNames: array of RawUtf8;
const aEvents: array of TSynMustacheHelperEvent); overload;
/// unregister one Expression Helper callback for a given list of helpers
class procedure HelperDelete(var Helpers: TSynMustacheHelpers;
const aName: RawUtf8);
/// search for one Expression Helper event by name
class function HelperFind(const Helpers: TSynMustacheHelpers;
aName: PUtf8Char; aNameLen: TStrLen): PtrInt;
/// returns a list of most used static Expression Helpers
// - registered helpers are DateTimeToText, DateToText, DateFmt, TimeLogToText,
// BlobToBase64, JsonQuote, JsonQuoteUri, ToJson, EnumTrim, EnumTrimRight,
// Lower / Upper (Unicode ready), PowerOfTwo, Equals (expecting two parameters),
// NewGuid, ExtractFileName, HumanBytes (calling KB function), Sub (as
// {{Sub AString,12,3}}), MarkdownToHtml, SimpleToHtml (Markdown with no
// HTML pass-through), WikiToHtml (callining TJsonWriter.AddHtmlEscapeWiki),
// Match / MatchI (as {{Match AString,startwith*}}), and Values / Keys (over
// a data object)
// - an additional #if helper is also registered, which would allow runtime
// view logic, via = < > <= >= <> operators over two values:
// $ {{#if .,"=",123}} {{#if Total,">",1000}} {{#if info,"<>",""}}
// which may be shortened as such:
// $ {{#if .=123}} {{#if Total>1000}} {{#if info<>""}}
class function HelpersGetStandardList: TSynMustacheHelpers; overload;
/// returns a list of most used static Expression Helpers, adding some
// custom callbacks
// - is just a wrapper around HelpersGetStandardList and HelperAdd()
class function HelpersGetStandardList(const aNames: array of RawUtf8;
const aEvents: array of TSynMustacheHelperEvent): TSynMustacheHelpers; overload;
/// renders the {{mustache}} template into a destination text buffer
// - the context is given via our abstract TSynMustacheContext wrapper
// - the rendering extended in fTags[] is supplied as parameters
// - you can specify a list of partials via TSynMustachePartials.CreateOwned
procedure RenderContext(Context: TSynMustacheContext;
TagStart, TagEnd: integer);
/// renders the {{mustache}} template from a variant defined context
// - the context is given via a custom variant type implementing
// TSynInvokeableVariantType.Lookup, e.g. TDocVariant or TSMVariant
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - can be used e.g. via a TDocVariant:
// !var
// ! mustache := TSynMustache;
// ! doc: variant;
// ! html: RawUtf8;
// !begin
// ! mustache := TSynMustache.Parse(
// ! 'Hello {{name}}'#13#10'You have just won {{value}} dollars!');
// ! TDocVariant.New(doc);
// ! doc.name := 'Chris';
// ! doc.value := 10000;
// ! html := mustache.Render(doc);
// ! // here html='Hello Chris'#13#10'You have just won 10000 dollars!'
// - you can also retrieve the context from an ORM query:
// ! dummy := TSynMustache.Parse(
// ! '{{#items}}'#13#10'{{Int}}={{Test}}'#13#10'{{/items}}').Render(
// ! aClient.RetrieveDocVariantArray(TOrmTest, 'items', 'Int,Test'));
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
function Render(const Context: variant;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8;
/// renders the {{mustache}} template from JSON defined context
// - the context is given via a JSON object, defined from UTF-8 buffer
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - is just a wrapper around Render(_JsonFast())
// - you can write e.g. with the extended JSON syntax:
// ! html := mustache.RenderJson('{things:["one", "two", "three"]}');
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
function RenderJson(const Json: RawUtf8;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8; overload;
/// renders the {{mustache}} template from JSON defined context
// - the context is given via a JSON object, defined with parameters
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - is just a wrapper around Render(_JsonFastFmt())
// - you can write e.g. with the extended JSON syntax:
// ! html := mustache.RenderJson('{name:?,value:?}',[],['Chris',10000]);
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
function RenderJson(const Json: RawUtf8;
const Args, Params: array of const;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8; overload;
/// renders the {{mustache}} template from a variable defined context
// - the context is given via a local variable and RTTI, which may be
// a record, a class, a variant, or a dynamic array instance
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
// - just redirects to the RenderDataRtti() method
function RenderData(const Value; ValueTypeInfo: PRttiInfo;
const OnGetData: TOnGetGlobalData = nil;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8;
/// renders the {{mustache}} template from a dynamic array variable
// - the supplied array is available within a {{.}} main section
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
// - just redirects to the RenderDataRtti() method
function RenderDataArray(const Value: TDynArray;
const OnGetData: TOnGetGlobalData = nil;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8;
/// renders the {{mustache}} template from a variable and its RTTI
// - the context is given via a local variable and RTTI, which may be
// a record, a class, a variant, or a dynamic array instance
// - you can specify a list of partials via TSynMustachePartials.CreateOwned,
// a list of Expression Helpers, or a custom {{"English text}} callback
// - set EscapeInvert = true to force {{value}} NOT to escape HTML chars
// and {{{value}} escaping chars (may be useful e.g. for code generation)
function RenderDataRtti(Value: pointer; ValueRtti: TRttiCustom;
const OnGetData: TOnGetGlobalData = nil;
Partials: TSynMustachePartials = nil;
const Helpers: TSynMustacheHelpers = nil;
const OnTranslate: TOnStringTranslate = nil;
EscapeInvert: boolean = false): RawUtf8;
/// read-only access to the raw UTF-8 {{mustache}} template content
property Template: RawUtf8
read fTemplate;
/// read-only access to the internal representation of the template
property Tags: TSynMustacheTagDynArray
read fTags;
/// the maximum possible number of nested contexts
// - i.e. the depth of nested {{#....}} {{/....}} sections
property SectionMaxCount: Integer
read fSectionMaxCount;
end;
const
/// Mustache-friendly JSON Serialization Options
// - as used e.g. from mormot.rest.mvc Data Context from Cookies
TEXTWRITEROPTIONS_MUSTACHE =
[twoForceJsonExtended,
twoEnumSetsAsBooleanInRecord,
twoTrimLeftEnumSets];
/// this constant can be used to define as JSON a tag value
NULL_OR_TRUE: array[boolean] of RawUtf8 = (
'null', 'true');
/// this constant can be used to define as JSON a tag value as separator
NULL_OR_COMMA: array[boolean] of RawUtf8 = (
'null', '","');
implementation
{ ************ Mustache Execution Data Context Types }
{ TSynMustacheContext }
constructor TSynMustacheContext.Create(Owner: TSynMustache;
WR: TJsonWriter; OwnWR: boolean);
begin
fOwner := Owner;
fOwnWriter := OwnWR;
fWriter := WR;
fPathDelim := '.';
end;
destructor TSynMustacheContext.Destroy;
begin
inherited Destroy;
if fOwnWriter then
fWriter.Free;
end;
procedure TSynMustacheContext.PopContext;
begin
if fContextCount > 1 then
dec(fContextCount);
end;
procedure TSynMustacheContext.TranslateBlock(Text: PUtf8Char; TextLen: Integer);
var
s: string;
begin
if Assigned(OnStringTranslate) then
begin
Utf8DecodeToString(Text, TextLen, s);
OnStringTranslate(s);
fWriter.AddNoJsonEscapeString(s);
end
else
fWriter.AddNoJsonEscape(Text, TextLen);
end;
procedure TSynMustacheContext.AppendVariant(
const Value: variant; UnEscape: boolean);
var
tmp: TTempUtf8;
wasString: boolean;
begin
if fEscapeInvert then
UnEscape := not UnEscape;
if TVarData(Value).VType > varNull then
if Unescape or
VarIsNumeric(Value) then
// avoid RawUtf8 conversion for plain numbers or if no HTML escaping
fWriter.AddVariant(Value, twNone)
else
begin
VariantToTempUtf8(Value, tmp, wasString);
fWriter.AddHtmlEscape(tmp.Text, tmp.Len);
if tmp.TempRawUtf8 <> nil then
FastAssignNew(tmp.TempRawUtf8);
end;
end;
function TSynMustacheContext.GetVariantFromContext(
const ValueName: RawUtf8): variant;
var
tmp: TVarData;
begin
if (ValueName = '') or
(ValueName[1] in ['-', '0'..'9', '"', '{', '[']) or
(ValueName = 'true') or
(ValueName = 'false') or
(ValueName = 'null') then
VariantLoadJson(result, ValueName, @JSON_[mFast])
else if fGetVarDataFromContextNeedsFree then
begin
if TRttiVarData(result).VType <> varEmpty then
VarClearProc(TVarData(result));
GetVarDataFromContext(-1, ValueName, TVarData(result)); // set directly
end
else
begin
GetVarDataFromContext(-1, ValueName, tmp); // get TVarData content
SetVariantByValue(variant(tmp), result); // assign/copy value
end;
end;
function TSynMustacheContext.ProcessHelper(const ValueName: RawUtf8;
space, helper: PtrInt; var Value: TVarData;
OwnValue: PPVarData): TSynMustacheSectionType;
var
valnam: RawUtf8;
p: PUtf8Char;
val: TVarData;
valArr: TDocVariantData absolute val;
valFree, valFound: boolean;
names: TRawUtf8DynArray;
j, k, n: PtrInt;
begin
valnam := Copy(ValueName, space + 1, maxInt);
TRttiVarData(val).VType := varEmpty;
valFree := fGetVarDataFromContextNeedsFree;
if valnam <> '' then
begin
if valnam = '.' then
GetVarDataFromContext(-1, valnam, val)
else if ((valnam <> '') and
(valnam[1] in ['1'..'9', '"', '{', '['])) or
(valnam = 'true') or
(valnam = 'false') or
(valnam = 'null') then
begin
// {{helper 123}} or {{helper "constant"}} or {{helper [1,2,3]}}
JsonToVariantInPlace(variant(val), pointer(valnam), JSON_FAST_FLOAT);
valFree := true;
end
else
begin
valFound := false;
for j := 1 to length(valnam) do
case valnam[j] of
' ':
// allows {{helper1 helper2 value}} recursive calls
break;
',':
begin
// {{helper value,123,"constant"}}
p := Pointer(valnam);
if j = 1 then
inc(p); // for {{helper ,"constant1","constant2",123}}
CsvToRawUtf8DynArray(p, names, ',', true);
// TODO: handle 123,"a,b,c"
valArr.InitFast;
for k := 0 to High(names) do
valArr.AddItem(GetVariantFromContext(names[k]));
valFound := true;
break;
end;
'<',
'>',
'=':
begin
// {{#if .=123}} -> {{#if .,"=",123}}
k := j + 1;
if valnam[k] in ['=', '>'] then
inc(k);
valArr.InitArray([
GetVariantFromContext(Copy(valnam, 1, j - 1)),
Copy(valnam, j, k - j),
GetVariantFromContext(Copy(valnam, k, maxInt))],
JSON_FAST_FLOAT);
valFound := true;
break;
end;
end;
if valFound then
valFree := true
else
GetVarDataFromContext(-1, valnam, val);
end;
end;
// call helper
if OwnValue <> nil then
begin
// result Value is owned by fTempProcessHelper[]
n := fContextCount + 4;
if length(fTempProcessHelper) < n then
SetLength(fTempProcessHelper, n);
OwnValue^ := @fTempProcessHelper[fContextCount - 1];
Helpers[helper].Event(variant(val), variant(OwnValue^^));
Value := OwnValue^^;
end
else
Helpers[helper].Event(variant(val), variant(Value));
if valFree then
VarClearProc(val);
result := msSinglePseudo;
end;
function TSynMustacheContext.GetHelperFromContext(ValueSpace: integer;
const ValueName: RawUtf8; var Value: TVarData;
OwnValue: PPVarData): TSynMustacheSectionType;
var
space, len, helper: PtrInt;
begin
space := ValueSpace;
if space < 0 then
space := PosExChar(' ', ValueName);
if space > 1 then
len := space - 1
else
begin
space := length(ValueName);
len := space;
end;
helper := TSynMustache.HelperFind(Helpers, pointer(ValueName), len);
if helper >= 0 then
result := ProcessHelper(ValueName, space, helper, Value, OwnValue)
else
result := msNothing;
end;
procedure TSynMustacheContext.CancelAll;
begin
fContextCount := 0;
fEscapeInvert := false;
fWriter.CancelAllAsNew;
if fTempProcessHelper <> nil then
VariantClearSeveral(pointer(fTempProcessHelper), length(fTempProcessHelper));
fReuse.UnLock;
end;
{ TSynMustacheContextVariant }
constructor TSynMustacheContextVariant.Create(Owner: TSynMustache;
WR: TJsonWriter; SectionMaxCount: integer; const aDocument: variant;
OwnWriter: boolean);
begin
inherited Create(Owner, WR, OwnWriter);
SetLength(fContext, SectionMaxCount + 4);
PushContext(TVarData(aDocument)); // weak copy
end;
function TSynMustacheContextVariant.Render(const aDoc: variant): RawUtf8;
begin
PushContext(TVarData(aDoc));
fOwner.RenderContext(self, 0, high(fOwner.fTags));
Writer.SetText(result);
CancelAll;
end;
procedure TSynMustacheContextVariant.PushContext(const aDoc: TVarData);
begin
if fContextCount >= length(fContext) then
// was roughtly set by SectionMaxCount
SetLength(fContext, fContextCount + 32);
with fContext[fContextCount] do
begin
Document := aDoc;
DocumentType := DocVariantType.FindSynVariantType(aDoc.VType);
ListCurrent := -1;
if DocumentType = nil then
ListCount := -1
else
begin
ListCount := DocumentType.IterateCount(aDoc, {GetObjectAsValues=}false);
if fContextCount = 0 then
ListCurrentDocument := aDoc; // allow {#.}...{/.} at first level
end;
end;
inc(fContextCount);
end;
function TSynMustacheContextVariant.GotoNextListItem: boolean;
begin
result := false;
if fContextCount > 0 then
with fContext[fContextCount - 1] do
begin
ListCurrentDocument.VType := varEmpty;
ListCurrentDocumentType := nil;
inc(ListCurrent);
if ListCurrent >= ListCount then
exit;
DocumentType.Iterate(ListCurrentDocument, Document, ListCurrent);
ListCurrentDocumentType := DocVariantType.FindSynVariantType(
ListCurrentDocument.VType);
result := true;
end;
end;
function TSynMustacheContextVariant.GetVarDataFromContext(ValueSpace: integer;
const ValueName: RawUtf8; var Value: TVarData): TSynMustacheSectionType;
var
i: PtrInt;
owned: PVarData;
begin
result := msNothing;
if PWord(ValueName)^ = ord('.') then
// {{.}} -> context = self
with fContext[fContextCount - 1] do
begin
if ListCount > 0 then
Value := ListCurrentDocument
else
Value := Document;
exit;
end;
// recursive search of {{value}}
for i := fContextCount - 1 downto 0 do
with fContext[i] do
if DocumentType <> nil then
if ListCount < 0 then
begin
// single item context
DocumentType.Lookup(Value, Document, pointer(ValueName), fPathDelim);
if Value.VType >= varNull then
exit;
end
else if PCardinal(ValueName)^ and $dfdfdfdf = (ord('-') and $df) +
ord('I') shl 8 + ord('N') shl 16 + ord('D') shl 24 then
begin
// {{-index}}
Value.VType := varInteger;
if ValueName[7] = '0' then
Value.VInteger := ListCurrent
else
Value.VInteger := ListCurrent + 1;
exit;
end
else if (ListCurrent < ListCount) and
(ListCurrentDocumentType <> nil) then
begin
ListCurrentDocumentType.Lookup(
Value, ListCurrentDocument, pointer(ValueName), fPathDelim);
if Value.VType >= varNull then
exit;
end;
// try {{helper value}} or {{helper}}
result := GetHelperFromContext(ValueSpace, ValueName, Value, @owned);
end;
procedure TSynMustacheContextVariant.AppendValue(ValueSpace: integer;
const ValueName: RawUtf8; UnEscape: boolean);
var
Value: TVarData;
begin
GetVarDataFromContext(ValueSpace, ValueName, Value);
AppendVariant(variant(Value), UnEscape);
end;
function SectionIsPseudo(const ValueName: RawUtf8; ListCount, ListCurrent: integer): boolean;
begin
result := ((ValueName = '-first') and
(ListCurrent = 0)) or
((ValueName = '-last') and
(ListCurrent = ListCount - 1)) or
((ValueName = '-odd') and
(ListCurrent and 1 = 0));
end;
function TSynMustacheContextVariant.AppendSection(ValueSpace: integer;
const ValueName: RawUtf8): TSynMustacheSectionType;
var
Value: TVarData;
c: cardinal;
void: boolean;
begin
result := msNothing;
if fContextCount = 0 then
exit;
if ValueName[1] = '-' then
with fContext[fContextCount - 1] do
if ListCount >= 0 then
begin
if SectionIsPseudo(ValueName, ListCount, ListCurrent) then
result := msSinglePseudo;
exit;
end;
result := GetVarDataFromContext(ValueSpace, ValueName, Value);
c := Value.VType;
void := (c <= varNull) or
((c = varBoolean) and
(Value.VWord = 0));
if (result <> msNothing) and // helper?
(c < varFirstCustom) then // simple helper values are not pushed
begin
if void then
result := msNothing;
exit;
end;
PushContext(Value);
if void then
// null or false value will not display the section
result := msNothing
else
with fContext[fContextCount - 1] do
if ListCount < 0 then
// single item
result := msSingle
else if ListCount = 0 then
// empty list will not display the section
result := msNothing
else
// non-empty list
result := msList;
end;
{ TSynMustacheContextData }
constructor TSynMustacheContextData.Create(Owner: TSynMustache;
WR: TJsonWriter; SectionMaxCount: integer; Value: pointer;
ValueRtti: TRttiCustom; OwnWriter: boolean);
begin
inherited Create(Owner, WR, OwnWriter);
fGetVarDataFromContextNeedsFree := true;
SetLength(fContext, SectionMaxCount + 4);
PushContext(Value, ValueRtti);
end;
function TSynMustacheContextData.RenderArray(const arr: TDynArray): RawUtf8;
var
n: PtrInt;
begin
n := arr.Count;
if n <> 0 then
DynArrayFakeLength(arr.Value^, n); // as required by RenderContext()
PushContext(arr.Value, arr.Info);
fOwner.RenderContext(self, 0, high(fOwner.fTags));
Writer.SetText(result);
CancelAll;
end;
function TSynMustacheContextData.RenderRtti(Value: pointer; Rtti: TRttiCustom): RawUtf8;
begin
PushContext(Value, Rtti);
fOwner.RenderContext(self, 0, high(fOwner.fTags));
Writer.SetText(result);
CancelAll;
end;
procedure TSynMustacheContextData.PushContext(Value: pointer; Rtti: TRttiCustom);
var
n: PtrInt;
begin
n := fContextCount;
if n >= length(fContext) then
// was roughtly set by SectionMaxCount
SetLength(fContext, n + 32);
with fContext[n] do
begin
Data := Value;
Info := Rtti;
if Rtti <> nil then
ListCount := Rtti.ValueIterateCount(Value);
ListCurrent := -1;
end;
inc(fContextCount);
end;
function TSynMustacheContextData.GotoNextListItem: boolean;
var
n: PtrInt;
begin
result := false;
n := fContextCount;
if n > 0 then
with fContext[n - 1] do
if ListCount >= 0 then
begin
inc(ListCurrent);
if ListCurrent >= ListCount then
ListCount := -1
else
result := true;
end;
end;
function TSynMustacheContextData.GetDataFromContext(const ValueName: RawUtf8;
out rc: TRttiCustom; out d: pointer): boolean;
var
i: PtrInt;
firstdata: pointer; // OnGetGlobalData is filled with self context
firstrc: TRttiCustom;
begin
result := true; // mark found on direct exit
// recursive search of {{value}}
firstdata := nil;
firstrc := nil; // makes Delphi compiler happy
for i := fContextCount - 1 downto 0 do
with fContext[i] do
begin
d := Data;
rc := Info;
if PWord(ValueName)^ = ord('.') then
// {{.}} -> context = self
exit;
if (d <> nil) and
(ListCount >= 0) then
// within a list
if PCardinal(ValueName)^ and $dfdfdfdf = (ord('-') and $df) +
ord('I') shl 8 + ord('N') shl 16 + ord('D') shl 24 then
begin
// {{-index}} pseudo name
d := @Temp.Data.VInteger;
rc := PT_RTTI[ptInteger];
PInteger(d)^ := ListCurrent;
if ValueName[7] <> '0' then
inc(PInteger(d)^);
exit;
end
else
// the current context is the current list item
d := rc.ValueIterate(d, ListCurrent, rc); // rkClass is dereferenced
if d <> nil then
begin
// we found a value in this context
if firstdata = nil then
begin
firstdata := d; // for OnGetGlobalData() below
firstrc := rc;
end;
// recursive lookup by {{name1.name2.name3}} against nested variant
// fields, record/class properties, or string/set/enum values
rc := rc.ValueByPath(d, pointer(ValueName), Temp.Data, fPathDelim);
if rc <> nil then
exit;
end;
end;
// try to resolve the name at runtime via the OnGetGlobalData callback
if Assigned(OnGetGlobalData) then
begin
d := firstdata; // provide the {{.}} self context to the callback
rc := firstrc;
result := OnGetGlobalData(self, ValueName, d, rc);
end
else
result := false; // not found
end;
function TSynMustacheContextData.GetVarDataFromContext(ValueSpace: integer;
const ValueName: RawUtf8; var Value: TVarData): TSynMustacheSectionType;
var
d: pointer;
rc: TRttiCustom;
begin
// called for Helpers() support: AppendValue() is used for regular values
if GetDataFromContext(ValueName, rc, d) then
begin
// found {{.}} or {{value}} or {{value1.value2.value3}} data
result := msNothing;
rc.ValueToVariant(d, Value, @JSON_[mFastFloat]);
end
else
// try {{helper value}} or {{helper}}
result := GetHelperFromContext(ValueSpace, ValueName, Value, {owned=}nil);
end;
procedure TSynMustacheContextData.AppendValue(ValueSpace: integer;
const ValueName: RawUtf8; UnEscape: boolean);
var
d: pointer;
p: PUtf8Char;
rc: TRttiCustom;
l: PtrInt;
tmp: TVarData;
begin
if GetDataFromContext(ValueName, rc, d) then
begin
// we can directly append the {{###}} found data
if fEscapeInvert then
UnEscape := not UnEscape;
case rc.Parser of // FPC generates a jump table for this case statement
// try direct UTF-8 and UTF-16 strings (escaped) rendering
{$ifndef UNICODE}
ptString,
{$endif UNICODE}
ptRawUtf8,
ptRawJson,
ptPUtf8Char:
begin
p := PPointer(d)^;
if p <> nil then
if UnEscape then
begin
if rc.Parser = ptPUtf8Char then
l := mormot.core.base.StrLen(p)
else
l := PStrLen(p - _STRLEN)^;
fWriter.AddNoJsonEscape(p, l);
end
else
fWriter.AddHtmlEscape(p); // faster with no length
end;
{$ifdef UNICODE}
ptString,
{$endif UNICODE}
{$ifdef HASVARUSTRING}
ptUnicodeString,
{$endif HASVARUSTRING}
ptSynUnicode,
ptWideString:
if UnEscape then
fWriter.AddNoJsonEscapeW(PPWord(d)^, 0)
else
fWriter.AddHtmlEscapeW(PPWideChar(d)^);
// unescaped (and unquoted) numbers, date/time, guid or hash
ptByte:
fWriter.AddU(PByte(d)^);
ptWord:
fWriter.AddU(PWord(d)^);
ptInteger:
fWriter.Add(PInteger(d)^);
ptCardinal:
fWriter.AddU(PCardinal(d)^);
ptInt64:
fWriter.Add(PInt64(d)^);
ptQWord:
fWriter.AddQ(PQWord(d)^);
ptDouble:
fWriter.AddDouble(unaligned(PDouble(d)^));
ptCurrency:
fWriter.AddCurr64(d);
ptBoolean:
fWriter.Add(PBoolean(d)^);
ptDateTime,
ptDateTimeMS:
fWriter.AddDateTime(d, 'T', #0, rc.Parser = ptDateTimeMS, {wtime=}true);
ptUnixTime:
fWriter.AddUnixTime(d);
ptUnixMSTime:
fWriter.AddUnixMSTime(d, {withms=}true);
ptTimeLog:
fWriter.AddTimeLog(d);
ptGuid:
fWriter.Add(PGuid(d), #0);
ptHash128,
ptHash256,
ptHash512:
fWriter.AddBinToHexDisplayLower(d, rc.Size);
else
if rcfIsNumber in rc.Cache.Flags then
// ordinals or floats don't need any HTML escape nor quote
fWriter.AddRttiCustomJson(d, rc, twNone, [])
else
begin
// use a temporary variant for any complex content (including JSON)
rc.ValueToVariant(d, tmp, @JSON_[mFastFloat]);
if fEscapeInvert then
UnEscape := not UnEscape; // AppendVariant() will reverse it
AppendVariant(variant(tmp), UnEscape);
VarClearProc(tmp);
end;
end;
end
else
begin
// try {{helper value}} or {{helper}}
GetHelperFromContext(ValueSpace, ValueName, tmp, {owned=}nil);
AppendVariant(variant(tmp), UnEscape);
VarClearProc(tmp);
end;
end;
function IsVoidContext(d: pointer; rc: TRttiCustom): boolean;
var
c: cardinal;
begin
result := true;
if d = nil then
exit;
if rc.Kind = rkClass then
begin
if PPointer(d)^ = nil then
exit;
end
else if rc.Kind = rkVariant then
begin
c := PVarData(d)^.VType;
if (c <= varNull) or
((c = varBoolean) and
(PVarData(d)^.VWord = 0)) then
exit;
end
else if rcfBoolean in rc.Cache.Flags then
if PByte(d)^ = 0 then
exit;
result := false; // not void context, in the Mustache terms
end;
function TSynMustacheContextData.AppendSection(ValueSpace: integer;
const ValueName: RawUtf8): TSynMustacheSectionType;
var
d: pointer;
rc: TRttiCustom;
tmp: TVarData;
void: boolean;
begin
result := msNothing;
if fContextCount = 0 then
exit;
if ValueName[1] = '-' then
with fContext[fContextCount - 1] do
if ListCount >= 0 then
begin
if SectionIsPseudo(ValueName, ListCount, ListCurrent) then
result := msSinglePseudo;
exit;
end;
if not GetDataFromContext(ValueName, rc, d) then
begin
result := GetHelperFromContext(ValueSpace, ValueName, tmp, {owned=}d);
if result = msNothing then
d := nil
else
rc := PT_RTTI[ptVariant]; // use temp variant value from helper
end;
void := IsVoidContext(d, rc);
if (result <> msNothing) and // helper?
(tmp.VType < varFirstCustom) then // simple helper values are not pushed
begin
if void then
result := msNothing;
exit;
end;
if d = nil then
exit;
PushContext(d, rc);
if void then
// null or false value will not display the section
result := msNothing
else
with fContext[fContextCount - 1] do
if ListCount < 0 then
// single item
result := msSingle
else if ListCount = 0 then
// empty list will not display the section
result := msNothing
else
// non-empty list
result := msList;
end;
{ TSynMustachePartials }
constructor TSynMustachePartials.Create;
begin
fList := TRawUtf8List.CreateEx([fNoDuplicate, fCaseSensitive, fThreadSafe]);
end;
constructor TSynMustachePartials.CreateOwned(
const NameTemplatePairs: array of RawUtf8);
var
A: PtrInt;
begin
Create;
fOwned := true;
for A := 0 to high(NameTemplatePairs) div 2 do
Add(NameTemplatePairs[A * 2], NameTemplatePairs[A * 2 + 1]);
end;
function TSynMustachePartials.Add(const aName, aTemplate: RawUtf8): TSynMustache;
begin
result := TSynMustache.Parse(aTemplate);
if (result <> nil) and
(fList.AddObject(aName, result) < 0) then
raise ESynMustache.CreateUtf8('%.Add(%) duplicated name', [self, aName]);
end;
function TSynMustachePartials.Add(const aName: RawUtf8;
aTemplateStart, aTemplateEnd: PUtf8Char): TSynMustache;
var
aTemplate: RawUtf8;
begin
FastSetString(aTemplate, aTemplateStart, aTemplateEnd - aTemplateStart);
result := Add(aName, aTemplate);
end;
function TSynMustachePartials.FoundInTemplate(const text: RawUtf8): PtrInt;
begin
if self <> nil then
result := fList.Contains(text)
else
result := -1;
end;
class function TSynMustachePartials.CreateOwned(
const Partials: variant): TSynMustachePartials;
var
ndx: PtrInt;
begin
result := nil;
with _Safe(Partials)^ do
if IsObject and
(Count > 0) then
begin
result := TSynMustachePartials.Create;
result.fOwned := true;
for ndx := 0 to Count - 1 do
result.Add(names[ndx], VariantToUtf8(Values[ndx]));
end;
end;
destructor TSynMustachePartials.Destroy;
begin
FreeAndNil(fList);
inherited;
end;
function TSynMustachePartials.GetPartial(
const PartialName: RawUtf8): TSynMustache;
var
i: PtrInt;
begin
if self = nil then
begin
result := nil;
exit;
end;
i := fList.IndexOf(PartialName); // using O(1) hashing
if i < 0 then
result := nil
else
result := TSynMustache(fList.Objects[i]);
end;
function KindToText(Kind: TSynMustacheTagKind): PShortString;
begin
result := GetEnumName(TypeInfo(TSynMustacheTagKind), ord(Kind));
end;
{ ************ TSynMustache Template Processing }
type
TSynMustacheParser = class
protected
fTagStartChars, fTagStopChars: word;
fPos, fPosMin, fPosMax, fPosTagStart: PUtf8Char;
fTagCount: PtrInt;
fTemplate: TSynMustache;
fScanStart, fScanEnd: PUtf8Char;
function Scan(ExpectedTag: cardinal): boolean;
procedure AddTag(aKind: TSynMustacheTagKind;
aStart: PUtf8Char = nil; aEnd: PUtf8Char = nil);
public
constructor Create(Template: TSynMustache;
const DelimiterStart, DelimiterStop: RawUtf8);
procedure Parse(p, PEnd: PUtf8Char);
end;
TSynMustacheCache = class(TRawUtf8List)
public
function Parse(const aTemplate: RawUtf8): TSynMustache;
function UnParse(const aTemplate: RawUtf8): boolean;
end;
var
SynMustacheCache: TSynMustacheCache = nil;
{ TSynMustacheParser }
procedure TSynMustacheParser.AddTag(aKind: TSynMustacheTagKind;
aStart, aEnd: PUtf8Char);
var
P: PUtf8Char;
begin
if (aStart = nil) or
(aEnd = nil) then
begin
aStart := fScanStart;
aEnd := fScanEnd;
case aKind of
mtComment,
mtSection,
mtSectionEnd,
mtInvertedSection,
mtSetDelimiter,
mtPartial:
begin
// (indented) standalone lines should be removed from the template
if aKind <> mtPartial then
while (fPosTagStart > fPosMin) and
(fPosTagStart[-1] in [' ', #9]) do
// ignore any indentation chars
dec(fPosTagStart);
if (fPosTagStart = fPosMin) or
(fPosTagStart[-1] = #$0A) then
// tag starts on a new line -> check if ends on the same line
if (fPos > fPosMax) or
(fPos^ = #$0A) or
(PWord(fPos)^ = $0A0D) then
begin
if fPos <= fPosMax then
if fPos^ = #$0A then
inc(fPos)
else if PWord(fPos)^ = $0A0D then
inc(fPos, 2);
if fTagCount > 0 then
// remove any indentation chars from previous text
with fTemplate.fTags[fTagCount - 1] do
if Kind = mtText then
while (TextLen > 0) and
(TextStart[TextLen - 1] in [' ', #9]) do
dec(TextLen);
end;
end;
mtVariable,
mtVariableUnescape,
mtVariableUnescapeAmp:
begin
// handle JSON object/array with nested }
// e.g. as {{helper [{a:{a:1,b:2}}]}}
P := PosChar(aStart, ' ');
if (P <> nil) and
(P < aEnd) then
begin
P := GotoNextNotSpaceSameLine(P + 1);
if P^ in ['{', '['] then
begin
P := GotoEndJsonItem(P);
if P <> nil then
begin
aEnd := P;
fPos := P;
if not Scan(fTagStopChars) then
raise ESynMustache.CreateUtf8('Unfinished {{%', [aStart]);
if (aKind = mtVariableUnescape) and
(fTagStopChars = $7d7d) and
(PWord(fPos - 1)^ = $7d7d) then
// {{{name}}} -> point after }}}
inc(fPos);
end;
end;
end;
end;
end;
end;
if aEnd <= aStart then
exit;
if fTagCount >= length(fTemplate.fTags) then
SetLength(fTemplate.fTags, NextGrow(fTagCount));
with fTemplate.fTags[fTagCount] do
begin
Kind := aKind;
SectionOppositeIndex := -1;
case aKind of
mtText,
mtComment,
mtTranslate:
begin
TextStart := aStart;
TextLen := aEnd - aStart;
end;
else
begin
TextStart := fPosTagStart;
TextLen := aEnd - fPosTagStart;
// superfluous in-tag whitespace should be ignored
while (aStart < aEnd) and
(aStart^ <= ' ') do
inc(aStart);
while (aEnd > aStart) and
(aEnd[-1] <= ' ') do
dec(aEnd);
if aEnd = aStart then
raise ESynMustache.CreateUtf8(
'Void % identifier', [KindToText(aKind)^]);
FastSetString(Value, aStart, aEnd - aStart);
ValueSpace := PosExChar(' ', Value);
end;
end;
end;
inc(fTagCount);
end;
constructor TSynMustacheParser.Create(Template: TSynMustache;
const DelimiterStart, DelimiterStop: RawUtf8);
begin
fTemplate := Template;
if length(DelimiterStart) <> 2 then
raise ESynMustache.CreateUtf8('DelimiterStart="%"', [DelimiterStart]);
if length(DelimiterStop) <> 2 then
raise ESynMustache.CreateUtf8('DelimiterStop="%"', [DelimiterStop]);
fTagStartChars := PWord(DelimiterStart)^;
fTagStopChars := PWord(DelimiterStop)^;
end;
function GotoNextTag(P, PMax: PUtf8Char; ExpectedTag: Word): PUtf8Char;
begin
if P < PMax then
repeat
if PWord(P)^ <> ExpectedTag then
begin
inc(P);
if P < PMax then
continue;
break;
end;
result := P;
exit;
until false;
result := nil;
end;
function TSynMustacheParser.Scan(ExpectedTag: cardinal): boolean;
var
P: PUtf8Char;
begin
P := GotoNextTag(fPos, fPosMax, ExpectedTag);
if P = nil then
result := false
else
begin
fScanEnd := P;
fScanStart := fPos;
fPos := P + 2;
result := true;
end;
end;
function SectionNameMatch(const start, finish: RawUtf8): boolean;
var
i: PtrInt;
begin
if start = finish then
result := true
else
begin
i := PosExChar(' ', start);
result := (i > 0) and
IdemPropNameU(finish, pointer(start), i - 1);
end;
end;
procedure TSynMustacheParser.Parse(P, PEnd: PUtf8Char);
var
Kind: TSynMustacheTagKind;
Symbol: AnsiChar;
i, j, secCount, secLevel: PtrInt;
begin
secCount := 0;
if P = nil then
exit;
fPos := P;
fPosMin := P;
fPosMax := PEnd - 1;
repeat
if not Scan(fTagStartChars) then
break;
fPosTagStart := fScanEnd;
AddTag(mtText);
if fPos >= fPosMax then
break;
Symbol := fPos^;
case Symbol of
'=':
Kind := mtSetDelimiter;
'{':
Kind := mtVariableUnescape;
'&':
Kind := mtVariableUnescapeAmp;
'#':
Kind := mtSection;
'^':
Kind := mtInvertedSection;
'/':
Kind := mtSectionEnd;
'!':
Kind := mtComment;
'>':
Kind := mtPartial;
'<':
Kind := mtSetPartial;
'"':
Kind := mtTranslate;
else
Kind := mtVariable;
end;
if Kind <> mtVariable then
inc(fPos);
if not Scan(fTagStopChars) then
raise ESynMustache.CreateUtf8('Unfinished {{tag [%]', [fPos]);
case Kind of
mtSetDelimiter:
begin
if (fScanEnd - fScanStart <> 6) or
(fScanEnd[-1] <> '=') then
raise ESynMustache.Create(
'mtSetDelimiter syntax is e.g. {{=<% %>=}}');
fTagStartChars := PWord(fScanStart)^;
fTagStopChars := PWord(fScanStart + 3)^;
continue; // do not call AddTag(Kind=mtSetDelimiter)
end;
mtVariableUnescape:
if (Symbol = '{') and
(fTagStopChars = $7d7d) and
(PWord(fPos - 1)^ = $7d7d) then
// {{{name}}} -> point after }}}
inc(fPos);
end;
AddTag(Kind);
until false;
AddTag(mtText, fPos, fPosMax + 1);
for i := 0 to fTagCount - 1 do
with fTemplate.fTags[i] do
case Kind of
mtSection,
mtInvertedSection,
mtSetPartial:
begin
inc(secCount);
if secCount > fTemplate.fSectionMaxCount then
fTemplate.fSectionMaxCount := secCount;
secLevel := 1;
for j := i + 1 to fTagCount - 1 do
case fTemplate.fTags[j].Kind of
mtSection,
mtInvertedSection,
mtSetPartial:
inc(secLevel);
mtSectionEnd:
begin
dec(secLevel);
if secLevel = 0 then
if SectionNameMatch(Value, fTemplate.fTags[j].Value) then
begin
fTemplate.fTags[j].SectionOppositeIndex := i;
SectionOppositeIndex := j;
if Kind = mtSetPartial then
begin
if fTemplate.fInternalPartials = nil then
fTemplate.fInternalPartials :=
TSynMustachePartials.Create;
fTemplate.fInternalPartials.Add(Value,
TextStart + TextLen + 2,
fTemplate.fTags[j].TextStart);
end;
break;
end
else
raise ESynMustache.CreateUtf8(
'Got {{/%}}, expected {{/%}}',
[Value, fTemplate.fTags[j].Value]);
end;
end;
if SectionOppositeIndex < 0 then
raise ESynMustache.CreateUtf8(
'Missing section end {{/%}}', [Value]);
end;
mtSectionEnd:
begin
dec(secCount);
if SectionOppositeIndex < 0 then
raise ESynMustache.CreateUtf8(
'Unexpected section end {{/%}}', [Value]);
end;
end;
SetLength(fTemplate.fTags, fTagCount);
end;
{ TSynMustacheCache }
function TSynMustacheCache.Parse(const aTemplate: RawUtf8): TSynMustache;
begin
result := GetObjectFrom(aTemplate);
if result = nil then
begin
result := TSynMustache.Create(aTemplate);
AddObjectUnique(aTemplate, @result);
end;
end;
function TSynMustacheCache.UnParse(const aTemplate: RawUtf8): boolean;
begin
result := Delete(aTemplate) >= 0;
end;
{ TSynMustache }
class function TSynMustache.Parse(const aTemplate: RawUtf8): TSynMustache;
begin
if SynMustacheCache = nil then
begin
GlobalLock; // RegisterGlobalShutdownRelease() will use it anyway
try
if SynMustacheCache = nil then
SynMustacheCache := RegisterGlobalShutdownRelease(
TSynMustacheCache.CreateEx([
fObjectsOwned, fNoDuplicate, fCaseSensitive, fThreadSafe]));
finally
GlobalUnLock;
end;
end;
result := SynMustacheCache.Parse(aTemplate);
end;
class function TSynMustache.UnParse(const aTemplate: RawUtf8): boolean;
begin
result := SynMustacheCache.UnParse(aTemplate);
end;
class function TSynMustache.TryRenderJson(const aTemplate, aJson: RawUtf8;
out aContent: RawUtf8): boolean;
var
mus: TSynMustache;
begin
if aTemplate <> '' then
try
mus := Parse(aTemplate);
aContent := mus.RenderJson(aJson);
result := true;
except
result := false;
end
else
result := false;
end;
constructor TSynMustache.Create(const aTemplate: RawUtf8);
begin
Create(pointer(aTemplate), length(aTemplate));
end;
constructor TSynMustache.Create(aTemplate: PUtf8Char; aTemplateLen: integer);
begin
inherited Create;
fTemplate := aTemplate;
with TSynMustacheParser.Create(self, '{{', '}}') do
try
Parse(aTemplate, aTemplate + aTemplateLen);
finally
Free;
end;
fCachedContextVariant := NewMustacheContextVariant;
fCachedContextData := NewMustacheContextData;
end;
function TSynMustache.NewMustacheContextVariant(
aBufSize: integer): TSynMustacheContextVariant;
begin
result := TSynMustacheContextVariant.Create(self,
TJsonWriter.CreateOwnedStream(aBufSize, {nosharedstream=}true),
SectionMaxCount + 4, Null, {ownwriter=}true);
result.CancelAll; // to be reused from a void context
end;
function TSynMustache.NewMustacheContextData(
aBufSize: integer): TSynMustacheContextData;
begin
result := TSynMustacheContextData.Create(self,
TJsonWriter.CreateOwnedStream(aBufSize, {nosharedstream=}true),
SectionMaxCount + 4, nil, nil, {ownwriter=}true);
result.CancelAll; // to be reused from a void context
end;
procedure TSynMustache.RenderContext(Context: TSynMustacheContext;
TagStart, TagEnd: integer);
var
partial: TSynMustache;
begin
while TagStart <= TagEnd do
begin
with fTags[TagStart] do
case Kind of
mtText:
if TextLen <> 0 then
// may be 0 e.g. for standalone without previous Line
Context.fWriter.AddNoJsonEscape(TextStart, TextLen);
mtVariable:
Context.AppendValue(ValueSpace, Value, {unescape=}false);
mtVariableUnescape,
mtVariableUnescapeAmp:
Context.AppendValue(ValueSpace, Value, {unescape=}true);
mtSection:
case Context.AppendSection(ValueSpace, Value) of
msNothing:
begin
// e.g. for no key, false value, or empty list
TagStart := SectionOppositeIndex;
continue; // ignore whole section
end;
msList:
begin
while Context.GotoNextListItem do
RenderContext(Context, TagStart + 1, SectionOppositeIndex - 1);
TagStart := SectionOppositeIndex;
// ignore whole section since we just rendered it as a list
continue;
end;
// msSingle, msSinglePseudo:
// process the section once with current context
end;
mtInvertedSection:
// display section for no key, false value, or empty list
if Context.AppendSection(ValueSpace, Value) <> msNothing then
begin
TagStart := SectionOppositeIndex;
continue; // ignore whole section
end;
mtSectionEnd:
if (fTags[SectionOppositeIndex].Kind in
[mtSection, mtInvertedSection]) and
(Value[1] <> '-') and
(fTags[SectionOppositeIndex].ValueSpace = 0) then
Context.PopContext;
mtComment:
; // just ignored
mtPartial:
begin
partial := fInternalPartials.GetPartial(Value);
if (partial = nil) and
(Context.fOwner <> self) then
// recursive call
partial := Context.fOwner.fInternalPartials.GetPartial(Value);
if (partial = nil) and
(Context.Partials <> nil) then
partial := Context.Partials.GetPartial(Value);
if partial <> nil then
partial.RenderContext(Context, 0, high(partial.fTags));
end;
mtSetPartial:
// ignore whole internal {{<partial}}
TagStart := SectionOppositeIndex;
mtTranslate:
if TextLen <> 0 then
Context.TranslateBlock(TextStart, TextLen);
else
raise ESynMustache.CreateUtf8('Kind=% not implemented yet',
[KindToText(fTags[TagStart].Kind)^]);
end;
inc(TagStart);
end;
end;
function TSynMustache.Render(const Context: variant;
Partials: TSynMustachePartials; const Helpers: TSynMustacheHelpers;
const OnTranslate: TOnStringTranslate; EscapeInvert: boolean): RawUtf8;
var
ctx: TSynMustacheContextVariant;
tmp: TTextWriterStackBuffer;
begin
ctx := fCachedContextVariant; // thread-safe reuse of shared rendering context
if ctx.fReuse.TryLock then
ctx.PushContext(TVarData(Context)) // weak copy
else
ctx := TSynMustacheContextVariant.Create(
self, TJsonWriter.CreateOwnedStream(tmp), SectionMaxCount, Context, true);
try
ctx.Helpers := Helpers;
ctx.Partials := Partials;
ctx.OnStringTranslate := OnTranslate;
ctx.EscapeInvert := EscapeInvert;
RenderContext(ctx, 0, high(fTags));
ctx.Writer.SetText(result);
finally
if (Partials <> nil) and
(Partials.fOwned) then
Partials.Free;
if ctx = fCachedContextVariant then
ctx.CancelAll
else
ctx.Free;
end;
end;
function TSynMustache.RenderJson(const Json: RawUtf8;
Partials: TSynMustachePartials; const Helpers: TSynMustacheHelpers;
const OnTranslate: TOnStringTranslate; EscapeInvert: boolean): RawUtf8;
var
context: variant;
begin
_Json(Json, context{%H-}, JSON_FAST_FLOAT);
result := Render(context, Partials, Helpers, OnTranslate, EscapeInvert);
end;
function TSynMustache.RenderJson(const Json: RawUtf8;
const Args, Params: array of const; Partials: TSynMustachePartials;
const Helpers: TSynMustacheHelpers; const OnTranslate: TOnStringTranslate;
EscapeInvert: boolean): RawUtf8;
var
context: variant;
begin
_Json(FormatJson(Json, Args, Params), context{%H-}, JSON_FAST_FLOAT);
result := Render(context, Partials, Helpers, OnTranslate, EscapeInvert);
end;
function TSynMustache.RenderData(const Value; ValueTypeInfo: PRttiInfo;
const OnGetData: TOnGetGlobalData; Partials: TSynMustachePartials;
const Helpers: TSynMustacheHelpers; const OnTranslate: TOnStringTranslate;
EscapeInvert: boolean): RawUtf8;
begin
result := RenderDataRtti(@Value, Rtti.RegisterType(ValueTypeInfo),
OnGetData, Partials, Helpers, OnTranslate, EscapeInvert);
end;
function TSynMustache.RenderDataArray(const Value: TDynArray;
const OnGetData: TOnGetGlobalData; Partials: TSynMustachePartials;
const Helpers: TSynMustacheHelpers; const OnTranslate: TOnStringTranslate;
EscapeInvert: boolean): RawUtf8;
var
n: PtrInt;
begin
n := Value.Count;
if n <> 0 then
DynArrayFakeLength(Value.Value^, n); // as required by RenderDataRtti()
result := RenderDataRtti(Value.Value, Value.Info,
OnGetData, Partials, Helpers, OnTranslate, EscapeInvert);
end;
function TSynMustache.RenderDataRtti(Value: pointer; ValueRtti: TRttiCustom;
const OnGetData: TOnGetGlobalData; Partials: TSynMustachePartials;
const Helpers: TSynMustacheHelpers; const OnTranslate: TOnStringTranslate;
EscapeInvert: boolean): RawUtf8;
var
ctx: TSynMustacheContextData;
tmp: TTextWriterStackBuffer;
begin
if ValueRtti = nil then
raise ESynMustache.CreateUtf8('%.RenderData: invalid TypeInfo', [self]);
ctx := fCachedContextData; // thread-safe reuse of shared rendering context
if ctx.fReuse.TryLock then
ctx.PushContext(Value, ValueRtti)
else
ctx := TSynMustacheContextData.Create(
self, TJsonWriter.CreateOwnedStream(tmp), SectionMaxCount,
Value, ValueRtti, {ownwriter=}true);
try
ctx.Helpers := Helpers;
ctx.Partials := Partials;
ctx.OnGetGlobalData := OnGetData;
ctx.OnStringTranslate := OnTranslate;
ctx.EscapeInvert := EscapeInvert;
RenderContext(ctx, 0, high(fTags));
ctx.Writer.SetText(result);
finally
if (Partials <> nil) and
(Partials.fOwned) then
Partials.Free;
if ctx = fCachedContextData then
ctx.CancelAll
else
ctx.Free;
end;
end;
destructor TSynMustache.Destroy;
begin
FreeAndNil(fInternalPartials);
inherited;
fCachedContextVariant.Free;
fCachedContextData.Free;
end;
function TSynMustache.FoundInTemplate(const text: RawUtf8): boolean;
begin
// internal partials are part of fTemplate
result := (self <> nil) and
(text <> '') and
(PosEx(text, fTemplate) > 0);
end;
class procedure TSynMustache.HelperAdd(var Helpers: TSynMustacheHelpers;
const aName: RawUtf8; aEvent: TSynMustacheHelperEvent);
var
n, i: PtrInt;
begin
n := length(Helpers);
for i := 0 to n - 1 do
if PropNameEquals(Helpers[i].Name, aName) then
begin
Helpers[i].Event := aEvent;
exit;
end;
SetLength(Helpers, n + 1);
Helpers[n].Name := aName;
Helpers[n].Event := aEvent;
end;
class procedure TSynMustache.HelperAdd(var Helpers: TSynMustacheHelpers;
const aNames: array of RawUtf8;
const aEvents: array of TSynMustacheHelperEvent);
var
n, i: PtrInt;
begin
n := length(aNames);
if n = length(aEvents) then
for i := 0 to n - 1 do
HelperAdd(Helpers, aNames[i], aEvents[i]);
end;
class procedure TSynMustache.HelperDelete(var Helpers: TSynMustacheHelpers;
const aName: RawUtf8);
var
n, i, j: PtrInt;
begin
n := length(Helpers);
for i := 0 to n - 1 do
if PropNameEquals(Helpers[i].Name, aName) then
begin
for j := i to n - 2 do
Helpers[j] := Helpers[j + 1];
SetLength(Helpers, n - 1);
exit;
end;
end;
class function TSynMustache.HelperFind(const Helpers: TSynMustacheHelpers;
aName: PUtf8Char; aNameLen: TStrLen): PtrInt;
var
h: ^TSynMustacheHelper;
p: PUtf8Char;
n: integer;
begin
h := pointer(Helpers);
if (h <> nil) and
(aNameLen > 0) then
begin
result := 0;
n := PDALen(PAnsiChar(h) - _DALEN)^ + _DAOFF;
repeat
P := pointer(h^.Name);
if (PStrLen(P - _STRLEN)^ = aNameLen) and
IdemPropNameUSameLenNotNull(P, aName, aNameLen) then
exit;
inc(h);
inc(result);
dec(n);
until n = 0;
end;
result := -1;
end;
var
HelpersStandardList: TSynMustacheHelpers;
class function TSynMustache.HelpersGetStandardList: TSynMustacheHelpers;
begin
if HelpersStandardList = nil then
HelperAdd(HelpersStandardList,
['DateTimeToText',
'DateToText',
'DateFmt',
'TimeLogToText',
'JsonQuote',
'JsonQuoteUri',
'ToJson',
'MarkdownToHtml',
'SimpleToHtml',
'WikiToHtml',
'BlobToBase64',
'EnumTrim',
'EnumTrimRight',
'PowerOfTwo',
'Equals',
'If',
'NewGuid',
'ExtractFileName',
'HumanBytes',
'Sub',
'Values',
'Keys',
'Match',
'MatchI',
'Lower',
'Upper'],
[DateTimeToText,
DateToText,
DateFmt,
TimeLogToText,
JsonQuote,
JsonQuoteUri,
ToJson,
MarkdownToHtml,
SimpleToHtml,
WikiToHtml,
BlobToBase64,
EnumTrim,
EnumTrimRight,
PowerOfTwo,
Equals_,
If_,
NewGuid,
ExtractFileName,
HumanBytes,
Sub,
Values,
Keys,
Match,
MatchI,
Lower,
Upper]);
result := HelpersStandardList;
end;
class function TSynMustache.HelpersGetStandardList(
const aNames: array of RawUtf8;
const aEvents: array of TSynMustacheHelperEvent): TSynMustacheHelpers;
begin
// make first a copy to not change/affect global HelpersStandardList
result := copy(HelpersGetStandardList);
HelperAdd(result, aNames, aEvents);
end;
class procedure TSynMustache.DateTimeToText(
const Value: variant; out Result: variant);
var
Time: TTimeLogBits;
dt: TDateTime;
begin
if VariantToDateTime(Value, dt) then
begin
Time.From(dt, false);
Result := Time.i18nText;
end
else
SetVariantNull(Result{%H-});
end;
class procedure TSynMustache.DateToText(const Value: variant;
out Result: variant);
var
Time: TTimeLogBits;
dt: TDateTime;
begin
if VariantToDateTime(Value, dt) then
begin
Time.From(dt, true);
Result := Time.i18nText;
end
else
SetVariantNull(Result{%H-});
end;
class procedure TSynMustache.DateFmt(const Value: variant;
out Result: variant);
var
dt: TDateTime;
dv: PDocVariantData;
begin
// {{DateFmt DateValue,"dd/mm/yyy"}}
if _SafeArray(Value, 2, dv) and
VariantToDateTime(dv^.Values[0], dt) then
Result := FormatDateTime(dv^.Values[1], dt)
else
SetVariantNull(Result{%H-});
end;
class procedure TSynMustache.TimeLogToText(const Value: variant;
out Result: variant);
var
Time: TTimeLogBits;
begin
if VariantToInt64(Value, Time.Value) then
Result := Time.i18nText
else
SetVariantNull(Result{%H-});
end;
class procedure TSynMustache.ToJson(const Value: variant;
out Result: variant);
var
u, r: RawUtf8;
wasstring: boolean;
begin
if VarIsEmptyOrNull(Value) then
exit;
VariantToUtf8(Value, u, wasstring);
if wasstring then
QuotedStrJson(u, r)
else if (u <> '') and
(GotoNextNotSpace(pointer(u))^ in ['[', '{']) then
r := JsonReformat(u) // e.g. from TDocVariantData
else
r := u; // false, true, number
RawUtf8ToVariant(r, Result);
end;
class procedure TSynMustache.JsonQuote(const Value: variant;
out Result: variant);
var
json: RawUtf8;
begin
if VariantToText(Value, json) then
RawUtf8ToVariant(QuotedStrJson(json), Result);
end;
class procedure TSynMustache.JsonQuoteUri(const Value: variant;
out Result: variant);
var
json: RawUtf8;
begin
if VariantToText(Value, json) then
RawUtf8ToVariant(UrlEncode(QuotedStrJson(json)), Result);
end;
procedure ToHtml(const Value: variant; var Result: variant;
fmt: TTextWriterHtmlEscape; wiki: boolean = false);
var
txt: RawUtf8;
d: PDocVariantData;
begin
// {{{SimpleToHtml content,browserhasnoemoji,nohtmlescape}}}
d := _Safe(Value);
if d^.IsArray and
(d^.Count >= 2) and
VariantToText(d^.Values[0], txt) then
begin
if not VarIsVoid(d^.Values[1]) then
exclude(fmt, heEmojiToUtf8);
if (d^.Count >= 3) and
not VarIsVoid(d^.Values[2]) then
exclude(fmt, heHtmlEscape);
end
else if not VariantToText(Value, txt) then
// {{{MarkdownToHtml content}}}
exit;
if txt <> '' then
if wiki then
txt := HtmlEscapeWiki(txt, fmt)
else
txt := HtmlEscapeMarkdown(txt, fmt);
RawUtf8ToVariant(txt, Result);
end;
class procedure TSynMustache.WikiToHtml(const Value: variant;
out Result: variant);
begin
ToHtml(Value, Result, [heHtmlEscape, heEmojiToUtf8], {wiki=}true);
end;
class procedure TSynMustache.MarkdownToHtml(const Value: variant;
out Result: variant);
begin
// default Markdown is to allow HTML tags
ToHtml(Value, Result, [heEmojiToUtf8]);
end;
class procedure TSynMustache.SimpleToHtml(const Value: variant;
out Result: variant);
begin
ToHtml(Value, Result, [heHtmlEscape, heEmojiToUtf8]);
end;
class procedure TSynMustache.BlobToBase64(const Value: variant;
out Result: variant);
var
tmp: RawUtf8;
wasString: boolean;
begin
VariantToUtf8(Value, tmp, wasString);
if wasString and
(pointer(tmp) <> nil) then
begin
if PInteger(tmp)^ and $00ffffff = JSON_BASE64_MAGIC_C then
delete(tmp, 1, 3);
RawUtf8ToVariant(tmp, Result);
end
else
Result := Value;
end;
class procedure TSynMustache.EnumTrim(const Value: variant;
out Result: variant);
var
tmp: RawUtf8;
wasString: boolean;
short: PUtf8Char;
begin
VariantToUtf8(Value, tmp, wasString);
if not wasString then
exit;
short := TrimLeftLowerCase(tmp);
RawUtf8ToVariant(short, StrLen(short), Result);
end;
class procedure TSynMustache.EnumTrimRight(const Value: variant;
out Result: variant);
var
tmp: RawUtf8;
wasString: boolean;
i, L: PtrInt;
begin
VariantToUtf8(Value, tmp, wasString);
if not wasString then
exit;
L := length(tmp);
for i := 1 to L do
if not (tmp[i] in ['a'..'z']) then
begin
L := i - 1;
break;
end;
RawUtf8ToVariant(Pointer(tmp), L, Result);
end;
class procedure TSynMustache.PowerOfTwo(const Value: variant;
out Result: variant);
var
V: Int64;
begin
if TVarData(Value).VType > varNull then
if VariantToInt64(Value, V) then
Result := Int64(1) shl V;
end;
class procedure TSynMustache.Equals_(const Value: variant;
out Result: variant);
var
dv: PDocVariantData;
begin
// {{#Equals .,12}}
if _SafeArray(Value, 2, dv) and
(FastVarDataComp(@dv^.Values[0], @dv^.Values[1], false) = 0) then
Result := VarTrue
else
SetVariantNull(Result{%H-});
end;
class procedure TSynMustache.If_(const Value: variant; out Result: variant);
var
cmp: integer;
oper: RawUtf8;
dv: PDocVariantData;
wasString: boolean;
begin
// {{#if .<>""}} or {{#if .,"=",123}}
SetVariantNull(result{%H-});
if not _SafeArray(Value, 3, dv) then
exit;
VariantToUtf8(dv^.Values[1], oper, wasString);
if (oper = '') or
not wasString then
exit;
cmp := FastVarDataComp(@dv^.Values[0], @dv^.Values[2], false);
case PWord(oper)^ of
ord('='):
if cmp = 0 then
result := VarTrue;
ord('>'):
if cmp > 0 then
result := VarTrue;
ord('<'):
if cmp < 0 then
result := VarTrue;
ord('>') + ord('=') shl 8:
if cmp >= 0 then
result := VarTrue;
ord('<') + ord('=') shl 8:
if cmp <= 0 then
result := VarTrue;
ord('<') + ord('>') shl 8:
if cmp <> 0 then
result := VarTrue;
end;
end;
class procedure TSynMustache.NewGuid(const Value: variant;
out Result: variant);
begin
RawUtf8ToVariant(GuidToRawUtf8(RandomGuid), Result);
end;
class procedure TSynMustache.ExtractFileName(const Value: variant;
out Result: variant);
begin
Result := SysUtils.ExtractFileName(Value);
end;
class procedure TSynMustache.HumanBytes(const Value: variant;
out Result: variant);
var
u: RawUtf8;
i64: Int64;
begin
if not VarIsEmptyOrNull(Value) then
if VariantToInt64(Value, i64) or
(VariantToUtf8(Value, u) and
ToInt64(u, i64)) then
KBU(i64, u);
RawUtf8ToVariant(u, Result);
end;
class procedure TSynMustache.Sub(const Value: variant;
out Result: variant);
var
utf: RawUtf8;
dv: PDocVariantData;
i, n: integer;
begin
// {{Sub AString,12,3}}
SetVariantNull(Result{%H-});
if _SafeArray(Value, 3, dv) and
VariantToText(dv^.Values[0], utf) and
VariantToInteger(dv^.Values[1], i) and
VariantToInteger(dv^.Values[2], n) then
RawUtf8ToVariant(copy(utf, i, n), Result);
end;
class procedure TSynMustache.Values(const Value: variant;
out Result: variant);
begin
TDocVariantData(Result).InitArrayFromObjectValues(Value, JSON_FAST);
end;
class procedure TSynMustache.Keys(const Value: variant;
out Result: variant);
begin
TDocVariantData(Result).InitArrayFromObjectNames(Value, JSON_FAST);
end;
procedure DoMatch(dv: PDocVariantData; ci: boolean; var res: variant);
var
s, p: RawUtf8;
begin
// {{Match AString,APattern}}
if VariantToText(dv^.Values[0], s) and
VariantToText(dv^.Values[1], p) and
IsMatch(p, s, ci) then
res := VarTrue;
end;
class procedure TSynMustache.Match(const Value: variant; out Result: variant);
var
dv: PDocVariantData;
begin
if _SafeArray(Value, 2, dv) then
DoMatch(dv, {caseinsens=}false, Result);
end;
class procedure TSynMustache.MatchI(const Value: variant; out Result: variant);
var
dv: PDocVariantData;
begin
if _SafeArray(Value, 2, dv) then
DoMatch(dv, {caseinsens=}true, Result);
end;
class procedure TSynMustache.Lower(const Value: variant;
out Result: variant);
var
u: RawUtf8;
begin
if VariantToText(Value, u) then
RawUtf8ToVariant(LowerCaseUnicode(u), Result);
end;
class procedure TSynMustache.Upper(const Value: variant;
out Result: variant);
var
u: RawUtf8;
begin
if VariantToText(Value, u) then
RawUtf8ToVariant(UpperCaseUnicode(u), Result);
end;
end.