├── .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 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
  230 | 231 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
{{logo.alt}}
249 | 250 |
251 | 252 | 253 | 254 |
255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 341 | 342 | 343 | 344 | 345 | 346 |
263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 279 | 280 | 281 | 282 | 283 | 284 | 317 | 318 | 319 | 320 | 321 | 322 | 333 | 334 | 335 | 336 | 337 | 338 |
271 | 272 |

{{content.title}}
273 |
274 | 275 |

276 | {{content.message}} 277 |

278 |

285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 309 | 310 | 311 | 312 | 313 | 314 |
293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 |
{{button.text}}
307 | 308 |
315 | 316 |
323 |
324 | 325 | 326 | 327 |
328 | 329 |

330 | {{content.footer}} 331 |

332 |
339 | 340 |
347 | 348 |
349 | 350 | 351 | 352 | 380 | 381 |
382 | 383 |
 
392 | 393 | 394 | -------------------------------------------------------------------------------- /tests/templates/template1_rendered.html: -------------------------------------------------------------------------------- 1 |  2 | Email Title 3 | 4 | 99 | 100 | 145 | 146 | 161 | 162 | 163 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
  230 | 231 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
Logo alt
249 | 250 |
251 | 252 | 253 | 254 |
255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 341 | 342 | 343 | 344 | 345 | 346 |
263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 279 | 280 | 281 | 282 | 283 | 284 | 317 | 318 | 319 | 320 | 321 | 322 | 333 | 334 | 335 | 336 | 337 | 338 |
271 | 272 |

Content title
273 |
274 | 275 |

276 | Content message 277 |

278 |

285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 309 | 310 | 311 | 312 | 313 | 314 |
293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 |
Go!
307 | 308 |
315 | 316 |
323 |
324 | 325 | 326 | 327 |
328 | 329 |

330 | Have a nice day :-) 331 |

332 |
339 | 340 |
347 | 348 |
349 | 350 | 351 | 352 | 380 | 381 |
382 | 383 |
 
392 | 393 | 394 | --------------------------------------------------------------------------------