LazSimpleTemplateParser
Very simple parser for Twig-like templates written in FreePascal / Lazarus
- Quiet · last change 12-27-2023
- ★ 4 on GitHub
README
LazSimpleTemplateParser
What Is It?
Sometimes your application needs to display some rich text, and it is easier and faster to use HTML or even a plain text than to program the behavior of visual components—for example, I use IPro components to display HTML. In such situations, it would be convenient to use some kind of text templates with data insertions (not just Format() or StringReplace() but something slightly more powerful). So one day I created this project.
[!CAUTION] As its name suggests, it is very simple thing that is not intended to be a good example of any programming techniques. I created the project for my own purposes and uploaded it in hope that it might be useful to someone else.
The SimpleTemplateParser class uses syntax inspired by Twig and other well-known HTML template systems. The template text seems like this:
<html>
<head>
<title>Dataset</title>
</head>
<body style="font-family: {{FONT_NAME}}; font-style: {{FONT_STYLE}}; font-weight: {{FONT_WEIGHT}}; font-size: {{FONT_SIZE}}pt; color: #{{FONT_COLOR}}; background-color: #{{BACK_COLOR}}">
<h1>Information on {{DATASET1_NAME}}</h1>
{%if Dataset1%}<dl>
{%for Dataset%}
<dt><strong>{{Dataset.Field1}}</strong></dt>
<dd>{{Dataset.Field2}}</dd>
{%endfor%}
</dl>
{%else%}<em>{{EMPTY_INFO}}</em>{%endif%}
</body>
</html>You may notice the identifier Query1, which hints that the template engine is working with data sets and data fields. This is true.
Template Syntax
All template text consists of regular text and special commands. The markers of these commands are the symbols {{, }}, {%, and %}. Double curly braces {{ }} indicate data to be inserted, and curly braces with a percentage {% %} mark keywords (these braces nesting not allowed). Additionally, the double question mark symbol ?? is used, see Data Output subsection. Here is a full list of the possible syntax entities:
Plain Text
- plain text: neither
{{nor{%open braces, output is the same text as input
Data Output
- variable or text file line:
{{VAR_NAME}}(the name is case-insensitive), output is a value of theTSimpleTemplateParser.Variablesarray, or a value fromAVarListparameter of theCreateContent()procedure (see Public Properties And Methods section), or a text file line (see Loop Operators subsection) - data field:
{{DataSet.FieldName}}or{{DataSet.FieldName??Coalesce string}}, output is a value of the field from current dataset record. If the field is null and coalesce string is specified then output is the coalesce string
Conditional Operators (If)
- if (with variable):
{%if VAR_NAME%}...{%else%}...{%endif%}or{%if VAR_NAME%}...{%endif%}. ChecksTSimpleTemplateParser.Variables[VAR_NAME]andAVarListparameter of theCreateContent()procedure (see Public Properties And Methods section). If it exists then process a part between{%if…}and{%else%}, otherwise process a part between{%else%}and{%endif%}(if specified). The parser doesn't check the variable value, only its existence! - if (with text file alias):
{%if Text%}...{%else%}...{%endif%}or{%if Text%}...{%endif%}. Checks if the text file alias is presented in theTSimpleTemplateParser.TextFilesarray. If it exists then process a part between{%if…}and{%else%}, otherwise process a part between{%else%}and{%endif%}(if specified) - if (with dataset):
{%if Dataset%}...{%else%}...{%endif%}or{%if Dataset%}...{%endif%}. Checks the dataset. If it is not empty then process a part between{%if…}and{%else%}, otherwise process a part between{%else%}and{%endif%}(if specified) - if (with data field):
{%if Dataset.Field%}...{%else%}...{%endif%}or{%if Dataset.Field%}...{%endif%}. Checks the field value. If it is not NULL then process a part between{%if…}and{%else%}, otherwise process a part between{%else%}and{%endif%}(if specified). The parser doesn't check the field type and value, only NULL / not NULL!
Loop Operators (For)
- for (with text file alias):
{%for Text%}...{{Text}}...{%endfor%}. Enumerates all lines in the text file (alias is a value in theTSimpleTemplateParser.TextFilesarray) and processes each line. Variable with the same name as the text file alias (Text) outputs as the text file line, empty lines are skipped - for (with dataset):
{%for Dataset%}...{%endfor%}. Enumerates all dataset records and processes each record (there might be field values, if-checks, etc.).
That's all, folks! ©
It is really simple parser.
Thus, the demo code above means:
<html>
<head>
<title>Dataset</title>
</head>
<body style="font-family: {{FONT_NAME}}; font-style: {{FONT_STYLE}}; font-weight: {{FONT_WEIGHT}}; font-size: {{FONT_SIZE}}pt; color: #{{FONT_COLOR}}; background-color: #{{BACK_COLOR}}"> <!-- substitute font properties, the calling application must specify these values via parser's Variables array-->
<h1>Information on {{DATASET1_NAME}}</h1> <!-- it is a variable too -->
{%if Dataset1%}<dl> <!-- if the dataset is not empty... -->
{%for Dataset%} <!-- for each record write two field values -->
<dt><strong>{{Dataset.Field1}}</strong></dt>
<dd>{{Dataset.Field2}}</dd>
{%endfor%}
</dl>
{%else%}<em>{{EMPTY_INFO}}</em>{%endif%} <!-- ...else write EMPTY_INFO variable value instead of the <dl> element -->
</body>
</html>Files
The simpletemplateparser.pas module contains the main TSimpleTemplateParser class.
The parserentities.inc module contains auxiliary classes that implement particular types of syntactic tokens. The textfilecontents.inc file contains another auxiliary class to simplify work with text files.
Folder demo belong to the demo project. It is very basic, just shows a list of fake news from text file news.txt.
Dependencies
It doesn't require anything special to compile the code other than the standard LCL package. Tested on Lazarus 2.2.6, should work on earlier (more or less modern) and later versions.
Public Properties And Methods
constructor Create(ATemplate: string);Create a new instance of the class. ATemplate is a template content (text).
constructor CreateFromFile(AFileName: string);Another constructor that creates a new instance of the class and loads a template from a file. It's easy to guess that AFileName is the name of the file.
procedure LoadFromStream(AStream: TStream);Load the template string from the stream.
procedure LoadFromFile(AFileName: string);Load the template string from the text file.
procedure AddDataset(Dataset: TDataset; DisableScrollEvents: Boolean = True);
procedure RemoveDataset(Dataset: TDataset);Add / remove the dataset. The DisableScrollEvents option means: if it is true then the engine will temporarily remove the dataset's BeforeScroll / AfterScroll event handlers when inserting the data.
[!WARNING] Add all datasets you need before parsing because
Parse()method uses the dataset list to properly parse some commands like{%if ...%},{%for ...%}, etc.
procedure Parse;Start parsing. Usually you do not need to call this method manually because it is called from CreateContent().
procedure CreateContent(AVarList: TStrings = nil);Insert the previously stored constants, variables, dataset data, etc. into the Content string. AVarList is a list of 'local' variables. But the Variables array takes precedence if some variable name is present here and there: the variables from AVarList are inserted in a place of {{VAR_NAME}} if the Variables array does not contain the given name. The parameter AVarList is useful when there are many parser instances with the same variables.
property Content: string read FContent;The content after inserting the data into the template (i.e. CreateContent() call).
property Variables[Name: string]: string;A list of variables for {{VAR_NAME}} substitution.
property TextFiles[Name: string]: string;A list of text file aliases and paths. E.g.: TextFiles['Text'] := 'C:\myproject\mytext.txt'.
Demo
The demo project uses IPro components and can be compiled under both Windows and GNU/Linux.
It is very basic: the ParserDemo.stp file contains a simple "news" template, and news.txt is the list of news to display.
Author
Copyright (c) 2023, Michael Demidov
Visit my GitHub page to check for updates, report issues, etc.: https://github.com/MichaelDemidov
Drop me an e-mail at: michael.v.demidov@gmail.com

