├── .gitignore
├── LICENSE
├── README.md
├── src
├── Liquid.Block.pas
├── Liquid.Condition.pas
├── Liquid.Context.pas
├── Liquid.Default.pas
├── Liquid.Document.pas
├── Liquid.Exceptions.pas
├── Liquid.Filters.pas
├── Liquid.Hash.pas
├── Liquid.Interfaces.pas
├── Liquid.Tag.pas
├── Liquid.Tags.pas
├── Liquid.Template.pas
├── Liquid.Tuples.pas
├── Liquid.Utils.pas
└── Liquid.Variable.pas
└── tests
├── LiquidTest.dpr
├── LiquidTest.dproj
├── LiquidTest.res
├── TestLiquid.pas
└── templates
├── template1.html
└── template1_rendered.html
/.gitignore:
--------------------------------------------------------------------------------
1 | __history
2 | /tests/Win32/Debug
3 | /tests/LiquidTest.dproj.local
4 | /tests/LiquidTest.identcache
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # liquid-delphi
2 | Delphi Port of Tobias Lütke's Liquid template language.
3 |
4 | ### What is this?
5 |
6 | Liquid-delphi is a Delphi port of the popular [Ruby Liquid templating
7 | language](https://shopify.github.io/liquid/) and [dotLiquid](https://github.com/dotliquid/dotliquid) implementation. It is a separate project that aims to
8 | retain the same template syntax as the original, while using delphi coding
9 | conventions where possible.
10 |
11 | This project uses/translates parts of the code of the following repositories:
12 | 1.
13 | 2.
14 |
--------------------------------------------------------------------------------
/src/Liquid.Block.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Block;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Generics.Collections,
7 | System.RegularExpressions, System.Classes,
8 | System.Rtti,
9 |
10 | Liquid.Interfaces,
11 | Liquid.Exceptions,
12 | Liquid.Tag,
13 | Liquid.Variable,
14 | Liquid.Context,
15 | Liquid.Default,
16 | Liquid.Utils;
17 |
18 | type
19 | TBlock = class(TTag)
20 | strict private
21 | FIsTag: TRegEx;
22 | FIsVariable: TRegEx;
23 | FContentOfVariable: TRegEx;
24 | FFullToken: TRegEx;
25 | FObjects: TList;
26 | function BlockName: string;
27 | protected
28 | procedure Parse(ATokens: TList); override;
29 | function BlockDelimiter: string; virtual;
30 | procedure AssertMissingDelimitation; virtual;
31 | procedure RenderAll(ANodeList: INodeList; Context: ILiquidContext;
32 | Writer: TTextWriter);
33 | procedure AddToGarbage(AObject: TObject);
34 | public
35 | constructor Create;
36 | destructor Destroy; override;
37 | procedure EndTag; virtual;
38 | procedure UnknownTag(const Tag: string; const Markup: string;
39 | Tokens: TList); virtual;
40 | function CreateVariable(const Token: string): TVariable;
41 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
42 | end;
43 |
44 | implementation
45 |
46 | uses
47 | Liquid.Template;
48 |
49 | { TBlock }
50 |
51 | procedure TBlock.AddToGarbage(AObject: TObject);
52 | begin
53 | FObjects.Add(AObject);
54 | end;
55 |
56 | procedure TBlock.AssertMissingDelimitation;
57 | begin
58 | raise ELiquidSyntaxException.CreateFmt(
59 | '%0:s tag was never closed', [BlockName]);
60 | end;
61 |
62 | function TBlock.BlockDelimiter: string;
63 | begin
64 | Result := Format('end%s', [BlockName]);
65 | end;
66 |
67 | function TBlock.BlockName: string;
68 | begin
69 | Result := TagName;
70 | end;
71 |
72 | constructor TBlock.Create;
73 | begin
74 | inherited Create;
75 | FIsTag := R.B('^%s', [LiquidRegexes.TagStart]);
76 | FIsVariable := R.B('^%0:s', [LiquidRegexes.VariableStart]);
77 | FContentOfVariable := R.B('^%0:s(.*)%1:s$', [LiquidRegexes.VariableStart, LiquidRegexes.VariableEnd]);
78 | FFullToken := R.B('^%0:s\s*(\w+)\s*(.*)?%1:s$', [LiquidRegexes.TagStart, LiquidRegexes.TagEnd]);
79 | FObjects := TObjectList.Create;
80 | end;
81 |
82 | function TBlock.CreateVariable(const Token: string): TVariable;
83 | begin
84 | var Match := FContentOfVariable.Match(Token);
85 | if Match.Success then
86 | Exit(TVariable.Create(Match.Groups[1].Value));
87 | raise ELiquidSyntaxException.CreateFmt(
88 | 'Variable ''%0:s'' was not properly terminated with regexp: %1:s',
89 | [Token, LiquidRegexes.VariableEnd]);
90 | end;
91 |
92 | destructor TBlock.Destroy;
93 | begin
94 | FObjects.Free;
95 | inherited;
96 | end;
97 |
98 | procedure TBlock.EndTag;
99 | begin
100 | end;
101 |
102 | procedure TBlock.Parse(ATokens: TList);
103 | begin
104 | NodeList.Clear;
105 | while ATokens.Count > 0 do
106 | begin
107 | var Token := ATokens.ExtractAt(0);
108 | var IsTagMatch := FIsTag.Match(Token);
109 | if IsTagMatch.Success then
110 | begin
111 | var FullTokenMatch := FFullToken.Match(Token);
112 | if FullTokenMatch.Success then
113 | begin
114 | // If we found the proper block delimitor just end parsing here and let the outer block
115 | // proceed
116 | if BlockDelimiter = FullTokenMatch.Groups[1].Value then
117 | begin
118 | EndTag;
119 | Exit;
120 | end;
121 |
122 | // Fetch the tag from registered blocks
123 | var Tag := TLiquidTemplate.CreateTag(FullTokenMatch.Groups[1].Value);
124 | if Tag <> nil then
125 | begin
126 | AddToGarbage(Tag);
127 | Tag.Initialize(FullTokenMatch.Groups[1].Value,
128 | FullTokenMatch.Groups[2].Value, ATokens);
129 | NodeList.Add(Tag);
130 |
131 | // If the tag has some rules (eg: it must occur once) then check for them
132 | Tag.AssertTagRulesViolation(NodeList);
133 | end
134 | else
135 | begin
136 | // This tag is not registered with the system
137 | // pass it to the current block for special handling or error reporting
138 | UnknownTag(FullTokenMatch.Groups[1].Value, FullTokenMatch.Groups[2].Value,
139 | ATokens);
140 | end;
141 | end
142 | else
143 | raise ELiquidSyntaxException.CreateFmt(
144 | 'Tag ''%0:s'' was not properly terminated with regexp: %1:s',
145 | [Token, LiquidRegexes.TagEnd]);
146 | end
147 | else if FIsVariable.Match(Token).Success then
148 | begin
149 | var Variable := CreateVariable(Token);
150 | NodeList.Add(Variable);
151 | AddToGarbage(Variable);
152 | end
153 | else if Token.IsEmpty then
154 | begin
155 | // Pass
156 | end
157 | else
158 | NodeList.Add(Token);
159 | end;
160 | end;
161 |
162 | procedure TBlock.Render(Context: ILiquidContext; Writer: TTextWriter);
163 | begin
164 | RenderAll(NodeList, Context, Writer);
165 | end;
166 |
167 | procedure TBlock.RenderAll(ANodeList: INodeList; Context: ILiquidContext;
168 | Writer: TTextWriter);
169 | begin
170 | for var Token in ANodeList do
171 | begin
172 | try
173 | if Token.IsType then
174 | Token.AsType.Render(Context, Writer)
175 | else if Token.IsType then
176 | Token.AsType.Render(Context, Writer)
177 | else
178 | Writer.Write(Token.AsString);
179 | except
180 | on E: ELiquidException do
181 | begin
182 | var Msg: string;
183 | if Context.HandleError(E, Msg) then
184 | Writer.Write(Msg)
185 | else
186 | raise;
187 | end;
188 | end;
189 | end;
190 | end;
191 |
192 | procedure TBlock.UnknownTag(const Tag, Markup: string; Tokens: TList);
193 | begin
194 | if Tag = 'else' then
195 | raise ELiquidSyntaxException.CreateFmt('%0:s tag does not expect else tag',
196 | [BlockName])
197 | else if Tag = 'end' then
198 | raise ELiquidSyntaxException.CreateFmt(
199 | '''end'' is not a valid delimiter for %0:s tags. Use %1:s',
200 | [BlockName, BlockDelimiter])
201 | else
202 | raise ELiquidSyntaxException.CreateFmt('Unknown tag ''%0:s''',
203 | [Tag]);
204 | end;
205 |
206 | end.
207 |
--------------------------------------------------------------------------------
/src/Liquid.Condition.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Condition;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils,
7 | System.Classes, System.Generics.Collections,
8 | System.Generics.Defaults,
9 | System.RegularExpressions,
10 | System.Rtti,
11 | System.TypInfo,
12 |
13 | Liquid.Default,
14 | Liquid.Interfaces,
15 | Liquid.Context,
16 | Liquid.Variable,
17 | Liquid.Exceptions,
18 | Liquid.Hash,
19 | Liquid.Utils,
20 | Liquid.Tag;
21 |
22 | type
23 | IConditionOperatorDelegate = interface
24 | ['{102E2511-5223-4CF4-9275-25EE917307F0}']
25 | function Evaluate(const Left, Right: TValue): boolean;
26 | end;
27 |
28 | TCondition = class
29 | strict private
30 | class var
31 | FOperators: TDictionary;
32 | private
33 | FChildRelation: string;
34 | FChildCondition: TCondition;
35 | FLeft: string;
36 | FOperator: string;
37 | FRight: string;
38 | FAttachment: INodeList;
39 | private
40 | class function InterpretCondition(const ALeft: string; const ARight: string;
41 | const AOperator: string; AContext: ILiquidContext): boolean;
42 | public
43 | class constructor Create;
44 | class destructor Destroy;
45 | class function Operators: TDictionary;
46 | class function Any(Enumerable: IEnumerable; Condition: TFunc): boolean;
47 | public
48 | constructor Create; overload;
49 | constructor Create(ASyntaxMatch: TMatch); overload;
50 | constructor Create(const ALeft: string; const AOperator: string;
51 | const ARight: string); overload;
52 | destructor Destroy; override;
53 | function IsElse: boolean; virtual;
54 | function Evaluate(Context: ILiquidContext; FormatSettings: TFormatSettings): boolean; virtual;
55 | procedure _Or(Condition: TCondition);
56 | procedure _And(Condition: TCondition);
57 | function Attach(Attachment: INodeList): INodeList;
58 | function ToString: string; override;
59 | property Left: string read FLeft write FLeft;
60 | property _Operator: string read FOperator write FOperator;
61 | property Right: string read FRight write FRight;
62 | property Attachment: INodeList read FAttachment;
63 | end;
64 |
65 | TElseCondition = class(TCondition)
66 | public
67 | function IsElse: boolean; override;
68 | function Evaluate(Context: ILiquidContext; FormatSettings: TFormatSettings): boolean; override;
69 | end;
70 |
71 | TConditionOperatorDelegate = class(TInterfacedObject, IConditionOperatorDelegate)
72 | strict private
73 | class function Compare(const Left, Right: IHash): boolean; overload;
74 | protected
75 | class function EqualVariables(const Left, Right: TValue): boolean;
76 | class function Compare(const Left, Right: TValue): integer; overload;
77 | class function Compare(const Left, Right: TValue; var Value: integer): boolean; overload;
78 | public
79 | function Evaluate(const Left, Right: TValue): boolean; virtual; abstract;
80 | end;
81 |
82 | TEqualOperatorDelegate = class(TConditionOperatorDelegate)
83 | public
84 | function Evaluate(const Left, Right: TValue): boolean; override;
85 | end;
86 |
87 | TNotEqualOperatorDelegate = class(TConditionOperatorDelegate)
88 | public
89 | function Evaluate(const Left, Right: TValue): boolean; override;
90 | end;
91 |
92 | TGreaterThanOperatorDelegate = class(TConditionOperatorDelegate)
93 | public
94 | function Evaluate(const Left, Right: TValue): boolean; override;
95 | end;
96 |
97 | TGreaterThanEqualOperatorDelegate = class(TConditionOperatorDelegate)
98 | public
99 | function Evaluate(const Left, Right: TValue): boolean; override;
100 | end;
101 |
102 | TLessThanOperatorDelegate = class(TConditionOperatorDelegate)
103 | public
104 | function Evaluate(const Left, Right: TValue): boolean; override;
105 | end;
106 |
107 | TLessThanEqualOperatorDelegate = class(TConditionOperatorDelegate)
108 | public
109 | function Evaluate(const Left, Right: TValue): boolean; override;
110 | end;
111 |
112 | TContainsOperatorDelegate = class(TConditionOperatorDelegate)
113 | public
114 | function Evaluate(const Left, Right: TValue): boolean; override;
115 | end;
116 |
117 | TStartsWithOperatorDelegate = class(TConditionOperatorDelegate)
118 | public
119 | function Evaluate(const Left, Right: TValue): boolean; override;
120 | end;
121 |
122 | TEndsWithOperatorDelegate = class(TConditionOperatorDelegate)
123 | public
124 | function Evaluate(const Left, Right: TValue): boolean; override;
125 | end;
126 |
127 | THasKeyOperatorDelegate = class(TConditionOperatorDelegate)
128 | public
129 | function Evaluate(const Left, Right: TValue): boolean; override;
130 | end;
131 |
132 | THasValueOperatorDelegate = class(TConditionOperatorDelegate)
133 | public
134 | function Evaluate(const Left, Right: TValue): boolean; override;
135 | end;
136 |
137 | implementation
138 |
139 | { TCondition }
140 |
141 | class function TCondition.Any(Enumerable: IEnumerable;
142 | Condition: TFunc): boolean;
143 | begin
144 | for var Value in Enumerable do
145 | if Condition(Value) then
146 | Exit(True);
147 | Result := False;
148 | end;
149 |
150 | function TCondition.Attach(Attachment: INodeList): INodeList;
151 | begin
152 | FAttachment := Attachment;
153 | Result := FAttachment;
154 | end;
155 |
156 | class constructor TCondition.Create;
157 | begin
158 | TCondition.FOperators := TDictionary.Create;
159 | var Operators := TCondition.Operators;
160 |
161 | Operators.Add('==', TEqualOperatorDelegate.Create);
162 | Operators.Add('!=', TNotEqualOperatorDelegate.Create);
163 | Operators.Add('<>', TNotEqualOperatorDelegate.Create);
164 | Operators.Add('>', TGreaterThanOperatorDelegate.Create);
165 | Operators.Add('>=', TGreaterThanEqualOperatorDelegate.Create);
166 | Operators.Add('<', TLessThanOperatorDelegate.Create);
167 | Operators.Add('<=', TLessThanEqualOperatorDelegate.Create);
168 | Operators.Add('contains', TContainsOperatorDelegate.Create);
169 | Operators.Add('startsWith', TStartsWithOperatorDelegate.Create);
170 | Operators.Add('endsWith', TEndsWithOperatorDelegate.Create);
171 | Operators.Add('hasKey', THasKeyOperatorDelegate.Create);
172 | Operators.Add('hasValue', THasValueOperatorDelegate.Create);
173 | end;
174 |
175 | constructor TCondition.Create(const ALeft, AOperator, ARight: string);
176 | begin
177 | Create;
178 | FLeft := ALeft;
179 | FOperator := AOperator;
180 | FRight := ARight;
181 | end;
182 |
183 | destructor TCondition.Destroy;
184 | begin
185 | FChildCondition.Free;
186 | end;
187 |
188 | constructor TCondition.Create(ASyntaxMatch: TMatch);
189 | begin
190 | if ASyntaxMatch.Groups.Count = 4 then
191 | begin
192 | Create(
193 | ASyntaxMatch.Groups[1].Value,
194 | ASyntaxMatch.Groups[2].Value,
195 | ASyntaxMatch.Groups[3].Value
196 | );
197 | end
198 | else
199 | Create(ASyntaxMatch.Groups[1].Value, '', '');
200 | end;
201 |
202 | class destructor TCondition.Destroy;
203 | begin
204 | TCondition.FOperators.Free;
205 | end;
206 |
207 | constructor TCondition.Create;
208 | begin
209 | FAttachment := TNodeList.Create;
210 | end;
211 |
212 | function TCondition.Evaluate(Context: ILiquidContext;
213 | FormatSettings: TFormatSettings): boolean;
214 | begin
215 | var OwnContext := False;
216 | if Context = nil then
217 | begin
218 | Context := TLiquidContext.Create(FormatSettings);
219 | OwnContext := True;
220 | end;
221 | try
222 | Result := InterpretCondition(Left, Right, _Operator, Context);
223 | if FChildRelation = 'or' then
224 | Result := Result or FChildCondition.Evaluate(Context, FormatSettings)
225 | else if FChildRelation = 'and' then
226 | Result := Result and FChildCondition.Evaluate(Context, FormatSettings);
227 | finally
228 | if OwnContext then
229 | Context := nil;
230 | end;
231 | end;
232 |
233 | class function TCondition.InterpretCondition(const ALeft, ARight,
234 | AOperator: string; AContext: ILiquidContext): boolean;
235 | begin
236 | // If the operator is empty this means that the decision statement is just
237 | // a single variable. We can just poll this variable from the context and
238 | // return this as the result.
239 | if string.IsNullOrEmpty(AOperator) then
240 | begin
241 | var Value := AContext[ALeft, False];
242 | Exit((not Value.IsEmpty) and
243 | ((not Value.IsType) or (Value.AsBoolean)));
244 | end;
245 |
246 | var LeftValue := AContext[ALeft];
247 | var RightValue := AContext[ARight];
248 |
249 | var OperatorKey: string := '';
250 | for var Opk in Operators.Keys do
251 | if Opk.Equals(AOperator) or Opk.ToLowerInvariant.Equals(AOperator) then
252 | begin
253 | OperatorKey := Opk;
254 | Break;
255 | end;
256 | if OperatorKey.IsEmpty then
257 | raise EArgumentException.CreateFmt('Unknown operator %s', [AOperator]);
258 | Result := Operators[OperatorKey].Evaluate(LeftValue, RightValue);
259 | end;
260 |
261 | function TCondition.IsElse: boolean;
262 | begin
263 | Result := False;
264 | end;
265 |
266 | class function TCondition.Operators: TDictionary;
267 | begin
268 | Result := FOperators;
269 | end;
270 |
271 | function TCondition.ToString: string;
272 | begin
273 | Result := Format('', [Left, _Operator, Right]);
274 | end;
275 |
276 | procedure TCondition._And(Condition: TCondition);
277 | begin
278 | FChildRelation := 'and';
279 | if FChildCondition <> nil then
280 | FChildCondition.Free;
281 | FChildCondition := Condition;
282 | end;
283 |
284 | procedure TCondition._Or(Condition: TCondition);
285 | begin
286 | FChildRelation := 'or';
287 | if FChildCondition <> nil then
288 | FChildCondition.Free;
289 | FChildCondition := Condition;
290 | end;
291 |
292 | { TElseCondition }
293 |
294 | function TElseCondition.Evaluate(Context: ILiquidContext;
295 | FormatSettings: TFormatSettings): boolean;
296 | begin
297 | Result := True;
298 | end;
299 |
300 | function TElseCondition.IsElse: boolean;
301 | begin
302 | Result := True;
303 | end;
304 |
305 | //class function TConditionComparer.EqualVariables(ALeft,
306 | // ARight: TValue): boolean;
307 | //begin
308 | // if ALeft.IsType(False) then
309 | // Exit(ALeft.AsType.EvaluationFunction(ARight));
310 | // if ARight.IsType(False) then
311 | // Exit(ARight.AsType.EvaluationFunction(ALeft));
312 | // Result := TCompareUtils.Compare(ALeft, ARight) = 0;
313 | //end;
314 |
315 | { TConditionOperatorDelegate }
316 |
317 | class function TConditionOperatorDelegate.Compare(const Left, Right: TValue;
318 | var Value: integer): boolean;
319 | begin
320 | // if Left.IsEmpty then
321 | // begin
322 | // Value := TDelegatedComparer.Default.Compare(Left, Right);
323 | // Exit(True);
324 | // end;
325 | if Right.IsEmpty or Left.IsEmpty then
326 | Exit(False);
327 | if Left.TypeInfo = Right.TypeInfo then
328 | begin
329 | Value := TCompareUtils.Compare(Left, Right);
330 | Exit(True);
331 | end;
332 |
333 | var RightChanged := TConverter.ChangeType(Right, Left.TypeInfo, Left.Kind);
334 | if (Left.TypeInfo = RightChanged.TypeInfo) or (Left.Kind = RightChanged.Kind) then
335 | begin
336 | Value := TCompareUtils.Compare(Left, RightChanged);
337 | Exit(True);
338 | end;
339 | Result := False;
340 | end;
341 |
342 | class function TConditionOperatorDelegate.EqualVariables(const Left,
343 | Right: TValue): boolean;
344 | begin
345 | if Left.IsEmpty and Right.IsEmpty then
346 | Exit(True);
347 | if Left.IsEmpty or Right.IsEmpty then
348 | Exit(False);
349 | // if Left.TypeInfo <> Right.TypeInfo then
350 | // Exit(False);
351 | if Left.IsType> and Right.IsType> then
352 | begin
353 | var LeftArray := Left.AsType>;
354 | var RightArray := Right.AsType>;
355 | if Length(LeftArray) <> Length(RightArray) then
356 | Exit(False);
357 | for var I := 0 to Length(LeftArray) - 1 do
358 | if not EqualVariables(LeftArray[I], RightArray[I]) then
359 | Exit(False);
360 | Result := True;
361 | end
362 | else if Left.IsType and Right.IsType then
363 | begin
364 | Result := Compare(Left.AsType, Right.AsType);
365 | end
366 | else
367 | begin
368 | var Res: integer;
369 | if Compare(Left, Right, Res) then
370 | Result := Res = 0
371 | else
372 | Result := False;
373 | end;
374 | end;
375 |
376 | class function TConditionOperatorDelegate.Compare(const Left, Right: TValue): integer;
377 | begin
378 | if not Compare(Left, Right, Result) then
379 | raise ECompareException.Create('Unrealized operation');
380 | end;
381 |
382 | class function TConditionOperatorDelegate.Compare(const Left, Right: IHash): boolean;
383 | begin
384 | if Left.Count <> Right.Count then
385 | Exit(False);
386 | for var LeftPair in Left.ToArray do
387 | begin
388 | if not Right.ContainsKey(LeftPair.Key) then
389 | Exit(False);
390 | if not EqualVariables(LeftPair.Value, Right[LeftPair.Key]) then
391 | Exit(False);
392 | end;
393 | Result := True;
394 | end;
395 |
396 | { TEqualOperatorDelegate }
397 |
398 | function TEqualOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
399 | begin
400 | Result := EqualVariables(Left, Right);
401 | end;
402 |
403 | { TNotEqualOperatorDelegate }
404 |
405 | function TNotEqualOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
406 | begin
407 | Result := not EqualVariables(Left, Right);
408 | end;
409 |
410 | { TGreaterThanOperatorDelegate }
411 |
412 | function TGreaterThanOperatorDelegate.Evaluate(const Left,
413 | Right: TValue): boolean;
414 | begin
415 | var Value: integer;
416 | if Compare(Left, Right, Value) then
417 | Result := Value > 0
418 | else
419 | Result := False;
420 | end;
421 |
422 | { TGreaterThanEqualOperatorDelegate }
423 |
424 | function TGreaterThanEqualOperatorDelegate.Evaluate(const Left,
425 | Right: TValue): boolean;
426 | begin
427 | var Value: integer;
428 | if Compare(Left, Right, Value) then
429 | Result := Value >= 0
430 | else
431 | Result := False;
432 | end;
433 |
434 | { TLessThanOperatorDelegate }
435 |
436 | function TLessThanOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
437 | begin
438 | var Value: integer;
439 | if Compare(Left, Right, Value) then
440 | Result := Value < 0
441 | else
442 | Result := False;
443 | end;
444 |
445 | { TLessThanEqualOperatorDelegate }
446 |
447 | function TLessThanEqualOperatorDelegate.Evaluate(const Left,
448 | Right: TValue): boolean;
449 | begin
450 | var Value: integer;
451 | if Compare(Left, Right, Value) then
452 | Result := Value <= 0
453 | else
454 | Result := False;
455 | end;
456 |
457 | { TContainsOperatorDelegate }
458 |
459 | function TContainsOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
460 | begin
461 | if Left.IsEmpty then
462 | Exit(False);
463 | if Left.IsType then
464 | begin
465 | if Right.IsType then
466 | Exit(Left.AsString.Contains(Right.AsString));
467 | end
468 | else if Left.IsArray then
469 | begin
470 | for var Item in Left.AsType> do
471 | begin
472 | if EqualVariables(Item, Right) then
473 | Exit(True);
474 | end;
475 | end;
476 | Result := False;
477 | end;
478 |
479 | { TStartsWithOperatorDelegate }
480 |
481 | function TStartsWithOperatorDelegate.Evaluate(const Left,
482 | Right: TValue): boolean;
483 | begin
484 | if Left.IsType then
485 | begin
486 | if Right.IsType then
487 | Exit(Left.AsString.StartsWith(Right.AsString));
488 | end
489 | else if Left.IsArray then
490 | begin
491 | var List := Left.AsType>;
492 | if Length(List) = 0 then
493 | Exit(False);
494 | var First := List[0];
495 | if EqualVariables(First, Right) then
496 | Exit(True);
497 | end;
498 | Result := False;
499 | end;
500 |
501 | { TEndsWithOperatorDelegate }
502 |
503 | function TEndsWithOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
504 | begin
505 | if Left.IsType then
506 | begin
507 | if Right.IsType then
508 | Exit(Left.AsString.EndsWith(Right.AsString));
509 | end
510 | else if Left.IsArray then
511 | begin
512 | var List := Left.AsType>;
513 | if Length(List) = 0 then
514 | Exit(False);
515 | var ArrayValue := Left.AsType>;
516 | var Last := ArrayValue[Length(ArrayValue) - 1];
517 | if EqualVariables(Last, Right) then
518 | Exit(True);
519 | end;
520 | Result := False;
521 | end;
522 |
523 | { THasKeyOperatorDelegate }
524 |
525 | function THasKeyOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
526 | begin
527 | if Left.IsEmpty then
528 | Exit(False);
529 | if not Right.IsType then
530 | Exit(False);
531 | if Left.IsType then
532 | Exit(Left.AsType.ContainsKey(Right.AsString));
533 | Result := False;
534 | end;
535 |
536 | { THasValueOperatorDelegate }
537 |
538 | function THasValueOperatorDelegate.Evaluate(const Left, Right: TValue): boolean;
539 | begin
540 | if Left.IsEmpty then
541 | Exit(False);
542 |
543 | if Left.IsType then
544 | begin
545 | for var Pair in Left.AsType.ToArray do
546 | if EqualVariables(Pair.Value, Right) then
547 | Exit(True);
548 | end;
549 | Result := False;
550 | end;
551 |
552 | end.
553 |
--------------------------------------------------------------------------------
/src/Liquid.Context.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Context;
2 |
3 | {$SCOPEDENUMS ON}
4 |
5 | interface
6 |
7 | uses
8 | System.Classes, System.SysUtils,
9 | System.Generics.Collections,
10 | System.RegularExpressions,
11 | System.Rtti,
12 |
13 | Liquid.Interfaces,
14 | Liquid.Default,
15 | Liquid.Hash,
16 | Liquid.Filters,
17 | Liquid.Exceptions,
18 | Liquid.Utils;
19 |
20 | type
21 | TLiquidContext = class(TInterfacedObject, ILiquidContext)
22 | strict private
23 | //
24 | FErrorsOutputMode: TErrorsOutputMode;
25 | FMaxIterations: integer;
26 | FEnvironments: TList;
27 | FScopes: TList;
28 | FRegisters: IHash;
29 | FErrors: TList;
30 | FFormatSettings: TFormatSettings;
31 | FStrainer: IStrainer;
32 | private
33 | function Variable(const Markup: string; NotifyNotFound: boolean): TValue;
34 | function FindVariable(const Key: string): TValue;
35 | function LookupAndEvaluate(Value: TValue; const Key: string): TValue;
36 | private
37 | function Resolve(const Key: string): TValue; overload;
38 | function Resolve(const Key: string; NotifyNotFound: boolean): TValue; overload;
39 | procedure SetVariable(const Key: string; const Value: TValue);
40 | function GetEnvironments: TList;
41 | function GetErrors: TList;
42 | function GetErrorsOutputMode: TErrorsOutputMode;
43 | function GetFormatSettings: TFormatSettings;
44 | function GetMaxIterations: integer;
45 | function GetRegisters: IHash;
46 | function GetScopes: TList;
47 | function GetStrainer: IStrainer;
48 | procedure SetErrorsOutputMode(const Value: TErrorsOutputMode);
49 | public
50 | class function Liquidize(Value: TValue): TValue;
51 | constructor Create(AEnvironments: TList; AOuterScope: IHash;
52 | ARegisters: IHash; AErrorsOutputMode: TErrorsOutputMode;
53 | AMaxIterations: integer; AFormatSettings: TFormatSettings); overload;
54 | constructor Create(AFormatSettings: TFormatSettings); overload;
55 | destructor Destroy; override;
56 | function HandleError(E: Exception; var Msg: string): boolean;
57 | procedure Push(NewScope: IHash);
58 | procedure Merge(NewScope: IHash);
59 | function Pop: IHash;
60 | procedure Stack(Callback: TProc); overload;
61 | procedure Stack(NewScope: IHash; Callback: TProc); overload;
62 | procedure ClearInstanceAssigns;
63 | function HasKey(const Key: string): boolean;
64 | property ErrorsOutputMode: TErrorsOutputMode read GetErrorsOutputMode write SetErrorsOutputMode;
65 | property MaxIterations: integer read GetMaxIterations;
66 | property Environments: TList read GetEnvironments;
67 | property Scopes: TList read GetScopes;
68 | property Registers: IHash read GetRegisters;
69 | property Strainer: IStrainer read GetStrainer;
70 | property Errors: TList read GetErrors;
71 | property FormatSettings: TFormatSettings read GetFormatSettings;
72 | property Items[const Key: string]: TValue read Resolve write SetVariable; default;
73 | property Items[const Key: string; NotifyNotFound: boolean]: TValue read Resolve; default;
74 | end;
75 |
76 | implementation
77 |
78 | { TLiquidContext }
79 |
80 | procedure TLiquidContext.ClearInstanceAssigns;
81 | begin
82 | FScopes[0].Clear;
83 | end;
84 |
85 | constructor TLiquidContext.Create(AEnvironments: TList;
86 | AOuterScope: IHash; ARegisters: IHash; AErrorsOutputMode: TErrorsOutputMode;
87 | AMaxIterations: integer; AFormatSettings: TFormatSettings);
88 | begin
89 | FEnvironments := AEnvironments;
90 | FScopes := TList.Create;
91 | if AOuterScope <> nil then
92 | FScopes.Add(AOuterScope);
93 | FRegisters := ARegisters;
94 | FErrors := TList.Create;
95 | FErrorsOutputMode := AErrorsOutputMode;
96 | FMaxIterations := AMaxIterations;
97 | FFormatSettings := AFormatSettings;
98 | FStrainer := TStrainer.Create(Self);
99 | end;
100 |
101 | constructor TLiquidContext.Create(AFormatSettings: TFormatSettings);
102 | begin
103 | Create(TList.Create, THash.Create,
104 | THash.Create, TErrorsOutputMode.Rethrow, 0, AFormatSettings)
105 | end;
106 |
107 | destructor TLiquidContext.Destroy;
108 | begin
109 | FEnvironments.Free;
110 | FScopes.Free;
111 | FRegisters := nil;
112 | FErrors.Free;
113 | inherited;
114 | end;
115 |
116 | function TLiquidContext.FindVariable(const Key: string): TValue;
117 | begin
118 | var Scope: IHash := nil;
119 | for var S in FScopes do
120 | if S.ContainsKey(Key) then
121 | begin
122 | Scope := S;
123 | Break;
124 | end;
125 | var Variable := TValue.Empty;
126 | if Scope = nil then
127 | begin
128 | for var E in Environments do
129 | begin
130 | Variable := LookupAndEvaluate(TValue.From(E), Key);
131 | if not Variable.IsEmpty then
132 | begin
133 | Scope := E;
134 | Break;
135 | end;
136 | end;
137 | end;
138 | if Scope = nil then
139 | begin
140 | if Environments.Count > 0 then
141 | Scope := Environments.Last;
142 | if Scope = nil then
143 | Scope := FScopes.Last;
144 | end;
145 | if Variable.IsEmpty then
146 | Variable := LookupAndEvaluate(TValue.From(Scope), Key);
147 | Result := Variable;
148 | end;
149 |
150 | function TLiquidContext.GetEnvironments: TList;
151 | begin
152 | Result := FEnvironments;
153 | end;
154 |
155 | function TLiquidContext.GetErrors: TList;
156 | begin
157 | Result := FErrors;
158 | end;
159 |
160 | function TLiquidContext.GetErrorsOutputMode: TErrorsOutputMode;
161 | begin
162 | Result := FErrorsOutputMode;
163 | end;
164 |
165 | function TLiquidContext.GetFormatSettings: TFormatSettings;
166 | begin
167 | Result := FFormatSettings;
168 | end;
169 |
170 | function TLiquidContext.GetMaxIterations: integer;
171 | begin
172 | Result := FMaxIterations;
173 | end;
174 |
175 | function TLiquidContext.GetRegisters: IHash;
176 | begin
177 | Result := FRegisters;
178 | end;
179 |
180 | function TLiquidContext.GetScopes: TList;
181 | begin
182 | Result := FScopes;
183 | end;
184 |
185 | function TLiquidContext.GetStrainer: IStrainer;
186 | begin
187 | Result := FStrainer;
188 | end;
189 |
190 | function TLiquidContext.HandleError(E: Exception; var Msg: string): boolean;
191 | begin
192 | Msg := '';
193 | if (E is EInterruptException) or (E is ERenderException) then
194 | Exit(False);
195 |
196 | if E is ELiquidSyntaxException then
197 | Msg := Format('Liquid syntax error: %s', [E.Message])
198 | else
199 | Msg := Format('Liquid error: %s', [E.Message]);
200 | FErrors.Add(Msg);
201 |
202 | if FErrorsOutputMode = TErrorsOutputMode.Suppress then
203 | Exit(True);
204 |
205 | if FErrorsOutputMode = TErrorsOutputMode.Rethrow then
206 | Exit(False);
207 |
208 | if E is ELiquidSyntaxException then
209 | Exit(True);
210 |
211 | Result := True;
212 | end;
213 |
214 | function TLiquidContext.HasKey(const Key: string): boolean;
215 | begin
216 | var Value := Resolve(Key, False);
217 | Result := not Value.IsEmpty;
218 | end;
219 |
220 | class function TLiquidContext.Liquidize(Value: TValue): TValue;
221 | begin
222 | Result := Value;
223 | end;
224 |
225 | function TLiquidContext.LookupAndEvaluate(Value: TValue;
226 | const Key: string): TValue;
227 | begin
228 | if Value.IsType then
229 | Result := Value.AsType[Key]
230 | else if Value.IsType> then
231 | begin
232 | Result := Value.AsType>[StrToInt(Key)];
233 | end
234 | else
235 | raise ENotSupportedException.Create('');
236 | end;
237 |
238 | procedure TLiquidContext.Merge(NewScope: IHash);
239 | begin
240 | for var Pair in NewScope.ToArray do
241 | FScopes[0].AddOrSetValue(Pair.Key, Pair.Value);
242 | end;
243 |
244 | function TLiquidContext.Pop: IHash;
245 | begin
246 | if FScopes.Count = 1 then
247 | raise EContextException.Create('Context error in pop operation');
248 | Result := FScopes.ExtractAt(0);
249 | end;
250 |
251 | procedure TLiquidContext.Push(NewScope: IHash);
252 | begin
253 | if FScopes.Count > 80 then
254 | raise EStackLevelException.Create('Nesting too deep');
255 | FScopes.Insert(0, NewScope);
256 | end;
257 |
258 | function TLiquidContext.Resolve(const Key: string): TValue;
259 | begin
260 | Result := Resolve(Key, True);
261 | end;
262 |
263 | function TLiquidContext.Resolve(const Key: string;
264 | NotifyNotFound: boolean): TValue;
265 | begin
266 | var Output: TValue;
267 | if TConverter.StringToRealType(Key, Output) then
268 | Exit(Output);
269 | Result := Variable(Key, NotifyNotFound);
270 | end;
271 |
272 | procedure TLiquidContext.Stack(Callback: TProc);
273 | begin
274 | var NewScope: IHash := THash.Create;
275 | Stack(NewScope, Callback);
276 | end;
277 |
278 | procedure TLiquidContext.SetErrorsOutputMode(const Value: TErrorsOutputMode);
279 | begin
280 | FErrorsOutputMode := Value;
281 | end;
282 |
283 | procedure TLiquidContext.SetVariable(const Key: string; const Value: TValue);
284 | begin
285 | FScopes[0][Key] := Value;
286 | end;
287 |
288 | procedure TLiquidContext.Stack(NewScope: IHash; Callback: TProc);
289 | begin
290 | Push(NewScope);
291 | try
292 | Callback();
293 | finally
294 | Pop;
295 | end;
296 | end;
297 |
298 | function TLiquidContext.Variable(const Markup: string;
299 | NotifyNotFound: boolean): TValue;
300 | begin
301 | var Parts := R.Scan(Markup, LiquidRegexes.VariableParserRegex);
302 | var FirstPart: string;
303 | if Length(Parts) > 0 then
304 | FirstPart := Parts[0]
305 | else
306 | FirstPart := '';
307 |
308 | var FirstPartSquareBracketedMatch := LiquidRegexes.SquareBracketedRegex.Match(FirstPart);
309 | if FirstPartSquareBracketedMatch.Success then
310 | FirstPart := Resolve(FirstPartSquareBracketedMatch.Groups[1].Value).AsString;
311 |
312 | var Value := FindVariable(FirstPart);
313 | if Value.IsEmpty then
314 | begin
315 | if NotifyNotFound then
316 | Errors.Add(Format('Variable ''%s'' could not be found', [Markup]));
317 | Exit(TValue.Empty);
318 | end;
319 |
320 | // try to resolve the rest of the parts (starting from the second item in the list)
321 | for var I := 1 to Length(Parts) - 1 do
322 | begin
323 | var ForEachPart := Parts[i];
324 |
325 | var PartSquareBracketedMatch := LiquidRegexes.SquareBracketedRegex.Match(ForEachPart);
326 | var PartResolved := PartSquareBracketedMatch.Success;
327 |
328 | var Part: TValue := ForEachPart;
329 | if PartResolved then
330 | Part := Resolve(PartSquareBracketedMatch.Groups[1].Value);
331 |
332 | if Value.IsType then
333 | begin
334 | var Res := LookupAndEvaluate(Value, Part.AsString);
335 | Value := Liquidize(Res);
336 | end
337 | else if (not PartResolved) and (Value.IsArray) and (Part.AsString.Equals('size') or Part.AsString.Equals('first') or Part.AsString.Equals('last')) then
338 | begin
339 | var ArrayValue := Value.AsType>;
340 | if Part.AsString.Equals('size') then
341 | Value := Length(ArrayValue)
342 | else
343 | begin
344 | if Length(ArrayValue) = 0 then
345 | Value := TValue.Empty
346 | else if Part.AsString.Equals('first') then
347 | begin
348 | var Res := ArrayValue[0];
349 | Value := Liquidize(Res);
350 | end
351 | else if Part.AsString.Equals('last') then
352 | begin
353 | var Res := ArrayValue[Length(ArrayValue) - 1];
354 | Value := Liquidize(Res);
355 | end;
356 | end;
357 | end
358 | else
359 | begin
360 | Errors.Add(Format('Error - Variable ''%s'' could not be found', [Markup]));
361 | Exit(TValue.Empty);
362 | end;
363 | end;
364 |
365 | Result := Value;
366 | end;
367 |
368 | end.
369 |
--------------------------------------------------------------------------------
/src/Liquid.Default.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Default;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Rtti,
7 | System.RegularExpressions,
8 | System.Generics.Collections;
9 |
10 | type
11 | TLiquid = class
12 | strict private
13 | FFilterSeparator: string;
14 | FArgumentSeparator: string;
15 | FFilterArgumentSeparator: string;
16 | FVariableAttributeSeparator: string;
17 | FTagStart: string;
18 | FTagEnd: string;
19 | FVariableSignature: string;
20 | FVariableSegment: string;
21 | FVariableStart: string;
22 | FVariableEnd: string;
23 | FVariableIncompleteEnd: string;
24 | FQuotedString: string;
25 | FQuotedFragment: string;
26 | FQuotedAssignFragment: string;
27 | FStrictQuotedFragment: string;
28 | FFirstFilterArgument: string;
29 | FOtherFilterArgument: string;
30 | FSpacelessFilter: string;
31 | FExpression: string;
32 | FTagAttributes: string;
33 | FAnyStartingTag: string;
34 | FPartialTemplateParser: string;
35 | FTemplateParser: string;
36 | FVariableParser: string;
37 | FLiteralShorthand: string;
38 | FCommentShorthand: string;
39 |
40 | // regexes
41 | FSingleQuotedRegex: TRegEx;
42 | FDoubleQuotedRegex: TRegEx;
43 | FIntegerRegex: TRegEx;
44 | FRangeRegex: TRegEx;
45 | FNumericRegex: TRegEx;
46 | FSquareBracketedRegex: TRegEx;
47 | FVariableParserRegex: TRegEx;
48 | private
49 | function GetAnyStartingTag: string;
50 | function GetArgumentSeparator: string;
51 | function GetCommentShorthand: string;
52 | function GetExpression: string;
53 | function GetFilterArgumentSeparator: string;
54 | function GetFilterSeparator: string;
55 | function GetFirstFilterArgument: string;
56 | function GetLiteralShorthand: string;
57 | function GetOtherFilterArgument: string;
58 | function GetPartialTemplateParser: string;
59 | function GetQuotedAssignFragment: string;
60 | function GetQuotedFragment: string;
61 | function GetQuotedString: string;
62 | function GetSpacelessFilter: string;
63 | function GetStrictQuotedFragment: string;
64 | function GetTagAttributes: string;
65 | function GetTagEnd: string;
66 | function GetTagStart: string;
67 | function GetTemplateParser: string;
68 | function GetVariableAttributeSeparator: string;
69 | function GetVariableEnd: string;
70 | function GetVariableIncompleteEnd: string;
71 | function GetVariableParser: string;
72 | function GetVariableSegment: string;
73 | function GetVariableSignature: string;
74 | function GetVariableStart: string;
75 | public
76 | constructor Create;
77 | property FilterSeparator: string read GetFilterSeparator;
78 | property ArgumentSeparator: string read GetArgumentSeparator;
79 | property FilterArgumentSeparator: string read GetFilterArgumentSeparator;
80 | property VariableAttributeSeparator: string read GetVariableAttributeSeparator;
81 | property TagStart: string read GetTagStart;
82 | property TagEnd: string read GetTagEnd;
83 | property VariableSignature: string read GetVariableSignature;
84 | property VariableSegment: string read GetVariableSegment;
85 | property VariableStart: string read GetVariableStart;
86 | property VariableEnd: string read GetVariableEnd;
87 | property VariableIncompleteEnd: string read GetVariableIncompleteEnd;
88 | property QuotedString: string read GetQuotedString;
89 | property QuotedFragment: string read GetQuotedFragment;
90 | property QuotedAssignFragment: string read GetQuotedAssignFragment;
91 | property StrictQuotedFragment: string read GetStrictQuotedFragment;
92 | property FirstFilterArgument: string read GetFirstFilterArgument;
93 | property OtherFilterArgument: string read GetOtherFilterArgument;
94 | property SpacelessFilter: string read GetSpacelessFilter;
95 | property Expression: string read GetExpression;
96 | property TagAttributes: string read GetTagAttributes;
97 | property AnyStartingTag: string read GetAnyStartingTag;
98 | property PartialTemplateParser: string read GetPartialTemplateParser;
99 | property TemplateParser: string read GetTemplateParser;
100 | property VariableParser: string read GetVariableParser;
101 | property LiteralShorthand: string read GetLiteralShorthand;
102 | property CommentShorthand: string read GetCommentShorthand;
103 | property SingleQuotedRegex: TRegEx read FSingleQuotedRegex;
104 | property DoubleQuotedRegex: TRegEx read FDoubleQuotedRegex;
105 | property IntegerRegex: TRegEx read FIntegerRegex;
106 | property RangeRegex: TRegEx read FRangeRegex;
107 | property NumericRegex: TRegEx read FNumericRegex;
108 | property SquareBracketedRegex: TRegEx read FSquareBracketedRegex;
109 | property VariableParserRegex: TRegEx read FVariableParserRegex;
110 | end;
111 |
112 | function LiquidRegexes: TLiquid;
113 |
114 | implementation
115 |
116 | uses
117 | Liquid.Template,
118 | Liquid.Utils;
119 |
120 | var
121 | _Liquid: TLiquid;
122 |
123 | function LiquidRegexes: TLiquid;
124 | begin
125 | if _Liquid = nil then
126 | _Liquid := TLiquid.Create;
127 | Result := _Liquid;
128 | end;
129 |
130 | { TLiquid }
131 |
132 | constructor TLiquid.Create;
133 | begin
134 | FFilterSeparator := R.Q('\|');
135 | FArgumentSeparator := R.Q(',');
136 | FFilterArgumentSeparator := R.Q(':');
137 | FVariableAttributeSeparator := R.Q('.');
138 | FTagStart := R.Q('\{\%');
139 | FTagEnd := R.Q('\%\}');
140 | FVariableSignature := R.Q('\(?[\w\-\.\[\]]\)?');
141 | FVariableSegment := R.Q('[\w\-]');
142 | FVariableStart := R.Q('\{\{');
143 | FVariableEnd := R.Q('\}\}');
144 | FVariableIncompleteEnd := R.Q('\}\}?');
145 | FQuotedString := R.Q('"[^"]*"|''[^'']*''');
146 | FQuotedFragment := R.Q('%0:s|(?:[^\s,\|''"]|%0:s)+');
147 | FQuotedAssignFragment := R.Q('%0:s|(?:[^\s\|''"]|%0:s)+');
148 | FStrictQuotedFragment := R.Q('"[^"]+"|''[^'']+''|[^\s\|\:\,]+');
149 | FFirstFilterArgument := R.Q('%0:s(?:%1:s)');
150 | FOtherFilterArgument := R.Q('%0:s(?:%1:s)');
151 | FSpacelessFilter := R.Q('^(?:''[^'']+''|"[^"]+"|[^''"])*%0:s(?:%1:s)(?:%2:s(?:%3:s)*)?');
152 | FExpression := R.Q('(?:%0:s(?:%1:s)*)');
153 | FTagAttributes := R.Q('(\w+)\s*\:\s*(%0:s)');
154 | FAnyStartingTag := R.Q('\{\{|\{\%');
155 | FPartialTemplateParser := R.Q('%0:s.*?%1:s|%2:s.*?%3:s');
156 | FTemplateParser := R.Q('(%0:s|%1:s)');
157 | FVariableParser := R.Q('\[[^\]]+\]|%0:s+\??');
158 | FLiteralShorthand := R.Q('^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$');
159 | FCommentShorthand := R.Q('^(?:\{\s?\#\s?)(.*?)(?:\s*\#\s?\})$');
160 |
161 | //
162 | // regexes
163 | FSingleQuotedRegex := R.C(R.Q('^''(.*)''$'));
164 | FDoubleQuotedRegex := R.C(R.Q('^"(.*)"$'));
165 | FIntegerRegex := R.C(R.Q('^([+-]?\d+)$'));
166 | FRangeRegex := R.C(R.Q('^\((\S+)\.\.(\S+)\)$'));
167 | FNumericRegex := R.C(R.Q('^([+-]?\d[\d\.|\,]+)$'));
168 | FSquareBracketedRegex := R.C(R.Q('^\[(.*)\]$'));
169 | FVariableParserRegex := R.C(VariableParser);
170 | end;
171 |
172 | function TLiquid.GetAnyStartingTag: string;
173 | begin
174 | Result := FAnyStartingTag;
175 | end;
176 |
177 | function TLiquid.GetArgumentSeparator: string;
178 | begin
179 | Result := FArgumentSeparator;
180 | end;
181 |
182 | function TLiquid.GetCommentShorthand: string;
183 | begin
184 | Result := FCommentShorthand;
185 | end;
186 |
187 | function TLiquid.GetExpression: string;
188 | begin
189 | Result := Format(FExpression, [QuotedFragment, SpacelessFilter]);
190 | end;
191 |
192 | function TLiquid.GetFilterArgumentSeparator: string;
193 | begin
194 | Result := FFilterArgumentSeparator;
195 | end;
196 |
197 | function TLiquid.GetFilterSeparator: string;
198 | begin
199 | Result := FFilterSeparator;
200 | end;
201 |
202 | function TLiquid.GetFirstFilterArgument: string;
203 | begin
204 | Result := Format(FFirstFilterArgument,
205 | [FilterArgumentSeparator, StrictQuotedFragment]);
206 | end;
207 |
208 | function TLiquid.GetLiteralShorthand: string;
209 | begin
210 | Result := FLiteralShorthand;
211 | end;
212 |
213 | function TLiquid.GetOtherFilterArgument: string;
214 | begin
215 | Result := Format(FOtherFilterArgument,
216 | [ArgumentSeparator, StrictQuotedFragment]);
217 | end;
218 |
219 | function TLiquid.GetPartialTemplateParser: string;
220 | begin
221 | Result := Format(FPartialTemplateParser,
222 | [TagStart, TagEnd, VariableStart, VariableIncompleteEnd]);
223 | end;
224 |
225 | function TLiquid.GetQuotedAssignFragment: string;
226 | begin
227 | Result := Format(FQuotedAssignFragment, [QuotedString]);
228 | end;
229 |
230 | function TLiquid.GetQuotedFragment: string;
231 | begin
232 | Result := Format(FQuotedFragment, [QuotedString]);
233 | end;
234 |
235 | function TLiquid.GetQuotedString: string;
236 | begin
237 | Result := FQuotedString;
238 | end;
239 |
240 | function TLiquid.GetSpacelessFilter: string;
241 | begin
242 | Result := Format(FSpacelessFilter,
243 | [FilterSeparator, StrictQuotedFragment, FirstFilterArgument,
244 | OtherFilterArgument]);
245 | end;
246 |
247 | function TLiquid.GetStrictQuotedFragment: string;
248 | begin
249 | Result := FStrictQuotedFragment;
250 | end;
251 |
252 | function TLiquid.GetTagAttributes: string;
253 | begin
254 | Result := Format(FTagAttributes, [QuotedFragment]);
255 | end;
256 |
257 | function TLiquid.GetTagEnd: string;
258 | begin
259 | Result := FTagEnd;
260 | end;
261 |
262 | function TLiquid.GetTagStart: string;
263 | begin
264 | Result := FTagStart;
265 | end;
266 |
267 | function TLiquid.GetTemplateParser: string;
268 | begin
269 | Result := Format(FTemplateParser, [PartialTemplateParser, AnyStartingTag]);
270 | end;
271 |
272 | function TLiquid.GetVariableAttributeSeparator: string;
273 | begin
274 | Result := FVariableAttributeSeparator;
275 | end;
276 |
277 | function TLiquid.GetVariableEnd: string;
278 | begin
279 | Result := FVariableEnd;
280 | end;
281 |
282 | function TLiquid.GetVariableIncompleteEnd: string;
283 | begin
284 | Result := FVariableIncompleteEnd;
285 | end;
286 |
287 | function TLiquid.GetVariableParser: string;
288 | begin
289 | Result := Format(FVariableParser, [VariableSegment]);
290 | end;
291 |
292 | function TLiquid.GetVariableSegment: string;
293 | begin
294 | Result := FVariableSegment;
295 | end;
296 |
297 | function TLiquid.GetVariableSignature: string;
298 | begin
299 | Result := FVariableSignature;
300 | end;
301 |
302 | function TLiquid.GetVariableStart: string;
303 | begin
304 | Result := FVariableStart;
305 | end;
306 |
307 | initialization
308 |
309 | finalization
310 | _Liquid.Free;
311 |
312 | end.
313 |
--------------------------------------------------------------------------------
/src/Liquid.Document.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Document;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Generics.Collections,
7 | System.RegularExpressions, System.Classes,
8 |
9 | Liquid.Interfaces,
10 | Liquid.Context,
11 | Liquid.Exceptions,
12 | Liquid.Tag,
13 | Liquid.Variable,
14 | Liquid.Block;
15 |
16 | type
17 | TDocument = class(TBlock)
18 | strict private
19 | protected
20 | function BlockDelimiter: string; override;
21 | procedure AssertMissingDelimitation; override;
22 | public
23 | constructor Create;
24 | procedure Initialize(const ATagName: string; const AMarkup: string;
25 | ATokens: TList); override;
26 | procedure Render(Context: ILiquidContext; TextWriter: TTextWriter); override;
27 | end;
28 |
29 | implementation
30 |
31 | { TDocument }
32 |
33 | procedure TDocument.AssertMissingDelimitation;
34 | begin
35 | // pass
36 | end;
37 |
38 | function TDocument.BlockDelimiter: string;
39 | begin
40 | Result := string.Empty;
41 | end;
42 |
43 | constructor TDocument.Create;
44 | begin
45 | inherited Create;
46 | end;
47 |
48 | procedure TDocument.Initialize(const ATagName, AMarkup: string;
49 | ATokens: TList);
50 | begin
51 | Parse(ATokens);
52 | end;
53 |
54 | procedure TDocument.Render(Context: ILiquidContext; TextWriter: TTextWriter);
55 | begin
56 | try
57 | inherited Render(Context, TextWriter);
58 | except
59 | on E: EBreakInterrupt do
60 | begin
61 | // BreakInterrupt exceptions are used to interrupt a rendering
62 | end;
63 | on E: EContinueInterrupt do
64 | begin
65 | // ContinueInterrupt exceptions are used to interrupt a rendering
66 | end;
67 | end;
68 | end;
69 |
70 | end.
71 |
--------------------------------------------------------------------------------
/src/Liquid.Exceptions.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Exceptions;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils;
7 |
8 | type
9 | ELiquidException = class(Exception);
10 | ERenderException = class(Exception);
11 |
12 | ELiquidSyntaxException = class(ELiquidException);
13 | EStackLevelException = class(ELiquidException);
14 | EContextException = class(ELiquidException);
15 | EFilterNotFoundException = class(ELiquidException);
16 | EMaximumIterationsExceededException = class(ERenderException);
17 |
18 | EInterruptException = class(ELiquidException);
19 | EBreakInterrupt = class(EInterruptException)
20 | public
21 | constructor Create; reintroduce;
22 | end;
23 | EContinueInterrupt = class(EInterruptException)
24 | public
25 | constructor Create; reintroduce;
26 | end;
27 |
28 | implementation
29 |
30 | { EBreakInterrupt }
31 |
32 | constructor EBreakInterrupt.Create;
33 | begin
34 | inherited Create('Misplaced ''break'' statement');
35 | end;
36 |
37 | { EContinueInterrupt }
38 |
39 | constructor EContinueInterrupt.Create;
40 | begin
41 | inherited Create('Misplaced ''continue'' statement');
42 | end;
43 |
44 | end.
45 |
--------------------------------------------------------------------------------
/src/Liquid.Filters.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Filters;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils,
7 | System.Classes, System.Math,
8 | System.Generics.Collections,
9 | System.Rtti, System.TypInfo,
10 | System.Character,
11 |
12 | Liquid.Interfaces,
13 | Liquid.Tuples,
14 | Liquid.Utils;
15 |
16 | type
17 | TStrainer = class(TInterfacedObject, IStrainer)
18 | strict private
19 | class var
20 | //FFilters: TDictionary;
21 | FFilterClasses: TDictionary;
22 | class constructor Create;
23 | class destructor Destroy;
24 | strict private
25 | [Weak]
26 | FContext: ILiquidContext;
27 | FMethods: TDictionary;
28 | function ResolveMethodName(const MethodName: string): string;
29 | public
30 | class procedure GlobalFilter(AClass: TClass);
31 | public
32 | constructor Create(AContext: ILiquidContext);
33 | destructor Destroy; override;
34 | function Invoke(const FilterName: string; Args: TArray): TValue;
35 | end;
36 |
37 | TFilterFunc = reference to function(Context: ILiquidContext;
38 | const Input: TValue; const Args: TArray): TValue;
39 |
40 | TGenericFilter = class(TInterfacedObject, IFilter)
41 | strict private
42 | FFunc: TFilterFunc;
43 | public
44 | constructor Create(AFilterFunc: TFilterFunc);
45 | function Filter(Context: ILiquidContext; const Input: TValue;
46 | const Args: TArray): TValue;
47 | end;
48 |
49 | TStandardFilters = class
50 | strict private
51 | public
52 | function Default(const Input: string; const DefaultValue: string): string;
53 | function Upcase(const Input: string): string;
54 | function Downcase(const Input: string): string;
55 | function Append(const Input: string; const Value: string): string;
56 | function Date(Context: ILiquidContext; const Input: TDateTime; const Format: string): string;
57 | function Slice(const Input: string; Start: integer): string; overload;
58 | function Slice(const Input: string; Start: integer; Length: integer): string; overload;
59 | function Round(const Input: double): double; overload;
60 | function Round(const Input: double; Places: integer): double; overload;
61 |
62 | function FormatFloat(Context: ILiquidContext; const Input: integer;
63 | const Format: string): string; overload;
64 | function FormatFloat(Context: ILiquidContext; const Input: double;
65 | const Format: string): string; overload;
66 | function FormatFloat(Context: ILiquidContext; const Input: extended;
67 | const Format: string): string; overload;
68 |
69 | function Abs(const Input: integer): integer; overload;
70 | end;
71 |
72 | implementation
73 |
74 | { TStrainer }
75 |
76 | constructor TStrainer.Create(AContext: ILiquidContext);
77 | begin
78 | FContext := AContext;
79 | FMethods := TDictionary.Create;
80 | var RttiContext:= TRttiContext.Create;
81 | try
82 | for var C in FFilterClasses do
83 | begin
84 | for var Method in RttiContext.GetType(C.Value).GetDeclaredMethods do
85 | begin
86 | if Method.IsConstructor then
87 | Continue;
88 | if Method.ReturnType = nil then
89 | Continue;
90 | if FMethods.ContainsKey(Method.Name) and (FMethods[Method.Name] = C.Value) then
91 | Continue;
92 | FMethods.Add(Method.Name, C.Value);
93 | end;
94 | end;
95 | finally
96 | RttiContext.Free;
97 | end;
98 | end;
99 |
100 | class constructor TStrainer.Create;
101 | begin
102 | FFilterClasses := TDictionary.Create;
103 | end;
104 |
105 | class destructor TStrainer.Destroy;
106 | begin
107 | FFilterClasses.Free;
108 | end;
109 |
110 | destructor TStrainer.Destroy;
111 | begin
112 | FMethods.Free;
113 | inherited;
114 | end;
115 |
116 | class procedure TStrainer.GlobalFilter(AClass: TClass);
117 | begin
118 | FFilterClasses.Add(AClass.QualifiedClassName, AClass);
119 | end;
120 |
121 | function TStrainer.Invoke(const FilterName: string;
122 | Args: TArray): TValue;
123 | begin
124 | Result := Args[0];
125 | var RttiContext := TRttiContext.Create;
126 | var InvokeArgs := TList.Create;
127 | try
128 | for var Method in FMethods do
129 | begin
130 | if ResolveMethodName(Method.Key) <> ResolveMethodName(FilterName) then
131 | Continue;
132 | for var RttiMethod in RttiContext.GetType(Method.Value).GetMethods(Method.Key) do
133 | begin
134 | InvokeArgs.Clear;
135 | if (Length(RttiMethod.GetParameters) > 0) and
136 | (RttiMethod.GetParameters[0].ParamType.Handle = TypeInfo(ILiquidContext)) then
137 | InvokeArgs.Add(TValue.From(FContext));
138 | InvokeArgs.AddRange(Args);
139 | if Length(RttiMethod.GetParameters) <> InvokeArgs.Count then
140 | Continue;
141 | for var I := 0 to Length(RttiMethod.GetParameters) - 1 do
142 | begin
143 | var Param := RttiMethod.GetParameters[I];
144 | var Arg := InvokeArgs[I];
145 | if Arg.IsEmpty then
146 | Arg := '';
147 | if Param.ParamType.TypeKind <> Arg.Kind then
148 | Exit;
149 | end;
150 |
151 | var Instance := Method.Value.Create;
152 | try
153 | var Output := TValue.Empty;
154 | Output := RttiMethod.Invoke(Instance, InvokeArgs.ToArray);
155 | if not Output.IsEmpty then
156 | Exit(Output);
157 | Break;
158 | finally
159 | Instance.Free;
160 | end;
161 | end;
162 | end;
163 | finally
164 | InvokeArgs.Free;
165 | RttiContext.Free;
166 | end;
167 | end;
168 |
169 | function TStrainer.ResolveMethodName(const MethodName: string): string;
170 | var
171 | I: Integer;
172 | Current, Before: Char;
173 | begin
174 | Result := MethodName;
175 | I := 2;
176 | while I <= Length(Result) do
177 | begin
178 | Current := Result[I];
179 | Before := Result[I - 1];
180 | if Current.IsUpper and (Before <> '_') and Before.IsLower then
181 | begin
182 | Insert('_', Result, I);
183 | Inc(I, 2);
184 | end
185 | else
186 | Inc(I);
187 | end;
188 | Result := LowerCase(Result);
189 | end;
190 |
191 | { TGenericFilter }
192 |
193 | constructor TGenericFilter.Create(AFilterFunc: TFilterFunc);
194 | begin
195 | FFunc := AFilterFunc;
196 | end;
197 |
198 | function TGenericFilter.Filter(Context: ILiquidContext; const Input: TValue;
199 | const Args: TArray): TValue;
200 | begin
201 | Result := FFunc(Context, Input, Args);
202 | end;
203 |
204 | { TStandardFilters }
205 |
206 | function TStandardFilters.Abs(const Input: integer): integer;
207 | begin
208 | Result := System.Abs(Input);
209 | end;
210 |
211 | function TStandardFilters.Append(const Input, Value: string): string;
212 | begin
213 | Result := Input + Value;
214 | end;
215 |
216 | function TStandardFilters.Date(Context: ILiquidContext; const Input: TDateTime;
217 | const Format: string): string;
218 | begin
219 | Result := FormatDateTime(Format, Input, Context.FormatSettings);
220 | end;
221 |
222 | function TStandardFilters.Default(const Input: string; const DefaultValue: string): string;
223 | begin
224 | if string.IsNullOrEmpty(Input) then
225 | Result := DefaultValue
226 | else
227 | Result := Input;
228 | end;
229 |
230 | function TStandardFilters.Downcase(const Input: string): string;
231 | begin
232 | Result := Input.ToLower;
233 | end;
234 |
235 | function TStandardFilters.FormatFloat(Context: ILiquidContext;
236 | const Input: double; const Format: string): string;
237 | begin
238 | Result := System.SysUtils.FormatFloat(Format, Input, Context.FormatSettings);
239 | end;
240 |
241 | function TStandardFilters.FormatFloat(Context: ILiquidContext;
242 | const Input: extended; const Format: string): string;
243 | begin
244 | Result := System.SysUtils.FormatFloat(Format, Input, Context.FormatSettings);
245 | end;
246 |
247 | function TStandardFilters.FormatFloat(Context: ILiquidContext;
248 | const Input: integer; const Format: string): string;
249 | begin
250 | Result := System.SysUtils.FormatFloat(Format, Input, Context.FormatSettings);
251 | end;
252 |
253 | function TStandardFilters.Round(const Input: double; Places: integer): double;
254 | begin
255 | try
256 | Result := RoundTo(Input, -1 * Places);
257 | except
258 | Result := Input;
259 | end;
260 | end;
261 |
262 | function TStandardFilters.Round(const Input: double): double;
263 | begin
264 | Result := Round(Input, 0);
265 | end;
266 |
267 | function TStandardFilters.Slice(const Input: string; Start: integer): string;
268 | begin
269 | Result := Slice(Input, Start, 1);
270 | end;
271 |
272 | function TStandardFilters.Slice(const Input: string; Start,
273 | Length: integer): string;
274 | begin
275 | if Start < 0 then
276 | begin
277 | Inc(Start, Input.Length);
278 | if Start < 0 then
279 | begin
280 | Length := Max(0, Length + Start);
281 | Start := 0;
282 | end;
283 | end;
284 | if (Start + Length > Input.Length) then
285 | Length := Input.Length - Start;
286 | Result := Input.Substring(Start, Length);
287 | end;
288 |
289 | function TStandardFilters.Upcase(const Input: string): string;
290 | begin
291 | Result := Input.ToUpper;
292 | end;
293 |
294 | initialization
295 | TStrainer.GlobalFilter(TStandardFilters);
296 |
297 | end.
298 |
--------------------------------------------------------------------------------
/src/Liquid.Hash.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Hash;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Rtti, System.JSON, System.DateUtils,
7 | System.RegularExpressions,
8 | System.Generics.Collections,
9 |
10 | Liquid.Interfaces;
11 |
12 | type
13 | THash = class(TInterfacedObject, IHash)
14 | strict private
15 | FHash: TDictionary;
16 | FNestedHashs: TList;
17 | FLambda: TFunc;
18 | FDefaultValue: TValue;
19 | private
20 | function GetItem(const Key: string): TValue;
21 | procedure SetItem(const Key: string; const Value: TValue);
22 | function GetCount: integer;
23 | procedure AddNestedHash(Value: TValue);
24 | public
25 | class function FromJson(const Json: string): IHash;
26 | public
27 | constructor Create; overload;
28 | constructor Create(ALambda: TFunc); overload;
29 | constructor Create(ADefaultValue: TValue); overload;
30 | destructor Destroy; override;
31 |
32 | procedure Clear;
33 | procedure Add(const Key: string; const Value: TValue);
34 | procedure AddOrSetValue(const Key: string; const Value: TValue);
35 | function ContainsKey(const Key: string): Boolean;
36 | function ContainsValue(const Value: TValue): Boolean;
37 | function ToArray: TArray>;
38 | property Items[const Key: string]: TValue read GetItem write SetItem; default;
39 | property Count: Integer read GetCount;
40 | end;
41 |
42 | IHashFactory = interface
43 | ['{4822E81F-3C1F-4093-A23E-C91EED3FDE2A}']
44 | function CreateHash: IHash;
45 | end;
46 |
47 | THashJsonFactory = class(TInterfacedObject, IHashFactory)
48 | strict private
49 | FJson: string;
50 | FLevelCount: integer;
51 | function GetHash(JValue: TJSONValue): IHash; overload;
52 | function GetHash(JObject: TJSONObject): IHash; overload;
53 | //
54 | function GetElementValue(JValue: TJSONValue): TValue;
55 | function GetStringValue(JString: TJSONString): TValue;
56 | function GetNumberValue(JNumber: TJSONNumber): TValue;
57 | function GetArrayValue(JArray: TJSONArray): TValue;
58 | public
59 | constructor Create(const AJson: string);
60 | function CreateHash: IHash;
61 | end;
62 |
63 | implementation
64 |
65 | { THash }
66 |
67 | procedure THash.Add(const Key: string; const Value: TValue);
68 | begin
69 | AddNestedHash(Value);
70 | FHash.Add(Key, Value);
71 | end;
72 |
73 | procedure THash.AddNestedHash(Value: TValue);
74 | begin
75 | if Value.IsType then
76 | FNestedHashs.Add(Value.AsType)
77 | else if Value.IsType> then
78 | begin
79 | for var E in Value.AsType> do
80 | if E.IsType then
81 | FNestedHashs.Add(E.AsType);
82 | end;
83 | end;
84 |
85 | procedure THash.AddOrSetValue(const Key: string; const Value: TValue);
86 | begin
87 | AddNestedHash(Value);
88 | FHash.AddOrSetValue(Key, Value);
89 | end;
90 |
91 | procedure THash.Clear;
92 | begin
93 | FHash.Clear;
94 | end;
95 |
96 | function THash.ContainsKey(const Key: string): Boolean;
97 | begin
98 | Result := FHash.ContainsKey(Key);
99 | end;
100 |
101 | function THash.ContainsValue(const Value: TValue): Boolean;
102 | begin
103 | Result := FHash.ContainsValue(Value);
104 | end;
105 |
106 | constructor THash.Create(ADefaultValue: TValue);
107 | begin
108 | Create;
109 | FDefaultValue := ADefaultValue;
110 | end;
111 |
112 | constructor THash.Create(ALambda: TFunc);
113 | begin
114 | Create;
115 | FLambda := ALambda;
116 | end;
117 |
118 | constructor THash.Create;
119 | begin
120 | FHash := TDictionary.Create;
121 | FNestedHashs := TList.Create;
122 | end;
123 |
124 | destructor THash.Destroy;
125 | begin
126 | FNestedHashs.Free;
127 | FHash.Free;
128 | inherited;
129 | end;
130 |
131 | class function THash.FromJson(const Json: string): IHash;
132 | begin
133 | var Factory: IHashFactory := THashJsonFactory.Create(Json);
134 | Result := Factory.CreateHash;
135 | end;
136 |
137 | function THash.GetCount: integer;
138 | begin
139 | Result := FHash.Count;
140 | end;
141 |
142 | function THash.GetItem(const Key: string): TValue;
143 | begin
144 | if FHash.ContainsKey(Key) then
145 | Exit(FHash[Key]);
146 | if Assigned(FLambda) then
147 | Exit(FLambda(Self, Key));
148 | if not FDefaultValue.IsEmpty then
149 | Exit(FDefaultValue);
150 | Result := TValue.Empty;
151 | end;
152 |
153 | procedure THash.SetItem(const Key: string; const Value: TValue);
154 | begin
155 | AddOrSetValue(Key, Value);
156 | end;
157 |
158 | function THash.ToArray: TArray>;
159 | begin
160 | Result := FHash.ToArray;
161 | end;
162 |
163 | { THashJsonFactory }
164 |
165 | constructor THashJsonFactory.Create(const AJson: string);
166 | begin
167 | FJson := AJson;
168 | end;
169 |
170 | function THashJsonFactory.CreateHash: IHash;
171 | begin
172 | FLevelCount := 0;
173 | var JValue := TJSONObject.ParseJSONValue(FJson);
174 | try
175 | Result := GetHash(JValue);
176 | finally
177 | JValue.Free;
178 | end;
179 | end;
180 |
181 | function THashJsonFactory.GetHash(JValue: TJSONValue): IHash;
182 | begin
183 | if JValue is TJSONObject then
184 | Result := GetHash(TJSONObject(JValue))
185 | else
186 | raise EArgumentException.Create('JSON value conversion to THash is not possible');
187 | end;
188 |
189 | function THashJsonFactory.GetArrayValue(JArray: TJSONArray): TValue;
190 | begin
191 | var ArrayList := TList.Create;
192 | try
193 | for var Item in JArray do
194 | ArrayList.Add(GetElementValue(Item));
195 | Result := TValue.From>(ArrayList.ToArray);
196 | finally
197 | ArrayList.Free;
198 | end;
199 | end;
200 |
201 | function THashJsonFactory.GetElementValue(JValue: TJSONValue): TValue;
202 | begin
203 | if JValue is TJSONNumber then
204 | Result := GetNumberValue(TJSONNumber(JValue))
205 | else if JValue is TJSONString then
206 | Result := GetStringValue(TJSONString(JValue))
207 | else if JValue is TJSONBool then
208 | Result := TJSONBool(JValue).AsBoolean
209 | else if JValue is TJSONObject then
210 | Result := TValue.From(GetHash(TJSONObject(JValue)))
211 | else if JValue is TJSONArray then
212 | Result := GetArrayValue(TJSONArray(JValue))
213 | else if JValue is TJSONNull then
214 | Result := TValue.Empty
215 | else
216 | raise EArgumentException.Create('JSON value conversion to TValue is not possible');
217 | end;
218 |
219 | function THashJsonFactory.GetHash(JObject: TJSONObject): IHash;
220 | begin
221 | Result := THash.Create;
222 | for var Member in JObject do
223 | Result.Add(Member.JsonString.Value, GetElementValue(Member.JsonValue));
224 | end;
225 |
226 | function THashJsonFactory.GetStringValue(JString: TJSONString): TValue;
227 | begin
228 | if JString.Value.Trim = '' then
229 | Exit(JString.Value);
230 | var DateTime: TDateTime;
231 | if TryISO8601ToDate(JString.Value, DateTime) then
232 | Exit(TValue.From(DateTime));
233 | Result := JString.Value
234 | end;
235 |
236 | function THashJsonFactory.GetNumberValue(JNumber: TJSONNumber): TValue;
237 | begin
238 | if Pos('.', JNumber.ToString) > 0 then
239 | Result := JNumber.AsDouble
240 | else
241 | begin
242 | var Int64Value := JNumber.AsInt64;
243 | var IntValue := JNumber.AsInt;
244 | if IntValue = Int64Value then
245 | Result := IntValue
246 | else
247 | Result := Int64Value;
248 | end;
249 | end;
250 |
251 | end.
252 |
--------------------------------------------------------------------------------
/src/Liquid.Interfaces.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Interfaces;
2 |
3 | interface
4 |
5 | uses
6 | System.Classes,
7 | System.Generics.Collections,
8 | System.SysUtils,
9 | System.Rtti;
10 |
11 | type
12 | TErrorsOutputMode = (Rethrow, Suppress, Display);
13 |
14 | IHash = interface;
15 | IStrainer = interface;
16 | IFilter = interface;
17 |
18 | ILiquidContext = interface
19 | ['{290DFAE7-9685-42B5-A5D8-CC1173411806}']
20 | function Resolve(const Key: string): TValue; overload;
21 | function Resolve(const Key: string; NotifyNotFound: boolean): TValue; overload;
22 | procedure SetVariable(const Key: string; const Value: TValue);
23 | function GetEnvironments: TList;
24 | function GetErrors: TList;
25 | function GetErrorsOutputMode: TErrorsOutputMode;
26 | function GetFormatSettings: TFormatSettings;
27 | function GetMaxIterations: integer;
28 | function GetRegisters: IHash;
29 | function GetScopes: TList;
30 | function GetStrainer: IStrainer;
31 | procedure SetErrorsOutputMode(const Value: TErrorsOutputMode);
32 | function HandleError(E: Exception; var Msg: string): boolean;
33 | procedure Push(NewScope: IHash);
34 | procedure Merge(NewScope: IHash);
35 | function Pop: IHash;
36 | procedure Stack(Callback: TProc); overload;
37 | procedure Stack(NewScope: IHash; Callback: TProc); overload;
38 | procedure ClearInstanceAssigns;
39 | function HasKey(const Key: string): boolean;
40 | property ErrorsOutputMode: TErrorsOutputMode read GetErrorsOutputMode write SetErrorsOutputMode;
41 | property MaxIterations: integer read GetMaxIterations;
42 | property Environments: TList read GetEnvironments;
43 | property Scopes: TList read GetScopes;
44 | property Registers: IHash read GetRegisters;
45 | property Strainer: IStrainer read GetStrainer;
46 | property Errors: TList read GetErrors;
47 | property FormatSettings: TFormatSettings read GetFormatSettings;
48 | property Items[const Key: string]: TValue read Resolve write SetVariable; default;
49 | property Items[const Key: string; NotifyNotFound: boolean]: TValue read Resolve; default;
50 | end;
51 |
52 | IHash = interface
53 | ['{5DB200EB-09E9-4505-A147-CB6F0E17B390}']
54 | procedure Clear;
55 | procedure Add(const Key: string; const Value: TValue);
56 | procedure AddOrSetValue(const Key: string; const Value: TValue);
57 | function ContainsKey(const Key: string): Boolean;
58 | function ContainsValue(const Value: TValue): Boolean;
59 | function GetCount: integer;
60 | function GetItem(const Key: string): TValue;
61 | procedure SetItem(const Key: string; const Value: TValue);
62 | function ToArray: TArray>;
63 | property Items[const Key: string]: TValue read GetItem write SetItem; default;
64 | property Count: Integer read GetCount;
65 | end;
66 |
67 | IStrainer = interface
68 | ['{6F17C663-7B86-4A3C-9102-92BFE67FB984}']
69 | // procedure AddFilter(const FilterName: string; Filter: IFilter);
70 | // procedure RegisterFilter(AFilterClass: TClass);
71 | function Invoke(const FilterName: string; Args: TArray): TValue;
72 | end;
73 |
74 | IRenderable = interface
75 | ['{7B71A921-EE11-493A-903D-38C3775C6B3B}']
76 | procedure Render(Context: ILiquidContext; Writer: TTextWriter);
77 | end;
78 |
79 | IFilter = interface
80 | ['{DA60855E-8E11-4D37-B52B-295EB60AFF08}']
81 | function Filter(Context: ILiquidContext; const Input: TValue;
82 | const Args: TArray): TValue;
83 | end;
84 |
85 | implementation
86 |
87 | end.
88 |
--------------------------------------------------------------------------------
/src/Liquid.Tag.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Tag;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils,
7 | System.Classes, System.Generics.Collections,
8 | System.RegularExpressions,
9 | System.Rtti,
10 |
11 | Liquid.Interfaces,
12 | Liquid.Default,
13 | Liquid.Context,
14 | Liquid.Variable,
15 | Liquid.Exceptions,
16 | Liquid.Utils;
17 |
18 | type
19 | INodeList = interface;
20 |
21 | TTag = class//(TInterfacedObject, IRenderable)
22 | strict private
23 | FTagName: string;
24 | FMarkup: string;
25 | FNodeList: INodeList;
26 | protected
27 | procedure SetNodeList(ANodeList: INodeList);
28 | procedure Parse(ATokens: TList); virtual;
29 | public
30 | constructor Create;
31 | procedure Initialize(const ATagName: string; const AMarkup: string;
32 | ATokens: TList); virtual;
33 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); virtual;
34 | procedure AssertTagRulesViolation(RootNodeList: INodeList); virtual;
35 | function Name: string;
36 | property NodeList: INodeList read FNodeList;
37 | property TagName: string read FTagName;
38 | property Markup: string read FMarkup;
39 | end;
40 |
41 | INodeList = interface
42 | ['{9D108281-A5D3-403D-BDB8-22AE9FF1A151}']
43 | procedure Add(Value: TValue);
44 | procedure Clear;
45 | function GetEnumerator: TEnumerator;
46 | end;
47 |
48 | TNodeList = class(TInterfacedObject, INodeList)
49 | strict private
50 | FValues: TList;
51 | FObjects: TList;
52 | public
53 | function GetEnumerator: TEnumerator;
54 | public
55 | constructor Create;
56 | destructor Destroy; override;
57 | procedure Add(Value: TValue);
58 | procedure Clear;
59 | end;
60 |
61 | TTagClass = class of TTag;
62 |
63 | ITagFactory = interface
64 | ['{769EA21E-363D-4CD3-9581-4B82BA7CA77C}']
65 | function GetTagName: string;
66 | function CreateTag: TTag;
67 | property TagName: string read GetTagName;
68 | end;
69 |
70 | TRttiTagFactory = class(TInterfacedObject, ITagFactory)
71 | strict private
72 | FTagName: string;
73 | FTagClass: TTagClass;
74 | private
75 | function GetTagName: string;
76 | public
77 | constructor Create(ATagClass: TTagClass; const ATagName: string);
78 | function CreateTag: TTag;
79 | property TagName: string read GetTagName;
80 | end;
81 |
82 | implementation
83 |
84 | { TTag }
85 |
86 | procedure TTag.AssertTagRulesViolation(RootNodeList: INodeList);
87 | begin
88 | end;
89 |
90 | constructor TTag.Create;
91 | begin
92 | FNodeList := TNodeList.Create;
93 | end;
94 |
95 | procedure TTag.Initialize(const ATagName, AMarkup: string;
96 | ATokens: TList);
97 | begin
98 | FTagName := ATagName;
99 | FMarkup := AMarkup;
100 | Parse(ATokens);
101 | end;
102 |
103 | function TTag.Name: string;
104 | begin
105 | Result := Self.ClassName.ToLower;
106 | end;
107 |
108 | procedure TTag.Parse(ATokens: TList);
109 | begin
110 | end;
111 |
112 | procedure TTag.Render(Context: ILiquidContext; Writer: TTextWriter);
113 | begin
114 | end;
115 |
116 | procedure TTag.SetNodeList(ANodeList: INodeList);
117 | begin
118 | FNodeList := ANodeList;
119 | end;
120 |
121 | { TRttiTagFactory }
122 |
123 | constructor TRttiTagFactory.Create(ATagClass: TTagClass; const ATagName: string);
124 | begin
125 | FTagClass := ATagClass;
126 | FTagName := ATagName;
127 | end;
128 |
129 | function TRttiTagFactory.CreateTag: TTag;
130 | var
131 | C: TRttiContext;
132 | RttiType: TRttiType;
133 | Method: TRttiMethod;
134 | begin
135 | C := TRttiContext.Create;
136 | try
137 | RttiType := C.GetType(FTagClass);
138 | for Method in RttiType.GetMethods do
139 | begin
140 | if Method.IsConstructor and (Length(Method.GetParameters) = 0) then
141 | Exit(Method.Invoke(FTagClass, []).AsType);
142 | end;
143 | Result := nil;
144 | finally
145 | C.Free;
146 | end;
147 | end;
148 |
149 | function TRttiTagFactory.GetTagName: string;
150 | begin
151 | Result := FTagName;
152 | end;
153 |
154 | { TNodeList }
155 |
156 | procedure TNodeList.Add(Value: TValue);
157 | begin
158 | FValues.Add(Value);
159 | if Value.IsObject then
160 | FObjects.Add(Value.AsObject);
161 | end;
162 |
163 | procedure TNodeList.Clear;
164 | begin
165 | FValues.Clear;
166 | FObjects.Clear;
167 | end;
168 |
169 | constructor TNodeList.Create;
170 | begin
171 | FValues := TList.Create;
172 | FObjects := TList.Create;
173 | end;
174 |
175 | destructor TNodeList.Destroy;
176 | begin
177 | FValues.Free;
178 | FObjects.Free;
179 | inherited;
180 | end;
181 |
182 | function TNodeList.GetEnumerator: TEnumerator;
183 | begin
184 | Result := FValues.GetEnumerator;
185 | end;
186 |
187 | end.
188 |
--------------------------------------------------------------------------------
/src/Liquid.Tags.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Tags;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils,
7 | System.Classes, System.Generics.Collections,
8 | System.RegularExpressions,
9 | System.Rtti,
10 |
11 | Liquid.Default,
12 | Liquid.Interfaces,
13 | Liquid.Context,
14 | Liquid.Tag,
15 | Liquid.Block,
16 | Liquid.Variable,
17 | Liquid.Condition,
18 | Liquid.Exceptions,
19 | Liquid.Utils,
20 | Liquid.Hash;
21 |
22 | type
23 | TAssign = class(TTag)
24 | strict private
25 | FTo: string;
26 | FFrom: TVariable;
27 | FSyntax: TRegEx;
28 | public
29 | constructor Create;
30 | destructor Destroy; override;
31 | procedure Initialize(const ATagName: string; const AMarkup: string;
32 | ATokens: TList); override;
33 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
34 | end;
35 |
36 | TIf = class(TBlock)
37 | strict private
38 | FExpressionsAndOperators: string;
39 | FSyntax: TRegEx;
40 | FExpressionsAndOperatorsRegex: TRegEx;
41 | FBlocks: TList;
42 | private
43 | procedure PushBlock(const ATagName: string; const AMarkup: string);
44 | protected
45 | property Blocks: TList read FBlocks;
46 | public
47 | constructor Create;
48 | destructor Destroy; override;
49 | procedure Initialize(const ATagName: string; const AMarkup: string;
50 | ATokens: TList); override;
51 | procedure UnknownTag(const Tag: string; const Markup: string;
52 | Tokens: TList); override;
53 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
54 | end;
55 |
56 | TFor = class(TBlock)
57 | strict private
58 | FSyntax: TRegEx;
59 | FVariableName: string;
60 | FCollectionName: string;
61 | FName: string;
62 | FReversed: boolean;
63 | FAttributes: TDictionary;
64 |
65 | FForBlock: INodeList;
66 | FElseBlock: TCondition;
67 | private
68 | function SliceCollectionUsingEach(AContext: ILiquidContext;
69 | ACollection: TArray; AFrom: integer; ATo: TValue): TArray;
70 | procedure BuildContext(AContext: ILiquidContext; const AParent: string;
71 | const AKey: string; AValue: TValue);
72 | public
73 | constructor Create;
74 | destructor Destroy; override;
75 | procedure Initialize(const ATagName: string; const AMarkup: string;
76 | ATokens: TList); override;
77 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
78 | procedure UnknownTag(const Tag: string; const Markup: string;
79 | Tokens: TList); override;
80 | end;
81 |
82 | TBreak = class(TTag)
83 | public
84 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
85 | end;
86 |
87 | TContinue = class(TTag)
88 | public
89 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); override;
90 | end;
91 |
92 | implementation
93 |
94 | uses
95 | Liquid.Template;
96 |
97 | const
98 | IfTagSyntaxExceptionMessage = 'Syntax Error in ''if'' tag - Valid syntax: if [expression]';
99 | IfTagTooMuchConditionsExceptionMessage = 'Syntax Error in ''if'' tag - max 500 conditions are allowed';
100 | ForTagMaximumIterationsExceededExceptionMessage = 'Render Error - Maximum number of iterations %d exceeded';
101 | ForTagSyntaxException = 'Syntax Error in ''for'' tag - Valid syntax: for [item] in [collection]';
102 | { TAssign }
103 |
104 | constructor TAssign.Create;
105 | begin
106 | FSyntax := R.B(R.Q('(%0:s+)\s*=\s*(.*)\s*'),
107 | [LiquidRegexes.VariableSignature]);
108 | end;
109 |
110 | destructor TAssign.Destroy;
111 | begin
112 | FFrom.Free;
113 | inherited;
114 | end;
115 |
116 | procedure TAssign.Initialize(const ATagName, AMarkup: string;
117 | ATokens: TList);
118 | begin
119 | var SyntaxMatch := FSyntax.Match(AMarkup);
120 | if SyntaxMatch.Success then
121 | begin
122 | FTo := SyntaxMatch.Groups[1].Value;
123 | FFrom := TVariable.Create(SyntaxMatch.Groups[2].Value);
124 | end
125 | else
126 | begin
127 | raise ELiquidSyntaxException.Create(
128 | 'Syntax Error in ''assign'' tag - Valid syntax: assign [var] = [source]');
129 | end;
130 | inherited Initialize(ATagName, AMarkup, ATokens);
131 | end;
132 |
133 | procedure TAssign.Render(Context: ILiquidContext; Writer: TTextWriter);
134 | begin
135 | Context.Scopes.Last.AddOrSetValue(FTo, FFrom.Render(Context));
136 | end;
137 |
138 | { TIf }
139 |
140 | constructor TIf.Create;
141 | begin
142 | inherited;
143 | FExpressionsAndOperators := Format(R.Q(
144 | '(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:%0:s|\S+)\s*)+)'),
145 | [LiquidRegexes.QuotedFragment]);
146 | FSyntax := R.B(R.Q('(%0:s)\s*([=!<>a-zA-Z_]+)?\s*(%0:s)?'),
147 | [LiquidRegexes.QuotedFragment]);
148 | FExpressionsAndOperatorsRegex := R.C(FExpressionsAndOperators);
149 | FBlocks := TObjectList.Create;
150 | end;
151 |
152 | destructor TIf.Destroy;
153 | begin
154 | FBlocks.Free;
155 | inherited;
156 | end;
157 |
158 | procedure TIf.Initialize(const ATagName, AMarkup: string;
159 | ATokens: TList);
160 | begin
161 | PushBlock('if', AMarkup);
162 | inherited Initialize(ATagName, AMarkup, ATokens);
163 | end;
164 |
165 | procedure TIf.PushBlock(const ATagName, AMarkup: string);
166 | begin
167 | var Block: TCondition;
168 | if ATagName.Equals('else') then
169 | Block := TElseCondition.Create
170 | else
171 | begin
172 | var Expressions := TList.Create;
173 | try
174 | Expressions.AddRange(R.Scan(AMarkup, FExpressionsAndOperatorsRegex));
175 | var Syntax := TListHelper.TryGetAtIndexReverse(Expressions, 0);
176 | if string.IsNullOrEmpty(Syntax) then
177 | raise ELiquidSyntaxException.Create(IfTagSyntaxExceptionMessage);
178 | var SyntaxMatch := FSyntax.Match(Syntax);
179 | if not SyntaxMatch.Success then
180 | raise ELiquidSyntaxException.Create(IfTagSyntaxExceptionMessage);
181 | var Condition := TCondition.Create(SyntaxMatch);
182 | try
183 | var ConditionCount := 1;
184 | var I := 1;
185 | // continue to process remaining items in the list backwards, in pairs
186 | while I < Expressions.Count do
187 | begin
188 | var Op := TListHelper.TryGetAtIndexReverse(Expressions, I).Trim;
189 | var ExpressionMatch := FSyntax.Match(
190 | TListHelper.TryGetAtIndexReverse(Expressions, I + 1));
191 | if not ExpressionMatch.Success then
192 | raise ELiquidSyntaxException.Create(IfTagSyntaxExceptionMessage);
193 | Inc(ConditionCount);
194 | if ConditionCount > 500 then
195 | raise ELiquidSyntaxException.Create(IfTagTooMuchConditionsExceptionMessage);
196 | var NewCondition := TCondition.Create(ExpressionMatch);
197 | if Op = 'and' then
198 | NewCondition._And(Condition)
199 | else if Op = 'or' then
200 | NewCondition._Or(Condition);
201 | Condition := NewCondition;
202 | Inc(I, 2);
203 | end;
204 | Block := Condition;
205 | except
206 | Condition.Free;
207 | raise;
208 | end;
209 | finally
210 | Expressions.Free;
211 | end;
212 | end;
213 | Blocks.Add(Block);
214 | SetNodeList(Block.Attach(TNodeList.Create));
215 | end;
216 |
217 | procedure TIf.Render(Context: ILiquidContext; Writer: TTextWriter);
218 | begin
219 | Context.Stack(
220 | procedure
221 | begin
222 | for var Block in Blocks do
223 | if Block.Evaluate(Context, Context.FormatSettings) then
224 | begin
225 | RenderAll(Block.Attachment, Context, Writer);
226 | Break;
227 | end;
228 | end
229 | );
230 | end;
231 |
232 | procedure TIf.UnknownTag(const Tag, Markup: string; Tokens: TList);
233 | begin
234 | if Tag.Equals('elsif') or Tag.Equals('elseif') or Tag.Equals('else') then
235 | PushBlock(Tag, Markup)
236 | else
237 | inherited UnknownTag(Tag, Markup, Tokens);
238 | end;
239 |
240 | { TFor }
241 |
242 | procedure TFor.BuildContext(AContext: ILiquidContext; const AParent,
243 | AKey: string; AValue: TValue);
244 | begin
245 | if not AValue.IsType then
246 | begin
247 | AContext[AParent + '.' + AKey] := AValue;
248 | Exit;
249 | end;
250 | var HashValue := AValue.AsType;
251 | HashValue['itemName'] := AKey;
252 | AContext[AParent] := AValue;
253 | for var HashItem in HashValue.ToArray do
254 | begin
255 | if not HashItem.Value.IsType then
256 | Continue;
257 | BuildContext(AContext, AParent + '.' + AKey, HashItem.Key, HashItem.Value);
258 | end;
259 | end;
260 |
261 | constructor TFor.Create;
262 | begin
263 | inherited;
264 | FSyntax := R.B(R.Q('(\w+)\s+in\s+(%s+)\s*(reversed)?'),
265 | [LiquidRegexes.QuotedFragment]);
266 | FAttributes := TDictionary.Create;
267 | end;
268 |
269 | destructor TFor.Destroy;
270 | begin
271 | FAttributes.Free;
272 | FElseBlock.Free;
273 | inherited;
274 | end;
275 |
276 | procedure TFor.Initialize(const ATagName, AMarkup: string;
277 | ATokens: TList);
278 | begin
279 | var Match := FSyntax.Match(AMarkup);
280 | if Match.Success then
281 | begin
282 | FForBlock := TNodeList.Create;
283 | SetNodeList(FForBlock);
284 | FVariableName := Match.Groups[1].Value;
285 | FCollectionName := Match.Groups[2].Value;
286 | FName := Format('%s-%s', [FVariableName, FCollectionName]);
287 | FReversed := (Match.Groups.Count >= 4) and (not string.IsNullOrEmpty(Match.Groups[3].Value));
288 | R.Scan(AMarkup, LiquidRegexes.TagAttributes,
289 | procedure(Key, Value: string)
290 | begin
291 | FAttributes.AddOrSetValue(Key, Value);
292 | end
293 | );
294 | end
295 | else
296 | begin
297 | raise ELiquidSyntaxException.Create(ForTagSyntaxException);
298 | end;
299 | inherited Initialize(ATagName, AMarkup, ATokens);
300 | end;
301 |
302 | procedure TFor.Render(Context: ILiquidContext; Writer: TTextWriter);
303 | begin
304 | if not Context.Registers.ContainsKey('for') then
305 | Context.Registers['for'] := TValue.From(THash.Create(0));
306 | var Collection := Context[FCollectionName, False];
307 | if not Collection.IsType> then
308 | Exit;
309 | var From: integer;
310 | if FAttributes.ContainsKey('offset') then
311 | begin
312 | var FromValue: TValue;
313 | if FAttributes['offset'] = 'continue' then
314 | FromValue := Context.Registers['for'].AsType[FName]
315 | else
316 | FromValue := Context[FAttributes['offset']];
317 | FromValue := TConverter.ChangeType(FromValue, TypeInfo(integer), tkInteger);
318 | From := FromValue.AsInteger;
319 | end
320 | else
321 | From := 0;
322 |
323 | var Limit := TValue.Empty;
324 | if FAttributes.ContainsKey('limit') then
325 | begin
326 | var LimitValue := Context[FAttributes['limit']];
327 | if not LimitValue.IsEmpty then
328 | begin
329 | LimitValue := TConverter.ChangeType(LimitValue, TypeInfo(integer), tkInteger);
330 | Limit := LimitValue.AsInteger;
331 | end;
332 | end;
333 | var _To := TValue.Empty;
334 | if not Limit.IsEmpty then
335 | _To := Limit.AsInteger + From;
336 |
337 | var Segment := TList.Create;
338 | try
339 | Segment.AddRange(SliceCollectionUsingEach(Context,
340 | Collection.AsType>, From, _To));
341 | if FReversed then
342 | Segment.Reverse;
343 | var Length := Segment.Count;
344 |
345 | // Store our progress through the collection for the continue flag
346 | Context.Registers['for'].AsType[FName] := From + Length;
347 |
348 | Context.Stack(
349 | procedure
350 | begin
351 | if Segment.Count = 0 then
352 | begin
353 | if FElseBlock <> nil then
354 | RenderAll(FElseBlock.Attachment, Context, Writer);
355 | Exit;
356 | end;
357 |
358 | for var Index := 0 to Segment.Count - 1 do
359 | begin
360 | var Item := Segment[Index];
361 | if Item.IsType then
362 | begin
363 | Context[FVariableName] := Item;
364 | for var HashItem in Item.AsType.ToArray do
365 | BuildContext(Context, FVariableName, HashItem.Key, HashItem.Value);
366 | end
367 | else
368 | begin
369 | Context[FVariableName] := Item;
370 | end;
371 |
372 | var ForLoop: IHash := THash.Create;
373 | ForLoop['name'] := FName;
374 | ForLoop['length'] := Length;
375 | ForLoop['index'] := Index + 1;
376 | ForLoop['index0'] := Index;
377 | ForLoop['rindex'] := Length - Index;
378 | ForLoop['rindex0'] := Length - Index - 1;
379 | ForLoop['first'] := Index = 0;
380 | ForLoop['last'] := Index = (Length - 1);
381 |
382 | Context['forloop'] := TValue.From(ForLoop);
383 |
384 | try
385 | RenderAll(FForBlock, Context, Writer);
386 | except
387 | on E: EBreakInterrupt do
388 | begin
389 | Break;
390 | end;
391 | on E: EContinueInterrupt do
392 | begin
393 | // ContinueInterrupt is used only to skip the current value
394 | // but not to stop the iteration
395 | end;
396 | end;
397 | end;
398 | end
399 | );
400 | finally
401 | Segment.Free;
402 | end;
403 | end;
404 |
405 | function TFor.SliceCollectionUsingEach(AContext: ILiquidContext;
406 | ACollection: TArray; AFrom: integer; ATo: TValue): TArray;
407 | begin
408 | var Segments := TList.Create;
409 | try
410 | var Index := 0;
411 | for var Item in ACollection do
412 | begin
413 | if (not ATo.IsEmpty) and (ATo.AsInteger <= Index) then
414 | Break;
415 | if AFrom <= Index then
416 | Segments.Add(Item);
417 | Inc(Index);
418 | if (AContext.MaxIterations > 0) and (Index > AContext.MaxIterations) then
419 | raise EMaximumIterationsExceededException.CreateFmt(
420 | ForTagMaximumIterationsExceededExceptionMessage,
421 | [AContext.MaxIterations]);
422 | end;
423 | Result := Segments.ToArray;
424 | finally
425 | Segments.Free;
426 | end;
427 | end;
428 |
429 | procedure TFor.UnknownTag(const Tag, Markup: string; Tokens: TList);
430 | begin
431 | if Tag.Equals('else') then
432 | begin
433 | FElseBlock := TElseCondition.Create;
434 | SetNodeList(FElseBlock.Attach(TNodeList.Create));
435 | end
436 | else
437 | inherited UnknownTag(Tag, Markup, Tokens);
438 | end;
439 |
440 | { TBreak }
441 |
442 | procedure TBreak.Render(Context: ILiquidContext; Writer: TTextWriter);
443 | begin
444 | raise EBreakInterrupt.Create;
445 | end;
446 |
447 | { TContinue }
448 |
449 | procedure TContinue.Render(Context: ILiquidContext; Writer: TTextWriter);
450 | begin
451 | raise EContinueInterrupt.Create;
452 | end;
453 |
454 | initialization
455 | TLiquidTemplate.RegisterTag(TAssign, 'assign');
456 | TLiquidTemplate.RegisterTag(TIf, 'if');
457 | TLiquidTemplate.RegisterTag(TFor, 'for');
458 | TLiquidTemplate.RegisterTag(TBreak, 'break');
459 | TLiquidTemplate.RegisterTag(TContinue, 'continue');
460 |
461 | end.
462 |
--------------------------------------------------------------------------------
/src/Liquid.Template.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Template;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Classes,
7 | System.Generics.Collections,
8 | System.RegularExpressions,
9 |
10 | Liquid.Interfaces,
11 | Liquid.Default,
12 | Liquid.Document,
13 | Liquid.Context,
14 | Liquid.Hash,
15 | Liquid.Tag,
16 | Liquid.Tags,
17 | Liquid.Tuples;
18 |
19 | type
20 | TRenderParameters = class;
21 |
22 | TLiquidTemplate = class
23 | strict private
24 | class var
25 | FTags: TDictionary>;
26 | class function Tags: TDictionary>;
27 | strict private
28 | FRoot: TDocument;
29 | FInstanceAssigns: IHash;
30 | private
31 | procedure ParseInternal(ASource: TArray);
32 | procedure RenderInternal(Writer: TTextWriter; Parameters: TRenderParameters);
33 | function GetInstanceAssigns: IHash;
34 | protected
35 | function Tokenize(ASource: TArray): TList; overload;
36 | function Tokenize(const ASource: string): TList; overload;
37 | public
38 | class function Parse(const ASource: string): TLiquidTemplate; overload;
39 | class function Parse(ASource: TArray): TLiquidTemplate; overload;
40 | class function CreateTag(const Name: string): TTag;
41 | class procedure RegisterTagFactory(TagFactory: ITagFactory);
42 | class procedure RegisterTag(ATagClass: TTagClass; const AName: string);
43 | class destructor Destroy;
44 | public
45 | constructor Create;
46 | destructor Destroy; override;
47 | function Render: string; overload;
48 | function Render(FormatSettings: TFormatSettings): string; overload;
49 | function Render(LocalVariables: IHash; FormatSettings: TFormatSettings): string; overload;
50 | function Render(Parameters: TRenderParameters): string; overload;
51 | function Render(Writer: TTextWriter; Parameters: TRenderParameters): string; overload;
52 | property InstanceAssigns: IHash read GetInstanceAssigns;
53 | end;
54 |
55 | TRenderParameters = class
56 | strict private
57 | FContext: ILiquidContext;
58 | FLocalVariables: IHash;
59 | FRegisters: IHash;
60 | // FFilters
61 |
62 | FErrorsOutputMode: TErrorsOutputMode;
63 | FMaxIterations: integer;
64 | FFormatSettings: TFormatSettings;
65 | private
66 | procedure SetLocalVariables(ALocalVariables: IHash);
67 | public
68 | class function FromContext(AContext: ILiquidContext;
69 | AFormatSettings: TFormatSettings): TRenderParameters;
70 | public
71 | constructor Create(AFormatSettings: TFormatSettings);
72 | destructor Destroy; override;
73 | procedure Evaluate(Template: TLiquidTemplate; out Context: ILiquidContext;
74 | out Registers: IHash);
75 | property ErrorsOutputMode: TErrorsOutputMode read FErrorsOutputMode;
76 | property MaxIterations: integer read FMaxIterations;
77 | end;
78 |
79 | implementation
80 |
81 | uses
82 | System.Rtti;
83 |
84 | { TLiquidTemplate }
85 |
86 | constructor TLiquidTemplate.Create;
87 | begin
88 |
89 | end;
90 |
91 | class function TLiquidTemplate.CreateTag(const Name: string): TTag;
92 | begin
93 | Result := nil;
94 | var Tuple: ITuple := nil;
95 | if Tags.TryGetValue(Name, Tuple) then
96 | Result := Tuple.Value1.CreateTag;
97 | end;
98 |
99 | class destructor TLiquidTemplate.Destroy;
100 | begin
101 | FTags.Free;
102 | end;
103 |
104 | destructor TLiquidTemplate.Destroy;
105 | begin
106 | FRoot.Free;
107 | inherited;
108 | end;
109 |
110 | function TLiquidTemplate.GetInstanceAssigns: IHash;
111 | begin
112 | if FInstanceAssigns = nil then
113 | FInstanceAssigns := THash.Create;
114 | Result := FInstanceAssigns;
115 | end;
116 |
117 | class function TLiquidTemplate.Parse(ASource: TArray): TLiquidTemplate;
118 | begin
119 | Result := TLiquidTemplate.Create;
120 | try
121 | Result.ParseInternal(ASource);
122 | except
123 | Result.Free;
124 | raise;
125 | end;
126 | end;
127 |
128 | procedure TLiquidTemplate.ParseInternal(ASource: TArray);
129 | begin
130 | // source = DotLiquid.Tags.Literal.FromShortHand(source);
131 | // source = DotLiquid.Tags.Comment.FromShortHand(source);
132 |
133 | var Tokens := Tokenize(ASource);
134 | try
135 | FRoot := TDocument.Create;
136 | FRoot.Initialize('', '', Tokens);
137 | finally
138 | Tokens.Free;
139 | end;
140 | end;
141 |
142 | class function TLiquidTemplate.Parse(const ASource: string): TLiquidTemplate;
143 | begin
144 | Result := Parse(TEncoding.UTF8.GetBytes(ASource));
145 | end;
146 |
147 | function TLiquidTemplate.Render(LocalVariables: IHash;
148 | FormatSettings: TFormatSettings): string;
149 | begin
150 | var Parameters := TRenderParameters.Create(FormatSettings);
151 | try
152 | Parameters.SetLocalVariables(LocalVariables);
153 | Result := Render(Parameters);
154 | finally
155 | Parameters.Free;
156 | end;
157 | end;
158 |
159 | class procedure TLiquidTemplate.RegisterTag(ATagClass: TTagClass;
160 | const AName: string);
161 | begin
162 | Tags.AddOrSetValue(AName, TTuple.Create(
163 | TRttiTagFactory.Create(ATagClass, AName), ATagClass));
164 | end;
165 |
166 | class procedure TLiquidTemplate.RegisterTagFactory(TagFactory: ITagFactory);
167 | begin
168 | Tags.AddOrSetValue(TagFactory.TagName, TTuple.Create(
169 | TagFactory, nil));
170 | end;
171 |
172 | function TLiquidTemplate.Render(Writer: TTextWriter;
173 | Parameters: TRenderParameters): string;
174 | begin
175 | if Writer = nil then
176 | raise EArgumentNilException.Create('Writer is missing');
177 | if Parameters = nil then
178 | raise EArgumentNilException.Create('Parameters is missing');
179 | RenderInternal(Writer, Parameters);
180 | Result := Writer.ToString;
181 | end;
182 |
183 | function TLiquidTemplate.Render(FormatSettings: TFormatSettings): string;
184 | begin
185 | var Parameters := TRenderParameters.Create(FormatSettings);
186 | try
187 | Result := Render(Parameters);
188 | finally
189 | Parameters.Free;
190 | end;
191 | end;
192 |
193 | function TLiquidTemplate.Render: string;
194 | begin
195 | Result := Render(TFormatSettings.Invariant);
196 | end;
197 |
198 | procedure TLiquidTemplate.RenderInternal(Writer: TTextWriter;
199 | Parameters: TRenderParameters);
200 | begin
201 | if FRoot = nil then
202 | Exit;
203 |
204 | var Context: ILiquidContext;
205 | var Registers: IHash;
206 | Parameters.Evaluate(Self, Context, Registers);
207 | FRoot.Render(Context, Writer);
208 | end;
209 |
210 | class function TLiquidTemplate.Tags: TDictionary>;
211 | begin
212 | if FTags = nil then
213 | FTags := TDictionary>.Create;
214 | Result := FTags;
215 | end;
216 |
217 | function TLiquidTemplate.Tokenize(const ASource: string): TList;
218 | begin
219 | Result := Tokenize(TEncoding.UTF8.getbytes(ASource));
220 | end;
221 |
222 | function TLiquidTemplate.Render(Parameters: TRenderParameters): string;
223 | begin
224 | var Writer := TStringWriter.Create;
225 | try
226 | Result := Render(Writer, Parameters);
227 | finally
228 | Writer.Free;
229 | end;
230 | end;
231 |
232 | function TLiquidTemplate.Tokenize(ASource: TArray): TList;
233 | begin
234 | var Source := TEncoding.UTF8.GetString(ASource);
235 | if string.IsNullOrEmpty(Source) then
236 | Exit(TList.Create);
237 |
238 | // Trim leading whitespace.
239 | Source := TRegEx.Replace(Source,
240 | Format('([ \t]+)?(%0:s|%1:s)-',
241 | [LiquidRegexes.VariableStart, LiquidRegexes.TagStart]),
242 | '$2', [roNone]);
243 |
244 | // Trim trailing whitespace.
245 | Source := TRegEx.Replace(Source,
246 | Format('-(%0:s|%1:s)(\n|\r\n|[ \t]+)?',
247 | [LiquidRegexes.VariableEnd, LiquidRegexes.TagEnd]),
248 | '$1', [roNone]);
249 |
250 | Result := TList.Create;
251 | try
252 | var Pattern := LiquidRegexes.TemplateParser;
253 | Result.AddRange(TRegEx.Split(Source, Pattern));
254 |
255 | // Trim any whitespace elements from the end of the array.
256 | for var I := Result.Count - 1 downto 0 do
257 | if Result[I].IsEmpty then
258 | Result.Delete(I);
259 |
260 | // Removes the rogue empty element at the beginning of the array
261 | if (Result.Count > 0) and (Result.First.IsEmpty) then
262 | Result.ExtractAt(0);
263 | except
264 | Result.Free;
265 | raise;
266 | end;
267 | end;
268 |
269 | { TRenderParameters }
270 |
271 | constructor TRenderParameters.Create(AFormatSettings: TFormatSettings);
272 | begin
273 | FMaxIterations := 0;
274 | FFormatSettings := AFormatSettings;
275 | end;
276 |
277 | destructor TRenderParameters.Destroy;
278 | begin
279 | FLocalVariables := nil;
280 | inherited;
281 | end;
282 |
283 | procedure TRenderParameters.Evaluate(Template: TLiquidTemplate;
284 | out Context: ILiquidContext; out Registers: IHash);
285 | begin
286 | if FContext <> nil then
287 | begin
288 | Context := FContext;
289 | Registers := nil;
290 | // Filters := nil;
291 | Exit;
292 | end;
293 | var Environments := TList.Create;
294 | if FLocalVariables <> nil then
295 | Environments.Add(FLocalVariables);
296 |
297 | Context := TLiquidContext.Create(Environments, THash.Create, THash.Create,
298 | FErrorsOutputMode, FMaxIterations, FFormatSettings);
299 |
300 | Registers := FRegisters;
301 | // Filters := FFilster;
302 | end;
303 |
304 | class function TRenderParameters.FromContext(AContext: ILiquidContext;
305 | AFormatSettings: TFormatSettings): TRenderParameters;
306 | begin
307 | if AContext = nil then
308 | raise EArgumentException.Create('Context');
309 | Result := TRenderParameters.Create(AFormatSettings);
310 | Result.FContext := AContext;
311 | end;
312 |
313 | procedure TRenderParameters.SetLocalVariables(ALocalVariables: IHash);
314 | begin
315 | FLocalVariables := ALocalVariables;
316 | end;
317 |
318 | end.
319 |
--------------------------------------------------------------------------------
/src/Liquid.Tuples.pas:
--------------------------------------------------------------------------------
1 | {****************************************************}
2 | { }
3 | { Generics.Tuples }
4 | { }
5 | { Copyright (C) 2014 Malcolm Groves }
6 | { }
7 | { https://github.com/malcolmgroves/generics.tuples }
8 | { }
9 | {****************************************************}
10 | { }
11 | { This Source Code Form is subject to the terms of }
12 | { the Mozilla Public License, v. 2.0. If a copy of }
13 | { the MPL was not distributed with this file, You }
14 | { can obtain one at }
15 | { }
16 | { http://mozilla.org/MPL/2.0/ }
17 | { }
18 | {****************************************************}
19 | unit Liquid.Tuples;
20 |
21 | interface
22 |
23 | type
24 | ITuple = interface
25 | procedure SetValue1(Value : T1);
26 | function GetValue1 : T1;
27 | procedure SetValue2(Value : T2);
28 | function GetValue2 : T2;
29 | property Value1 : T1 read GetValue1 write SetValue1;
30 | property Value2 : T2 read GetValue2 write SetValue2;
31 | end;
32 |
33 | ITuple = interface(ITuple)
34 | procedure SetValue3(Value : T3);
35 | function GetValue3 : T3;
36 | property Value3 : T3 read GetValue3 write SetValue3;
37 | end;
38 |
39 | TTuple = class(TInterfacedObject, ITuple)
40 | protected
41 | FValue1 : T1;
42 | FValue2 : T2;
43 | procedure SetValue1(Value : T1);
44 | function GetValue1 : T1;
45 | procedure SetValue2(Value : T2);
46 | function GetValue2 : T2;
47 | public
48 | constructor Create(Value1 : T1; Value2 : T2); virtual;
49 | destructor Destroy; override;
50 | property Value1 : T1 read FValue1 write FValue1;
51 | property Value2 : T2 read FValue2 write FValue2;
52 | end;
53 |
54 | TTuple = class(TTuple, ITuple)
55 | protected
56 | FValue1 : T1;
57 | FValue2 : T2;
58 | FValue3 : T3;
59 | procedure SetValue3(Value : T3);
60 | function GetValue3 : T3;
61 | public
62 | constructor Create(Value1 : T1; Value2 : T2; Value3 : T3); reintroduce;
63 | destructor Destroy; override;
64 | property Value3 : T3 read GetValue3 write SetValue3;
65 | end;
66 |
67 | implementation
68 | uses
69 | System.RTTI;
70 |
71 | { TPair }
72 |
73 | constructor TTuple.Create(Value1: T1; Value2: T2);
74 | begin
75 | self.Value1 := Value1;
76 | self.Value2 := Value2;
77 | end;
78 |
79 | destructor TTuple.Destroy;
80 | {$IFNDEF AUTOREFCOUNT}
81 | var
82 | LValue1Holder, LValue2Holder : TValue;
83 | {$ENDIF}
84 | begin
85 | {$IFNDEF AUTOREFCOUNT}
86 | LValue1Holder := TValue.From(FValue1);
87 | if LValue1Holder.IsObject then
88 | LValue1Holder.AsObject.Free;
89 |
90 | LValue2Holder := TValue.From(FValue2);
91 | if LValue2Holder.IsObject then
92 | LValue2Holder.AsObject.Free;
93 | inherited;
94 | {$ENDIF}
95 | end;
96 |
97 | function TTuple.GetValue1: T1;
98 | begin
99 | Result := FValue1;
100 | end;
101 |
102 | function TTuple.GetValue2: T2;
103 | begin
104 | Result := FValue2;
105 | end;
106 |
107 | procedure TTuple.SetValue1(Value: T1);
108 | begin
109 | FValue1 := Value;
110 | end;
111 |
112 | procedure TTuple.SetValue2(Value: T2);
113 | begin
114 | FValue2 := Value;
115 | end;
116 |
117 | { TTuple3 }
118 |
119 | constructor TTuple.Create(Value1: T1; Value2: T2; Value3: T3);
120 | begin
121 | inherited Create(Value1, Value2);
122 | self.Value3 := Value3;
123 | end;
124 |
125 | destructor TTuple.Destroy;
126 | {$IFNDEF AUTOREFCOUNT}
127 | var
128 | LValue3Holder : TValue;
129 | {$ENDIF}
130 | begin
131 | {$IFNDEF AUTOREFCOUNT}
132 | LValue3Holder := TValue.From(FValue3);
133 | if LValue3Holder.IsObject then
134 | LValue3Holder.AsObject.Free;
135 | {$ENDIF}
136 | inherited;
137 | end;
138 |
139 | function TTuple.GetValue3: T3;
140 | begin
141 | Result := FValue3;
142 | end;
143 |
144 | procedure TTuple.SetValue3(Value: T3);
145 | begin
146 | FValue3 := Value;
147 | end;
148 |
149 | end.
150 |
--------------------------------------------------------------------------------
/src/Liquid.Utils.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Utils;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils, System.Classes,
7 | System.RegularExpressions,
8 | System.Generics.Collections,
9 | System.Generics.Defaults,
10 | System.Rtti, System.TypInfo,
11 | System.DateUtils,
12 |
13 | Liquid.Default;
14 |
15 | type
16 | R = class
17 | public
18 | class function Scan(const Input: string; RegEx: TRegEx): TArray; overload;
19 | class procedure Scan(const Input: string; const Pattern: string;
20 | ACallback: TProc); overload;
21 | class function Q(const Regex: string): string;
22 | class function B(const Format: string; const Args: array of const): TRegEx;
23 | class function C(const Pattern: string; Options: TRegExOptions = [TRegExOption.roNone]): TRegEx;
24 | end;
25 |
26 | TSymbol = class
27 | strict private
28 | FEvaluationFunction: TFunc;
29 | public
30 | constructor Create(AEvaluationFunction: TFunc);
31 | property EvaluationFunction: TFunc read FEvaluationFunction write FEvaluationFunction;
32 | end;
33 |
34 | TCompareUtils = class
35 | public
36 | class function Comparer(Kind: TTypeKind): IComparer;
37 | class function Compare(Left, Right: TValue): integer;
38 | end;
39 |
40 | TConverter = class
41 | private
42 | class function StringConverter(FromValue: TValue; ToTypeInfo: PTypeInfo;
43 | ToTypeKind: TTypeKind): TValue;
44 | class function IntegerConverter(FromValue: TValue; ToTypeInfo: PTypeInfo;
45 | ToTypeKind: TTypeKind): TValue;
46 | class function Int64Converter(FromValue: TValue; ToTypeInfo: PTypeInfo;
47 | ToTypeKind: TTypeKind): TValue;
48 | class function FloatConverter(FromValue: TValue; ToTypeInfo: PTypeInfo;
49 | ToTypeKind: TTypeKind): TValue;
50 | public
51 | class function ChangeType(FromValue: TValue; ToTypeInfo: PTypeInfo;
52 | ToTypeKind: TTypeKind): TValue;
53 | class function StringToRealType(const Value: string; var Output: TValue): boolean;
54 | end;
55 |
56 | TListHelper = class
57 | public
58 | class function TryGetAtIndexReverse(AList: TList; RIndex: integer): string; overload;
59 | class function TryGetAtIndexReverse(AList: TList; RIndex: integer): TValue; overload;
60 | end;
61 |
62 | ECompareException = class(Exception);
63 | EUnsupportedType = class(ECompareException);
64 |
65 | implementation
66 |
67 | { R }
68 |
69 | class function R.B(const Format: string; const Args: array of const): TRegEx;
70 | begin
71 | Result := C(string.Format(Format, Args));
72 | end;
73 |
74 | class function R.C(const Pattern: string; Options: TRegExOptions): TRegEx;
75 | begin
76 | Result := TRegEx.Create(Pattern);
77 | Result.IsMatch(string.Empty);
78 | end;
79 |
80 | class function R.Q(const Regex: string): string;
81 | begin
82 | Result := string.Format('(?-mix:%0:s)', [Regex]);
83 | end;
84 |
85 | class procedure R.Scan(const Input: string; const Pattern: string;
86 | ACallback: TProc);
87 | begin
88 | var Matches := TRegEx.Matches(Input, Pattern);
89 | for var M in Matches do
90 | ACallback(M.Groups[1].Value, M.Groups[2].Value);
91 | end;
92 |
93 | class function R.Scan(const Input: string; RegEx: TRegEx): TArray;
94 | begin
95 | var L := TList.Create;
96 | try
97 | var Matches := RegEx.Matches(Input);
98 | for var M in Matches do
99 | begin
100 | if M.Groups.Count = 2 then
101 | L.Add(M.Groups[1].Value)
102 | else
103 | L.Add(M.Value);
104 | end;
105 | Result := L.ToArray;
106 | finally
107 | L.Free;
108 | end;
109 | end;
110 |
111 | { TSymbol }
112 |
113 | constructor TSymbol.Create(AEvaluationFunction: TFunc);
114 | begin
115 | FEvaluationFunction := AEvaluationFunction;
116 | end;
117 |
118 | { TCompareUtils }
119 |
120 | class function TCompareUtils.Compare(Left, Right: TValue): integer;
121 | var
122 | LocalComparer: IComparer;
123 | begin
124 | if Left.TypeInfo = TypeInfo(TDateTime) then
125 | LocalComparer := TDelegatedComparer.Create(
126 | function(const Left, Right: TValue): integer
127 | begin
128 | Result := CompareDateTime(Left.AsType,
129 | Right.AsType);
130 | end
131 | )
132 | else if Left.IsOrdinal then
133 | LocalComparer := TDelegatedComparer.Create(
134 | function(const Left, Right: TValue): integer
135 | begin
136 | Result := TComparer.Default.Compare(
137 | Left.AsOrdinal, Right.AsOrdinal);
138 | end
139 | )
140 | else
141 | LocalComparer := Comparer(Left.Kind);
142 | Result := LocalComparer.Compare(Left, Right);
143 | end;
144 |
145 | class function TCompareUtils.Comparer(Kind: TTypeKind): IComparer;
146 | begin
147 | case Kind of
148 | tkChar, tkString, tkWChar, tkLString, tkWString, tkUString:
149 | begin
150 | Result := TDelegatedComparer.Create(
151 | function(const Left, Right: TValue): integer
152 | begin
153 | Result := TComparer.Default.Compare(
154 | Left.AsString, Right.AsString);
155 | end
156 | );
157 | end;
158 |
159 | tkInteger, tkEnumeration:
160 | begin
161 | Result := TDelegatedComparer.Create(
162 | function(const Left, Right: TValue): integer
163 | begin
164 | Result := TComparer.Default.Compare(
165 | Left.AsInteger, Right.AsInteger);
166 | end
167 | );
168 | end;
169 |
170 | tkInt64:
171 | begin
172 | Result := TDelegatedComparer.Create(
173 | function(const Left, Right: TValue): integer
174 | begin
175 | Result := TComparer.Default.Compare(
176 | Left.AsInt64, Right.AsInt64);
177 | end
178 | );
179 | end;
180 |
181 | tkFloat:
182 | begin
183 | Result := TDelegatedComparer.Create(
184 | function(const Left, Right: TValue): integer
185 | begin
186 | Result := TComparer.Default.Compare(
187 | Left.AsExtended, Right.AsExtended);
188 | end
189 | );
190 | end;
191 | else
192 | raise EUnsupportedType.CreateFmt('Unsupported type: %s',
193 | [TRttiEnumerationType.GetName(Kind)]);
194 | end;
195 | end;
196 |
197 | { TListHelper }
198 |
199 | class function TListHelper.TryGetAtIndexReverse(AList: TList;
200 | RIndex: integer): TValue;
201 | begin
202 | if (AList <> nil) and (AList.Count > RIndex) and (RIndex >= 0) then
203 | Exit(AList[AList.Count - 1 - Rindex]);
204 | Result := TValue.Empty;
205 | end;
206 |
207 | class function TListHelper.TryGetAtIndexReverse(AList: TList;
208 | RIndex: integer): string;
209 | begin
210 | if (AList <> nil) and (AList.Count > RIndex) and (RIndex >= 0) then
211 | Exit(AList[AList.Count - 1 - Rindex]);
212 | Result := '';
213 | end;
214 |
215 | { TConverter }
216 |
217 | class function TConverter.StringToRealType(const Value: string;
218 | var Output: TValue): boolean;
219 | begin
220 | if Value.Equals('') or Value.Equals('nil') or Value.Equals('null') then
221 | begin
222 | Output := TValue.Empty;
223 | Exit(True);
224 | end;
225 | if Value.Equals('true') then
226 | begin
227 | Output := True;
228 | Exit(True);
229 | end;
230 | if Value.Equals('false') then
231 | begin
232 | Output := False;
233 | Exit(True);
234 | end;
235 | if Value.Equals('blank') or Value.Equals('empty') then
236 | begin
237 | Output := '';
238 | Exit(True);
239 | end;
240 |
241 | var Match := LiquidRegexes.SingleQuotedRegex.Match(Value);
242 | if Match.Success then
243 | begin
244 | Output := Match.Groups[1].Value;
245 | Exit(True);
246 | end;
247 |
248 | Match := LiquidRegexes.DoubleQuotedRegex.Match(Value);
249 | if Match.Success then
250 | begin
251 | Output := Match.Groups[1].Value;
252 | Exit(True);
253 | end;
254 |
255 | Match := LiquidRegexes.IntegerRegex.Match(Value);
256 | if Match.Success then
257 | begin
258 | try
259 | Output := StrToInt(Match.Groups[1].Value);
260 | Exit(True);
261 | except
262 | on E: EConvertError do
263 | begin
264 | Output := StrToInt64(Match.Groups[1].Value);
265 | Exit(True);
266 | end;
267 | end;
268 | end;
269 |
270 | // Match := LiquidRegexes.RangeRegex.Match(Value);
271 | // if Match.Success then
272 | // begin
273 | // raise Exception.Create('not implemented');
274 | // end;
275 |
276 | Match := LiquidRegexes.NumericRegex.Match(Value);
277 | if Match.Success then
278 | begin
279 | var Number := Match.Groups[1].Value;
280 | var DoubleNumber: double;
281 | if TryStrToFloat(Number, DoubleNumber, FormatSettings) then
282 | begin
283 | Output := DoubleNumber;
284 | Exit(True);
285 | end;
286 |
287 | var ExtendedNumber: double;
288 | if TryStrToFloat(Number, ExtendedNumber, FormatSettings) then
289 | begin
290 | Output := ExtendedNumber;
291 | Exit(True);
292 | end;
293 | end;
294 |
295 | Result := False;
296 | end;
297 |
298 | class function TConverter.FloatConverter(FromValue: TValue;
299 | ToTypeInfo: PTypeInfo; ToTypeKind: TTypeKind): TValue;
300 | begin
301 | if ToTypeInfo = TypeInfo(boolean) then
302 | begin
303 | if FromValue.IsEmpty then
304 | Exit(False);
305 | Exit(True);
306 | end;
307 | Result := FromValue;
308 | end;
309 |
310 | class function TConverter.Int64Converter(FromValue: TValue;
311 | ToTypeInfo: PTypeInfo; ToTypeKind: TTypeKind): TValue;
312 | begin
313 | if ToTypeInfo = TypeInfo(boolean) then
314 | begin
315 | if FromValue.IsEmpty then
316 | Exit(False);
317 | Exit(True);
318 | end;
319 |
320 | case ToTypeKind of
321 | tkFloat:
322 | begin
323 | Result := FromValue.AsExtended;
324 | end;
325 | else
326 | Result := FromValue;
327 | end;
328 | end;
329 |
330 | class function TConverter.IntegerConverter(FromValue: TValue;
331 | ToTypeInfo: PTypeInfo; ToTypeKind: TTypeKind): TValue;
332 | begin
333 | if ToTypeInfo = TypeInfo(boolean) then
334 | begin
335 | if FromValue.IsEmpty then
336 | Exit(False);
337 | Exit(True);
338 | end;
339 |
340 | case ToTypeKind of
341 | tkInt64:
342 | begin
343 | Result := FromValue.AsInt64;
344 | end;
345 |
346 | tkFloat:
347 | begin
348 | Result := FromValue.AsExtended;
349 | end;
350 | else
351 | Result := FromValue;
352 | end;
353 | end;
354 |
355 | class function TConverter.StringConverter(FromValue: TValue;
356 | ToTypeInfo: PTypeInfo; ToTypeKind: TTypeKind): TValue;
357 | begin
358 | if ToTypeInfo = TypeInfo(boolean) then
359 | begin
360 | if FromValue.IsEmpty or string.IsNullOrEmpty(FromValue.AsString) then
361 | Exit(False);
362 | Exit(True);
363 | end;
364 |
365 | Result := FromValue;
366 | end;
367 |
368 | class function TConverter.ChangeType(FromValue: TValue;
369 | ToTypeInfo: PTypeInfo; ToTypeKind: TTypeKind): TValue;
370 | begin
371 | case FromValue.Kind of
372 | tkChar, tkString, tkWChar, tkLString, tkWString, tkUString:
373 | begin
374 | Result := StringConverter(FromValue, ToTypeInfo, ToTypeKind);
375 | end;
376 |
377 | tkInteger, tkEnumeration:
378 | begin
379 | Result := IntegerConverter(FromValue, ToTypeInfo, ToTypeKind);
380 | end;
381 |
382 | tkInt64:
383 | begin
384 | Result := Int64Converter(FromValue, ToTypeInfo, ToTypeKind);
385 | end;
386 |
387 | tkFloat:
388 | begin
389 | Result := FloatConverter(FromValue, ToTypeInfo, ToTypeKind);
390 | end;
391 | else
392 | Result := FromValue;
393 | end;
394 | end;
395 |
396 | end.
397 |
--------------------------------------------------------------------------------
/src/Liquid.Variable.pas:
--------------------------------------------------------------------------------
1 | unit Liquid.Variable;
2 |
3 | interface
4 |
5 | uses
6 | System.SysUtils,
7 | System.Classes, System.Generics.Collections,
8 | System.RegularExpressions,
9 | System.Rtti,
10 | System.TypInfo,
11 |
12 | Liquid.Default,
13 | Liquid.Interfaces,
14 | Liquid.Context,
15 | Liquid.Utils;
16 |
17 | type
18 | TVariable = class
19 | type
20 | TFilter = class
21 | strict private
22 | FName: string;
23 | FArguments: TArray;
24 | public
25 | constructor Create(AName: string; AArguments: TArray);
26 | property Name: string read FName;
27 | property Arguments: TArray read FArguments;
28 | end;
29 | strict private
30 | FFilterParserRegex: TRegEx;
31 | FFilterArgRegex: TRegEx;
32 | FQuotedAssignFragmentRegex: TRegEx;
33 | FFilterSeparatorRegex: TRegEx;
34 | FFilterNameRegex: TRegEx;
35 | //
36 | FName: string;
37 | FFilters: TList;
38 | FMarkup: string;
39 | private
40 | function RenderInternal(Context: ILiquidContext): TValue; overload;
41 | procedure RenderInternal(Context: ILiquidContext; Writer: TTextWriter;
42 | Value: TValue); overload;
43 | public
44 | constructor Create(const AMarkup: string);
45 | destructor Destroy; override;
46 | procedure Render(Context: ILiquidContext; Writer: TTextWriter); overload;
47 | function Render(Context: ILiquidContext): TValue; overload;
48 | property Name: string read FName;
49 | property Filters: TList read FFilters;
50 | end;
51 |
52 | implementation
53 |
54 | { TVariable }
55 |
56 | constructor TVariable.Create(const AMarkup: string);
57 | begin
58 | FFilterParserRegex := R.B(R.Q(
59 | '(?:%0:s|(?:\s*(?!(?:%0:s))(?:%1:s|\S+)\s*)+)'),
60 | [LiquidRegexes.FilterSeparator, LiquidRegexes.QuotedFragment]);
61 | FFilterArgRegex := R.B(R.Q(
62 | '(?:%0:s|%1:s)\s*(%2:s)'),
63 | [LiquidRegexes.FilterArgumentSeparator, LiquidRegexes.ArgumentSeparator, LiquidRegexes.QuotedFragment]);
64 | FQuotedAssignFragmentRegex := R.B(R.Q(
65 | '\s*(%0:s)(.*)'), [LiquidRegexes.QuotedAssignFragment]);
66 | FFilterSeparatorRegex := R.B(R.Q(
67 | '%0:s\s*(.*)'), [LiquidRegexes.FilterSeparator]);
68 | FFilterNameRegex := R.B(R.Q('\s*(\w+)'), []);
69 |
70 | FFilters := TObjectList.Create;
71 | FMarkup := AMarkup;
72 | FName := '';
73 |
74 | var Match := FQuotedAssignFragmentRegex.Match(AMarkup);
75 | if Match.Success then
76 | begin
77 | FName := Match.Groups[1].Value;
78 | var FilterMatch := FFilterSeparatorRegex.Match(Match.Groups[2].Value);
79 | if FilterMatch.Success then
80 | begin
81 | for var F in R.Scan(FilterMatch.Value, FFilterParserRegex) do
82 | begin
83 | var FilterNameMatch := FFilterNameRegex.Match(F);
84 | if FilterNameMatch.Success then
85 | begin
86 | var FilterName := FilterNameMatch.Groups[1].Value;
87 | var FilterArgs := R.Scan(F, FFilterArgRegex);
88 | Filters.Add(TFilter.Create(FilterName, FilterArgs));
89 | end;
90 | end;
91 | end;
92 | end;
93 | end;
94 |
95 | destructor TVariable.Destroy;
96 | begin
97 | FFilters.Free;
98 | inherited;
99 | end;
100 |
101 | procedure TVariable.Render(Context: ILiquidContext; Writer: TTextWriter);
102 | begin
103 | var Value := RenderInternal(Context);
104 | if not Value.IsEmpty then
105 | RenderInternal(Context, Writer, Value);
106 | end;
107 |
108 | function TVariable.Render(Context: ILiquidContext): TValue;
109 | begin
110 | Result := RenderInternal(Context);
111 | end;
112 |
113 | procedure TVariable.RenderInternal(Context: ILiquidContext; Writer: TTextWriter;
114 | Value: TValue);
115 | begin
116 | case Value.Kind of
117 | tkChar, tkString, tkWChar, tkLString, tkWString, tkUString:
118 | begin
119 | Writer.Write(Value.AsString);
120 | end;
121 |
122 | tkInteger, tkEnumeration:
123 | begin
124 | if Value.TypeInfo = TypeInfo(boolean) then
125 | Writer.Write(BoolToStr(Value.AsBoolean, True).ToLower)
126 | else
127 | Writer.Write(Value.AsInteger);
128 | end;
129 |
130 | tkInt64:
131 | begin
132 | Writer.Write(Value.AsInt64);
133 | end;
134 |
135 | tkFloat:
136 | begin
137 | if (Value.TypeInfo = TypeInfo(TDateTime)) then
138 | begin
139 | var DateFormatted: string;
140 | if Frac(Value.AsExtended) > 0 then
141 | DateFormatted := DateTimeToStr(Value.AsType, Context.FormatSettings)
142 | else
143 | DateFormatted := DateToStr(Value.AsType, Context.FormatSettings);
144 | Writer.Write(DateFormatted);
145 | end
146 | else
147 | Writer.Write(Value.AsExtended);
148 | end;
149 | else
150 | Writer.Write(Value.AsString);
151 | end;
152 | end;
153 |
154 | function TVariable.RenderInternal(Context: ILiquidContext): TValue;
155 | begin
156 | if Name = '' then
157 | Exit(TValue.Empty);
158 |
159 | var Value := Context[Name];
160 |
161 | // process filters
162 | for var Filter in Filters do
163 | begin
164 | var FilterArgs := TList.Create;
165 | try
166 | FilterArgs.Add(Value);
167 | for var Arg in Filter.Arguments do
168 | begin
169 | var ArgResolved := Context[Arg, False];
170 | FilterArgs.Add(ArgResolved);
171 | end;
172 | try
173 | Value := Context.Strainer.Invoke(Filter.Name, FilterArgs.ToArray);
174 | except
175 | on E: EFileNotFoundException do
176 | begin
177 | //
178 | // raise EFileNotFoundException.CreateFmt(
179 | // 'Error - Filter ''%s'' in ''%s'' could not be found.',
180 | // [Filter.Name, FMarkup.Trim]);
181 | end;
182 | end;
183 | finally
184 | FilterArgs.Free;
185 | end;
186 | end;
187 |
188 | // process IValueTypeConvertible
189 | // !! not implemented
190 |
191 | Result := Value;
192 | end;
193 |
194 | { TVariable.TFilter }
195 |
196 | constructor TVariable.TFilter.Create(AName: string; AArguments: TArray);
197 | begin
198 | FName := AName;
199 | FArguments := AArguments;
200 | end;
201 |
202 | end.
203 |
--------------------------------------------------------------------------------
/tests/LiquidTest.dpr:
--------------------------------------------------------------------------------
1 | program LiquidTest;
2 | {
3 |
4 | Delphi DUnit Test Project
5 | -------------------------
6 | This project contains the DUnit test framework and the GUI/Console test runners.
7 | Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options
8 | to use the console test runner. Otherwise the GUI test runner will be used by
9 | default.
10 |
11 | }
12 |
13 | {$IFDEF CONSOLE_TESTRUNNER}
14 | {$APPTYPE CONSOLE}
15 | {$ENDIF}
16 |
17 | uses
18 | DUnitTestRunner,
19 | TestLiquid in 'TestLiquid.pas',
20 | Liquid.Block in '..\src\Liquid.Block.pas',
21 | Liquid.Condition in '..\src\Liquid.Condition.pas',
22 | Liquid.Context in '..\src\Liquid.Context.pas',
23 | Liquid.Default in '..\src\Liquid.Default.pas',
24 | Liquid.Document in '..\src\Liquid.Document.pas',
25 | Liquid.Exceptions in '..\src\Liquid.Exceptions.pas',
26 | Liquid.Filters in '..\src\Liquid.Filters.pas',
27 | Liquid.Hash in '..\src\Liquid.Hash.pas',
28 | Liquid.Interfaces in '..\src\Liquid.Interfaces.pas',
29 | Liquid.Tag in '..\src\Liquid.Tag.pas',
30 | Liquid.Tags in '..\src\Liquid.Tags.pas',
31 | Liquid.Template in '..\src\Liquid.Template.pas',
32 | Liquid.Tuples in '..\src\Liquid.Tuples.pas',
33 | Liquid.Utils in '..\src\Liquid.Utils.pas',
34 | Liquid.Variable in '..\src\Liquid.Variable.pas';
35 |
36 | {$R *.RES}
37 |
38 | begin
39 | ReportMemoryLeaksOnShutdown := True;
40 | DUnitTestRunner.RunRegisteredTests;
41 | end.
42 |
43 |
--------------------------------------------------------------------------------
/tests/LiquidTest.dproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | {B0FC4230-FA9B-4008-948C-73A490D5BC64}
4 | 19.2
5 | None
6 | True
7 | Debug
8 | Win32
9 | 1
10 | Console
11 | LiquidTest.dpr
12 |
13 |
14 | true
15 |
16 |
17 | true
18 | Base
19 | true
20 |
21 |
22 | true
23 | Base
24 | true
25 |
26 |
27 | true
28 | Base
29 | true
30 |
31 |
32 | true
33 | Cfg_1
34 | true
35 | true
36 |
37 |
38 | true
39 | Base
40 | true
41 |
42 |
43 | .\$(Platform)\$(Config)
44 | .\$(Platform)\$(Config)
45 | false
46 | false
47 | false
48 | false
49 | false
50 | System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)
51 | $(BDS)\Source\DUnit\src;$(DCC_UnitSearchPath)
52 | _CONSOLE_TESTRUNNER;$(DCC_Define)
53 | LiquidTest
54 | 1033
55 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=
56 |
57 |
58 | DBXSqliteDriver;DBXDb2Driver;vclactnband;vclFireDAC;tethering;FireDACADSDriver;JvPluginSystem;frx27;ACBr_BPe;FireDACMSSQLDriver;vcltouch;JvBands;ACBr_NFe;vcldb;SKIA_FlexCel_Core;svn;JvJans;FlexCel_Report;ACBr_NFeDanfeFR;fs27;JvDotNetCtrls;VCL_FlexCel_Components;vclib;TMSCryptoPkgDEDXE13;FireDACDBXDriver;ACBr_NFSeDanfseFR;vclx;RESTBackendComponents;ACBr_Reinf;VCLRESTComponents;vclie;bindengine;CloudService;JvHMI;FireDACMySQLDriver;TMSWEBCorePkgLibDXE13;DataSnapClient;bindcompdbx;frxDB27;fsFD27;ACBr_TCP;IndyIPServer;DBXSybaseASEDriver;ACBr_CTe;sparkle;tmsbcl;IndySystem;bindcompvclwinx;dsnapcon;ACBre_Social;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;Jcl;fsIBX27;FlexCel_XlsAdapter;ACBr_MDFe;emshosting;frxIntIO27;TMSWEBCorePkgDXE13;frxFD27;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;ACBr_CIOT;JvManagedThreads;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;JvTimeFramework;rtl;emsserverresource;DbxClientDriver;DBXSybaseASADriver;JvSystem;JvStdCtrls;appanalytics;IndyIPClient;bindcompvcl;FMX_FlexCel_Components;JvDocking;JvPascalInterpreter;VclSmp;FireDACODBCDriver;JclVcl;DataSnapIndy10ServerTransport;aurelius;frxcs27;ACBr_Boleto;DataSnapProviderClient;FireDACMongoDBDriver;JvControls;JvPrintPreview;ACBr_MDFeDamdfeFR;ACBr_NFSe;DataSnapServerMidas;RESTComponents;DBXInterBaseDriver;TMSLogging;FlexCel_Pdf;FMX_FlexCel_Core;fsDB27;ACBr_NF3e;bindcompvclsmp;emsclientfiredac;DataSnapFireDAC;svnui;JvGlobus;DBXMSSQLDriver;JvMM;DatasnapConnectorsFreePascal;ACBr_ONE;frxIBX27;bindcompfmx;JvNet;DBXOracleDriver;inetdb;JvAppFrm;ACBr_Diversos;ACBr_GNREGuiaFR;emsedge;fmx;FireDACIBDriver;fmxdae;vcledge;ACBr_CTeDacteFR;JvWizards;dbexpress;IndyCore;xdata;JvPageComps;dsnap;emsclient;DataSnapCommon;FireDACCommon;JvDB;DataSnapConnectors;soapserver;ACBr_SAT;JclDeveloperTools;FireDACOracleDriver;DBXMySQLDriver;JvCmp;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;ACBr_GNRE;TMSCryptoPkgDXE13;inet;IndyIPCommon;JvCustom;vcl;JvXPCtrls;FireDACDb2Driver;ACBr_Integrador;frxIntIOIndy27;FireDAC;JvCore;ACBr_Comum;JvCrypt;FireDACSqliteDriver;FireDACPgDriver;ibmonitor;FireDACASADriver;JvDlgs;JvRuntimeDesign;FlexCel_Core;ibxpress;DataSnapServer;ibxbindings;vclwinx;FireDACDSDriver;ACBr_OpenSSL;CustomIPTransport;vcldsnap;bindcomp;frxADO27;DBXInformixDriver;fsADO27;frxe27;ACBr_BlocoX;dbxcds;VCL_FlexCel_Core;frxDBX27;adortl;ACBr_BoletoFR;FlexCel_Render;ACBr_ANe;dsnapxml;dbrtl;IndyProtocols;inetdbxpress;JclContainers;fmxase;$(DCC_UsePackage)
59 | Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)
60 | Debug
61 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=
62 | 1033
63 |
64 |
65 | DBXSqliteDriver;DBXDb2Driver;vclactnband;vclFireDAC;tethering;FireDACADSDriver;FireDACMSSQLDriver;vcltouch;vcldb;FlexCel_Report;VCL_FlexCel_Components;vclib;FireDACDBXDriver;vclx;RESTBackendComponents;VCLRESTComponents;vclie;bindengine;CloudService;FireDACMySQLDriver;DataSnapClient;bindcompdbx;IndyIPServer;DBXSybaseASEDriver;sparkle;tmsbcl;IndySystem;bindcompvclwinx;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;FlexCel_XlsAdapter;emshosting;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;rtl;emsserverresource;DbxClientDriver;DBXSybaseASADriver;appanalytics;IndyIPClient;bindcompvcl;FMX_FlexCel_Components;VclSmp;FireDACODBCDriver;DataSnapIndy10ServerTransport;aurelius;DataSnapProviderClient;FireDACMongoDBDriver;DataSnapServerMidas;RESTComponents;DBXInterBaseDriver;TMSLogging;FlexCel_Pdf;FMX_FlexCel_Core;bindcompvclsmp;emsclientfiredac;DataSnapFireDAC;DBXMSSQLDriver;DatasnapConnectorsFreePascal;bindcompfmx;DBXOracleDriver;inetdb;emsedge;fmx;FireDACIBDriver;fmxdae;vcledge;dbexpress;IndyCore;xdata;dsnap;emsclient;DataSnapCommon;FireDACCommon;DataSnapConnectors;soapserver;FireDACOracleDriver;DBXMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;inet;IndyIPCommon;vcl;FireDACDb2Driver;FireDAC;FireDACSqliteDriver;FireDACPgDriver;ibmonitor;FireDACASADriver;FlexCel_Core;ibxpress;DataSnapServer;ibxbindings;vclwinx;FireDACDSDriver;CustomIPTransport;vcldsnap;bindcomp;DBXInformixDriver;dbxcds;VCL_FlexCel_Core;adortl;FlexCel_Render;dsnapxml;dbrtl;IndyProtocols;inetdbxpress;fmxase;$(DCC_UsePackage)
66 |
67 |
68 | DEBUG;$(DCC_Define)
69 | true
70 | false
71 | true
72 | true
73 | true
74 |
75 |
76 | false
77 | .\$(Platform)\$(Config)
78 | 1033
79 | (None)
80 |
81 |
82 | false
83 | RELEASE;$(DCC_Define)
84 | 0
85 | 0
86 |
87 |
88 |
89 | MainSource
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | Cfg_2
109 | Base
110 |
111 |
112 | Base
113 |
114 |
115 | Cfg_1
116 | Base
117 |
118 |
119 |
120 | Delphi.Personality.12
121 | Application
122 |
123 |
124 |
125 | LiquidTest.dpr
126 |
127 |
128 | Microsoft Office 2000 Sample Automation Server Wrapper Components
129 | Microsoft Office XP Sample Automation Server Wrapper Components
130 | File C:\Users\Arimateia\Delphi\Componentes\fortesreport-ce\Binary\LibD27\frce.bpl not found
131 |
132 |
133 |
134 |
135 |
136 | true
137 |
138 |
139 |
140 |
141 | true
142 |
143 |
144 |
145 |
146 | true
147 |
148 |
149 |
150 |
151 | LiquidTest.exe
152 | true
153 |
154 |
155 |
156 |
157 | 1
158 |
159 |
160 | Contents\MacOS
161 | 1
162 |
163 |
164 | 0
165 |
166 |
167 |
168 |
169 | classes
170 | 1
171 |
172 |
173 | classes
174 | 1
175 |
176 |
177 |
178 |
179 | res\xml
180 | 1
181 |
182 |
183 | res\xml
184 | 1
185 |
186 |
187 |
188 |
189 | library\lib\armeabi-v7a
190 | 1
191 |
192 |
193 |
194 |
195 | library\lib\armeabi
196 | 1
197 |
198 |
199 | library\lib\armeabi
200 | 1
201 |
202 |
203 |
204 |
205 | library\lib\armeabi-v7a
206 | 1
207 |
208 |
209 |
210 |
211 | library\lib\mips
212 | 1
213 |
214 |
215 | library\lib\mips
216 | 1
217 |
218 |
219 |
220 |
221 | library\lib\armeabi-v7a
222 | 1
223 |
224 |
225 | library\lib\arm64-v8a
226 | 1
227 |
228 |
229 |
230 |
231 | library\lib\armeabi-v7a
232 | 1
233 |
234 |
235 |
236 |
237 | res\drawable
238 | 1
239 |
240 |
241 | res\drawable
242 | 1
243 |
244 |
245 |
246 |
247 | res\values
248 | 1
249 |
250 |
251 | res\values
252 | 1
253 |
254 |
255 |
256 |
257 | res\values-v21
258 | 1
259 |
260 |
261 | res\values-v21
262 | 1
263 |
264 |
265 |
266 |
267 | res\values
268 | 1
269 |
270 |
271 | res\values
272 | 1
273 |
274 |
275 |
276 |
277 | res\drawable
278 | 1
279 |
280 |
281 | res\drawable
282 | 1
283 |
284 |
285 |
286 |
287 | res\drawable-xxhdpi
288 | 1
289 |
290 |
291 | res\drawable-xxhdpi
292 | 1
293 |
294 |
295 |
296 |
297 | res\drawable-xxxhdpi
298 | 1
299 |
300 |
301 | res\drawable-xxxhdpi
302 | 1
303 |
304 |
305 |
306 |
307 | res\drawable-ldpi
308 | 1
309 |
310 |
311 | res\drawable-ldpi
312 | 1
313 |
314 |
315 |
316 |
317 | res\drawable-mdpi
318 | 1
319 |
320 |
321 | res\drawable-mdpi
322 | 1
323 |
324 |
325 |
326 |
327 | res\drawable-hdpi
328 | 1
329 |
330 |
331 | res\drawable-hdpi
332 | 1
333 |
334 |
335 |
336 |
337 | res\drawable-xhdpi
338 | 1
339 |
340 |
341 | res\drawable-xhdpi
342 | 1
343 |
344 |
345 |
346 |
347 | res\drawable-mdpi
348 | 1
349 |
350 |
351 | res\drawable-mdpi
352 | 1
353 |
354 |
355 |
356 |
357 | res\drawable-hdpi
358 | 1
359 |
360 |
361 | res\drawable-hdpi
362 | 1
363 |
364 |
365 |
366 |
367 | res\drawable-xhdpi
368 | 1
369 |
370 |
371 | res\drawable-xhdpi
372 | 1
373 |
374 |
375 |
376 |
377 | res\drawable-xxhdpi
378 | 1
379 |
380 |
381 | res\drawable-xxhdpi
382 | 1
383 |
384 |
385 |
386 |
387 | res\drawable-xxxhdpi
388 | 1
389 |
390 |
391 | res\drawable-xxxhdpi
392 | 1
393 |
394 |
395 |
396 |
397 | res\drawable-small
398 | 1
399 |
400 |
401 | res\drawable-small
402 | 1
403 |
404 |
405 |
406 |
407 | res\drawable-normal
408 | 1
409 |
410 |
411 | res\drawable-normal
412 | 1
413 |
414 |
415 |
416 |
417 | res\drawable-large
418 | 1
419 |
420 |
421 | res\drawable-large
422 | 1
423 |
424 |
425 |
426 |
427 | res\drawable-xlarge
428 | 1
429 |
430 |
431 | res\drawable-xlarge
432 | 1
433 |
434 |
435 |
436 |
437 | res\values
438 | 1
439 |
440 |
441 | res\values
442 | 1
443 |
444 |
445 |
446 |
447 | 1
448 |
449 |
450 | Contents\MacOS
451 | 1
452 |
453 |
454 | 0
455 |
456 |
457 |
458 |
459 | Contents\MacOS
460 | 1
461 | .framework
462 |
463 |
464 | Contents\MacOS
465 | 1
466 | .framework
467 |
468 |
469 | 0
470 |
471 |
472 |
473 |
474 | 1
475 | .dylib
476 |
477 |
478 | 1
479 | .dylib
480 |
481 |
482 | 1
483 | .dylib
484 |
485 |
486 | Contents\MacOS
487 | 1
488 | .dylib
489 |
490 |
491 | Contents\MacOS
492 | 1
493 | .dylib
494 |
495 |
496 | 0
497 | .dll;.bpl
498 |
499 |
500 |
501 |
502 | 1
503 | .dylib
504 |
505 |
506 | 1
507 | .dylib
508 |
509 |
510 | 1
511 | .dylib
512 |
513 |
514 | Contents\MacOS
515 | 1
516 | .dylib
517 |
518 |
519 | Contents\MacOS
520 | 1
521 | .dylib
522 |
523 |
524 | 0
525 | .bpl
526 |
527 |
528 |
529 |
530 | 0
531 |
532 |
533 | 0
534 |
535 |
536 | 0
537 |
538 |
539 | 0
540 |
541 |
542 | 0
543 |
544 |
545 | Contents\Resources\StartUp\
546 | 0
547 |
548 |
549 | Contents\Resources\StartUp\
550 | 0
551 |
552 |
553 | 0
554 |
555 |
556 |
557 |
558 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
559 | 1
560 |
561 |
562 |
563 |
564 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
565 | 1
566 |
567 |
568 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
569 | 1
570 |
571 |
572 |
573 |
574 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
575 | 1
576 |
577 |
578 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
579 | 1
580 |
581 |
582 |
583 |
584 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
585 | 1
586 |
587 |
588 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
589 | 1
590 |
591 |
592 |
593 |
594 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
595 | 1
596 |
597 |
598 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
599 | 1
600 |
601 |
602 |
603 |
604 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
605 | 1
606 |
607 |
608 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
609 | 1
610 |
611 |
612 |
613 |
614 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
615 | 1
616 |
617 |
618 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
619 | 1
620 |
621 |
622 |
623 |
624 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
625 | 1
626 |
627 |
628 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
629 | 1
630 |
631 |
632 |
633 |
634 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
635 | 1
636 |
637 |
638 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
639 | 1
640 |
641 |
642 |
643 |
644 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
645 | 1
646 |
647 |
648 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
649 | 1
650 |
651 |
652 |
653 |
654 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
655 | 1
656 |
657 |
658 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
659 | 1
660 |
661 |
662 |
663 |
664 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
665 | 1
666 |
667 |
668 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
669 | 1
670 |
671 |
672 |
673 |
674 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
675 | 1
676 |
677 |
678 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
679 | 1
680 |
681 |
682 |
683 |
684 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
685 | 1
686 |
687 |
688 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset
689 | 1
690 |
691 |
692 |
693 |
694 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
695 | 1
696 |
697 |
698 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
699 | 1
700 |
701 |
702 |
703 |
704 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
705 | 1
706 |
707 |
708 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
709 | 1
710 |
711 |
712 |
713 |
714 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
715 | 1
716 |
717 |
718 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
719 | 1
720 |
721 |
722 |
723 |
724 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
725 | 1
726 |
727 |
728 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
729 | 1
730 |
731 |
732 |
733 |
734 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
735 | 1
736 |
737 |
738 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
739 | 1
740 |
741 |
742 |
743 |
744 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
745 | 1
746 |
747 |
748 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset
749 | 1
750 |
751 |
752 |
753 |
754 | 1
755 |
756 |
757 | 1
758 |
759 |
760 |
761 |
762 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF
763 | 1
764 |
765 |
766 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF
767 | 1
768 |
769 |
770 |
771 |
772 | ..\
773 | 1
774 |
775 |
776 | ..\
777 | 1
778 |
779 |
780 |
781 |
782 | 1
783 |
784 |
785 | 1
786 |
787 |
788 | 1
789 |
790 |
791 |
792 |
793 | ..\$(PROJECTNAME).launchscreen
794 | 64
795 |
796 |
797 | ..\$(PROJECTNAME).launchscreen
798 | 64
799 |
800 |
801 |
802 |
803 | 1
804 |
805 |
806 | 1
807 |
808 |
809 | 1
810 |
811 |
812 |
813 |
814 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF
815 | 1
816 |
817 |
818 |
819 |
820 | ..\
821 | 1
822 |
823 |
824 | ..\
825 | 1
826 |
827 |
828 |
829 |
830 | Contents
831 | 1
832 |
833 |
834 | Contents
835 | 1
836 |
837 |
838 |
839 |
840 | Contents\Resources
841 | 1
842 |
843 |
844 | Contents\Resources
845 | 1
846 |
847 |
848 |
849 |
850 | library\lib\armeabi-v7a
851 | 1
852 |
853 |
854 | library\lib\arm64-v8a
855 | 1
856 |
857 |
858 | 1
859 |
860 |
861 | 1
862 |
863 |
864 | 1
865 |
866 |
867 | 1
868 |
869 |
870 | Contents\MacOS
871 | 1
872 |
873 |
874 | Contents\MacOS
875 | 1
876 |
877 |
878 | 0
879 |
880 |
881 |
882 |
883 | library\lib\armeabi-v7a
884 | 1
885 |
886 |
887 |
888 |
889 | 1
890 |
891 |
892 | 1
893 |
894 |
895 |
896 |
897 | Assets
898 | 1
899 |
900 |
901 | Assets
902 | 1
903 |
904 |
905 |
906 |
907 | Assets
908 | 1
909 |
910 |
911 | Assets
912 | 1
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 | False
928 | True
929 | False
930 |
931 |
932 | DUnit / Delphi Win32
933 | GUI
934 |
935 |
936 |
937 |
938 | 12
939 |
940 |
941 |
942 |
943 |
944 |
--------------------------------------------------------------------------------
/tests/LiquidTest.res:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arimateia/liquid-delphi/0d0a6ea26fe6b7fb4e0431ae08cd256e6f640fe2/tests/LiquidTest.res
--------------------------------------------------------------------------------
/tests/TestLiquid.pas:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arimateia/liquid-delphi/0d0a6ea26fe6b7fb4e0431ae08cd256e6f640fe2/tests/TestLiquid.pas
--------------------------------------------------------------------------------
/tests/templates/template1.html:
--------------------------------------------------------------------------------
1 |
2 | {{html.title}}
3 |
4 |
99 |
100 |
145 |
146 |
161 |
162 |
163 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | |
228 |
229 |
230 |
231 |
233 |
234 | |