├── .github └── workflows │ └── publish-wiki.yml ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── build.yaml ├── doc ├── template │ ├── CHANGELOG.md.template │ ├── LICENSE.template │ ├── README.md.template │ ├── doc │ │ └── wiki │ │ │ ├── 01-Template-Engine.md.template │ │ │ ├── 02-Getting-Started.md.template │ │ │ ├── 03-Usage.md.template │ │ │ ├── 04-Tags.md.template │ │ │ ├── 05-Data-types-in-tag-expressions.md.template │ │ │ ├── 06-Constants-in-tag-expressions.md.template │ │ │ ├── 07-Variables-in-tag-expressions.md.template │ │ │ ├── 08-Functions-in-tag-expressions.md.template │ │ │ ├── 09-Operators-in-tag-expressions.md.template │ │ │ ├── 10-Examples.md.template │ │ │ ├── Home.md.template │ │ │ ├── template_engine.png.template │ │ │ ├── template_engine.xcf │ │ │ └── template_engine_original.png │ └── example │ │ └── example.md.template └── wiki │ ├── 01-Template-Engine.md │ ├── 02-Getting-Started.md │ ├── 03-Usage.md │ ├── 04-Tags.md │ ├── 05-Data-types-in-tag-expressions.md │ ├── 06-Constants-in-tag-expressions.md │ ├── 07-Variables-in-tag-expressions.md │ ├── 08-Functions-in-tag-expressions.md │ ├── 09-Operators-in-tag-expressions.md │ ├── 10-Examples.md │ ├── Home.md │ └── template_engine.png ├── example └── example.md ├── lib ├── src │ ├── parser │ │ ├── error.dart │ │ ├── error_parser.dart │ │ ├── override_message_parser.dart │ │ ├── parser.dart │ │ ├── tag │ │ │ ├── expression │ │ │ │ ├── constant │ │ │ │ │ └── constant.dart │ │ │ │ ├── data_type │ │ │ │ │ └── data_type.dart │ │ │ │ ├── expression.dart │ │ │ │ ├── expression_parser.dart │ │ │ │ ├── function │ │ │ │ │ ├── default.dart │ │ │ │ │ ├── documentation.dart │ │ │ │ │ ├── function.dart │ │ │ │ │ ├── import.dart │ │ │ │ │ ├── math.dart │ │ │ │ │ ├── path.dart │ │ │ │ │ └── string.dart │ │ │ │ ├── identifier.dart │ │ │ │ ├── operator │ │ │ │ │ ├── addition.dart │ │ │ │ │ ├── assignment.dart │ │ │ │ │ ├── comparison.dart │ │ │ │ │ ├── multiplication.dart │ │ │ │ │ ├── operator.dart │ │ │ │ │ ├── parentheses.dart │ │ │ │ │ └── prefixes.dart │ │ │ │ ├── tag_expression_parser.dart │ │ │ │ └── variable │ │ │ │ │ └── variable.dart │ │ │ ├── group.dart │ │ │ └── tag.dart │ │ └── value_context_map_parser.dart │ ├── project_file_path.dart │ ├── render.dart │ ├── source.dart │ ├── template.dart │ └── template_engine.dart └── template_engine.dart ├── pubspec.yaml └── test └── src ├── hello.template ├── hello_name.template ├── parser ├── error_parser_test.dart ├── parser_test.dart └── tag │ ├── expression │ ├── constant │ │ ├── constant_name_test.dart │ │ ├── custom_constant_test.dart │ │ ├── e_test.dart │ │ ├── ln10_test.dart │ │ ├── ln2_test.dart │ │ ├── log10e_test.dart │ │ ├── log2e_test.dart │ │ └── pi_test.dart │ ├── data_type │ │ ├── bool_test.dart │ │ ├── custom_data_type_test.dart │ │ ├── num_test.dart │ │ └── string_test.dart │ ├── expression_parser_test.dart │ ├── function │ │ ├── custom_function_test.dart │ │ ├── documentation_test.dart │ │ ├── function_test.dart │ │ ├── import │ │ │ ├── import.md.template │ │ │ ├── import_json_test.dart │ │ │ ├── import_pure_test.dart │ │ │ ├── import_template_test.dart │ │ │ ├── import_template_with_errors1.md.template │ │ │ ├── import_template_with_errors2.md.template │ │ │ ├── import_template_with_errors3.md.template │ │ │ ├── import_template_with_errors_test.dart │ │ │ ├── import_xml_test.dart │ │ │ ├── import_yaml_test.dart │ │ │ ├── multiple_import_test.dart │ │ │ ├── person.json │ │ │ ├── person.xml │ │ │ ├── person.yaml │ │ │ └── to_import.md.template │ │ ├── math │ │ │ ├── acos_test.dart │ │ │ ├── asin_test.dart │ │ │ ├── atan_test.dart │ │ │ ├── cos_test.dart │ │ │ ├── exp_test.dart │ │ │ ├── log_test.dart │ │ │ ├── round_test.dart │ │ │ ├── sin_test.dart │ │ │ ├── sqrt_test.dart │ │ │ └── tan_test.dart │ │ ├── string │ │ │ └── length_test.dart │ │ └── template │ │ │ ├── source_test.dart │ │ │ └── template_test.dart │ ├── identifier_test.dart │ ├── operator │ │ ├── addition │ │ │ ├── bool_or_test.dart │ │ │ ├── num_addition_test.dart │ │ │ ├── num_subtract_test.dart │ │ │ └── string_concatenate_test.dart │ │ ├── assignment │ │ │ └── assignment_test.dart │ │ ├── comparison │ │ │ ├── equals_test.dart │ │ │ ├── greater_than_or_equal_test.dart │ │ │ ├── greater_than_test.dart │ │ │ ├── less_than_or_equal_test.dart │ │ │ ├── less_than_test.dart │ │ │ └── not_equals_test.dart │ │ ├── custom_operator_test.dart │ │ ├── multiplication │ │ │ ├── bool_and_test.dart │ │ │ ├── bool_xor_test.dart │ │ │ ├── num_divide_test.dart │ │ │ ├── num_modulo_test.dart │ │ │ ├── num_multiply_test.dart │ │ │ ├── num_power_test.dart │ │ │ └── string_concatenate_test.dart │ │ ├── operator_test.dart │ │ ├── parentheses_test.dart │ │ └── prefix │ │ │ ├── negative_test.dart │ │ │ ├── not_test.dart │ │ │ └── positive_test.dart │ ├── tag_expression_parser_test.dart │ └── variable │ │ ├── nested_variable_test.dart │ │ ├── unknown_variable_test.dart │ │ ├── variable_extra_test.dart │ │ └── variable_test.dart │ └── tag_test.dart ├── project_file_path_test.dart ├── template_engine_template_error_test.dart ├── template_engine_template_example_test.dart ├── template_engine_template_test.dart ├── template_engine_template_type_test.dart ├── template_engine_templates_error_test.dart ├── template_engine_templates_test.dart ├── template_engine_test.dart ├── template_engine_text_error_test.dart └── template_engine_text_test.dart /.github/workflows/publish-wiki.yml: -------------------------------------------------------------------------------- 1 | name: Publish Wiki 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - doc/wiki/** 8 | - .github/workflows/publish-wiki.yml 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | publish-wiki: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout main repository 18 | uses: actions/checkout@v3 19 | - name: Set up Git 20 | run: | 21 | git config --global user.name "github-actions[bot]" 22 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 23 | - name: Clone the wiki repository 24 | run: | 25 | git clone "https://github.com/domain-centric/template_engine.wiki.git" /tmp/wiki 26 | - name: Remove existing files in wiki 27 | run: | 28 | rm -rf /tmp/wiki/* 29 | - name: Copy wiki files 30 | run: | 31 | cp -r doc/wiki/* /tmp/wiki/ 32 | - name: Commit and push changes to wiki 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: | 36 | cd /tmp/wiki 37 | git config user.name "github-actions[bot]" 38 | git config user.email "github-actions[bot]@users.noreply.github.com" 39 | git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.wiki.git 40 | git add . 41 | git commit -m "Update wiki from main repo [skip ci]" || echo "No changes to commit" 42 | git push origin HEAD:master 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 84a1e904f44f9b0e9c4510138010edcc653163f8 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart (tool\\generate_documentation.dart)", 9 | "type": "dart", 10 | "request": "launch", 11 | "program": "tool\\generate_documentation.dart" 12 | }, 13 | { 14 | "name": "All Tests", 15 | "type": "dart", 16 | "request": "launch", 17 | "program": "test/", // or tests/foo for a subset of tests 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "errno", 4 | "fals", 5 | "lder", 6 | "nilsth", 7 | "petitparser", 8 | "recase", 9 | "shouldly", 10 | "sublist", 11 | "trueparameter", 12 | "webflow" 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/CHANGELOG.md.template using the documentation_builder package) 2 | ## [0.0.1 Milestone](https://github.com/domain-centric/template_engine/milestone/1?closed=1) 3 | 4 | ## [0.0.2 Milestone](https://github.com/domain-centric/template_engine/milestone/2?closed=1) 5 | 6 | ## [1.0.0 Milestone](https://github.com/domain-centric/template_engine/milestone/3?closed=1) 7 | 8 | ## [1.0.1 Milestone](https://github.com/domain-centric/template_engine/milestone/4?closed=1) 9 | 10 | ## [1.1.0 Milestone](https://github.com/domain-centric/template_engine/milestone/5?closed=1) 11 | 12 | ## [1.2.0 Milestone](https://github.com/domain-centric/template_engine/milestone/6?closed=1) 13 | 14 | ## [1.3.0 Milestone](https://github.com/domain-centric/template_engine/milestone/7?closed=1) 15 | 16 | ## [1.4.1 Milestone](https://github.com/domain-centric/template_engine/milestone/9?closed=1) 17 | 18 | ## [1.5.0 Milestone](https://github.com/domain-centric/template_engine/milestone/10?closed=1) 19 | 20 | ## [1.5.1 Milestone](https://github.com/domain-centric/template_engine/milestone/11?closed=1) 21 | 22 | ## [1.5.2 Milestone](https://github.com/domain-centric/template_engine/milestone/12?closed=1) 23 | 24 | ## [1.4.0 Milestone](https://github.com/domain-centric/template_engine/milestone/8?closed=1) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Nils ten Hoeve 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/README.md.template using the documentation_builder package) 2 | [![Pub Package](https://img.shields.io/pub/v/template_engine)](https://pub.dev/packages/template_engine) 3 | [![Project on github.com](https://img.shields.io/badge/repository-git%20hub-informational)](https://github.com/domain-centric/template_engine) 4 | [![Project Wiki pages on github.com](https://img.shields.io/badge/documentation-wiki-informational)](https://github.com/domain-centric/template_engine/wiki) 5 | [![Pub Scores](https://img.shields.io/pub/likes/template_engine)](https://pub.dev/packages/template_engine/score) 6 | [![Stars ranking on github.com](https://img.shields.io/github/stars/domain-centric/template_engine?style=flat)](https://github.com/domain-centric/template_engine/stargazers) 7 | [![Open issues on github.com](https://img.shields.io/github/issues/domain-centric/template_engine)](https://github.com/domain-centric/template_engine/issues) 8 | [![Open pull requests on github.com](https://img.shields.io/github/issues-pr/domain-centric/template_engine)](https://github.com/domain-centric/template_engine/pulls) 9 | [![Project License](https://img.shields.io/github/license/domain-centric/template_engine)](https://github.com/domain-centric/template_engine/blob/main/LICENSE) 10 | 11 | ![](https://github.com/domain-centric/template_engine/wiki/template_engine.png) 12 | 13 | The [TemplateEngine](https://github.com/domain-centric/template_engine/blob/c4d91128cc587bd1fa84a5ed5b293f6a6e56e3a1/lib/src/template_engine.dart#L4) can: 14 | * Parse the [Template](https://pub.dev/packages/Template) text into a 15 | [parser tree](https://en.wikipedia.org/wiki/Parse_tree) 16 | * Render the [parser tree](https://en.wikipedia.org/wiki/Parse_tree) 17 | to a output such as: 18 | * [Html](https://en.wikipedia.org/wiki/HTML) 19 | * [Programming code](https://en.wikipedia.org/wiki/Programming_language) 20 | * [Markdown](https://en.wikipedia.org/wiki/Markdown) 21 | * [Xml](https://en.wikipedia.org/wiki/XML), 22 | * [Json](https://en.wikipedia.org/wiki/JSON), 23 | * [Yaml](https://en.wikipedia.org/wiki/YAML) 24 | * Etc... 25 | 26 | # Features 27 | * Template expressions that can contain (combinations of): 28 | * Data types 29 | * Constants 30 | * Variables 31 | * Operators 32 | * Functions 33 | e.g. functions to import: 34 | * Pure files (to be imported as is) 35 | * Template files (to be parsed and rendered) 36 | * XML files (to be used as a data source) 37 | * JSON files (to be used as a data source) 38 | * YAML files (to be used as a data source) 39 | * All of the above can be customized or you could add your own. 40 | 41 | # Getting Started 42 | To get started: 43 | * [Read the wiki documentation](https://github.com/domain-centric/template_engine/wiki) 44 | * [See the examples](https://github.com/domain-centric/template_engine/wiki/10%20Examples) 45 | * [Install the template_engine package in your project](https://pub.dev/packages/template_engine/install) 46 | * Start coding 47 | 48 | # Usage 49 | A typical way to use the [template_engine](https://pub.dev/packages/template_engine): 50 | 51 | `test/src/template_engine_template_example_test.dart` 52 | ```dart 53 | import 'package:shouldly/shouldly.dart'; 54 | import 'package:template_engine/template_engine.dart'; 55 | import 'package:test/test.dart'; 56 | 57 | Future main() async { 58 | test('example should work', () async { 59 | var engine = TemplateEngine(); 60 | var parseResult = await engine.parseText('Hello {{name}}.'); 61 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 62 | renderResult.text.should.be('Hello world.'); 63 | }); 64 | } 65 | 66 | ``` 67 | 68 | 69 | For more see: [Examples](https://pub.dev/packages/template_engine/example) 70 | 71 | # Documentation 72 | For more information read the [template_engine wiki](https://github.com/domain-centric/template_engine/wiki) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | # Additional information about this file can be found at 3 | # https://dart.dev/guides/language/analysis-options 4 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | - doc/** 5 | - lib/** 6 | - bin/** 7 | - test/** 8 | - pubspec.* 9 | - $package$ 10 | builders: 11 | documentation_builder|documentation_builder: 12 | enabled: True -------------------------------------------------------------------------------- /doc/template/CHANGELOG.md.template: -------------------------------------------------------------------------------- 1 | {{gitHubMileStones()}} -------------------------------------------------------------------------------- /doc/template/LICENSE.template: -------------------------------------------------------------------------------- 1 | {{license(type='BSD3', name='Nils ten Hoeve')}} -------------------------------------------------------------------------------- /doc/template/README.md.template: -------------------------------------------------------------------------------- 1 | {{allPubGitHubBadges()}} 2 | 3 | {{importTemplate('doc/template/doc/wiki/01-Template-Engine.md.template')}} 4 | 5 | # Getting Started 6 | {{importTemplate('doc/template/doc/wiki/02-Getting-Started.md.template')}} 7 | 8 | # Usage 9 | {{importTemplate('doc/template/doc/wiki/03-Usage.md.template')}} 10 | 11 | # Documentation 12 | For more information read the {{gitHubWikiLink()}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/01-Template-Engine.md.template: -------------------------------------------------------------------------------- 1 | ![]({{gitHubWikiUri('/template_engine.png')}}) 2 | 3 | {{importDartDoc('lib/src/template_engine.dart#TemplateEngine')}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/02-Getting-Started.md.template: -------------------------------------------------------------------------------- 1 | To get started: 2 | * {{gitHubWikiLink(text='Read the wiki documentation')}} 3 | * {{gitHubWikiLink(text='See the examples', suffix='/10 Examples')}} 4 | * {{pubDevInstallLink(text='Install the template_engine package in your project')}} 5 | * Start coding -------------------------------------------------------------------------------- /doc/template/doc/wiki/03-Usage.md.template: -------------------------------------------------------------------------------- 1 | A typical way to use the {{referenceLink('template_engine')}}: 2 | 3 | {{importDartCode('test/src/template_engine_template_example_test.dart')}} 4 | 5 | For more see: [Examples](https://pub.dev/packages/template_engine/example) -------------------------------------------------------------------------------- /doc/template/doc/wiki/04-Tags.md.template: -------------------------------------------------------------------------------- 1 | {{tagDocumentation(2)}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/05-Data-types-in-tag-expressions.md.template: -------------------------------------------------------------------------------- 1 | {{dataTypeDocumentation(2)}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/06-Constants-in-tag-expressions.md.template: -------------------------------------------------------------------------------- 1 | {{constantDocumentation(2)}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/07-Variables-in-tag-expressions.md.template: -------------------------------------------------------------------------------- 1 | {{variableDocumentation()}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/08-Functions-in-tag-expressions.md.template: -------------------------------------------------------------------------------- 1 | {{functionDocumentation(2)}} 2 | -------------------------------------------------------------------------------- /doc/template/doc/wiki/09-Operators-in-tag-expressions.md.template: -------------------------------------------------------------------------------- 1 | {{operatorDocumentation(2)}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/10-Examples.md.template: -------------------------------------------------------------------------------- 1 | {{importTemplate('doc/template/example/example.md.template')}} 2 | 3 | -------------------------------------------------------------------------------- /doc/template/doc/wiki/Home.md.template: -------------------------------------------------------------------------------- 1 | {{allPubGitHubBadges()}} 2 | 3 | **Table of contents** 4 | {{tableOfContents(path='doc/template/doc/wiki', gitHubWiki=true)}} -------------------------------------------------------------------------------- /doc/template/doc/wiki/template_engine.png.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domain-centric/template_engine/13ea7f6c8d7dfdb4e7c8e7094aed6cd10ee7864e/doc/template/doc/wiki/template_engine.png.template -------------------------------------------------------------------------------- /doc/template/doc/wiki/template_engine.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domain-centric/template_engine/13ea7f6c8d7dfdb4e7c8e7094aed6cd10ee7864e/doc/template/doc/wiki/template_engine.xcf -------------------------------------------------------------------------------- /doc/template/doc/wiki/template_engine_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domain-centric/template_engine/13ea7f6c8d7dfdb4e7c8e7094aed6cd10ee7864e/doc/template/doc/wiki/template_engine_original.png -------------------------------------------------------------------------------- /doc/template/example/example.md.template: -------------------------------------------------------------------------------- 1 | # TemplateEngine usage 2 | * {{gitHubRawLink('/test/src/template_engine_text_test.dart','template_engine_text_test.dart')}} 3 | * {{gitHubRawLink('/test/src/template_engine_template_test.dart','template_engine_template_test.dart')}} 4 | * {{gitHubRawLink('/test/src/template_engine_templates_test.dart','template_engine_templates_test.dart')}} 5 | * {{gitHubRawLink('/test/src/template_engine_template_type_test.dart','template_engine_template_type_test.dart')}} 6 | 7 | {{exampleDocumentation()}} 8 | 9 | # Documentation generation 10 | The documentation of the {{referenceLink('template_engine')}} package was generated by the {{referenceLink('documentation_builder')}} package. 11 | {{referenceLink('documentation_builder')}} is build on top of the {{referenceLink('template_engine')}} package. Here are some examples: 12 | ### README.md 13 | * {{referenceLink('doc/template/README.md.template', 'Template example')}} 14 | * {{gitHubRawLink('/README.md', 'Generated example')}} 15 | * {{pubDevLink(text='Rendered example')}} 16 | 17 | ### LICENSE 18 | * {{referenceLink('doc/template/LICENSE.template', 'Template example')}} 19 | * {{gitHubRawLink('/LICENSE', 'Generated example')}} 20 | 21 | ### CHANGELOG.md 22 | * {{referenceLink('doc/template/CHANGELOG.md.template', 'Template example')}} 23 | * {{gitHubRawLink('/CHANGELOG.md', 'Generated example')}} 24 | * {{pubDevChangeLogLink(text='Rendered example')}} 25 | 26 | ### example.md 27 | * {{referenceLink('doc/template/example/example.md.template', 'Template example')}} 28 | * {{gitHubRawLink('/example/example.md', 'Generated example')}} 29 | * {{pubDevExampleLink(text='Rendered example')}} 30 | 31 | ### Wiki pages 32 | * {{referenceLink('doc/template/doc/wiki', 'Template examples')}} (click on the raw button to see the generated markdown) 33 | * {{gitHubWikiLink(text='Rendered examples')}} -------------------------------------------------------------------------------- /doc/wiki/01-Template-Engine.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/01-Template-Engine.md.template using the documentation_builder package) 2 | ![](https://github.com/domain-centric/template_engine/wiki/template_engine.png) 3 | 4 | The [TemplateEngine](https://github.com/domain-centric/template_engine/blob/db47eea4b9cddc4c13b9447260877484fd66ab04/lib/src/template_engine.dart#L4) can: 5 | * Parse the [Template](https://pub.dev/packages/Template) text into a 6 | [parser tree](https://en.wikipedia.org/wiki/Parse_tree) 7 | * Render the [parser tree](https://en.wikipedia.org/wiki/Parse_tree) 8 | to a output such as: 9 | * [Html](https://en.wikipedia.org/wiki/HTML) 10 | * [Programming code](https://en.wikipedia.org/wiki/Programming_language) 11 | * [Markdown](https://en.wikipedia.org/wiki/Markdown) 12 | * [Xml](https://en.wikipedia.org/wiki/XML), 13 | * [Json](https://en.wikipedia.org/wiki/JSON), 14 | * [Yaml](https://en.wikipedia.org/wiki/YAML) 15 | * Etc... 16 | 17 | # Features 18 | * Template expressions that can contain (combinations of): 19 | * Data types 20 | * Constants 21 | * Variables 22 | * Operators 23 | * Functions 24 | e.g. functions to import: 25 | * Pure files (to be imported as is) 26 | * Template files (to be parsed and rendered) 27 | * XML files (to be used as a data source) 28 | * JSON files (to be used as a data source) 29 | * YAML files (to be used as a data source) 30 | * All of the above can be customized or you could add your own. -------------------------------------------------------------------------------- /doc/wiki/02-Getting-Started.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/02-Getting-Started.md.template using the documentation_builder package) 2 | To get started: 3 | * [Read the wiki documentation](https://github.com/domain-centric/template_engine/wiki) 4 | * [See the examples](https://github.com/domain-centric/template_engine/wiki/10%20Examples) 5 | * [Install the template_engine package in your project](https://pub.dev/packages/template_engine/install) 6 | * Start coding -------------------------------------------------------------------------------- /doc/wiki/03-Usage.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/03-Usage.md.template using the documentation_builder package) 2 | A typical way to use the [template_engine](https://pub.dev/packages/template_engine): 3 | 4 | `test/src/template_engine_template_example_test.dart` 5 | ```dart 6 | import 'package:shouldly/shouldly.dart'; 7 | import 'package:template_engine/template_engine.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | Future main() async { 11 | test('example should work', () async { 12 | var engine = TemplateEngine(); 13 | var parseResult = await engine.parseText('Hello {{name}}.'); 14 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 15 | renderResult.text.should.be('Hello world.'); 16 | }); 17 | } 18 | 19 | ``` 20 | 21 | 22 | For more see: [Examples](https://pub.dev/packages/template_engine/example) -------------------------------------------------------------------------------- /doc/wiki/04-Tags.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/04-Tags.md.template using the documentation_builder package) 2 | Tags are specific texts in templates that are replaced by the [template_engine](https://pub.dev/packages/template_engine) with other information. 3 | 4 | A tag: 5 | * Starts with some bracket and/or character combination, e.g.: {{ 6 | * Followed by some contents 7 | * Ends with some closing bracket and/or character combination, e.g.: }} 8 | 9 | A tag example: {{customer.name}} 10 | 11 | By default the TemplateEngine tags start with {{ and end with }} brackets, 12 | just like the popular template engines 13 | [Mustache](https://mustache.github.io/) and 14 | [Handlebars](https://handlebarsjs.com). 15 | 16 | You can also define alternative Tag brackets in the TemplateEngine 17 | constructor parameters. See TemplateEngine.tagStart and 18 | TemplateEngine.tagEnd 19 | 20 | It is recommended to use a start and end combination that is not used 21 | elsewhere in your templates, e.g.: Do not use < > as Tag start and end 22 | if your template contains HTML or XML 23 | 24 | ## Custom tags 25 | The [template_engine](https://pub.dev/packages/template_engine) comes with DefaultTags. You can replace or add your 26 | own Tags by manipulating the the TemplateEngine.tags field. 27 | 28 | ## Available tags 29 | ### Expression Tag 30 | 31 | 32 | 33 | 34 |
description:Evaluates an expression that can contain:
* Data Types (e.g. boolean, number or String)
* Constants (e.g. pi)
* Variables (e.g. person.name )
* Operators (e.g. + - * /)
* Functions (e.g. cos(7) )
* or any combination of the above
expression example:The volume of a sphere = {{ round( (3/4) * pi * (radius ^ 3) )}}.
code example:tag_expression_parser_test.dart
35 | -------------------------------------------------------------------------------- /doc/wiki/05-Data-types-in-tag-expressions.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/05-Data-types-in-tag-expressions.md.template using the documentation_builder package) 2 | A [data type](https://en.wikipedia.org/wiki/Data_type) defines what the 3 | possible values an expression, such as a variable, operator 4 | or a function call, might use. 5 | 6 | The [template_engine](https://pub.dev/packages/template_engine) supports several default DataTypes. 7 | 8 | ## Custom DataTypes 9 | You can adopt existing DataTypes or add your own custom DataTypes by 10 | manipulating the TemplateEngine.dataTypes field. 11 | See [example](https://github.com/domain-centric/template_engine/blob/main/test/src/parser/tag/expression/data_type/custom_data_type_test.dart) 12 | 13 | ## Available Data Types 14 | ### String Data Type 15 | 16 | 17 | 18 | 19 |
description:A form of data containing a sequence of characters
syntax:A string is declared with a chain of characters, surrounded by two single (') or double (") quotes to indicate the start and end of a string. In example: 'Hello' or "Hello"
example:string_test.dart
20 | 21 | ### Number Data Type 22 | 23 | 24 | 25 | 26 |
description:A form of data to express the size of something.
syntax:A number is declared with:
* optional: positive (e.g. +12) or negative (e.g. -12) prefix or no prefix (12=positive)
* one or more digits (e.g. 12)
* optional fragments (e.g. 0.12)
* optional: scientific notation: the letter E is used to mean"10 to the power of." (e.g. 1.314E+1 means 1.314 * 10^1which is 13.14).
example:num_test.dart
27 | 28 | ### Boolean Data Type 29 | 30 | 31 | 32 | 33 |
description:A form of data with only two possible values: true or false
syntax:A boolean is declared with the word true or false. The letters are case insensitive.
example:bool_test.dart
34 | -------------------------------------------------------------------------------- /doc/wiki/06-Constants-in-tag-expressions.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/06-Constants-in-tag-expressions.md.template using the documentation_builder package) 2 | A [Constant](https://en.wikipedia.org/wiki/Constant_(computer_programming)) is a value that does not change value over time. 3 | 4 | The [template_engine](https://pub.dev/packages/template_engine) comes with several mathematical constants. 5 | 6 | ## Custom Constants 7 | You can create and add your own Constants by 8 | manipulating the TemplateEngine.constants field. 9 | See [Example](https://github.com/domain-centric/template_engine/blob/main/test/src/parser/tag/expression/constant/custom_constant_test.dart) 10 | 11 | ## Available Constants 12 | ### e Constant 13 | 14 | 15 | 16 | 17 |
description:Base of the natural logarithms.
return type:double
code example:e_test.dart
18 | 19 | ### ln10 Constant 20 | 21 | 22 | 23 | 24 |
description:Natural logarithm of 10.
return type:double
code example:ln10_test.dart
25 | 26 | ### ln2 Constant 27 | 28 | 29 | 30 | 31 |
description:Natural logarithm of 2.
return type:double
code example:ln2_test.dart
32 | 33 | ### log10e Constant 34 | 35 | 36 | 37 | 38 |
description:Base-10 logarithm of e.
return type:double
code example:log10e_test.dart
39 | 40 | ### log2e Constant 41 | 42 | 43 | 44 | 45 |
description:Base-2 logarithm of e.
return type:double
code example:log2e_test.dart
46 | 47 | ### pi Constant 48 | 49 | 50 | 51 | 52 |
description:The ratio of a circle's circumference to its diameter
return type:double
code example:pi_test.dart
53 | -------------------------------------------------------------------------------- /doc/wiki/07-Variables-in-tag-expressions.md: -------------------------------------------------------------------------------- 1 | [//]: # (This file was generated from: doc/template/doc/wiki/07-Variables-in-tag-expressions.md.template using the documentation_builder package) 2 | A [variable](https://en.wikipedia.org/wiki/Variable_(computer_science)) is 3 | a named container for some type of information 4 | (like a [number](https://en.wikipedia.org/wiki/Double-precision_floating-point_format), [boolean](https://en.wikipedia.org/wiki/Boolean_data_type), [string](https://en.wikipedia.org/wiki/String_(computer_science)), etc...) 5 | 6 | * Variables are stored as key, value pairs in a dart Map where: 7 | * String=Variable name 8 | * dynamic=Variable value 9 | * Variables can be used in an ExpressionTag 10 | * Initial variable values are passed to the TemplateEngine.render method 11 | * Variables can be modified during rendering 12 | 13 | The [variable name](https://en.wikipedia.org/wiki/Variable_(computer_science)) 14 | identifies the variable and corresponds with the keys 15 | in the variable map. 16 | 17 | The [variable names](https://en.wikipedia.org/wiki/Variable_(computer_science)): 18 | * are [case sensitive](https://en.wikipedia.org/wiki/Case_sensitivity) 19 | * must start with a lower case letter, optionally followed by (lower or upper) letters and or digits. 20 | * conventions: use [lowerCamelCase](https://en.wikipedia.org/wiki/Camel_case) 21 | * must be unique and does not match a other tag syntax 22 | 23 | Variables can be nested. Concatenate [variable names](https://en.wikipedia.org/wiki/Variable_(computer_science)) separated with dot's 24 | to get the variable value of a nested [variable name](https://en.wikipedia.org/wiki/Variable_(computer_science)): 25 | E.g.:
26 | Variable map: {'person': {'name': 'John Doe', 'age',30}}
27 | Variable name person.name: refers to the variable value of 'John Doe' 28 | 29 | Examples: 30 | * [Variable Example](https://github.com/domain-centric/template_engine/blob/main/test/src/parser/tag/expression/variable/variable_test.dart) 31 | * [Nested Variable Example](https://github.com/domain-centric/template_engine/blob/main/test/src/parser/tag/expression/variable/nested_variable_test.dart) -------------------------------------------------------------------------------- /doc/wiki/template_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domain-centric/template_engine/13ea7f6c8d7dfdb4e7c8e7094aed6cd10ee7864e/doc/wiki/template_engine.png -------------------------------------------------------------------------------- /lib/src/parser/error.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:template_engine/src/template.dart'; 3 | 4 | /// Parser or Render error or warnings are collected as [TemplateError]s instead 5 | /// of being thrown so that the parser and rendering process can continue. 6 | /// This way the caller can see and fix all [TemplateError]s without having to fix and 7 | /// recall the [TemplateEngine.parse] and [TemplateEngine.render] methods 8 | /// for each [TemplateError] 9 | 10 | abstract class TemplateError implements Exception { 11 | String toIndentedString(int indent); 12 | 13 | String indentation(int indent) => ' ' * indent; 14 | 15 | @override 16 | String toString() => toIndentedString(0); 17 | } 18 | 19 | class RenderError extends TemplateError { 20 | final String message; 21 | 22 | /// A cursor position within the [Template.text] in format row, column 23 | final String position; 24 | 25 | RenderError({required this.message, required this.position}); 26 | 27 | @override 28 | String toIndentedString(int indent) => 29 | '${indentation(indent)}$position: $message'; 30 | } 31 | 32 | class ParseError extends TemplateError { 33 | final String message; 34 | 35 | /// A cursor position within the [Template.text] in format row, column 36 | final String position; 37 | 38 | ParseError({required this.message, required this.position}); 39 | 40 | ParseError.fromFailure(Failure failure) 41 | : message = failure.message, 42 | position = failure.toPositionString(); 43 | 44 | @override 45 | String toIndentedString(int indent) => 46 | '${indentation(indent)}$position: $message'; 47 | } 48 | 49 | class ImportError extends TemplateError { 50 | final Template template; 51 | final String positionOfImport; 52 | final List importErrors; 53 | ImportError(this.positionOfImport, this.template, this.importErrors); 54 | 55 | @override 56 | String toIndentedString(int indent) => 57 | '${indentation(indent)}$positionOfImport: ' 58 | 'Error${importErrors.length > 1 ? 's' : ''} ' 59 | 'while importing ${template.sourceTitle}:\n' 60 | '${importErrors.map((e) => e.toIndentedString(indent + 1)).join('\n')}'; 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/parser/error_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | 4 | /// This [Parser] looks for any remaining tags that where not recognized 5 | /// by the [Tag][Parser]s. 6 | /// It will return this unknown tag as a [String] and add detailed error(s) 7 | /// to the [ParserContext]. 8 | class InvalidTagParser extends Parser { 9 | final ParserContext parserContext; 10 | final Parser tagStartParser; 11 | final Parser anythingBeforeEndParser; 12 | final Parser tagEndParser; 13 | InvalidTagParser(this.parserContext) 14 | : tagStartParser = string(parserContext.engine.tagStart), 15 | anythingBeforeEndParser = untilEndOfTagParser( 16 | parserContext.engine.tagStart, 17 | parserContext.engine.tagEnd, 18 | ), 19 | tagEndParser = string(parserContext.engine.tagEnd); 20 | 21 | @override 22 | Parser copy() => InvalidTagParser(parserContext); 23 | 24 | /// see dart doc if this class. 25 | @override 26 | Result parseOn(Context context) { 27 | var errors = []; 28 | var tagStartResult = tagStartParser.parseOn(context); 29 | if (tagStartResult is Failure) { 30 | return tagStartResult; 31 | } 32 | var expressionResult = expressionParser( 33 | parserContext, 34 | verboseErrors: true, 35 | ).parseOn(tagStartResult); 36 | if (expressionResult.position > tagStartResult.position) { 37 | if (expressionResult is Failure) { 38 | errors.add(ParseError.fromFailure(expressionResult)); 39 | } 40 | } 41 | 42 | var anythingBeforeEndResult = anythingBeforeEndParser.parseOn( 43 | expressionResult, 44 | ); 45 | if (errors.isEmpty && 46 | anythingBeforeEndResult is Success && 47 | anythingBeforeEndResult.value.isNotEmpty) { 48 | errors.add( 49 | ParseError( 50 | message: 'invalid tag syntax', 51 | position: expressionResult.toPositionString(), 52 | ), 53 | ); 54 | } 55 | 56 | var tagEndResult = tagEndParser.parseOn(anythingBeforeEndResult); 57 | if (tagEndResult is Failure) { 58 | return tagEndResult; 59 | } 60 | 61 | if (errors.isNotEmpty) { 62 | parserContext.errors.addAll(errors); 63 | String tag = context.buffer.substring( 64 | context.position, 65 | tagEndResult.position, 66 | ); 67 | return tagEndResult.success(tag); 68 | } else { 69 | return tagEndResult.failure('not an invalid tag'); 70 | } 71 | } 72 | } 73 | 74 | /// Adds an error if a [Tag] end is found but not a [Tag] start. 75 | /// It replaces the [Tag] end to a [String] e.g. containing: }} 76 | Parser missingTagStartParser(ParserContext parserContext) => 77 | string(parserContext.engine.tagEnd).valueContextMap((value, context) { 78 | parserContext.errors.add( 79 | ParseError( 80 | message: 81 | 'Found tag end: ${parserContext.engine.tagEnd}, ' 82 | 'but it was not preceded with a tag start: ' 83 | '${parserContext.engine.tagStart}', 84 | position: context.toPositionString(), 85 | ), 86 | ); 87 | return value; 88 | }); 89 | 90 | // Adds an error if a [Tag] start is found but no [Tag] start because if 91 | /// they are both present they would have been parsed already. 92 | /// It replaces the [Tag] end to a [String] e.g. containing: {{ 93 | Parser missingTagEndParser(ParserContext parserContext) => 94 | (string(parserContext.engine.tagStart) & 95 | any().star() & 96 | string(parserContext.engine.tagEnd).not()) 97 | .valueContextMap((values, context) { 98 | parserContext.errors.add( 99 | ParseError( 100 | message: 101 | 'Found tag start: ${parserContext.engine.tagStart}, ' 102 | 'but it was not followed with a tag end: ' 103 | '${parserContext.engine.tagEnd}', 104 | position: context.toPositionString(), 105 | ), 106 | ); 107 | return values.first; 108 | }); 109 | -------------------------------------------------------------------------------- /lib/src/parser/override_message_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// Overrides the error message when the [super.delegate] fails. 4 | class OverrideMessageParser extends Parser { 5 | final Parser delegate; 6 | 7 | /// Error message to indicate parse failures with. 8 | final String message; 9 | 10 | OverrideMessageParser(this.delegate, this.message); 11 | 12 | @override 13 | Result parseOn(Context context) { 14 | // If we have a message we can switch to fast mode. 15 | return delegate.parseOn(context); 16 | } 17 | 18 | @override 19 | int fastParseOn(String buffer, int position) => 20 | delegate.fastParseOn(buffer, position); 21 | 22 | @override 23 | bool hasEqualProperties(FlattenParser other) => 24 | super.hasEqualProperties(other) && message == other.message; 25 | 26 | @override 27 | OverrideMessageParser copy() => 28 | OverrideMessageParser(delegate, message); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/parser/parser.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:petitparser/parser.dart'; 3 | 4 | import 'package:template_engine/template_engine.dart'; 5 | 6 | Parser optionalWhiteSpace() => whitespace().star().flatten(); 7 | 8 | /// Creates a parser that can convert a [Template] text to a 9 | /// [parse tree](https://en.wikipedia.org/wiki/Parse_tree) 10 | /// containing [Renderer]s. 11 | /// 12 | /// Note that: 13 | /// * Errors or warnings are stored in [ParserContext.errors] 14 | /// and are can be later accessed in the [TemplateParseResult]. 15 | /// * The start en end of a [Tag] or [Variable] can be escaped so that you 16 | /// can use them in a [Template] without being parsed as [Tag] or [Variable]. 17 | /// e.g. \{{ this is not a tag or variable and does not throw errors \}} 18 | Parser> templateParser(ParserContext context) { 19 | //context.variables.validateNames(); 20 | return delegatingParser( 21 | delegates: [ 22 | escapedTagStartParser(context.engine.tagStart), 23 | escapedTagEndParser(context.engine.tagEnd), 24 | ...context.engine.tags.map((tag) => tag.createTagParser(context)), 25 | InvalidTagParser(context), 26 | missingTagStartParser(context), 27 | missingTagEndParser(context), 28 | ], 29 | tagStart: context.engine.tagStart, 30 | tagEnd: context.engine.tagEnd, 31 | ); 32 | } 33 | 34 | /// A [delegatingParser] delegates to work to other parsers. 35 | /// Text that is not handled by the delegates will also be collected 36 | Parser> delegatingParser({ 37 | required List> delegates, 38 | required String tagStart, 39 | required String tagEnd, 40 | }) { 41 | if (delegates.isEmpty) { 42 | return any().star().flatten().map((value) => [value]); 43 | } 44 | 45 | var parser = ChoiceParser([ 46 | ChoiceParser(delegates), 47 | untilEndOfTagParser(tagStart, tagEnd), 48 | untilEndParser(), 49 | ]); 50 | return parser.plus(); 51 | } 52 | 53 | Parser untilEndOfTagParser(String tagStart, String tagEnd) => any() 54 | .plusLazy( 55 | ChoiceParser([ 56 | string('\\$tagStart'), 57 | string('\\$tagEnd'), 58 | string(tagStart), 59 | string(tagEnd), 60 | ]), 61 | ) 62 | .flatten(); 63 | 64 | Parser untilEndParser() => any().plus().flatten(); 65 | 66 | /// Replaces an escaped [Tag] start (e.g. : \{{ ) 67 | /// to a [String] e.g. containing: {{ (without escape) 68 | /// so that it is not parsed as a [Tag] or [Variable] 69 | Parser escapedTagStartParser(String tagStart) => 70 | string('\\$tagStart').map((value) => tagStart); 71 | 72 | /// Replaces an escaped [Tag] end (e.g. : \}} ) 73 | /// to a [String] e.g. containing: }} (without escape) 74 | /// so that it is not parsed as a [Tag] or [Variable] 75 | Parser escapedTagEndParser(String tagEnd) => 76 | string('\\$tagEnd').map((value) => tagEnd); 77 | 78 | class ParserContext { 79 | /// The template being parsed (for error or warning logging) 80 | final Template template; 81 | 82 | TemplateEngine engine; 83 | 84 | final List errors; 85 | 86 | ParserContext(this.engine, this.template) : errors = []; 87 | } 88 | 89 | /// The result of parsing a single [Template] 90 | class TemplateParseResult extends ParserTree { 91 | final List errors; 92 | final Template template; 93 | 94 | TemplateParseResult({ 95 | required this.template, 96 | required List children, 97 | this.errors = const [], 98 | }) : super(children); 99 | 100 | String get errorMessage { 101 | switch (errors.length) { 102 | case 0: 103 | return ''; 104 | case 1: 105 | return 'Parse error in: ${template.sourceTitle}:\n' 106 | '${errors.map((error) => ' $error').join('\n')}'; 107 | default: 108 | return 'Parse errors in: ${template.sourceTitle}:\n' 109 | '${errors.map((error) => ' $error').join('\n')}'; 110 | } 111 | } 112 | } 113 | 114 | /// The result of parsing one or more [Template]s 115 | class ParseResult extends ParserTree { 116 | ParseResult(super.templateParseResults); 117 | 118 | String get errorMessage => children 119 | .where((result) => result.errorMessage.isNotEmpty) 120 | .map((result) => result.errorMessage) 121 | .join('\n'); 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/constant/constant.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:petitparser/petitparser.dart'; 5 | import 'package:template_engine/template_engine.dart'; 6 | 7 | /// A [Constant] is a value that does not change value over time. 8 | /// 9 | /// The [TemplateEngine] comes with several mathematical constants. 10 | class Constant implements DocumentationFactory, ExampleFactory { 11 | /// See [IdentifierName] 12 | final String name; 13 | final String description; 14 | final ProjectFilePath? codeExample; 15 | final T value; 16 | 17 | Constant({ 18 | required this.name, 19 | required this.description, 20 | this.codeExample, 21 | required this.value, 22 | }) { 23 | IdentifierName.validate(name); 24 | } 25 | 26 | @override 27 | List createMarkdownDocumentation( 28 | RenderContext renderContext, 29 | int titleLevel, 30 | ) { 31 | var writer = HtmlTableWriter(); 32 | writer.setHeader(titleLevel + 1, '$name Constant'); 33 | writer.addRow(['description:', description]); 34 | writer.addRow(['return type:', typeDescription()]); 35 | if (codeExample != null) { 36 | writer.addRow(['code example:', codeExample!.githubMarkdownLink]); 37 | } 38 | return writer.toHtmlLines(); 39 | } 40 | 41 | @override 42 | List createMarkdownExamples( 43 | RenderContext renderContext, 44 | int titleLevel, 45 | ) => codeExample == null ? [] : ['* ${codeExample!.githubMarkdownLink}']; 46 | } 47 | 48 | class DefaultConstants extends DelegatingList { 49 | DefaultConstants() 50 | : super([ 51 | Constant( 52 | name: 'e', 53 | description: 'Base of the natural logarithms.', 54 | codeExample: ProjectFilePath( 55 | 'test/src/parser/tag/expression/constant/e_test.dart', 56 | ), 57 | value: e, 58 | ), 59 | Constant( 60 | name: 'ln10', 61 | description: 'Natural logarithm of 10.', 62 | codeExample: ProjectFilePath( 63 | 'test/src/parser/tag/expression/constant/ln10_test.dart', 64 | ), 65 | value: ln10, 66 | ), 67 | Constant( 68 | name: 'ln2', 69 | description: 'Natural logarithm of 2.', 70 | codeExample: ProjectFilePath( 71 | 'test/src/parser/tag/expression/constant/ln2_test.dart', 72 | ), 73 | value: ln2, 74 | ), 75 | Constant( 76 | name: 'log10e', 77 | description: 'Base-10 logarithm of e.', 78 | codeExample: ProjectFilePath( 79 | 'test/src/parser/tag/expression/constant/log10e_test.dart', 80 | ), 81 | value: log10e, 82 | ), 83 | Constant( 84 | name: 'log2e', 85 | description: 'Base-2 logarithm of e.', 86 | codeExample: ProjectFilePath( 87 | 'test/src/parser/tag/expression/constant/log2e_test.dart', 88 | ), 89 | value: log2e, 90 | ), 91 | Constant( 92 | name: 'pi', 93 | description: "The ratio of a circle's circumference to its diameter", 94 | codeExample: ProjectFilePath( 95 | 'test/src/parser/tag/expression/constant/pi_test.dart', 96 | ), 97 | value: pi, 98 | ), 99 | ]); 100 | } 101 | 102 | Parser constantParser(List constants) { 103 | return ChoiceParser( 104 | constants.map( 105 | (constant) => string(constant.name) & (letter() | digit()).not(), 106 | ), 107 | ) 108 | .flatten('constant expected') 109 | .trim() 110 | .map( 111 | (name) => Value( 112 | constants.firstWhere((constant) => constant.name == name).value, 113 | ), 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/data_type/data_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | /// A [data type](https://en.wikipedia.org/wiki/Data_type) defines what the 6 | /// possible values an expression, such as a variable, operator 7 | /// or a function call, might use. 8 | /// 9 | /// The [TemplateEngine] supports several default [DataType]s. 10 | abstract class DataType 11 | implements DocumentationFactory, ExampleFactory { 12 | String get name; 13 | String get description; 14 | String get syntaxDescription; 15 | List get examples; 16 | Parser> createParser(); 17 | 18 | @override 19 | List createMarkdownDocumentation( 20 | RenderContext renderContext, 21 | int titleLevel, 22 | ) { 23 | var writer = HtmlTableWriter(); 24 | writer.setHeader(titleLevel + 1, '$name Data Type'); 25 | writer.addRow(['description:', description]); 26 | writer.addRow(['syntax:', syntaxDescription]); 27 | 28 | if (examples.isNotEmpty) { 29 | writer.addRow([ 30 | examples.length == 1 ? 'example:' : 'examples:', 31 | examples.map((example) => example.githubMarkdownLink).join('
\n'), 32 | ]); 33 | } 34 | return writer.toHtmlLines(); 35 | } 36 | 37 | @override 38 | List createMarkdownExamples( 39 | RenderContext renderContext, 40 | int titleLevel, 41 | ) => examples.map((e) => '* ${e.githubMarkdownLink}').toList(); 42 | } 43 | 44 | class DefaultDataTypes extends DelegatingList { 45 | DefaultDataTypes() : super([QuotesString(), Number(), Boolean()]); 46 | } 47 | 48 | List> dataTypeParsers(List dataTypes) => 49 | dataTypes.map((dataType) => dataType.createParser()).toList(); 50 | 51 | class Boolean extends DataType { 52 | @override 53 | String get name => 'Boolean'; 54 | 55 | @override 56 | String get description => 57 | 'A form of data with only two possible values: true or false'; 58 | 59 | @override 60 | String get syntaxDescription => 61 | 'A boolean is declared with the word true or false. ' 62 | 'The letters are case insensitive.'; 63 | 64 | @override 65 | List get examples => [ 66 | ProjectFilePath('test/src/parser/tag/expression/data_type/bool_test.dart'), 67 | ]; 68 | 69 | @override 70 | Parser> createParser() => 71 | (stringIgnoreCase('true') | stringIgnoreCase('false')) 72 | .flatten('boolean expected') 73 | .trim() 74 | .map((value) => Value(value.toLowerCase() == 'true')); 75 | } 76 | 77 | class Number extends DataType { 78 | @override 79 | String get name => 'Number'; 80 | 81 | @override 82 | String get description => 'A form of data to express the size of something.'; 83 | 84 | @override 85 | String get syntaxDescription => 86 | 'A number is declared with:
' 87 | '* optional: positive (e.g. +12) or negative (e.g. -12) prefix or no prefix (12=positive)
' 88 | '* one or more digits (e.g. 12)
' 89 | '* optional fragments (e.g. 0.12)
' 90 | '* optional: scientific notation: the letter E is used to mean' 91 | '"10 to the power of." (e.g. 1.314E+1 means 1.314 * 10^1' 92 | 'which is 13.14).
'; 93 | 94 | @override 95 | List get examples => [ 96 | ProjectFilePath('test/src/parser/tag/expression/data_type/num_test.dart'), 97 | ]; 98 | 99 | @override 100 | Parser> createParser() => 101 | (digit().plus() & 102 | (char('.') & digit().plus()).optional() & 103 | (pattern('eE') & pattern('+-').optional() & digit().plus()) 104 | .optional()) 105 | .flatten('number expected') 106 | .trim() 107 | .map((value) => Value(num.parse(value))); 108 | } 109 | 110 | class QuotesString extends DataType { 111 | @override 112 | String get name => 'String'; 113 | 114 | @override 115 | String get description => 116 | 'A form of data containing a sequence of characters'; 117 | 118 | @override 119 | String get syntaxDescription => 120 | 'A string is declared with a chain of characters, surrounded by ' 121 | 'two single (\') or double (") quotes to indicate the ' 122 | 'start and end of a string. In example: \'Hello\' or "Hello"'; 123 | 124 | @override 125 | List get examples => [ 126 | ProjectFilePath( 127 | 'test/src/parser/tag/expression/data_type/string_test.dart', 128 | ), 129 | ]; 130 | 131 | @override 132 | Parser> createParser() => OverrideMessageParser( 133 | ((char("'") & any().starLazy(char("'")).flatten() & char("'")) | 134 | (char('"') & any().starLazy(char('"')).flatten() & char('"'))) 135 | .trim() 136 | .map((values) => Value(values[1])), 137 | 'quoted string expected', 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/expression.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/template_engine.dart'; 2 | 3 | /// A subset of [Renderer] for expressions 4 | /// It is a combination of one or more: 5 | /// * [Value]s 6 | /// * [Variable]s 7 | /// * [ExpressionFunction]s 8 | /// * [Operator]s 9 | abstract class Expression extends Renderer {} 10 | 11 | abstract class ExpressionWithSourcePosition 12 | extends Expression { 13 | final String position; 14 | 15 | ExpressionWithSourcePosition({required this.position}); 16 | } 17 | 18 | /// A [Value] [Expression] holds a value that does not change when 19 | /// rendered (evaluated). 20 | /// e.g. it can be like a [num], [bool], [String] etc... 21 | class Value extends Expression { 22 | Value(T value) : _originalValue = value, value = Future.value(value); 23 | 24 | final Future value; 25 | final T _originalValue; 26 | 27 | @override 28 | Future render(RenderContext context) => value; 29 | 30 | @override 31 | String toString() => 'Value{$_originalValue}'; 32 | 33 | @override 34 | bool operator ==(Object other) => 35 | identical(this, other) || 36 | other is Value && 37 | // ignore: runtimeType == other.runtimeType, because value is cast to an object with class Value: 38 | _originalValue == other._originalValue; 39 | 40 | @override 41 | int get hashCode => _originalValue.hashCode; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/function/default.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | 4 | class DefaultFunctionGroups extends DelegatingList { 5 | DefaultFunctionGroups() 6 | : super([ 7 | MathFunctions(), 8 | StringFunctions(), 9 | DocumentationFunctions(), 10 | PathFunctions(), 11 | ImportFunctions(), 12 | ]); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/function/path.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/template_engine.dart'; 2 | 3 | class PathFunctions extends FunctionGroup { 4 | PathFunctions() : super('Path Functions', [TemplateSource()]); 5 | } 6 | 7 | class TemplateSource extends ExpressionFunction { 8 | TemplateSource() 9 | : super( 10 | name: "templateSource", 11 | description: 'Gives the relative path of the current template', 12 | exampleExpression: "{{templateSource()}}", 13 | exampleCode: ProjectFilePath( 14 | 'test/src/parser/tag/expression/function/template/template_test.dart', 15 | ), 16 | function: 17 | ( 18 | String position, 19 | RenderContext renderContext, 20 | Map parameterValues, 21 | ) => Future.value(renderContext.templateBeingRendered.sourceTitle), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/function/string.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/template_engine.dart'; 2 | 3 | class StringFunctions extends FunctionGroup { 4 | StringFunctions() : super('String Functions', [StringLength()]); 5 | } 6 | 7 | class StringLength extends ExpressionFunction { 8 | StringLength() 9 | : super( 10 | name: 'length', 11 | description: 'Returns the length of a string', 12 | exampleExpression: '{{length("Hello")}}', 13 | exampleResult: "Hello".length.toString(), 14 | exampleCode: ProjectFilePath( 15 | 'test/src/parser/tag/expression/function/string/length_test.dart', 16 | ), 17 | parameters: [ 18 | Parameter(name: 'string', presence: Presence.mandatory()), 19 | ], 20 | function: (position, renderContext, parameters) { 21 | var value = parameters['string']; 22 | if (value is String) { 23 | return Future.value(value.length); 24 | } else { 25 | throw ParameterException('String expected'); 26 | } 27 | }, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/identifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// ** [IdentifierName]'s e.g. for variables, constants, functions, parameters and arguments: ** 4 | /// * are [case sensitive](https://en.wikipedia.org/wiki/Case_sensitivity) 5 | /// * must start with a lower case letter, optionally followed by (lower or upper case) letters and or digits. 6 | /// * conventions: use [lowerCamelCase](https://en.wikipedia.org/wiki/Camel_case) 7 | /// * must be unique and does not match a other [Tag] syntax 8 | class IdentifierName { 9 | static final Parser parser = 10 | (lowercase() & (letter() | digit()).star()).flatten(); 11 | 12 | static void validate(String name) { 13 | var result = parser.end('letter OR digit expected').parse(name); 14 | if (result is Failure) { 15 | throw IdentifierException( 16 | "Invalid identifier name: '$name', " 17 | "${result.message} at position ${result.position}", 18 | ); 19 | } 20 | } 21 | } 22 | 23 | class IdentifierException implements Exception { 24 | final String message; 25 | IdentifierException(this.message); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/operator/addition.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/template_engine.dart'; 2 | 3 | class Additions extends OperatorGroup { 4 | Additions() 5 | : super('Additions', [AddOperator(), SubtractOperator(), OrOperator()]); 6 | } 7 | 8 | class AddOperator extends OperatorWith2Values { 9 | AddOperator() 10 | : super( 11 | name: 'Add', 12 | symbol: '+', 13 | associativity: OperatorAssociativity.left, 14 | variants: [ 15 | TwoValueOperatorVariant( 16 | description: 'Adds two numbers', 17 | expressionExample: '{{2+3}}', 18 | expressionExampleResult: '5', 19 | codeExample: ProjectFilePath( 20 | 'test/src/parser/tag/expression/operator/addition/' 21 | 'num_addition_test.dart', 22 | ), 23 | function: (left, right) => left + right, 24 | ), 25 | TwoValueOperatorVariant( 26 | description: 'Concatenates two strings', 27 | expressionExample: '{{"Hel"+"lo"}}', 28 | expressionExampleResult: "Hello", 29 | codeExample: ProjectFilePath( 30 | 'test/src/parser/tag/expression/operator/addition/' 31 | 'string_concatenate_test.dart', 32 | ), 33 | function: (left, right) => '$left$right', 34 | ), 35 | ], 36 | ); 37 | } 38 | 39 | class SubtractOperator extends OperatorWith2Values { 40 | SubtractOperator() 41 | : super( 42 | name: 'Subtract', 43 | symbol: '-', 44 | associativity: OperatorAssociativity.left, 45 | variants: [ 46 | TwoValueOperatorVariant( 47 | description: 'Subtracts two numbers', 48 | expressionExample: '{{5-3}}', 49 | expressionExampleResult: '2', 50 | codeExample: ProjectFilePath( 51 | 'test/src/parser/tag/expression/operator/addition/' 52 | 'num_subtract_test.dart', 53 | ), 54 | function: (left, right) => left - right, 55 | ), 56 | ], 57 | ); 58 | } 59 | 60 | class OrOperator extends OperatorWith2Values { 61 | OrOperator() 62 | : super( 63 | name: 'Or', 64 | symbol: '|', 65 | associativity: OperatorAssociativity.left, 66 | variants: [ 67 | TwoValueOperatorVariant( 68 | description: 'Logical OR operation on two booleans', 69 | expressionExample: '{{false|true}}', 70 | expressionExampleResult: 'true', 71 | codeExample: ProjectFilePath( 72 | 'test/src/parser/tag/expression/operator/addition/' 73 | 'bool_or_test.dart', 74 | ), 75 | function: (left, right) => left | right, 76 | ), 77 | ], 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/operator/assignment.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/parser.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | 4 | class Assignments extends OperatorGroup { 5 | Assignments() : super('Assignment', [AssignmentOperator()]); 6 | } 7 | 8 | class AssignmentOperator extends Operator { 9 | static const String operator = '='; 10 | static final codeExample = ProjectFilePath( 11 | 'test/src/parser/tag/expression/operator/assignment/assignment_test.dart', 12 | ); 13 | 14 | AssignmentOperator(); 15 | 16 | @override 17 | List createMarkdownDocumentation( 18 | RenderContext renderContext, 19 | int titleLevel, 20 | ) { 21 | var writer = HtmlTableWriter(); 22 | writer.setHeader(titleLevel, 'Assignment Operator ='); 23 | writer.addRow([ 24 | 'description:', 25 | 'Assigns a value to a variable. ' 26 | 'A new variable will be created when it did not exist before, ' 27 | 'otherwise it will be overridden with a new value.', 28 | ]); 29 | writer.addRow([ 30 | 'expression example:', 31 | '{{x=2}}{{x=x+3}}{{x}} should render: 5', 32 | ]); 33 | writer.addRow(['code example:', codeExample.githubMarkdownLink]); 34 | return writer.toHtmlLines(); 35 | } 36 | 37 | @override 38 | List createMarkdownExamples( 39 | RenderContext renderContext, 40 | int titleLevel, 41 | ) => ['* ${codeExample.githubMarkdownLink}']; 42 | 43 | @override 44 | String toString() => 'Operator{$operator}'; 45 | 46 | @override 47 | addParser(Template template, ExpressionGroup2> group) { 48 | group.right( 49 | char(operator).trim(), 50 | (context, left, op, right) => AssignVariableExpression( 51 | position: context.toPositionString(), 52 | left: left, 53 | right: right, 54 | ), 55 | ); 56 | } 57 | } 58 | 59 | class AssignVariableExpression extends ExpressionWithSourcePosition { 60 | final Expression left; 61 | final Expression right; 62 | 63 | AssignVariableExpression({ 64 | required super.position, 65 | required this.left, 66 | required this.right, 67 | }); 68 | 69 | @override 70 | Future render(RenderContext context) async { 71 | var variableExpression = left; 72 | if (variableExpression is! VariableExpression) { 73 | throw RenderException( 74 | message: 75 | 'The left side of the = operation must be a valid variable name', 76 | position: super.position, 77 | ); 78 | } 79 | if (variableExpression.namePath.contains('.')) { 80 | throw RenderException( 81 | message: 82 | 'The left side of the = operation ' 83 | 'must be a name of a root variable ' 84 | '(not contain dots)', 85 | position: super.position, 86 | ); 87 | } 88 | 89 | var value = await right.render(context); 90 | context.variables[variableExpression.namePath] = value; 91 | return ''; // variable assignment returns an empty string. 92 | } 93 | 94 | @override 95 | String toString() => 'AssignVariableExpression{}'; 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/operator/parentheses.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | 4 | class Parentheses extends OperatorGroup { 5 | Parentheses() : super('Parentheses', [ParenthesesOperator()]); 6 | } 7 | 8 | class ParenthesesOperator extends Operator { 9 | static final codeExample = ProjectFilePath( 10 | 'test/src/parser/tag/expression/operator/parentheses_test.dart', 11 | ); 12 | 13 | @override 14 | addParser(Template template, ExpressionGroup2> group) { 15 | group.wrapper( 16 | char('(').trim(), 17 | char(')').trim(), 18 | (left, value, right) => value, 19 | ); 20 | } 21 | 22 | @override 23 | List createMarkdownDocumentation( 24 | RenderContext renderContext, 25 | int titleLevel, 26 | ) { 27 | var writer = HtmlTableWriter(); 28 | writer.setHeader(titleLevel, 'Parentheses Operator ()'); 29 | writer.addRow([ 30 | 'description:', 31 | 'Groups expressions together: What is between parentheses gets processed first', 32 | ]); 33 | writer.addRow([ 34 | 'expression example:', 35 | '{{2+1*3}} should render: 6
{{(2+1)*3}} should render: 9', 36 | ]); 37 | writer.addRow(['code example:', codeExample.githubMarkdownLink]); 38 | return writer.toHtmlLines(); 39 | } 40 | 41 | @override 42 | List createMarkdownExamples( 43 | RenderContext renderContext, 44 | int titleLevel, 45 | ) => ['* ${codeExample.githubMarkdownLink}']; 46 | 47 | @override 48 | String toString() => 'ParenthesesOperator{}'; 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/operator/prefixes.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/template_engine.dart'; 2 | 3 | class Prefixes extends OperatorGroup { 4 | Prefixes() 5 | : super('Prefixes', [ 6 | PositiveOperator(), 7 | NegativeOperator(), 8 | NotOperator(), 9 | ]); 10 | } 11 | 12 | class PositiveOperator extends PrefixOperator { 13 | PositiveOperator() 14 | : super( 15 | name: 'Positive', 16 | symbol: '+', 17 | description: 'Optional prefix for positive numbers', 18 | expressionExample: '{{+3}}', 19 | expressionExampleResult: '3', 20 | codeExample: ProjectFilePath( 21 | 'test/src/parser/tag/expression/operator/prefix/positive_test.dart', 22 | ), 23 | function: (number) => number, 24 | ); 25 | } 26 | 27 | class NegativeOperator extends PrefixOperator { 28 | NegativeOperator() 29 | : super( 30 | name: 'Negative', 31 | symbol: '-', 32 | description: 'Prefix for a negative number', 33 | expressionExample: '{{-3}}', 34 | expressionExampleResult: '-3', 35 | codeExample: ProjectFilePath( 36 | 'test/src/parser/tag/expression/operator/prefix/negative_test.dart', 37 | ), 38 | function: (number) => -number, 39 | ); 40 | } 41 | 42 | class NotOperator extends PrefixOperator { 43 | NotOperator() 44 | : super( 45 | name: 'Not', 46 | symbol: '!', 47 | description: 'Prefix to invert a boolean, e.g.: !true =false', 48 | expressionExample: '{{!true}}', 49 | expressionExampleResult: 'false', 50 | codeExample: ProjectFilePath( 51 | 'test/src/parser/tag/expression/operator/prefix/not_test.dart', 52 | ), 53 | function: (boolean) => !boolean, 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/parser/tag/expression/tag_expression_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | class ExpressionTag extends Tag { 6 | ExpressionTag() 7 | : super( 8 | name: 'Expression', 9 | description: [ 10 | 'Evaluates an expression that can contain:', 11 | '* Data Types (e.g. boolean, number or String)', 12 | '* Constants (e.g. pi)', 13 | '* Variables (e.g. person.name )', 14 | '* Operators (e.g. + - * /)', 15 | '* Functions (e.g. cos(7) )', 16 | '* or any combination of the above', 17 | ], 18 | exampleExpression: 19 | 'The volume of a sphere = ' 20 | '{{ round( (3/4) * pi * (radius ^ 3) )}}.', 21 | exampleCode: ProjectFilePath( 22 | 'test/src/parser/tag/expression/' 23 | 'tag_expression_parser_test.dart', 24 | ), 25 | ); 26 | 27 | @override 28 | Parser createTagParser(ParserContext context) => 29 | (string(context.engine.tagStart) & 30 | (whitespace().star()) & 31 | expressionParser(context) & 32 | (whitespace().star()) & 33 | string(context.engine.tagEnd)) 34 | .valueContextMap((values, parsePosition) => values[2]); 35 | 36 | @override 37 | List createMarkdownExamples( 38 | RenderContext renderContext, 39 | int titleLevel, 40 | ) => [ 41 | '${'#' * titleLevel} $name', 42 | '* ${exampleCode.githubMarkdownLink}', 43 | ..._createMarkDownExamplesFor( 44 | renderContext: renderContext, 45 | title: 'Data Types', 46 | customExample: ProjectFilePath( 47 | 'test/src/parser/tag/expression/data_type/' 48 | 'custom_data_type_test.dart', 49 | ), 50 | exampleFactories: renderContext.engine.dataTypes, 51 | titleLevel: titleLevel + 1, 52 | ), 53 | ..._createMarkDownExamplesFor( 54 | renderContext: renderContext, 55 | title: 'Constants', 56 | customExample: ProjectFilePath( 57 | 'test/src/parser/tag/expression/constant/' 58 | 'custom_constant_test.dart', 59 | ), 60 | exampleFactories: renderContext.engine.constants, 61 | titleLevel: titleLevel + 1, 62 | ), 63 | ..._createMarkDownExamplesFor( 64 | renderContext: renderContext, 65 | title: 'Variables', 66 | exampleFactories: [VariableExamples()], 67 | titleLevel: titleLevel + 1, 68 | ), 69 | ..._createMarkDownExamplesFor( 70 | renderContext: renderContext, 71 | title: 'Functions', 72 | customExample: ProjectFilePath( 73 | 'test/src/parser/tag/expression/function/' 74 | 'custom_function_test.dart', 75 | ), 76 | exampleFactories: renderContext.engine.functionGroups, 77 | titleLevel: titleLevel + 1, 78 | ), 79 | ..._createMarkDownExamplesFor( 80 | renderContext: renderContext, 81 | title: 'Operators', 82 | customExample: ProjectFilePath( 83 | 'test/src/parser/tag/expression/operator/' 84 | 'custom_operator_test.dart', 85 | ), 86 | exampleFactories: renderContext.engine.operatorGroups, 87 | titleLevel: titleLevel + 1, 88 | ), 89 | ]; 90 | } 91 | 92 | List _createMarkDownExamplesFor({ 93 | required RenderContext renderContext, 94 | title, 95 | ProjectFilePath? customExample, 96 | required List exampleFactories, 97 | required int titleLevel, 98 | }) { 99 | if (exampleFactories.isEmpty) { 100 | return []; 101 | } else { 102 | return [ 103 | '${'#' * (titleLevel)} $title', 104 | if (customExample != null) '* ${customExample.githubMarkdownLink}', 105 | ...exampleFactories 106 | .map( 107 | (dataType) => 108 | dataType.createMarkdownExamples(renderContext, titleLevel + 1), 109 | ) 110 | .flattened, 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/src/parser/tag/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:template_engine/src/parser/tag/expression/tag_expression_parser.dart'; 3 | import 'package:template_engine/src/parser/tag/tag.dart'; 4 | import 'package:template_engine/src/template_engine.dart'; 5 | 6 | class StandardTags extends DelegatingList { 7 | StandardTags(TemplateEngine templateEngine) : super([ExpressionTag()]); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/parser/tag/tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | /// [Tag]s are specific texts in [Template]s that are replaced by the 6 | /// [TemplateEngine] with other information. 7 | /// 8 | /// A [Tag]: 9 | /// * Starts with some bracket and/or character combination, e.g.: \{{ 10 | /// * Followed by some contents 11 | /// * Ends with some closing bracket and/or character combination, e.g.: \}} 12 | /// 13 | /// A tag example: \{{customer.name\}} 14 | /// 15 | /// By default the TemplateEngine [Tag]s start with \{{ and end with \}} brackets, 16 | /// just like the popular template engines 17 | /// [Mustache](https://mustache.github.io/) and 18 | /// [Handlebars](https://handlebarsjs.com). 19 | /// 20 | /// You can also define alternative [Tag] brackets in the [TemplateEngine] 21 | /// constructor parameters. See [TemplateEngine.tagStart] and 22 | /// [TemplateEngine.tagEnd]. 23 | /// 24 | /// It is recommended to use a start and end combination that is not used 25 | /// elsewhere in your templates, e.g.: Do not use < > as [Tag] start and end 26 | /// if your template contains HTML or XML 27 | /// 28 | /// The [TemplateEngine] comes with [DefaultTags]. You can replace or add your 29 | /// own [Tag]s by manipulating the the [TemplateEngine.tags] field. 30 | abstract class Tag 31 | implements DocumentationFactory, ExampleFactory { 32 | final String name; 33 | final List description; 34 | final String exampleExpression; 35 | final ProjectFilePath exampleCode; 36 | final String? exampleResult; 37 | Tag({ 38 | required this.name, 39 | required this.description, 40 | required this.exampleExpression, 41 | this.exampleResult, 42 | required this.exampleCode, 43 | }) { 44 | TagName.validate(name); 45 | } 46 | 47 | @override 48 | List createMarkdownDocumentation( 49 | RenderContext renderContext, 50 | int titleLevel, 51 | ) { 52 | var writer = HtmlTableWriter(); 53 | writer.setHeader(titleLevel, '$name Tag'); 54 | writer.addRow(['description:', description.join('
')]); 55 | writer.addRow(['expression example:', _createExpressionExample()], [1, 4]); 56 | writer.addRow(['code example:', exampleCode.githubMarkdownLink], [1, 4]); 57 | return writer.toHtmlLines(); 58 | } 59 | 60 | String _createExpressionExample() { 61 | var example = exampleExpression; 62 | if (exampleResult != null && exampleResult!.trim().isNotEmpty) { 63 | example += ' should render: $exampleResult'; 64 | } 65 | return example; 66 | } 67 | 68 | @override 69 | createMarkdownExamples(RenderContext renderContext, int titleLevel); 70 | 71 | Parser createTagParser(ParserContext context); 72 | } 73 | 74 | /// A [TagName]: 75 | /// * may not be empty 76 | /// * is case un-sensitive 77 | /// * may contain letters, numbers and dots. e.g.: 'project.path' 78 | /// * must be unique 79 | class TagName { 80 | static final nameParser = (letter().plus() & digit().star()).plus(); 81 | static final namePathParser = (nameParser & (char('.') & nameParser).star()); 82 | static final namePathParserUntilEnd = namePathParser.end(); 83 | 84 | static void validate(String namePath) { 85 | var result = namePathParserUntilEnd.parse(namePath); 86 | if (result is Failure) { 87 | throw TagException( 88 | 'Tag name: "$namePath" is invalid: ${result.message} ' 89 | 'at position: ${result.position}', 90 | ); 91 | } 92 | } 93 | } 94 | 95 | class TagException implements Exception { 96 | final String message; 97 | 98 | TagException(this.message); 99 | } 100 | 101 | class DefaultTags extends DelegatingList { 102 | DefaultTags() : super([ExpressionTag()]); 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/parser/value_context_map_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | extension ValueContextMapParserExtension on Parser { 4 | /// Inspired by [MapParserExtension]: with te addition that [Callback2] 5 | /// also provides the current parser position for error or warnings. 6 | /// 7 | /// Returns a parser that evaluates a [callback] as the production action 8 | /// on success of the receiver. 9 | /// 10 | /// For example, the parser `digit().map((char) => int.parse(char))` returns 11 | /// the number `1` for the input string `'1'`. If the delegate fails, the 12 | /// production action is not executed and the failure is passed on. 13 | Parser valueContextMap( 14 | Callback2 callback, { 15 | @Deprecated('All callbacks are considered to have side-effects') 16 | bool hasSideEffects = true, 17 | }) => ValueContextMapParser(this, callback); 18 | } 19 | 20 | /// A parser that performs a transformation with a given function on the 21 | /// successful parse result of the delegate. 22 | class ValueContextMapParser extends DelegateParser { 23 | ValueContextMapParser(super.delegate, this.callback); 24 | 25 | /// The production action to be called. 26 | final Callback2 callback; 27 | 28 | @override 29 | Result parseOn(Context context) { 30 | final result = delegate.parseOn(context); 31 | if (result is Success) { 32 | var callbackResults = callback(result.value, context); 33 | return result.success(callbackResults); 34 | } else { 35 | return result.failure(result.message); 36 | } 37 | } 38 | 39 | @override 40 | bool hasEqualProperties(ValueContextMapParser other) => 41 | super.hasEqualProperties(other) && callback == other.callback; 42 | 43 | @override 44 | ValueContextMapParser copy() => 45 | ValueContextMapParser(delegate, callback); 46 | } 47 | 48 | /// We pass the parse position so that errors and or warnings can be logged 49 | /// with the current parse position within the [Template] 50 | typedef Callback2 = R Function(T value, Context context); 51 | -------------------------------------------------------------------------------- /lib/src/project_file_path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | class ProjectFilePath { 6 | final String relativePath; 7 | 8 | static Parser _fileOrFolderName() => (ChoiceParser([ 9 | letter(), 10 | digit(), 11 | char('('), 12 | char(')'), 13 | char('_'), 14 | char('-'), 15 | char('.'), 16 | ], failureJoiner: selectFarthestJoined)).plus().flatten(); 17 | 18 | static Parser _slashAndFileOrFolderName() => 19 | (char('/') & _fileOrFolderName()).map((values) => values[1]); 20 | 21 | static Parser> _pathParser() => 22 | (_fileOrFolderName() & (_slashAndFileOrFolderName().star())) 23 | .map>((values) => [values[0], ...values[1]]) 24 | .endWithBetterFailure(); 25 | 26 | ProjectFilePath(this.relativePath) { 27 | validate(relativePath); 28 | } 29 | 30 | void validate(String path) { 31 | var result = _pathParser().parse(path); 32 | if (result is Failure) { 33 | throw Exception( 34 | "Invalid project file path: '$path': ${result.message} " 35 | "at position: ${result.position + 1}", 36 | ); 37 | } 38 | } 39 | 40 | String get fileName { 41 | var value2 = _pathParser().parse(relativePath).value; 42 | return value2.last; 43 | } 44 | 45 | File get file { 46 | var projectPath = Directory.current.path; 47 | var filePath = [ 48 | ...projectPath.split(Platform.pathSeparator), 49 | ...relativePath.split('/'), 50 | ].join(Platform.pathSeparator); 51 | return File(filePath); 52 | } 53 | 54 | Uri get githubUri => Uri.parse( 55 | 'https://github.com/domain-centric/template_engine/blob/main/' 56 | '$relativePath', 57 | ); 58 | 59 | String get githubMarkdownLink => '$fileName'; 60 | 61 | @override 62 | String toString() => relativePath; 63 | } 64 | 65 | extension EndWithBetterFailureExtension on Parser { 66 | Parser endWithBetterFailure() => 67 | skip(after: EndOfInputWithBetterFailureParser(this)); 68 | } 69 | 70 | /// A parser that succeeds at the end of input. 71 | /// OR results with an failure with the message of the owning parser 72 | /// Inspired by [EndOfInputParser] 73 | class EndOfInputWithBetterFailureParser extends Parser { 74 | final Parser parser; 75 | EndOfInputWithBetterFailureParser(this.parser); 76 | 77 | @override 78 | Result parseOn(Context context) { 79 | if (context.position < context.buffer.length) { 80 | var contextUpToFault = parser.parseOn(context); 81 | var fault = parser.parseOn(contextUpToFault); 82 | return fault; 83 | } 84 | return context.success(null); 85 | } 86 | 87 | @override 88 | int fastParseOn(String buffer, int position) => 89 | position < buffer.length ? -1 : position; 90 | 91 | @override 92 | String toString() => '${super.toString()}[$parser]'; 93 | 94 | @override 95 | EndOfInputWithBetterFailureParser copy() => 96 | EndOfInputWithBetterFailureParser(parser); 97 | 98 | @override 99 | bool hasEqualProperties(EndOfInputWithBetterFailureParser other) => 100 | super.hasEqualProperties(other) && parser == other.parser; 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | /// [source] is the location of a file. 5 | /// [source] can be a [File.path], [ProjectFilePath] or [Uri] 6 | Future readSource(String source) async { 7 | try { 8 | return await readFromFilePath(source); 9 | } on UnsupportedSourceException { 10 | try { 11 | return await readFromHttpUri(source); 12 | } on UnsupportedSourceException { 13 | try { 14 | return await readFromFileUri(source); 15 | } on Exception catch (e) { 16 | var message = e 17 | .toString() 18 | .replaceFirst('Exception: ', '') 19 | .replaceAll('\r', '') 20 | .replaceAll('\n', ''); 21 | throw Exception(message); 22 | } 23 | } 24 | } 25 | } 26 | 27 | /// Reads a file from a operation systems 28 | /// [File Path](https://en.wikipedia.org/wiki/Path_(computing))/ 29 | Future readFromFilePath(String source) async { 30 | try { 31 | var file = File(source); 32 | return await file.readAsString(); 33 | } on Exception { 34 | return Future.error(UnsupportedSourceException()); 35 | } 36 | } 37 | 38 | Future readFromHttpUri(String source) async { 39 | var url = Uri.parse(source); 40 | if (url.isScheme('HTTP') || url.isScheme("HTTPS")) { 41 | try { 42 | var request = await HttpClient().getUrl(url); 43 | var response = await request.close(); 44 | if (response.statusCode != 200) { 45 | return Future.error( 46 | Exception( 47 | 'Error reading: $source, status code: ${response.statusCode}', 48 | ), 49 | ); 50 | } 51 | return response.transform(const Utf8Decoder()).join(); 52 | } on Exception catch (e) { 53 | return Future.error(Exception('Error reading: $source, $e')); 54 | } 55 | } 56 | return Future.error(UnsupportedSourceException()); 57 | } 58 | 59 | /// Reads from a 60 | /// [File URI scheme](https://en.wikipedia.org/wiki/File_URI_scheme). 61 | /// This includes a [ProjectFilePath]. 62 | Future readFromFileUri(String source) async { 63 | try { 64 | var uri = Uri.parse(source); 65 | var file = File.fromUri(uri); 66 | return await file.readAsString(); 67 | } on Exception catch (e) { 68 | return Future.error(Exception('Error reading: $source, $e')); 69 | } 70 | } 71 | 72 | class UnsupportedSourceException implements Exception { 73 | UnsupportedSourceException(); 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/template.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | /// A template is a text that can contain [Tag]s. 6 | /// This text is parsed by the [TemplateEngine] into [Renderer]s. 7 | /// These [Renderer]s render a new text. 8 | /// [TagRenderer]s are replaced with some other text, depending on the 9 | /// implementation of the [TagRenderer] 10 | abstract class Template { 11 | /// Explains where the template text came from. 12 | /// A [source] results in a [text]. 13 | /// We use the [source] to identify and cash [Template]s for performance. 14 | late String source; 15 | 16 | /// Explains where the template text came from. 17 | /// It is used in [Error]s and can be shorter then [source] 18 | late String sourceTitle; 19 | 20 | /// The text to be parsed by the [TemplateEngine] 21 | late Future text; 22 | 23 | /// Assuming that the text is the same for the same source 24 | /// so we can cash the Templates for performance 25 | 26 | @override 27 | bool operator ==(Object other) => 28 | other is Template && other.source.toLowerCase() == source.toLowerCase(); 29 | 30 | @override 31 | int get hashCode => source.toLowerCase().hashCode ^ text.hashCode; 32 | } 33 | 34 | class TextTemplate extends Template { 35 | TextTemplate(String text) { 36 | super.source = text; 37 | super.sourceTitle = createTitle(text); 38 | super.text = Future.value(text); 39 | } 40 | 41 | String createTitle(String text) { 42 | String firstLine = text.trim().split(RegExp('\\n')).first; 43 | 44 | if (firstLine.length > 40) { 45 | return "'${firstLine.substring(0, 40)}...'"; 46 | } else { 47 | return "'$firstLine'"; 48 | } 49 | } 50 | } 51 | 52 | class FileTemplate extends Template { 53 | @override 54 | FileTemplate(File file) { 55 | super.source = file.path; 56 | super.sourceTitle = file.path; 57 | super.text = readFromFilePath(file.path); 58 | } 59 | 60 | FileTemplate.fromProjectFilePath(ProjectFilePath projectFilePath) { 61 | super.source = projectFilePath.toString(); 62 | super.sourceTitle = projectFilePath.toString(); 63 | super.text = readFromFilePath(projectFilePath.file.path); 64 | } 65 | } 66 | 67 | class HttpTemplate extends Template { 68 | HttpTemplate(Uri url) { 69 | super.source = url.toString(); 70 | super.sourceTitle = url.toString(); 71 | super.text = readFromHttpUri(url.toString()); 72 | } 73 | } 74 | 75 | class ImportedTemplate extends Template { 76 | ImportedTemplate({required String source, required String text}) { 77 | super.source = source; 78 | super.sourceTitle = source; 79 | super.text = Future.value(text); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/template_engine.dart: -------------------------------------------------------------------------------- 1 | export 'src/parser/error.dart'; 2 | export 'src/parser/error_parser.dart'; 3 | export 'src/parser/override_message_parser.dart'; 4 | export 'src/parser/parser.dart'; 5 | export 'src/parser/tag/expression/constant/constant.dart'; 6 | export 'src/parser/tag/expression/data_type/data_type.dart'; 7 | export 'src/parser/tag/expression/expression.dart'; 8 | export 'src/parser/tag/expression/expression_parser.dart'; 9 | export 'src/parser/tag/expression/function/default.dart'; 10 | export 'src/parser/tag/expression/function/documentation.dart'; 11 | export 'src/parser/tag/expression/function/function.dart'; 12 | export 'src/parser/tag/expression/function/import.dart'; 13 | export 'src/parser/tag/expression/function/math.dart'; 14 | export 'src/parser/tag/expression/function/path.dart'; 15 | export 'src/parser/tag/expression/function/string.dart'; 16 | export 'src/parser/tag/expression/identifier.dart'; 17 | export 'src/parser/tag/expression/operator/addition.dart'; 18 | export 'src/parser/tag/expression/operator/assignment.dart'; 19 | export 'src/parser/tag/expression/operator/comparison.dart'; 20 | export 'src/parser/tag/expression/operator/multiplication.dart'; 21 | export 'src/parser/tag/expression/operator/operator.dart'; 22 | export 'src/parser/tag/expression/operator/parentheses.dart'; 23 | export 'src/parser/tag/expression/operator/prefixes.dart'; 24 | export 'src/parser/tag/expression/tag_expression_parser.dart'; 25 | export 'src/parser/tag/expression/variable/variable.dart'; 26 | export 'src/parser/tag/group.dart'; 27 | export 'src/parser/tag/tag.dart'; 28 | export 'src/parser/value_context_map_parser.dart'; 29 | export 'src/project_file_path.dart'; 30 | export 'src/render.dart'; 31 | export 'src/source.dart'; 32 | export 'src/template.dart'; 33 | export 'src/template_engine.dart'; 34 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: template_engine 2 | description: A Dart package to parse and render templates, using (nested) variables and custom tags. 3 | version: 1.5.2 4 | repository: https://github.com/domain-centric/template_engine 5 | issue_tracker: https://github.com/domain-centric/template_engine/pulls 6 | documentation: https://github.com/domain-centric/template_engine/wiki 7 | 8 | environment: 9 | sdk: ^3.8.0 10 | 11 | dependencies: 12 | collection: ^1.19.1 13 | http: ^1.3.0 14 | petitparser: ^6.1.0 15 | recase: ^4.1.0 16 | xml: ^6.5.0 17 | yaml: ^3.1.3 18 | 19 | 20 | dev_dependencies: 21 | lints: ^6.0.0 22 | test: ^1.25.1 23 | shouldly: ^0.5.0+1 24 | build_runner: ^2.4.15 25 | documentation_builder: ^1.0.0 26 | 27 | topics: 28 | - template 29 | - parse 30 | - render 31 | - customizable -------------------------------------------------------------------------------- /test/src/hello.template: -------------------------------------------------------------------------------- 1 | Hello -------------------------------------------------------------------------------- /test/src/hello_name.template: -------------------------------------------------------------------------------- 1 | Hello {{name}}. -------------------------------------------------------------------------------- /test/src/parser/error_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('unknownTagOrVariableParser', () { 7 | test('should return error for undefined tag', () async { 8 | final template = TextTemplate('Hello {{notDefined attribute="1"}}.'); 9 | final engine = TemplateEngine(); 10 | 11 | final result = await engine.parseTemplate(template); 12 | 13 | result.errorMessage.should.be( 14 | 'Parse error in: \'Hello {{notDefined attribute="1"}}.\':\n' 15 | ' 1:20: invalid tag syntax', 16 | ); 17 | }); 18 | 19 | test('should return errors for two undefined tags', () async { 20 | final template = TextTemplate( 21 | 'Hello {{notDefined1 attribute="1"}} {{notDefined2 attribute="1"}}.', 22 | ); 23 | final engine = TemplateEngine(); 24 | 25 | final result = await engine.parseTemplate(template); 26 | 27 | result.errorMessage.should.be( 28 | 'Parse errors in: \'Hello {{notDefined1 attribute="1"}} {{no...\':\n' 29 | ' 1:21: invalid tag syntax\n' 30 | ' 1:51: invalid tag syntax', 31 | ); 32 | }); 33 | }); 34 | 35 | group('missingTagStartParser', () { 36 | test( 37 | 'should return error for missing tag end in "Hello {{ world."', 38 | () async { 39 | final template = TextTemplate('Hello {{ world.'); 40 | final engine = TemplateEngine(); 41 | 42 | final result = await engine.parseTemplate(template); 43 | 44 | result.errorMessage.should.be( 45 | 'Parse error in: \'Hello {{ world.\':\n' 46 | ' 1:7: Found tag start: {{, but it was not followed with a tag end: }}', 47 | ); 48 | }, 49 | ); 50 | 51 | test('should return error for escaped and unclosed tag', () async { 52 | final template = TextTemplate('Hello \\{{ {{ world.'); 53 | final engine = TemplateEngine(); 54 | 55 | final result = await engine.parseTemplate(template); 56 | 57 | result.errorMessage.should.be( 58 | 'Parse error in: \'Hello \\{{ {{ world.\':\n' 59 | ' 1:11: Found tag start: {{, but it was not followed with a tag end: }}', 60 | ); 61 | }); 62 | 63 | test('should return error for unclosed tag after valid one', () async { 64 | final template = TextTemplate('Hello {{name}} {{.'); 65 | final engine = TemplateEngine(); 66 | 67 | final result = await engine.parseTemplate(template); 68 | 69 | result.errorMessage.should.be( 70 | 'Parse error in: \'Hello {{name}} {{.\':\n' 71 | ' 1:16: Found tag start: {{, but it was not followed with a tag end: }}', 72 | ); 73 | }); 74 | }); 75 | 76 | group('missingTagEndParser', () { 77 | test('should return error for unmatched tag end', () async { 78 | final template = TextTemplate('Hello }} world.'); 79 | final engine = TemplateEngine(); 80 | 81 | final result = await engine.parseTemplate(template); 82 | 83 | result.errorMessage.should.be( 84 | 'Parse error in: \'Hello }} world.\':\n' 85 | ' 1:7: Found tag end: }}, but it was not preceded with a tag start: {{', 86 | ); 87 | }); 88 | 89 | test('should return error for escaped and unmatched tag end', () async { 90 | final template = TextTemplate('Hello \\}} }} world.'); 91 | final engine = TemplateEngine(); 92 | 93 | final result = await engine.parseTemplate(template); 94 | 95 | result.errorMessage.should.be( 96 | 'Parse error in: \'Hello \\}} }} world.\':\n' 97 | ' 1:11: Found tag end: }}, but it was not preceded with a tag start: {{', 98 | ); 99 | }); 100 | 101 | test('should return error for unmatched tag end after valid tag', () async { 102 | final template = TextTemplate('Hello {{name}} }}.'); 103 | final engine = TemplateEngine(); 104 | 105 | final result = await engine.parseTemplate(template); 106 | 107 | result.errorMessage.should.be( 108 | 'Parse error in: \'Hello {{name}} }}.\':\n' 109 | ' 1:16: Found tag end: }}, but it was not preceded with a tag start: {{', 110 | ); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /test/src/parser/parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('escapedTagStartParser and escapedTagEndParser', () { 7 | test('should parse and render escaped tag start "{{"', () async { 8 | final template = TextTemplate('Hello \\{{ world.'); 9 | final engine = TemplateEngine(); 10 | 11 | final parseResult = await engine.parseTemplate(template); 12 | final templateParseResult = parseResult.children.first; 13 | 14 | Should.satisfyAllConditions([ 15 | () => templateParseResult.children.length.should.be(3), 16 | () => templateParseResult.children.first.should 17 | .beOfType()! 18 | .should 19 | .be('Hello '), 20 | () => templateParseResult.children[1].should 21 | .beOfType()! 22 | .should 23 | .be('{{'), 24 | () => templateParseResult.children.last.should 25 | .beOfType()! 26 | .should 27 | .be(' world.'), 28 | ]); 29 | 30 | final result = await engine.render(parseResult, {'name': 'world'}); 31 | result.text.should.be('Hello {{ world.'); 32 | }); 33 | 34 | test('should parse and render escaped tag end "}}"', () async { 35 | final template = TextTemplate('Hello \\}} world.'); 36 | final engine = TemplateEngine(); 37 | 38 | final parseResult = await engine.parseTemplate(template); 39 | final templateParseResult = parseResult.children.first; 40 | 41 | Should.satisfyAllConditions([ 42 | () => templateParseResult.children.length.should.be(3), 43 | () => templateParseResult.children.first.should 44 | .beOfType()! 45 | .should 46 | .be('Hello '), 47 | () => templateParseResult.children[1].should 48 | .beOfType()! 49 | .should 50 | .be('}}'), 51 | () => templateParseResult.children.last.should 52 | .beOfType()! 53 | .should 54 | .be(' world.'), 55 | ]); 56 | 57 | final result = await engine.render(parseResult, {'name': 'world'}); 58 | result.text.should.be('Hello }} world.'); 59 | }); 60 | 61 | test('should parse and render escaped tag block "{{ ... }}"', () async { 62 | final template = TextTemplate('\\{{ this is not a tag or variable \\}}'); 63 | final engine = TemplateEngine(); 64 | 65 | final parseResult = await engine.parseTemplate(template); 66 | final templateParseResult = parseResult.children.first; 67 | 68 | Should.satisfyAllConditions([ 69 | () => templateParseResult.children.length.should.be(3), 70 | () => templateParseResult.children.first.should 71 | .beOfType()! 72 | .should 73 | .be('{{'), 74 | () => templateParseResult.children[1].should 75 | .beOfType()! 76 | .should 77 | .be(' this is not a tag or variable '), 78 | () => templateParseResult.children.last.should 79 | .beOfType()! 80 | .should 81 | .be('}}'), 82 | ]); 83 | 84 | final result = await engine.render(parseResult, {'name': 'world'}); 85 | result.text.should.be('{{ this is not a tag or variable }}'); 86 | }); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/constant_name_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Constant.name', () { 7 | test( 8 | 'Variable "east" to be found and not taken hostage by constant "e"', 9 | () async { 10 | var engine = TemplateEngine(); 11 | var parseResult = await engine.parseText('{{east}}'); 12 | parseResult.errorMessage.should.beBlank(); 13 | var renderResult = await engine.render(parseResult, { 14 | 'east': 'Direction of where the sun rises.', 15 | }); 16 | renderResult.text.should.be('Direction of where the sun rises.'); 17 | }, 18 | ); 19 | 20 | test('valid constant names should not throw an error', () { 21 | var validConstantNames = ['e', 'ln10', 'log10e', 'myConstant']; 22 | Should.satisfyAllConditions([ 23 | for (var validName in validConstantNames) 24 | () => Should.notThrowError( 25 | () => Constant(name: validName, description: 'dummy', value: 10), 26 | ), 27 | ]); 28 | }); 29 | 30 | test('in-valid constant names should not throw an argument error', () { 31 | var inValidConstantNames = ['1e', 'Ln10', 'log>10e', '_myConstant']; 32 | Should.satisfyAllConditions([ 33 | for (var inValidName in inValidConstantNames) 34 | () => Should.throwException( 35 | () => Constant(name: inValidName, description: 'dummy', value: 10), 36 | ), 37 | ]); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/custom_constant_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('Custom constant', () async { 7 | var engine = TemplateEngine(); 8 | engine.constants.add(TemplateEngineUrl()); 9 | var template = TextTemplate('{{templateEngineUrl}}'); 10 | var parseResult = await engine.parseTemplate(template); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be('https://pub.dev/packages/template_engine'); 13 | }); 14 | } 15 | 16 | class TemplateEngineUrl extends Constant { 17 | TemplateEngineUrl() 18 | : super( 19 | name: 'templateEngineUrl', 20 | description: 'A URL to the template_engine dart package on pub.dev', 21 | value: 'https://pub.dev/packages/template_engine', 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/e_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{e}} should render: $e', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{e}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(e.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/ln10_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{ln10}} should render: $ln10', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{ln10}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(ln10.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/ln2_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{ln2}} should render: $ln2', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{ln2}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(ln2.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/log10e_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{log10e}} should render: $log10e', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{log10e}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(log10e.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/log2e_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{log2e}} should render: $log2e', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{log2e}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(log2e.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/constant/pi_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{pi}} should render: $pi', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{pi}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(pi.toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/data_type/bool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | void main() { 6 | group('test: true', () { 7 | test('{{true}} should be rendered as true', () async { 8 | var engine = TemplateEngine(); 9 | var parserResult = await engine.parseTemplate(TextTemplate('{{true}}')); 10 | var renderResult = await engine.render(parserResult); 11 | renderResult.text.should.be('true'); 12 | }); 13 | 14 | test('{{TRUE}} should be rendered as true', () async { 15 | var engine = TemplateEngine(); 16 | var parserResult = await engine.parseTemplate(TextTemplate('{{TRUE}}')); 17 | var renderResult = await engine.render(parserResult); 18 | renderResult.text.should.be('true'); 19 | }); 20 | 21 | test('{{ TRue}} should be rendered as true', () async { 22 | var engine = TemplateEngine(); 23 | var parserResult = await engine.parseTemplate( 24 | TextTemplate('{{ TRue}}'), 25 | ); 26 | var renderResult = await engine.render(parserResult); 27 | renderResult.text.should.be('true'); 28 | }); 29 | 30 | test('{{trUE }} should be rendered as true', () async { 31 | var engine = TemplateEngine(); 32 | var parserResult = await engine.parseTemplate( 33 | TextTemplate('{{trUE }}'), 34 | ); 35 | var renderResult = await engine.render(parserResult); 36 | renderResult.text.should.be('true'); 37 | }); 38 | 39 | test('{{ true }} should be rendered as true', () async { 40 | var engine = TemplateEngine(); 41 | var parserResult = await engine.parseTemplate( 42 | TextTemplate('{{ true }}'), 43 | ); 44 | var renderResult = await engine.render(parserResult); 45 | renderResult.text.should.be('true'); 46 | }); 47 | }); 48 | group('test: false', () { 49 | test('{{false}} should be rendered as false', () async { 50 | var engine = TemplateEngine(); 51 | var parserResult = await engine.parseTemplate(TextTemplate('{{false}}')); 52 | var renderResult = await engine.render(parserResult); 53 | renderResult.text.should.be('false'); 54 | }); 55 | 56 | test('{{FALSE}} should be rendered as false', () async { 57 | var engine = TemplateEngine(); 58 | var parserResult = await engine.parseTemplate(TextTemplate('{{FALSE}}')); 59 | var renderResult = await engine.render(parserResult); 60 | renderResult.text.should.be('false'); 61 | }); 62 | 63 | test('{{ FAlse}} should be rendered as false', () async { 64 | var engine = TemplateEngine(); 65 | var parserResult = await engine.parseTemplate( 66 | TextTemplate('{{ FAlse}}'), 67 | ); 68 | var renderResult = await engine.render(parserResult); 69 | renderResult.text.should.be('false'); 70 | }); 71 | 72 | test('{{faLSE }} should be rendered as false', () async { 73 | var engine = TemplateEngine(); 74 | var parserResult = await engine.parseTemplate( 75 | TextTemplate('{{faLSE }}'), 76 | ); 77 | var renderResult = await engine.render(parserResult); 78 | renderResult.text.should.be('false'); 79 | }); 80 | 81 | test('{{ false }} should be rendered as false', () async { 82 | var engine = TemplateEngine(); 83 | var parserResult = await engine.parseTemplate( 84 | TextTemplate('{{ false }}'), 85 | ); 86 | var renderResult = await engine.render(parserResult); 87 | renderResult.text.should.be('false'); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/data_type/custom_data_type_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | test( 8 | '{{1110b}} should be rendered as 14 by a custom binary data type', 9 | () async { 10 | var engine = TemplateEngine(); 11 | engine.dataTypes.insert(0, Binary()); 12 | var parseResult = await engine.parseTemplate(TextTemplate('{{1110b}}')); 13 | var renderResult = await engine.render(parseResult); 14 | renderResult.text.should.be('14'); 15 | }, 16 | ); 17 | } 18 | 19 | class Binary extends DataType { 20 | @override 21 | String get name => 'Binary'; 22 | 23 | @override 24 | String get description => 'a integer in binary format'; 25 | 26 | @override 27 | String get syntaxDescription => 28 | "A binary number is declared with '1's and '0's, " 29 | "followed by a lower case letter b (for binary)"; 30 | 31 | @override 32 | List get examples => []; 33 | 34 | @override 35 | Parser> createParser() => 36 | ((char('0') | char('1')).plus().flatten('binary integer expected') & 37 | char('b')) 38 | .trim() 39 | .map((values) => Value(int.parse(values.first, radix: 2))); 40 | } 41 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/data_type/num_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{0}} should be rendered as 0', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseTemplate(TextTemplate('{{0}}')); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('0'); 11 | }); 12 | 13 | test('{{-42}} should be rendered as -42', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseTemplate(TextTemplate('{{-42}}')); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('-42'); 18 | }); 19 | 20 | test('{{3.141}} should be rendered as 3.141', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseTemplate(TextTemplate('{{3.141}}')); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('3.141'); 25 | }); 26 | 27 | test('{{-1.2e5}} should be rendered as -120000.0', () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseTemplate(TextTemplate('{{-1.2e5}}')); 30 | var renderResult = await engine.render(parseResult); 31 | renderResult.text.should.be('-120000.0'); 32 | }); 33 | 34 | test('{{-1.2e-5}} should be rendered as -0.000012', () async { 35 | var engine = TemplateEngine(); 36 | var parseResult = await engine.parseTemplate(TextTemplate('{{-1.2e-5}}')); 37 | var renderResult = await engine.render(parseResult); 38 | renderResult.text.should.be('-0.000012'); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/data_type/string_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test("{{'Hello}} should return a correct error message", () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText("{{'Hello}}"); 9 | parseResult.errorMessage.should.be( 10 | 'Parse error in: \'{{\'Hello}}\':\n' 11 | ' 1:3: invalid tag syntax', 12 | ); 13 | }); 14 | 15 | test('{{Hello"}} should return a correct error message', () async { 16 | var engine = TemplateEngine(); 17 | var parseResult = await engine.parseText('{{Hello"}}'); 18 | parseResult.errorMessage.should.be( 19 | 'Parse error in: \'{{Hello"}}\':\n' 20 | ' 1:3: invalid tag syntax', 21 | ); 22 | }); 23 | 24 | test("{{'Hello'}} should render: 'Hello'", () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText("{{'Hello'}}"); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('Hello'); 29 | }); 30 | 31 | test('{{"Hello"}} should render: \'Hello\'', () async { 32 | var engine = TemplateEngine(); 33 | var parseResult = await engine.parseText('{{"Hello"}}'); 34 | var renderResult = await engine.render(parseResult); 35 | renderResult.text.should.be('Hello'); 36 | }); 37 | 38 | test("{{'Hello' }} should render: 'Hello'", () async { 39 | var engine = TemplateEngine(); 40 | var parseResult = await engine.parseText("{{'Hello' }}"); 41 | var renderResult = await engine.render(parseResult); 42 | renderResult.text.should.be('Hello'); 43 | }); 44 | 45 | test('{{ "Hello"}} should render: \'Hello\'', () async { 46 | var engine = TemplateEngine(); 47 | var parseResult = await engine.parseText('{{ "Hello"}}'); 48 | var renderResult = await engine.render(parseResult); 49 | renderResult.text.should.be('Hello'); 50 | }); 51 | 52 | test('{{\'Hel\' + \'l\' & "o"}} should render: \'Hello\'', () async { 53 | var engine = TemplateEngine(); 54 | var parseResult = await engine.parseText('{{\'Hel\' + \'l\' & "o"}}'); 55 | var renderResult = await engine.render(parseResult); 56 | renderResult.text.should.be('Hello'); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/expression_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('expressionParser(ParserContext()).end()', () { 7 | final engine = TemplateEngine(); 8 | final template = TextTemplate(''); 9 | final parser = expressionParser(ParserContext(engine, template)); 10 | final context = RenderContext( 11 | engine: engine, 12 | templateBeingRendered: template, 13 | parsedTemplates: [], 14 | ); 15 | 16 | test('should parse and render string literal', () async { 17 | final parseResult = parser.parse("'Hello'"); 18 | final renderResult = (await parseResult.value.render(context)); 19 | Should.satisfyAllConditions([ 20 | () => parseResult.value.should.beAssignableTo>(), 21 | () => renderResult.should.be('Hello'), 22 | ]); 23 | }); 24 | 25 | test('should parse and render numeric literal', () async { 26 | final parseResult = parser.parse("123.45"); 27 | var renderResult = (await parseResult.value.render(context)); 28 | Should.satisfyAllConditions([ 29 | () => renderResult.should.beAssignableTo(), 30 | () => (renderResult as num).should.beCloseTo(123.45, delta: 0.001), 31 | ]); 32 | }); 33 | 34 | test('should parse and render function call: length(\'Hello\')', () async { 35 | final parseResult = parser.parse("length('Hello')"); 36 | var renderResult = await parseResult.value.render(context); 37 | Should.satisfyAllConditions([ 38 | () => parseResult.value.should.beAssignableTo(), 39 | () => (renderResult as num).should.be(5), 40 | ]); 41 | }); 42 | 43 | test('should parse and render expression: length(\'Hello\') + 3', () async { 44 | final parseResult = parser.parse("length('Hello')+3"); 45 | var renderResult = await parseResult.value.render(context); 46 | 47 | Should.satisfyAllConditions([ 48 | () => parseResult.value.should.beAssignableTo(), 49 | () => (renderResult as num).should.be(8), 50 | ]); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/custom_function_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | var engine = TemplateEngine(); 7 | engine.functionGroups.add( 8 | FunctionGroup('Greeting', [GreetingWithParameter()]), 9 | ); 10 | test("'{{greeting()}}.' should render: 'Hello world.'", () async { 11 | var parseResult = await engine.parseText('{{greeting()}}.'); 12 | var renderResult = await engine.render(parseResult); 13 | renderResult.text.should.be('Hello world.'); 14 | }); 15 | 16 | test( 17 | "'{{greeting(\"Jane Doe\")}}.' should render: 'Hello Jane Doe.'", 18 | () async { 19 | var engine = TemplateEngine(); 20 | engine.functionGroups.add( 21 | FunctionGroup('Greeting', [GreetingWithParameter()]), 22 | ); 23 | var parseResult = await engine.parseText('{{greeting("Jane Doe")}}.'); 24 | var renderResult = await engine.render(parseResult); 25 | renderResult.text.should.be('Hello Jane Doe.'); 26 | }, 27 | ); 28 | } 29 | 30 | class GreetingWithParameter extends ExpressionFunction { 31 | GreetingWithParameter() 32 | : super( 33 | name: 'greeting', 34 | description: 'A tag that shows a greeting using attribute: name', 35 | parameters: [ 36 | Parameter( 37 | name: "name", 38 | presence: Presence.optionalWithDefaultValue('world'), 39 | ), 40 | ], 41 | function: (position, renderContext, parameters) => 42 | Future.value('Hello ${parameters['name']}'), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/documentation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | import 'package:http/http.dart' as http; 5 | import 'package:petitparser/petitparser.dart'; 6 | 7 | void main() { 8 | group('TemplateEngine Documentation Tags', () { 9 | final engine = TemplateEngine() 10 | ..functionGroups.clear() 11 | ..functionGroups.addAll([DocumentationFunctions(), DummyFunctionGroup()]) 12 | ..dataTypes.clear() 13 | ..dataTypes.add(Boolean()); 14 | 15 | test('should render tagDocumentation() correctly', () async { 16 | final parseResult = await engine.parseText('{{tagDocumentation()}}'); 17 | 18 | Should.satisfyAllConditions([ 19 | () => parseResult.errorMessage.should.beNullOrEmpty(), 20 | ]); 21 | 22 | final renderResult = await engine.render(parseResult); 23 | renderResult.text.should.contain( 24 | 'Evaluates an expression that can contain', 25 | ); 26 | renderResult.text.should.contain(''); 27 | }); 28 | 29 | test('should render dataTypeDocumentation() correctly', () async { 30 | final template = TextTemplate('{{dataTypeDocumentation()}}'); 31 | final parseResult = await engine.parseTemplate(template); 32 | 33 | Should.satisfyAllConditions([ 34 | () => parseResult.errorMessage.should.beNullOrEmpty(), 35 | ]); 36 | 37 | final renderResult = await engine.render(parseResult); 38 | 39 | Should.satisfyAllConditions([ 40 | () => renderResult.errorMessage.should.beNullOrEmpty(), 41 | () => renderResult.text.should.contain('## Boolean Data Type\n'), 42 | () => renderResult.text.should.contain('## String Data Type\n'), 43 | () => renderResult.text.should.contain('## Number Data Type\n'), 44 | ]); 45 | }); 46 | 47 | test('should render functionDocumentation() correctly', () async { 48 | final template = TextTemplate('{{functionDocumentation()}}'); 49 | final parseResult = await engine.parseTemplate(template); 50 | 51 | Should.satisfyAllConditions([ 52 | () => parseResult.errorMessage.should.beNullOrEmpty(), 53 | ]); 54 | 55 | final renderResult = await engine.render(parseResult); 56 | final expected = await FunctionDocumentation().function( 57 | '', 58 | RenderContext( 59 | engine: engine, 60 | templateBeingRendered: template, 61 | parsedTemplates: [], 62 | ), 63 | {'titleLevel': 1}, 64 | ); 65 | 66 | renderResult.text.should.be(expected); 67 | }); 68 | }); 69 | 70 | test('example documentation should contain existing urls only', () async { 71 | var engine = TemplateEngine(); 72 | var parseResult = await engine.parseText('{{exampleDocumentation()}}'); 73 | parseResult.errorMessage.should.beNullOrEmpty(); 74 | var renderResult = await engine.render(parseResult); 75 | var text = renderResult.text; 76 | var urls = urlParser().allMatches(text); 77 | final errors = []; 78 | for (final url in urls) { 79 | Uri? uri; 80 | try { 81 | uri = Uri.parse(url); 82 | if (!uri.hasScheme || !uri.hasAuthority) { 83 | errors.add('Invalid URI structure: $url'); 84 | continue; 85 | } 86 | } catch (_) { 87 | errors.add('Invalid URI format: $url'); 88 | continue; 89 | } 90 | 91 | try { 92 | final response = await http.head(uri); 93 | if (response.statusCode < 200 || response.statusCode >= 400) { 94 | errors.add('URL "$url" responded with status ${response.statusCode}'); 95 | } 96 | } catch (e) { 97 | errors.add('Request to "$url" failed: $e'); 98 | } 99 | } 100 | 101 | errors.should.beEmpty(); 102 | }); 103 | } 104 | 105 | Parser urlParser() => 106 | ((stringIgnoreCase('https://') | stringIgnoreCase('http://')) & 107 | (letter() | digit() | char('-') | pattern('\$_.+! *\'(),/&?=: %')) 108 | .plus()) 109 | .flatten(); 110 | 111 | class DummyFunctionGroup extends FunctionGroup { 112 | DummyFunctionGroup() : super('Test Functions', [DummyFunction()]); 113 | } 114 | 115 | class DummyFunction extends ExpressionFunction { 116 | DummyFunction() 117 | : super( 118 | name: 'testFunction', 119 | description: 'TestDescription', 120 | exampleCode: ProjectFilePath( 121 | 'test/src/parser/tag/expression/function/math/exp_test.dart', 122 | ), 123 | parameters: [ 124 | Parameter( 125 | name: 'parameter1', 126 | presence: Presence.optionalWithDefaultValue('Hello'), 127 | ), 128 | Parameter(name: 'parameter2'), 129 | Parameter(name: 'parameter3'), 130 | ], 131 | function: (position, renderContext, parameters) => 132 | Future.value('Dummy'), 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import.md.template: -------------------------------------------------------------------------------- 1 | {{importTemplate('test/src/parser/tag/expression/function/import/to_import.md.template')}} 2 | Hello World. -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_json_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | const path = 'test/src/parser/tag/expression/function/import/person.json'; 7 | 8 | test('test importJson on existing json file', () async { 9 | DataMap jsonMap = { 10 | 'person': { 11 | 'name': 'John Doe', 12 | 'age': 30, 13 | 'child': {'name': 'Jane Doe', 'age': 5}, 14 | }, 15 | }; 16 | var engine = TemplateEngine(); 17 | var parseResult = await engine.parseText("{{importJson('$path')}}"); 18 | var renderResult = await engine.render(parseResult); 19 | renderResult.text.should.be(jsonMap.toString()); 20 | }); 21 | 22 | test('test assigning importJson to a variable', () async { 23 | var engine = TemplateEngine(); 24 | var input = 25 | "{{json=importJson('$path')}}" 26 | "{{json.person.child.name}}"; 27 | var parseResult = await engine.parseText(input); 28 | var renderResult = await engine.render(parseResult); 29 | renderResult.text.should.be('Jane Doe'); 30 | }); 31 | 32 | test('test importJson with none existing file', () async { 33 | var engine = TemplateEngine(); 34 | var input = 35 | "{{json=importJson('none_existing.json')}}" 36 | "{{json.person.child.name}}"; 37 | var parseResult = await engine.parseText(input); 38 | var renderResult = await engine.render(parseResult); 39 | renderResult.text.should.be('{{ERROR}}{{ERROR}}'); 40 | renderResult.errorMessage.should.contain( 41 | "Render errors in: " 42 | "'{{json=importJson('none_existing.json')}", 43 | ); 44 | renderResult.errorMessage.should.contain( 45 | " 1:8: Error importing a Json file: " 46 | "Error reading: none_existing.json, " 47 | "PathNotFoundException: Cannot open file, path = 'none_existing.json'", 48 | ); 49 | renderResult.errorMessage.should.contain( 50 | " 1:44: Variable " 51 | "does not exist: json", 52 | ); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_pure_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('The importPure function should import a file as is', () async { 7 | var engine = TemplateEngine(); 8 | var filePath = 'test/src/template_engine_template_example_test.dart'; 9 | var input = "{{importPure('$filePath')}}"; 10 | var parseResult = await engine.parseText(input); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.errorMessage.should.beNullOrEmpty(); 13 | var expected = await readFromFilePath(filePath); 14 | renderResult.text.should.be(expected); 15 | }); 16 | 17 | test('test importPure with none existing file', () async { 18 | var engine = TemplateEngine(); 19 | var input = "{{importPure('none_existent.dart')}}"; 20 | var parseResult = await engine.parseText(input); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('{{ERROR}}'); 23 | renderResult.errorMessage.should.contain( 24 | "Render error in: '{{importPure('none_existent.dart')}}':", 25 | ); 26 | renderResult.errorMessage.should.contain( 27 | "1:3: Error importing a pure file: Error reading: " 28 | "none_existent.dart, PathNotFoundException: Cannot open file, " 29 | "path = 'none_existent.dart'", 30 | ); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_template_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:documentation_builder/documentation_builder.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | test('The import function should import an template file', () async { 8 | var engine = TemplateEngine(); 9 | var templatePath = ProjectFilePath( 10 | 'test/src/parser/tag/expression/function/import/import.md.template', 11 | ); 12 | var template = FileTemplate.fromProjectFilePath(templatePath); 13 | var parseResult = await engine.parseTemplate(template); 14 | var renderResult = await engine.render(parseResult); 15 | renderResult.errorMessage.should.beNullOrEmpty(); 16 | renderResult.text.should.be( 17 | 'Line to import.$newLine' 18 | 'Hello World.', 19 | ); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_template_with_errors1.md.template: -------------------------------------------------------------------------------- 1 | {{importTemplate('test/src/parser/tag/expression/function/import/none_existing.file')}} 2 | {{importTemplate('test/src/parser/tag/expression/function/import/import_template_with_errors2.md.template')}} 3 | 1: Hello {{name}}. This is wrong: }} -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_template_with_errors2.md.template: -------------------------------------------------------------------------------- 1 | {{importTemplate('test/src/parser/tag/expression/function/import/none_existing.file')}} 2 | {{importTemplate('test/src/parser/tag/expression/function/import/import_template_with_errors3.md.template')}} 3 | 2: Hello {{name}}. This is wrong: }} -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_template_with_errors3.md.template: -------------------------------------------------------------------------------- 1 | {{importTemplate('/invalid.path')}} 2 | 3: Hello {{name}}. This is wrong: }} -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_xml_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | const path = 'test/src/parser/tag/expression/function/import/person.xml'; 7 | 8 | test('test importXml on existing xml file', () async { 9 | DataMap xmlMap = { 10 | 'person': { 11 | 'name': 'John Doe', 12 | 'age': 30, 13 | 'child': {'name': 'Jane Doe', 'age': 5}, 14 | }, 15 | }; 16 | var engine = TemplateEngine(); 17 | var parseResult = await engine.parseText("{{importXml('$path')}}"); 18 | var renderResult = await engine.render(parseResult); 19 | renderResult.text.should.be(xmlMap.toString()); 20 | }); 21 | 22 | test('test assigning importXml to a variable', () async { 23 | var engine = TemplateEngine(); 24 | var input = 25 | "{{xml=importXml('$path')}}" 26 | "{{xml.person.child.name}}"; 27 | var parseResult = await engine.parseText(input); 28 | var renderResult = await engine.render(parseResult); 29 | renderResult.text.should.be('Jane Doe'); 30 | }); 31 | 32 | test('test importXml with none existing file', () async { 33 | var engine = TemplateEngine(); 34 | var input = 35 | "{{xml=importXml('none_existing.xml')}}" 36 | "{{xml.person.child.name}}"; 37 | var parseResult = await engine.parseText(input); 38 | var renderResult = await engine.render(parseResult); 39 | renderResult.text.should.be('{{ERROR}}{{ERROR}}'); 40 | renderResult.errorMessage.should.contain( 41 | "Render errors in: " 42 | "'{{xml=importXml('none_existing.xml')}", 43 | ); 44 | renderResult.errorMessage.should.contain( 45 | " 1:7: Error importing a XML file: " 46 | "Error reading: none_existing.xml, " 47 | "PathNotFoundException: Cannot open file, path = 'none_existing.xml'", 48 | ); 49 | renderResult.errorMessage.should.contain("none_existing.xml"); 50 | renderResult.errorMessage.should.contain( 51 | " 1:41: Variable " 52 | "does not exist: xml", 53 | ); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/import_yaml_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | const path = 'test/src/parser/tag/expression/function/import/person.yaml'; 7 | 8 | test('test importYaml on existing yaml file', () async { 9 | DataMap yamlMap = { 10 | 'person': { 11 | 'name': 'John Doe', 12 | 'age': 30, 13 | 'child': {'name': 'Jane Doe', 'age': 5}, 14 | }, 15 | }; 16 | var engine = TemplateEngine(); 17 | var parseResult = await engine.parseText("{{importYaml('$path')}}"); 18 | var renderResult = await engine.render(parseResult); 19 | renderResult.text.should.be(yamlMap.toString()); 20 | }); 21 | 22 | test('test assigning importYaml to a variable', () async { 23 | var engine = TemplateEngine(); 24 | var input = 25 | "{{yaml=importYaml('$path')}}" 26 | "{{yaml.person.child.name}}"; 27 | var parseResult = await engine.parseText(input); 28 | var renderResult = await engine.render(parseResult); 29 | renderResult.text.should.be('Jane Doe'); 30 | }); 31 | 32 | test('test importYaml with none existing file', () async { 33 | var engine = TemplateEngine(); 34 | var input = 35 | "{{yaml=importYaml('none_existing.yaml')}}" 36 | "{{yaml.person.child.name}}"; 37 | var parseResult = await engine.parseText(input); 38 | var renderResult = await engine.render(parseResult); 39 | renderResult.text.should.be('{{ERROR}}{{ERROR}}'); 40 | renderResult.errorMessage.should.contain( 41 | "Render errors in: " 42 | "'{{yaml=importYaml('none_existing.yaml')}", 43 | ); 44 | renderResult.errorMessage.should.contain( 45 | " 1:8: Error importing a YAML file: " 46 | "Error reading: none_existing.yaml, " 47 | "PathNotFoundException: Cannot open file, path = 'none_existing.yaml'", 48 | ); 49 | renderResult.errorMessage.should.contain("none_existing.yaml"); 50 | renderResult.errorMessage.should.contain( 51 | " 1:44: Variable " 52 | "does not exist: yaml", 53 | ); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/multiple_import_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('Multiple imports of the same file, should only parse it once', () async { 7 | var engine = TemplateEngine(); 8 | var path = 9 | 'test/src/parser/tag/expression/function/import/to_import.md.template'; 10 | var input = 11 | "{{importTemplate('$path')}}\n" 12 | "{{importTemplate('$path')}}\n" 13 | "Hello World.\n" 14 | "{{importTemplate('$path')}}"; 15 | var parseResults = await engine.parseText(input); 16 | var parseResult = parseResults.children.first; 17 | var template = parseResult.template; 18 | var renderContext = RenderContext( 19 | engine: engine, 20 | templateBeingRendered: template, 21 | variables: {}, 22 | parsedTemplates: parseResults.children, 23 | ); 24 | var result = await parseResult.render(renderContext); 25 | 26 | renderContext.parsedTemplates.length.should.be(2); 27 | 28 | result.text.should.be( 29 | 'Line to import.\n' 30 | 'Line to import.\n' 31 | 'Hello World.\n' 32 | 'Line to import.', 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "person": { 3 | "name": "John Doe", 4 | "age": 30, 5 | "child": { 6 | "name": "Jane Doe", 7 | "age": 5 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/person.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | John Doe 4 | 30 5 | 6 | Jane Doe 7 | 5 8 | 9 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/person.yaml: -------------------------------------------------------------------------------- 1 | person: 2 | name: John Doe 3 | age: 30 4 | child: 5 | name: Jane Doe 6 | age: 5 -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/import/to_import.md.template: -------------------------------------------------------------------------------- 1 | Line to import. -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/acos_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{acos(0.5)}} should render as: ${acos(0.5).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseText('{{acos(0.5)}}'); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(acos(0.5).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/asin_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{asin(0.5)}} should render as: ${asin(0.5).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{asin(0.5)}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(asin(0.5).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/atan_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{atan(0.5)}} should render as: ${atan(0.5).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{atan(0.5)}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(atan(0.5).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/cos_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{cos(7)}} should render as: ${cos(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{cos(7)}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(cos(7).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/exp_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{exp(7)}} should render as: ${exp(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{exp(7)}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(exp(7).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/log_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{log(7)}} should render as: ${log(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseTemplate(TextTemplate('{{log(7)}}')); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(log(7).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/round_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{round(4.5)}} should render as: 5', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{round(4.5)}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('5'); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/sin_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{sin(7)}} should render as: ${sin(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseText('{{sin(7)}}'); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(sin(7).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/sqrt_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{sqrt(9)}} should render as: ${sqrt(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseText('{{sqrt(9)}}'); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(sqrt(9).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/math/tan_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:template_engine/template_engine.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('{{tan(7)}} should render as: ${tan(7).toString()}', () async { 9 | var engine = TemplateEngine(); 10 | var parseResult = await engine.parseText('{{tan(7)}}'); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be(tan(7).toString()); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/string/length_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{length(\'Hello\'}} should render: 5', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{length(\'Hello\')}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('5'); 11 | }); 12 | 13 | test('{{length(\'Hello\' + " world"}} should render: 5', () async { 14 | var engine = TemplateEngine(); 15 | var input = '{{length(\'Hello\' + " world")}}'; 16 | var parseResult = await engine.parseText(input); 17 | var renderResult = await engine.render(parseResult); 18 | renderResult.text.should.be('11'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/template/source_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('readSource from relative ProjectFilePath (or URI)', () async { 7 | var source = 'test/src/parser/tag/expression/function/import/person.yaml'; 8 | var text = await readSource(source); 9 | text.should.contain('person'); 10 | text.should.contain('name'); 11 | text.should.contain(':'); 12 | text.should.contain('John Doe'); 13 | text.should.contain('age'); 14 | text.should.contain('child'); 15 | text.should.contain('Jane Doe'); 16 | }); 17 | 18 | test('readSource from URI with FILE schema', () async { 19 | var source = ProjectFilePath( 20 | 'test/src/parser/tag/expression/function/import/person.yaml', 21 | ).file.absolute.uri.toString(); 22 | var text = await readSource(source); 23 | text.should.contain('person'); 24 | text.should.contain('name'); 25 | text.should.contain(':'); 26 | text.should.contain('John Doe'); 27 | text.should.contain('age'); 28 | text.should.contain('child'); 29 | text.should.contain('Jane Doe'); 30 | }); 31 | 32 | test( 33 | 'readSource with absolute file path (in style of operating system)', 34 | () async { 35 | var source = ProjectFilePath( 36 | 'test/src/parser/tag/expression/function/import/person.yaml', 37 | ).file.absolute.path; 38 | var text = await readSource(source); 39 | text.should.contain('person'); 40 | text.should.contain('name'); 41 | text.should.contain(':'); 42 | text.should.contain('John Doe'); 43 | text.should.contain('age'); 44 | text.should.contain('child'); 45 | text.should.contain('Jane Doe'); 46 | }, 47 | ); 48 | 49 | test('readSource with URI with HTTPS schema', () async { 50 | var source = 51 | 'https://raw.githubusercontent.com/domain-centric/template_engine/' 52 | 'main/test/src/parser/tag/expression/function/import/person.yaml'; 53 | var text = await readSource(source); 54 | text.should.contain('person'); 55 | text.should.contain('name'); 56 | text.should.contain(':'); 57 | text.should.contain('John Doe'); 58 | text.should.contain('age'); 59 | text.should.contain('child'); 60 | text.should.contain('Jane Doe'); 61 | }); 62 | 63 | test('readSource with URI with HTTPS schema to none existing host', () async { 64 | try { 65 | await readSource('https://none_existing.com'); 66 | throw Exception('Should have failed'); 67 | } on Exception catch (e) { 68 | e.toString().should.contain("Failed host lookup: 'none_existing.com'"); 69 | } 70 | }); 71 | 72 | test( 73 | 'readSource with URI with HTTPS schema to a none existing page', 74 | () async { 75 | try { 76 | await readSource('https://your-site.webflow.io/does-not-exist'); 77 | throw Exception('Should have failed'); 78 | } on Exception catch (e) { 79 | e.toString().should.contain( 80 | "Error reading: https://your-site.webflow.io/does-not-exist," 81 | " status code: 404", 82 | ); 83 | } 84 | }, 85 | ); 86 | 87 | test('readSource with of a none existing file path', () async { 88 | try { 89 | await readSource('none_existing.file'); 90 | throw Exception('Should have failed'); 91 | } on Exception catch (e) { 92 | e.toString().should.contain( 93 | "Error reading: none_existing.file, " 94 | "PathNotFoundException: Cannot open file", 95 | ); 96 | } 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/function/template/template_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine with TemplateSource function', () { 7 | test( 8 | 'should render templateSource() to the template source path', 9 | () async { 10 | final engine = TemplateEngine(); 11 | final template = TemplateWithTemplateSourceFunction(); 12 | 13 | final parseResult = await engine.parseTemplate(template); 14 | final renderResult = await engine.render(parseResult); 15 | 16 | renderResult.text.should.be(template.source); 17 | }, 18 | ); 19 | }); 20 | } 21 | 22 | class TemplateWithTemplateSourceFunction extends Template { 23 | TemplateWithTemplateSourceFunction() { 24 | source = 'doc/template/generic/generated.md.template'; 25 | sourceTitle = source; 26 | text = Future.value('{{templateSource()}}'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/identifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/src/parser/tag/expression/identifier.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('IdentifierName class', () { 7 | test('valid function name should not throw an exception', () { 8 | Should.notThrowException( 9 | () => IdentifierName.validate('goodFunctionName'), 10 | ); 11 | }); 12 | test('valid function name with numbers should not throw an exception', () { 13 | Should.notThrowException( 14 | () => IdentifierName.validate('good1Function2Name3'), 15 | ); 16 | }); 17 | 18 | test('Invalid identifier name starting with a capital letter ' 19 | 'should throw exception', () async { 20 | Should.throwException( 21 | () => IdentifierName.validate('Wrong'), 22 | )!.message.should.be( 23 | "Invalid identifier name: 'Wrong', " 24 | "lowercase letter expected at position 0", 25 | ); 26 | }); 27 | 28 | test( 29 | 'Invalid identifier name starting with a number should throw exception', 30 | () async { 31 | Should.throwException( 32 | () => IdentifierName.validate('1Wrong'), 33 | )!.message.should.be( 34 | "Invalid identifier name: '1Wrong', " 35 | "lowercase letter expected at position 0", 36 | ); 37 | }, 38 | ); 39 | 40 | test( 41 | 'Invalid identifier name starting with a symbol should throw exception', 42 | () async { 43 | Should.throwException( 44 | () => IdentifierName.validate('!Wrong'), 45 | )!.message.should.be( 46 | "Invalid identifier name: '!Wrong', " 47 | "lowercase letter expected at position 0", 48 | ); 49 | }, 50 | ); 51 | 52 | test( 53 | 'Invalid identifier name with a symbol in the middle should throw exception', 54 | () { 55 | Should.throwException( 56 | () => IdentifierName.validate('wro!ng'), 57 | )!.message.should.be( 58 | "Invalid identifier name: 'wro!ng', " 59 | "letter OR digit expected at position 3", 60 | ); 61 | }, 62 | ); 63 | 64 | test('Invalid Identifier name with a symbol in the end ' 65 | 'should throw exception', () { 66 | Should.throwException( 67 | () => IdentifierName.validate('wrong!'), 68 | )!.message.should.be( 69 | "Invalid identifier name: 'wrong!', " 70 | "letter OR digit expected at position 5", 71 | ); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/addition/bool_or_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{false|false}} should render: false', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{false|false}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('false'); 11 | }); 12 | test('{{false|true}} should render: true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{false|true}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | test('{{true|false}} should render: true', () async { 19 | var engine = TemplateEngine(); 20 | var parseResult = await engine.parseText('{{true|false}}'); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('true'); 23 | }); 24 | test('{{true|true}} should render: true', () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText('{{true|true}}'); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('true'); 29 | }); 30 | 31 | test('{{false | FALSE | falsE}} should render: false', () async { 32 | var engine = TemplateEngine(); 33 | var parseResult = await engine.parseText('{{false | FALSE | falsE}}'); 34 | var renderResult = await engine.render(parseResult); 35 | renderResult.text.should.be('false'); 36 | }); 37 | test('{{ true | FALSE | truE }} should render: true', () async { 38 | var engine = TemplateEngine(); 39 | var parseResult = await engine.parseText('{{ true | FALSE | truE }}'); 40 | var renderResult = await engine.render(parseResult); 41 | renderResult.text.should.be('true'); 42 | }); 43 | test('{{"text"|"text"}} should result in an error', () async { 44 | var engine = TemplateEngine(); 45 | var parseResult = await engine.parseText('{{"text"|"text"}}'); 46 | var renderResult = await engine.render(parseResult); 47 | renderResult.text.should.be('{{ERROR}}'); 48 | renderResult.errorMessage.should.be( 49 | 'Render error in: \'{{"text"|"text"}}\':\n' 50 | ' 1:9: left and right of the | operator must be a boolean', 51 | ); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/addition/num_addition_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{2+3}} should render: 5', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{2+3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('5'); 11 | }); 12 | 13 | test('{{ 2 + 3 + 4 }} should render: 9', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{ 2 + 3 + 4 }}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('9'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/addition/num_subtract_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{5-3}} should render: 2', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{5-3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('2'); 11 | }); 12 | 13 | test('{{ 5 - 3 - 4 }} should render: -2', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{ 5 - 3 - 4 }}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('-2'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/addition/string_concatenate_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{"Hel" + \'lo\'}} should render: "Hello"', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{"Hel" + \'lo\'}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('Hello'); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/assignment/assignment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{x=12}} should return ""', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{x=12}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be(''); 11 | }); 12 | 13 | test('{{x=12}}{{x}} should return 12', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{x=12}}{{x}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('12'); 18 | }); 19 | 20 | test('{{ x = 12 }}{{ x }} should return 12', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{ x = 12 }}{{ x }}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('12'); 25 | }); 26 | 27 | test('{{x=12}}{{x+3}} should return 15', () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseText('{{x=12}}{{x+3}}'); 30 | var renderResult = await engine.render(parseResult); 31 | renderResult.text.should.be('15'); 32 | }); 33 | 34 | test('{{x=2}}{{x=x+3}}{{x}} should return 5', () async { 35 | var engine = TemplateEngine(); 36 | var parseResult = await engine.parseText('{{x=2}}{{x=x+3}}{{x}}'); 37 | var renderResult = await engine.render(parseResult); 38 | renderResult.text.should.be('5'); 39 | }); 40 | 41 | test('{{1=2}} should result in an error', () async { 42 | var engine = TemplateEngine(); 43 | var parseResult = await engine.parseText('{{1=2}}'); 44 | var renderResult = await engine.render(parseResult); 45 | renderResult.text.should.be('{{ERROR}}'); 46 | renderResult.errorMessage.should.be( 47 | "Render error in: '{{1=2}}':\n" 48 | " 1:4: The left side of the = operation " 49 | "must be a valid variable name", 50 | ); 51 | }); 52 | 53 | test('{{x.y=2}} should result in an error', () async { 54 | var engine = TemplateEngine(); 55 | var parseResult = await engine.parseText('{{x.y=2}}'); 56 | var renderResult = await engine.render(parseResult); 57 | renderResult.text.should.be('{{ERROR}}'); 58 | renderResult.errorMessage.should.be( 59 | "Render error in: '{{x.y=2}}':\n" 60 | " 1:6: The left side of the = operation " 61 | "must be a name of a root variable (not contain dots)", 62 | ); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/equals_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{5==2+3}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{5==2+3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | 13 | test('{{ 5 == 2 + 3 }} should return true', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{ 5 == 2 + 3 }}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('true'); 18 | }); 19 | 20 | test('{{5==4}} should return false', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{5==4}}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('false'); 25 | }); 26 | 27 | test("{{name=='World'}} should return true", () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseText("{{name=='World'}}"); 30 | var renderResult = await engine.render(parseResult, {'name': 'World'}); 31 | renderResult.text.should.be('true'); 32 | }); 33 | 34 | test("{{name=='Joe'}} should return false", () async { 35 | var engine = TemplateEngine(); 36 | var parseResult = await engine.parseText("{{name=='Joe'}}"); 37 | var renderResult = await engine.render(parseResult, {'name': 'World'}); 38 | renderResult.text.should.be('false'); 39 | }); 40 | 41 | test("{{'1'==1}} should result in false", () async { 42 | var engine = TemplateEngine(); 43 | var parseResult = await engine.parseText("{{'1'==1}}"); 44 | var renderResult = await engine.render(parseResult); 45 | renderResult.text.should.be('false'); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/greater_than_or_equal_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{2>=1}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{2>=1}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | test('{{2>=2}} should return true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{2>=2}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | test('{{6>=2+3}} should return true', () async { 19 | var engine = TemplateEngine(); 20 | var parseResult = await engine.parseText('{{6>=2+3}}'); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('true'); 23 | }); 24 | test('{{6>=3+3}} should return true', () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText('{{6>=3+3}}'); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('true'); 29 | }); 30 | 31 | test('{{ 6 >= 3 + 3 }} should return true', () async { 32 | var engine = TemplateEngine(); 33 | var parseResult = await engine.parseText('{{ 6 >= 3 + 3 }}'); 34 | var renderResult = await engine.render(parseResult); 35 | renderResult.text.should.be('true'); 36 | }); 37 | 38 | test('{{1>=2}} should return false', () async { 39 | var engine = TemplateEngine(); 40 | var parseResult = await engine.parseText('{{1>=2}}'); 41 | var renderResult = await engine.render(parseResult); 42 | renderResult.text.should.be('false'); 43 | }); 44 | 45 | test("{{two>=1}} should return true", () async { 46 | var engine = TemplateEngine(); 47 | var parseResult = await engine.parseText("{{two>=1}}"); 48 | var renderResult = await engine.render(parseResult, {'two': 2}); 49 | renderResult.text.should.be('true'); 50 | }); 51 | 52 | test("{{two>=3}} should return false", () async { 53 | var engine = TemplateEngine(); 54 | var parseResult = await engine.parseText("{{two>=3}}"); 55 | var renderResult = await engine.render(parseResult, {'two': 2}); 56 | renderResult.text.should.be('false'); 57 | }); 58 | 59 | test("{{'2'>=1}} should result in false", () async { 60 | var engine = TemplateEngine(); 61 | var parseResult = await engine.parseText("{{'2'>=1}}"); 62 | var renderResult = await engine.render(parseResult); 63 | renderResult.text.should.be('{{ERROR}}'); 64 | renderResult.errorMessage.should.be( 65 | "Render error in: '{{'2'>=1}}':\n" 66 | " 1:6: left of the >= operator must be a number", 67 | ); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/greater_than_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{2>1}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{2>1}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | test('{{6>2+3}} should return true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{6>2+3}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | 19 | test('{{ 6 > 2 + 3 }} should return true', () async { 20 | var engine = TemplateEngine(); 21 | var parseResult = await engine.parseText('{{ 6 > 2 + 3 }}'); 22 | var renderResult = await engine.render(parseResult); 23 | renderResult.text.should.be('true'); 24 | }); 25 | 26 | test('{{1>2}} should return false', () async { 27 | var engine = TemplateEngine(); 28 | var parseResult = await engine.parseText('{{1>2}}'); 29 | var renderResult = await engine.render(parseResult); 30 | renderResult.text.should.be('false'); 31 | }); 32 | 33 | test("{{two>1}} should return true", () async { 34 | var engine = TemplateEngine(); 35 | var parseResult = await engine.parseText("{{two>1}}"); 36 | var renderResult = await engine.render(parseResult, {'two': 2}); 37 | renderResult.text.should.be('true'); 38 | }); 39 | 40 | test("{{two>3}} should return false", () async { 41 | var engine = TemplateEngine(); 42 | var parseResult = await engine.parseText("{{two>3}}"); 43 | var renderResult = await engine.render(parseResult, {'two': 2}); 44 | renderResult.text.should.be('false'); 45 | }); 46 | 47 | test("{{'2'>1}} should result in false", () async { 48 | var engine = TemplateEngine(); 49 | var parseResult = await engine.parseText("{{'2'>1}}"); 50 | var renderResult = await engine.render(parseResult); 51 | renderResult.text.should.be('{{ERROR}}'); 52 | renderResult.errorMessage.should.be( 53 | "Render error in: '{{'2'>1}}':\n" 54 | " 1:6: left of the > operator must be a number", 55 | ); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/less_than_or_equal_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{1<=2}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{1<=2}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | test('{{2<=2}} should return true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{2<=2}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | test('{{5<=2+3}} should return true', () async { 19 | var engine = TemplateEngine(); 20 | var parseResult = await engine.parseText('{{5<=2+3}}'); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('true'); 23 | }); 24 | test('{{6<=3+3}} should return true', () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText('{{6<=3+3}}'); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('true'); 29 | }); 30 | 31 | test('{{ 6 <= 3 + 3 }} should return true', () async { 32 | var engine = TemplateEngine(); 33 | var parseResult = await engine.parseText('{{ 6 <= 3 + 3 }}'); 34 | var renderResult = await engine.render(parseResult); 35 | renderResult.text.should.be('true'); 36 | }); 37 | 38 | test('{{1<=2}} should return true', () async { 39 | var engine = TemplateEngine(); 40 | var parseResult = await engine.parseText('{{1<=2}}'); 41 | var renderResult = await engine.render(parseResult); 42 | renderResult.text.should.be('true'); 43 | }); 44 | 45 | test("{{two<=1}} should return false", () async { 46 | var engine = TemplateEngine(); 47 | var parseResult = await engine.parseText("{{two<=1}}"); 48 | var renderResult = await engine.render(parseResult, {'two': 2}); 49 | renderResult.text.should.be('false'); 50 | }); 51 | 52 | test("{{two<=3}} should return true", () async { 53 | var engine = TemplateEngine(); 54 | var parseResult = await engine.parseText("{{two<=3}}"); 55 | var renderResult = await engine.render(parseResult, {'two': 2}); 56 | renderResult.text.should.be('true'); 57 | }); 58 | 59 | test("{{'2'<=1}} should result in false", () async { 60 | var engine = TemplateEngine(); 61 | var parseResult = await engine.parseText("{{'2'<=1}}"); 62 | var renderResult = await engine.render(parseResult); 63 | renderResult.text.should.be('{{ERROR}}'); 64 | renderResult.errorMessage.should.be( 65 | "Render error in: '{{'2'<=1}}':\n" 66 | " 1:6: left of the <= operator must be a number", 67 | ); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/less_than_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{1<2}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{1<2}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | test('{{4<2+3}} should return true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{4<2+3}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | 19 | test('{{ 4 < 2 + 3 }} should return true', () async { 20 | var engine = TemplateEngine(); 21 | var parseResult = await engine.parseText('{{ 4 < 2 + 3 }}'); 22 | var renderResult = await engine.render(parseResult); 23 | renderResult.text.should.be('true'); 24 | }); 25 | 26 | test('{{2<1}} should return false', () async { 27 | var engine = TemplateEngine(); 28 | var parseResult = await engine.parseText('{{2<1}}'); 29 | var renderResult = await engine.render(parseResult); 30 | renderResult.text.should.be('false'); 31 | }); 32 | 33 | test("{{two<1}} should return false", () async { 34 | var engine = TemplateEngine(); 35 | var parseResult = await engine.parseText("{{two<1}}"); 36 | var renderResult = await engine.render(parseResult, {'two': 2}); 37 | renderResult.text.should.be('false'); 38 | }); 39 | 40 | test("{{two<3}} should return true", () async { 41 | var engine = TemplateEngine(); 42 | var parseResult = await engine.parseText("{{two<3}}"); 43 | var renderResult = await engine.render(parseResult, {'two': 2}); 44 | renderResult.text.should.be('true'); 45 | }); 46 | 47 | test("{{'2'<1}} should result in false", () async { 48 | var engine = TemplateEngine(); 49 | var parseResult = await engine.parseText("{{'2'<1}}"); 50 | var renderResult = await engine.render(parseResult); 51 | renderResult.text.should.be('{{ERROR}}'); 52 | renderResult.errorMessage.should.be( 53 | "Render error in: '{{'2'<1}}':\n" 54 | " 1:6: left of the < operator must be a number", 55 | ); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/comparison/not_equals_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{4!=2+3}} should return true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{4!=2+3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | 13 | test('{{ 4 != 2 + 3 }} should return true', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{ 4 != 2 + 3 }}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('true'); 18 | }); 19 | 20 | test('{{4!=4}} should return false', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{4!=4}}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('false'); 25 | }); 26 | 27 | test("{{name!='World'}} should return true", () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseText("{{name!='World'}}"); 30 | var renderResult = await engine.render(parseResult, {'name': 'Joe'}); 31 | renderResult.text.should.be('true'); 32 | }); 33 | 34 | test("{{name!='Devil'}} should return true", () async { 35 | var engine = TemplateEngine(); 36 | var parseResult = await engine.parseText("{{name!='Devil'}}"); 37 | var renderResult = await engine.render(parseResult, {'name': 'Joe'}); 38 | renderResult.text.should.be('true'); 39 | }); 40 | 41 | test("{{'1'!=1}} should result in true", () async { 42 | var engine = TemplateEngine(); 43 | var parseResult = await engine.parseText("{{'1'!=1}}"); 44 | var renderResult = await engine.render(parseResult); 45 | renderResult.text.should.be('true'); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/custom_operator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('Custom operator : should work correctly', () async { 7 | var engine = TemplateEngine(); 8 | var group = engine.operatorGroups.firstWhere( 9 | (group) => group is Multiplication, 10 | ); 11 | group.add(DivideOperator()); 12 | var parseResult = await engine.parseText('{{6 : 3}}'); 13 | var renderResult = await engine.render(parseResult); 14 | renderResult.text.should.be('2.0'); 15 | }); 16 | } 17 | 18 | class DivideOperator extends OperatorWith2Values { 19 | DivideOperator() 20 | : super( 21 | name: 'Divide', 22 | symbol: ':', 23 | associativity: OperatorAssociativity.left, 24 | variants: [ 25 | TwoValueOperatorVariant( 26 | description: 'Divides 2 numbers', 27 | expressionExample: '{{6:4}}', 28 | expressionExampleResult: '1.5', 29 | function: (left, right) => left / right, 30 | ), 31 | ], 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/bool_and_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{false&false}} should render: false', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{false&false}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('false'); 11 | }); 12 | test('{{false&true}} should render: false', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{false&true}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('false'); 17 | }); 18 | test('{{true&false}} should render: false', () async { 19 | var engine = TemplateEngine(); 20 | var parseResult = await engine.parseText('{{true&false}}'); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('false'); 23 | }); 24 | test('{{true&true}} should render: true', () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText('{{true&true}}'); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('true'); 29 | }); 30 | 31 | test('{{false & true & false}} should render: false', () async { 32 | var engine = TemplateEngine(); 33 | var parseResult = await engine.parseText('{{false & true & false}}'); 34 | var renderResult = await engine.render(parseResult); 35 | renderResult.text.should.be('false'); 36 | }); 37 | test('{{ true & TRUE & truE }} should render: true', () async { 38 | var engine = TemplateEngine(); 39 | var parseResult = await engine.parseText('{{ true & TRUE & truE }}'); 40 | var renderResult = await engine.render(parseResult); 41 | renderResult.text.should.be('true'); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/bool_xor_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{false^false}} should render: false', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{false^false}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('false'); 11 | }); 12 | test('{{false^true}} should render: true', () async { 13 | var engine = TemplateEngine(); 14 | var parseResult = await engine.parseText('{{false^true}}'); 15 | var renderResult = await engine.render(parseResult); 16 | renderResult.text.should.be('true'); 17 | }); 18 | test('{{true^false}} should render: true', () async { 19 | var engine = TemplateEngine(); 20 | var parseResult = await engine.parseText('{{true^false}}'); 21 | var renderResult = await engine.render(parseResult); 22 | renderResult.text.should.be('true'); 23 | }); 24 | test('{{true^true}} should render: false', () async { 25 | var engine = TemplateEngine(); 26 | var parseResult = await engine.parseText('{{true^true}}'); 27 | var renderResult = await engine.render(parseResult); 28 | renderResult.text.should.be('false'); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/num_divide_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{6/4}} should render: 1.5', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{6/4}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('1.5'); 11 | }); 12 | 13 | test('{{ 6 / 3 / 2 }} should render: 1.0', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{ 6 / 3 / 2}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('1.0'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/num_modulo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{5%3}} should render: 2', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{5%3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('2'); 11 | }); 12 | 13 | test('{{-5 % 3}} should render: 1', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{-5 % 3}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('1'); 18 | }); 19 | 20 | test('{{ 20 % 15 % 3 }} should render: 2', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{ 20 % 15 % 3 }}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('2'); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/num_multiply_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{2*3}} should render: 6', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{2*3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('6'); 11 | }); 12 | 13 | test('{{1.3*4}} should render: 5.2', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{1.3*4}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('5.2'); 18 | }); 19 | 20 | test('{{ 2 * 3 * 4 }} should render: 24', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{ 2 * 3 * 4 }}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('24'); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/num_power_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{2^3}} should render: 8', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{2^3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('8'); 11 | }); 12 | 13 | test('{{2^-3}} should render: 0.125', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{2^-3}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('0.125'); 18 | }); 19 | 20 | test('{{ -2 ^ 3 }} should render: -8', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{ -2 ^ 3 }}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('-8'); 25 | }); 26 | 27 | test('{{ -2 ^ -3 }} should render: -0.125', () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseText('{{ -2 ^ -3 }}'); 30 | var renderResult = await engine.render(parseResult); 31 | renderResult.text.should.be('-0.125'); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/multiplication/string_concatenate_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{"Hel" & \'lo\'}} should render: "Hello"', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{"Hel" & \'lo\'}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('Hello'); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/operator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine operator type errors', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should error on "{{false & 3}}"', () async { 10 | final parseResult = await engine.parseText("{{false & 3}}"); 11 | final renderResult = await engine.render(parseResult); 12 | 13 | renderResult.errorMessage.should.be( 14 | 'Render error in: \'{{false & 3}}\':\n' 15 | ' 1:9: right of the & operator must be a boolean, ' 16 | 'or left and right of the & operator must be a String', 17 | ); 18 | }); 19 | 20 | test('should error on "{{2 & true}}"', () async { 21 | final parseResult = await engine.parseText("{{2 & true}}"); 22 | final renderResult = await engine.render(parseResult); 23 | 24 | renderResult.errorMessage.should.be( 25 | 'Render error in: \'{{2 & true}}\':\n' 26 | ' 1:5: left of the & operator must be a boolean, ' 27 | 'or left and right of the & operator must be a String', 28 | ); 29 | }); 30 | 31 | test('should error on "{{2 & 3}}"', () async { 32 | final parseResult = await engine.parseText("{{2 & 3}}"); 33 | final renderResult = await engine.render(parseResult); 34 | 35 | renderResult.errorMessage.should.be( 36 | 'Render error in: \'{{2 & 3}}\':\n' 37 | ' 1:5: left and right of the & operator must be a boolean, ' 38 | 'or left and right of the & operator must be a String', 39 | ); 40 | }); 41 | 42 | test('should error on "{{4 + true}}"', () async { 43 | final parseResult = await engine.parseText("{{4 + true}}"); 44 | final renderResult = await engine.render(parseResult); 45 | 46 | renderResult.errorMessage.should.be( 47 | 'Render error in: \'{{4 + true}}\':\n' 48 | ' 1:5: right of the + operator must be a number, ' 49 | 'or left and right of the + operator must be a String', 50 | ); 51 | }); 52 | 53 | test('should error on "{{false - 4}}"', () async { 54 | final parseResult = await engine.parseText("{{false - 4}}"); 55 | final renderResult = await engine.render(parseResult); 56 | 57 | renderResult.errorMessage.should.be( 58 | 'Render error in: \'{{false - 4}}\':\n' 59 | ' 1:9: left of the - operator must be a number', 60 | ); 61 | }); 62 | 63 | test('should error on "{{true - \'String\'}}"', () async { 64 | final parseResult = await engine.parseText("{{true - 'String'}}"); 65 | final renderResult = await engine.render(parseResult); 66 | 67 | renderResult.errorMessage.should.be( 68 | 'Render error in: \'{{true - \'String\'}}\':\n' 69 | ' 1:8: left and right of the - operator must be a number', 70 | ); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/parentheses_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{1 + 3 * 2}} should render: 7', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText("{{1 + 3 * 2}}"); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('7'); 11 | }); 12 | 13 | test('{{(1 + 3) * 2}} should render: 8', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText("{{(1 + 3) * 2}}"); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('8'); 18 | }); 19 | 20 | test('{{true & (false | true)}} should render: true', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText("{{true & (false | true)}}"); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('true'); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/prefix/negative_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | import 'dart:math'; 5 | 6 | void main() { 7 | test('{{-3}} should render: -3', () async { 8 | var engine = TemplateEngine(); 9 | var parseResult = await engine.parseText('{{-3}}'); 10 | var renderResult = await engine.render(parseResult); 11 | renderResult.text.should.be('-3'); 12 | }); 13 | test('{{-pi}} should render: -$pi', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{-pi}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('-$pi'); 18 | }); 19 | 20 | test('{{-"text"}} should result in an error', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('{{-"text"}}'); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('{{ERROR}}'); 25 | renderResult.errorMessage.should.be( 26 | 'Render error in: \'{{-"text"}}\':\n' 27 | ' 1:3: number expected after the - operator', 28 | ); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/prefix/not_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{!FALSE}} should render: true', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText("{{!FALSE}}"); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('true'); 11 | }); 12 | 13 | test('{{ ! true }} should render: false', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText("{{ ! true }}"); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('false'); 18 | }); 19 | 20 | test('{{false | !FAlse}} should render: true', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText("{{false | !FAlse}}"); 23 | var renderResult = await engine.render(parseResult); 24 | renderResult.text.should.be('true'); 25 | }); 26 | 27 | test('{{!"text"}} should throw in an error', () async { 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseText('{{!"text"}}'); 30 | var renderResult = await engine.render(parseResult); 31 | renderResult.text.should.be('{{ERROR}}'); 32 | renderResult.errorMessage.should.be( 33 | 'Render error in: \'{{!"text"}}\':\n' 34 | ' 1:3: boolean expected after the ! operator', 35 | ); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/operator/prefix/positive_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{+3}} should render: 3', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{+3}}'); 9 | var renderResult = await engine.render(parseResult); 10 | renderResult.text.should.be('3'); 11 | }); 12 | 13 | test('{{+"text"}} should throw in an error', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{+"text"}}'); 16 | var renderResult = await engine.render(parseResult); 17 | renderResult.text.should.be('{{ERROR}}'); 18 | renderResult.errorMessage.should.be( 19 | 'Render error in: \'{{+"text"}}\':\n' 20 | ' 1:3: number expected after the + operator', 21 | ); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/tag_expression_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:template_engine/src/template.dart'; 2 | import 'package:template_engine/src/template_engine.dart'; 3 | import 'package:shouldly/shouldly.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | test('"The cos of 2 pi = {{cos(2 * pi)}}." should render : 1', () async { 8 | var engine = TemplateEngine(); 9 | var template = TextTemplate('The cos of 2 pi = {{cos(2 * pi)}}.'); 10 | var parseResult = await engine.parseTemplate(template); 11 | var renderResult = await engine.render(parseResult); 12 | renderResult.text.should.be('The cos of 2 pi = 1.0.'); 13 | }); 14 | 15 | test('"The volume of a sphere = ' 16 | '{{ round( (3/4) * pi * (radius ^ 3) )}}." should render: ' 17 | 'The volume of a sphere = 2356.', () async { 18 | var engine = TemplateEngine(); 19 | var template = TextTemplate( 20 | 'The volume of a sphere = ' 21 | '{{ round( (3/4) * pi * (radius ^ 3) )}}.', 22 | ); 23 | var parseResult = await engine.parseTemplate(template); 24 | var renderResult = await engine.render(parseResult, {'radius': 10}); 25 | renderResult.text.should.be('The volume of a sphere = 2356.'); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/variable/nested_variable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{person}} should render the person variable', () async { 7 | var engine = TemplateEngine(); 8 | VariableMap variables = { 9 | 'person': { 10 | 'name': 'John Doe', 11 | 'age': 30, 12 | 'child': {'name': 'Jane Doe', 'age': 5}, 13 | }, 14 | }; 15 | var parseResult = await engine.parseText('{{person}}'); 16 | var renderResult = await engine.render(parseResult, variables); 17 | renderResult.text.should.be( 18 | '{name: John Doe, age: 30, child: {name: Jane Doe, age: 5}}', 19 | ); 20 | }); 21 | 22 | test( 23 | '{{person.name}} should render the nested person.name variable', 24 | () async { 25 | var engine = TemplateEngine(); 26 | VariableMap variables = { 27 | 'person': { 28 | 'name': 'John Doe', 29 | 'age': 30, 30 | 'child': {'name': 'Jane Doe', 'age': 5}, 31 | }, 32 | }; 33 | var parseResult = await engine.parseText('{{person.name}}'); 34 | var renderResult = await engine.render(parseResult, variables); 35 | renderResult.text.should.be('John Doe'); 36 | }, 37 | ); 38 | 39 | test( 40 | '{{person.child.name}} should render the nested person.child.name variable', 41 | () async { 42 | var engine = TemplateEngine(); 43 | VariableMap variables = { 44 | 'person': { 45 | 'name': 'John Doe', 46 | 'age': 30, 47 | 'child': {'name': 'Jane Doe', 'age': 5}, 48 | }, 49 | }; 50 | var parseResult = await engine.parseText('{{person.child.name}}'); 51 | var renderResult = await engine.render(parseResult, variables); 52 | renderResult.text.should.be('Jane Doe'); 53 | }, 54 | ); 55 | 56 | test( 57 | '{{person.child.age}} should render the nested person.child.age variable', 58 | () async { 59 | var engine = TemplateEngine(); 60 | VariableMap variables = { 61 | 'person': { 62 | 'name': 'John Doe', 63 | 'age': 30, 64 | 'child': {'name': 'Jane Doe', 'age': 5}, 65 | }, 66 | }; 67 | var parseResult = await engine.parseText('{{person.child.age}}'); 68 | var renderResult = await engine.render(parseResult, variables); 69 | renderResult.text.should.be('5'); 70 | }, 71 | ); 72 | 73 | test('"Hello {{person.child.name}}." should render a greeting', () async { 74 | var engine = TemplateEngine(); 75 | VariableMap variables = { 76 | 'person': { 77 | 'name': 'John Doe', 78 | 'age': 30, 79 | 'child': {'name': 'Jane Doe', 'age': 5}, 80 | }, 81 | }; 82 | var parseResult = await engine.parseText('Hello {{person.child.name}}.'); 83 | var renderResult = await engine.render(parseResult, variables); 84 | renderResult.text.should.be('Hello Jane Doe.'); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/variable/unknown_variable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | 4 | Future main() async { 5 | var engine = TemplateEngine(); 6 | var parseResult = await engine.parseText('Hello {{world}}.'); 7 | var renderResult = await engine.render(parseResult); 8 | var expected = 9 | "Render error in: 'Hello {{world}}.':\n" 10 | " 1:9: Variable does not exist: world"; 11 | renderResult.errorMessage.should.be(expected); 12 | } 13 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/variable/variable_extra_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | /// Variable tests that are not suited as an example 6 | 7 | void main() { 8 | test('"Hello {{name}}." should render a an error ' 9 | 'if the variable does not exist', () async { 10 | var engine = TemplateEngine(); 11 | var parseResult = await engine.parseText('Hello {{name}}.'); 12 | var renderResult = await engine.render(parseResult); 13 | renderResult.errorMessage.should.be( 14 | 'Render error in: \'Hello {{name}}.\':\n' 15 | ' 1:9: Variable does not exist: name', 16 | ); 17 | }); 18 | 19 | test('"Hello {{ \t name}}." should render a proper greeting', () async { 20 | var engine = TemplateEngine(); 21 | var parseResult = await engine.parseText('Hello {{ \t name}}.'); 22 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 23 | renderResult.text.should.be('Hello world.'); 24 | }); 25 | 26 | test('"Hello {{name \t\n }}." should render a proper greeting', () async { 27 | var engine = TemplateEngine(); 28 | var parseResult = await engine.parseText('Hello {{name \t\n }}.'); 29 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 30 | renderResult.text.should.be('Hello world.'); 31 | }); 32 | 33 | test('"Hello {{ name \t\n }}." should render a proper greeting', () async { 34 | var engine = TemplateEngine(); 35 | var parseResult = await engine.parseText('Hello {{ name \t\n }}.'); 36 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 37 | renderResult.text.should.be('Hello world.'); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/src/parser/tag/expression/variable/variable_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('{{x}} should render variable x', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('{{x}}'); 9 | var renderResult = await engine.render(parseResult, {'x': 42}); 10 | renderResult.text.should.be('42'); 11 | }); 12 | 13 | test('{{x / y}} should render variable x divided by variable y', () async { 14 | var engine = TemplateEngine(); 15 | var parseResult = await engine.parseText('{{x / y}}'); 16 | var renderResult = await engine.render(parseResult, {'x': 6, 'y': 2}); 17 | renderResult.text.should.be('3.0'); 18 | }); 19 | 20 | test('"Hello {{name}}." should render a proper greeting', () async { 21 | var engine = TemplateEngine(); 22 | var parseResult = await engine.parseText('Hello {{name}}.'); 23 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 24 | renderResult.text.should.be('Hello world.'); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/parser/tag/tag_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | import 'package:petitparser/src/core/parser.dart'; 5 | 6 | void main() { 7 | group('TagName.validate', () { 8 | test('should not throw for valid names', () { 9 | Should.satisfyAllConditions([ 10 | () => Should.notThrowError(() => TagName.validate('a')), 11 | () => Should.notThrowError(() => TagName.validate('ab')), 12 | () => Should.notThrowError(() => TagName.validate('a1')), 13 | () => Should.notThrowError(() => TagName.validate('a11')), 14 | ]); 15 | }); 16 | 17 | test('should throw for invalid name "1"', () { 18 | Should.throwException(() => TagName.validate('1'))! 19 | .message 20 | .should 21 | .be('Tag name: "1" is invalid: letter expected at position: 0'); 22 | }); 23 | 24 | test('should throw for invalid name "@"', () { 25 | Should.throwException(() => TagName.validate('@'))! 26 | .message 27 | .should 28 | .be('Tag name: "@" is invalid: letter expected at position: 0'); 29 | }); 30 | 31 | test('should throw for invalid name "ab.1"', () { 32 | Should.throwException(() => TagName.validate('1'))! 33 | .message 34 | .should 35 | .be('Tag name: "1" is invalid: letter expected at position: 0'); 36 | }); 37 | 38 | test('should throw for invalid name "ab@"', () { 39 | Should.throwException( 40 | () => TagName.validate('ab@'), 41 | )!.message.should.be( 42 | 'Tag name: "ab@" is invalid: end of input expected at position: 2', 43 | ); 44 | }); 45 | 46 | test('should throw for invalid name "ab1.@"', () { 47 | Should.throwException( 48 | () => TagName.validate('ab1.@'), 49 | )!.message.should.be( 50 | 'Tag name: "ab1.@" is invalid: end of input expected at position: 3', 51 | ); 52 | }); 53 | }); 54 | 55 | group('Tag constructor', () { 56 | test('should throw TagException for invalid tag name', () { 57 | Should.throwException( 58 | () => TagWithInvalidName(), 59 | )!.message.should.be( 60 | 'Tag name: "inv@lid" is invalid: end of input expected at position: 3', 61 | ); 62 | }); 63 | }); 64 | } 65 | 66 | class TagWithInvalidName extends Tag { 67 | TagWithInvalidName() 68 | : super( 69 | name: 'inv@lid', 70 | description: ['A tag with an invalid name for testing'], 71 | exampleExpression: 'dummy', 72 | exampleCode: ProjectFilePath('dummy'), 73 | ); 74 | 75 | @override 76 | Parser createTagParser(ParserContext context) { 77 | throw UnimplementedError(); 78 | } 79 | 80 | @override 81 | List createMarkdownExamples( 82 | RenderContext renderContext, 83 | int titleLevel, 84 | ) => []; 85 | } 86 | -------------------------------------------------------------------------------- /test/src/project_file_path_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test( 7 | 'ProjectFilePath starting with a slash should throw an exception', 8 | () async { 9 | Should.throwException( 10 | () => ProjectFilePath('/test'), 11 | ).toString().should.be( 12 | 'Exception: Invalid project file path: \'/test\': ' 13 | 'letter expected OR digit expected OR "(" expected OR ")"' 14 | ' expected OR "_" expected OR "-" expected OR "." expected' 15 | ' at position: 1', 16 | ); 17 | }, 18 | ); 19 | 20 | test('ProjectFilePath "test" should not throw an exception', () async { 21 | Should.notThrowException(() => ProjectFilePath('test')); 22 | }); 23 | 24 | test('ProjectFilePath should contain a valid file name', () async { 25 | Should.throwException( 26 | () => ProjectFilePath('f@lder/file'), 27 | ).toString().should.be( 28 | 'Exception: Invalid project file path: \'f@lder/file\': ' 29 | 'letter expected OR digit expected OR "(" expected OR ")" ' 30 | 'expected OR "_" expected OR "-" expected OR "." expected ' 31 | 'at position: 2', 32 | ); 33 | }); 34 | 35 | test('ProjectFilePath should contain a valid folder name', () async { 36 | Should.throwException( 37 | () => ProjectFilePath('folder/f@le'), 38 | ).toString().should.be( 39 | 'Exception: Invalid project file path: \'folder/f@le\':' 40 | ' letter expected OR digit expected OR "(" expected OR ")" ' 41 | 'expected OR "_" expected OR "-" expected OR "." ' 42 | 'expected at position: 9', 43 | ); 44 | }); 45 | 46 | test('ProjectFilePath in root should be fine', () async { 47 | Should.notThrowException(() => ProjectFilePath('README.md')); 48 | }); 49 | 50 | test('ProjectFilePath in a folder should be fine', () async { 51 | Should.notThrowException( 52 | () => ProjectFilePath('lib/src/template_engine.dart'), 53 | ); 54 | }); 55 | 56 | test('ProjectFilePath.file should be the correct file', () async { 57 | ProjectFilePath('README.md').file.path.should.contain('README.md'); 58 | }); 59 | 60 | test('ProjectFilePath.fileName should be the correct file', () async { 61 | ProjectFilePath('README.md').fileName.should.be('README.md'); 62 | }); 63 | 64 | test('ProjectFilePath.githubUri should be the correct URI', () async { 65 | ProjectFilePath('README.md').githubUri.toString().should.be( 66 | 'https://github.com/domain-centric/template_engine/blob/main/README.md', 67 | ); 68 | }); 69 | 70 | test( 71 | 'ProjectFilePath.githubMarkdownLink should be the correct URI', 72 | () async { 73 | ProjectFilePath('README.md').githubMarkdownLink.should.be( 74 | 'README.md', 76 | ); 77 | }, 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /test/src/template_engine_template_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should handle single parse error', () async { 10 | final parseResult = await engine.parseText('Hello {{name}.'); 11 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 12 | 13 | Should.satisfyAllConditions([ 14 | () => renderResult.errorMessage.should.be( 15 | 'Parse error in: \'Hello {{name}.\':\n' 16 | ' 1:7: Found tag start: {{, but it was not followed with a tag end: }}', 17 | ), 18 | () => renderResult.text.should.be('Hello {{'), 19 | ]); 20 | }); 21 | 22 | test('should handle multiple parse errors', () async { 23 | final parseResult = await engine.parseText('Hello }}name{{.'); 24 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 25 | 26 | Should.satisfyAllConditions([ 27 | () => renderResult.errorMessage.should.be( 28 | 'Parse errors in: \'Hello }}name{{.\':\n' 29 | ' 1:7: Found tag end: }}, but it was not preceded with a tag start: {{\n' 30 | ' 1:13: Found tag start: {{, but it was not followed with a tag end: }}', 31 | ), 32 | () => renderResult.text.should.be('Hello }}name{{'), 33 | ]); 34 | }); 35 | 36 | test('should handle single render error', () async { 37 | final parseResult = await engine.parseText('Hello {{name}}.'); 38 | final renderResult = await engine.render(parseResult, {'age': '13'}); 39 | 40 | Should.satisfyAllConditions([ 41 | () => renderResult.errorMessage.should.be( 42 | 'Render error in: \'Hello {{name}}.\':\n' 43 | ' 1:9: Variable does not exist: name', 44 | ), 45 | () => renderResult.text.should.be('Hello {{ERROR}}.'), 46 | ]); 47 | }); 48 | 49 | test('should handle multiple render errors', () async { 50 | final parseResult = await engine.parseText( 51 | 'Hello {{name}}. Welcome in {{location}}.', 52 | ); 53 | final renderResult = await engine.render(parseResult, {'age': '13'}); 54 | 55 | Should.satisfyAllConditions([ 56 | () => renderResult.errorMessage.should.be( 57 | 'Render errors in: \'Hello {{name}}. Welcome in {{location}}.\':\n' 58 | ' 1:9: Variable does not exist: name\n' 59 | ' 1:30: Variable does not exist: location', 60 | ), 61 | () => renderResult.text.should.be( 62 | 'Hello {{ERROR}}. Welcome in {{ERROR}}.', 63 | ), 64 | ]); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/src/template_engine_template_example_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | Future main() async { 6 | test('example should work', () async { 7 | var engine = TemplateEngine(); 8 | var parseResult = await engine.parseText('Hello {{name}}.'); 9 | var renderResult = await engine.render(parseResult, {'name': 'world'}); 10 | renderResult.text.should.be('Hello world.'); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/src/template_engine_template_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should render correctly with valid input', () async { 10 | final parseResult = await engine.parseText('Hello {{name}}.'); 11 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 12 | 13 | Should.satisfyAllConditions([ 14 | () => renderResult.errorMessage.should.beNullOrEmpty(), 15 | () => renderResult.text.should.be('Hello world.'), 16 | ]); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/src/template_engine_template_type_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:shouldly/shouldly.dart'; 2 | import 'package:template_engine/template_engine.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Template types', () { 7 | final variables = {'name': 'World'}; 8 | final templatePath = ProjectFilePath("test/src/hello_name.template"); 9 | const expected = "Hello World."; 10 | 11 | test('using engine.parseText()', () async { 12 | var engine = TemplateEngine(); 13 | var parseResult = await engine.parseText('Hello {{name}}.'); 14 | var renderResult = await engine.render(parseResult, variables); 15 | renderResult.text.should.be(expected); 16 | }); 17 | 18 | test('using a TextTemplate', () async { 19 | var template = TextTemplate('Hello {{name}}.'); 20 | var engine = TemplateEngine(); 21 | var parseResult = await engine.parseTemplate(template); 22 | var renderResult = await engine.render(parseResult, variables); 23 | renderResult.text.should.be(expected); 24 | }); 25 | 26 | test('using FileTemplate.fromProjectFilePath(filePath)', () async { 27 | var template = FileTemplate.fromProjectFilePath(templatePath); 28 | var engine = TemplateEngine(); 29 | var parseResult = await engine.parseTemplate(template); 30 | var renderResult = await engine.render(parseResult, variables); 31 | renderResult.text.should.be(expected); 32 | }); 33 | 34 | test('using FileTemplate(file)', () async { 35 | var template = FileTemplate(templatePath.file); 36 | var engine = TemplateEngine(); 37 | var parseResult = await engine.parseTemplate(template); 38 | var renderResult = await engine.render(parseResult, variables); 39 | renderResult.text.should.be(expected); 40 | }); 41 | 42 | test('using HttpTemplate', () async { 43 | final url = Uri.parse( 44 | "https://raw.githubusercontent.com/domain-centric/template_engine/" 45 | "main/test/src/hello_name.template", 46 | ); 47 | var template = HttpTemplate(url); 48 | var engine = TemplateEngine(); 49 | var parseResult = await engine.parseTemplate(template); 50 | var renderResult = await engine.render(parseResult, variables); 51 | renderResult.text.should.be(expected); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/src/template_engine_templates_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should handle single parse error with parseTemplates', () async { 10 | final parseResult = await engine.parseTemplates([ 11 | TextTemplate('Hello '), 12 | TextTemplate('{{name}.'), 13 | ]); 14 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 15 | 16 | Should.satisfyAllConditions([ 17 | () => renderResult.errorMessage.should.be( 18 | 'Parse error in: \'{{name}.\':\n' 19 | ' 1:1: Found tag start: {{, but it was not followed with a tag end: }}', 20 | ), 21 | () => renderResult.text.should.be('Hello {{'), 22 | ]); 23 | }); 24 | 25 | test('should handle multiple parse errors with parseTemplates', () async { 26 | final parseResult = await engine.parseTemplates([ 27 | TextTemplate('Hello '), 28 | TextTemplate('}}name{{.'), 29 | ]); 30 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 31 | 32 | Should.satisfyAllConditions([ 33 | () => renderResult.errorMessage.should.be( 34 | 'Parse errors in: \'}}name{{.\':\n' 35 | ' 1:1: Found tag end: }}, but it was not preceded with a tag start: {{\n' 36 | ' 1:7: Found tag start: {{, but it was not followed with a tag end: }}', 37 | ), 38 | () => renderResult.text.should.be('Hello }}name{{'), 39 | ]); 40 | }); 41 | 42 | test('should handle single render error with parseTemplates', () async { 43 | final parseResult = await engine.parseTemplates([ 44 | TextTemplate('Hello '), 45 | TextTemplate('{{name}}.'), 46 | ]); 47 | final renderResult = await engine.render(parseResult, {'age': '13'}); 48 | 49 | Should.satisfyAllConditions([ 50 | () => renderResult.errorMessage.should.be( 51 | 'Render error in: \'{{name}}.\':\n' 52 | ' 1:3: Variable does not exist: name', 53 | ), 54 | () => renderResult.text.should.be('Hello {{ERROR}}.'), 55 | ]); 56 | }); 57 | 58 | test('should handle multiple render errors with parseTemplates', () async { 59 | final parseResult = await engine.parseTemplates([ 60 | TextTemplate('Hello {{name}}. '), 61 | TextTemplate('Welcome in {{location}}.'), 62 | ]); 63 | final renderResult = await engine.render(parseResult, {'age': '13'}); 64 | 65 | Should.satisfyAllConditions([ 66 | () => renderResult.errorMessage.should.be( 67 | 'Render error in: \'Hello {{name}}.\':\n' 68 | ' 1:9: Variable does not exist: name\n' 69 | 'Render error in: \'Welcome in {{location}}.\':\n' 70 | ' 1:14: Variable does not exist: location', 71 | ), 72 | () => renderResult.text.should.be( 73 | 'Hello {{ERROR}}. Welcome in {{ERROR}}.', 74 | ), 75 | ]); 76 | }); 77 | 78 | test( 79 | 'should handle multiple errors (parse and render) with parseTemplates', 80 | () async { 81 | final parseResult = await engine.parseTemplates([ 82 | TextTemplate('}}Hello {{name}}. '), 83 | TextTemplate('Welcome in {{location}}.'), 84 | ]); 85 | final renderResult = await engine.render(parseResult, {'age': '13'}); 86 | 87 | Should.satisfyAllConditions([ 88 | () => renderResult.errorMessage.should.be( 89 | 'Errors in: \'}}Hello {{name}}.\':\n' 90 | ' Parse error:\n' 91 | ' 1:1: Found tag end: }}, but it was not preceded with a tag start: {{\n' 92 | ' Render error:\n' 93 | ' 1:11: Variable does not exist: name\n' 94 | 'Render error in: \'Welcome in {{location}}.\':\n' 95 | ' 1:14: Variable does not exist: location', 96 | ), 97 | () => renderResult.text.should.be( 98 | '}}Hello {{ERROR}}. Welcome in {{ERROR}}.', 99 | ), 100 | ]); 101 | }, 102 | ); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /test/src/template_engine_templates_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test( 10 | 'should render correctly using FileTemplate and TextTemplate', 11 | () async { 12 | final parseResult = await engine.parseTemplates([ 13 | FileTemplate.fromProjectFilePath( 14 | ProjectFilePath('test/src/hello.template'), 15 | ), 16 | TextTemplate('{{name}}.'), 17 | ]); 18 | final renderResult = await engine.render(parseResult, { 19 | 'name': 'world', 20 | }); 21 | 22 | Should.satisfyAllConditions([ 23 | () => renderResult.errorMessage.should.beNullOrEmpty(), 24 | () => renderResult.text.should.be('Hello world.'), 25 | ]); 26 | }, 27 | ); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/src/template_engine_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | import 'package:petitparser/petitparser.dart'; 5 | 6 | void main() { 7 | group('TemplateEngine Tag Validation', () { 8 | test('should not throw when tag names are unique', () { 9 | Should.notThrowException( 10 | () => TemplateEngine(tags: [TestTag('name1'), TestTag('name2')]), 11 | ); 12 | }); 13 | 14 | test('should throw when one tag name is duplicated', () { 15 | Should.throwException( 16 | () => TemplateEngine( 17 | tags: [ 18 | TestTag('name1'), 19 | TestTag('name2'), 20 | TestTag('name2'), 21 | TestTag('name3'), 22 | ], 23 | ), 24 | )!.message.should.be('Tag name: name2 is not unique'); 25 | }); 26 | 27 | test('should throw when two tag names are duplicated', () { 28 | Should.throwException( 29 | () => TemplateEngine( 30 | tags: [ 31 | TestTag('name1'), 32 | TestTag('name2'), 33 | TestTag('name2'), 34 | TestTag('name2'), 35 | TestTag('name3'), 36 | ], 37 | ), 38 | )!.message.should.be('Tag name: name2 is not unique'); 39 | }); 40 | 41 | test('should throw when multiple tag names are duplicated', () { 42 | Should.throwException( 43 | () => TemplateEngine( 44 | tags: [ 45 | TestTag('name1'), 46 | TestTag('name2'), 47 | TestTag('name2'), 48 | TestTag('name3'), 49 | TestTag('name4'), 50 | TestTag('name4'), 51 | TestTag('name4'), 52 | TestTag('name5'), 53 | ], 54 | ), 55 | )!.message.should.be('Tag names: name2, name4 are not unique'); 56 | }); 57 | }); 58 | } 59 | 60 | class TestTag extends Tag { 61 | TestTag(String name) 62 | : super( 63 | name: name, 64 | description: ['Tag to test unique tag names'], 65 | exampleExpression: 'dummy', 66 | exampleCode: ProjectFilePath('dummy'), 67 | ); 68 | 69 | @override 70 | Parser createTagParser(ParserContext context) { 71 | throw UnimplementedError(); 72 | } 73 | 74 | @override 75 | List createMarkdownExamples( 76 | RenderContext renderContext, 77 | int titleLevel, 78 | ) => []; 79 | } 80 | 81 | class DummyTemplate extends TextTemplate { 82 | DummyTemplate() : super('Hello {{name}}.'); 83 | } 84 | -------------------------------------------------------------------------------- /test/src/template_engine_text_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should handle single parse error', () async { 10 | final parseResult = await engine.parseText('Hello {{name}.'); 11 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 12 | 13 | Should.satisfyAllConditions([ 14 | () => renderResult.errorMessage.should.be( 15 | 'Parse error in: \'Hello {{name}.\':\n' 16 | ' 1:7: Found tag start: {{, but it was not followed with a tag end: }}', 17 | ), 18 | () => renderResult.text.should.be('Hello {{'), 19 | ]); 20 | }); 21 | 22 | test('should handle multiple parse errors', () async { 23 | final parseResult = await engine.parseText('Hello }}name{{.'); 24 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 25 | 26 | Should.satisfyAllConditions([ 27 | () => renderResult.errorMessage.should.be( 28 | 'Parse errors in: \'Hello }}name{{.\':\n' 29 | ' 1:7: Found tag end: }}, but it was not preceded with a tag start: {{\n' 30 | ' 1:13: Found tag start: {{, but it was not followed with a tag end: }}', 31 | ), 32 | () => renderResult.text.should.be('Hello }}name{{'), 33 | ]); 34 | }); 35 | 36 | test('should handle single render error', () async { 37 | final parseResult = await engine.parseText('Hello {{name}}.'); 38 | final renderResult = await engine.render(parseResult, {'age': '13'}); 39 | 40 | Should.satisfyAllConditions([ 41 | () => renderResult.errorMessage.should.be( 42 | 'Render error in: \'Hello {{name}}.\':\n' 43 | ' 1:9: Variable does not exist: name', 44 | ), 45 | () => renderResult.text.should.be('Hello {{ERROR}}.'), 46 | ]); 47 | }); 48 | 49 | test('should handle multiple render errors', () async { 50 | final parseResult = await engine.parseText( 51 | 'Hello {{name}}. Welcome in {{location}}.', 52 | ); 53 | final renderResult = await engine.render(parseResult, {'age': '13'}); 54 | 55 | Should.satisfyAllConditions([ 56 | () => renderResult.errorMessage.should.be( 57 | 'Render errors in: \'Hello {{name}}. Welcome in {{location}}.\':\n' 58 | ' 1:9: Variable does not exist: name\n' 59 | ' 1:30: Variable does not exist: location', 60 | ), 61 | () => renderResult.text.should.be( 62 | 'Hello {{ERROR}}. Welcome in {{ERROR}}.', 63 | ), 64 | ]); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /test/src/template_engine_text_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:shouldly/shouldly.dart'; 3 | import 'package:template_engine/template_engine.dart'; 4 | 5 | void main() { 6 | group('TemplateEngine', () { 7 | final engine = TemplateEngine(); 8 | 9 | test('should render correctly with valid input', () async { 10 | final parseResult = await engine.parseText('Hello {{name}}.'); 11 | final renderResult = await engine.render(parseResult, {'name': 'world'}); 12 | 13 | Should.satisfyAllConditions([ 14 | () => renderResult.errorMessage.should.beNullOrEmpty(), 15 | () => renderResult.text.should.be('Hello world.'), 16 | ]); 17 | }); 18 | }); 19 | } 20 | --------------------------------------------------------------------------------