├── .github └── workflows │ ├── artifacts.yml │ ├── build.yml │ ├── ci-rust.yml │ └── mdbook.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── docs ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── api.md │ ├── cli │ ├── intro.md │ └── usage.md │ ├── install.md │ ├── intro.md │ ├── quickstart.md │ └── templates │ ├── expressions.md │ ├── filters.md │ ├── functions.md │ ├── intro.md │ └── syntax.md ├── examples ├── basic.rs ├── custom_filter.rs ├── example.json ├── kitchen_sink.tmpl └── self_referring.yml ├── justfile ├── templar ├── Cargo.toml ├── benches │ └── lib.rs ├── src │ ├── cli │ │ ├── command.rs │ │ ├── context.rs │ │ ├── mod.rs │ │ └── util.rs │ ├── context │ │ ├── dynamic │ │ │ ├── context_map.rs │ │ │ ├── context_walk.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── scoped.rs │ │ └── standard.rs │ ├── error │ │ └── mod.rs │ ├── execution │ │ ├── data.rs │ │ ├── executors.rs │ │ ├── mod.rs │ │ ├── node.rs │ │ └── operation.rs │ ├── filters │ │ ├── common.rs │ │ └── mod.rs │ ├── functions │ │ ├── common.rs │ │ └── mod.rs │ ├── lib.rs │ ├── macros.rs │ ├── main.rs │ ├── parser │ │ ├── mod.rs │ │ ├── rules.rs │ │ └── tree.rs │ ├── templar.pest │ ├── templar │ │ ├── builder.rs │ │ ├── mod.rs │ │ └── template.rs │ └── test │ │ ├── dynamic_context.rs │ │ ├── expressions.rs │ │ ├── mod.rs │ │ ├── parsing.rs │ │ └── shared_context_safe.rs └── tests │ ├── basic.rs │ └── experiments.rs └── templar_macros ├── Cargo.toml └── src ├── attr.rs ├── lib.rs └── transforms.rs /.github/workflows/artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | on: 3 | push: 4 | tags: ['v**'] 5 | 6 | jobs: 7 | build-linux: 8 | name: Build Linux Artifacts 9 | 10 | strategy: 11 | matrix: 12 | include: 13 | - target: x86_64-unknown-linux-musl 14 | file-tag: "x86_64" 15 | strip: "x86_64-linux-musl-strip" 16 | - target: i586-unknown-linux-musl 17 | file-tag: "i586" 18 | strip: "i586-linux-musl-strip" 19 | - target: i686-unknown-linux-musl 20 | file-tag: "i686" 21 | strip: "i686-linux-musl-strip" 22 | - target: aarch64-unknown-linux-musl 23 | file-tag: "aarch64" 24 | strip: "aarch64-linux-musl-strip" 25 | - target: armv7-unknown-linux-musleabihf 26 | file-tag: "armv7l" 27 | strip: "arm-linux-musleabihf-strip" 28 | - target: arm-unknown-linux-musleabihf 29 | file-tag: "armv6" 30 | strip: "arm-linux-musleabihf-strip" 31 | 32 | runs-on: 'ubuntu-latest' 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v2 37 | 38 | - name: Use stable toolchain 39 | uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: stable 42 | target: '${{ matrix.target }}' 43 | override: true 44 | default: true 45 | 46 | - name: 'Build binary for ${{ matrix.target }}' 47 | uses: actions-rs/cargo@v1 48 | with: 49 | use-cross: true 50 | command: build 51 | args: '--all-features --target ${{ matrix.target }} --release' 52 | 53 | - name: Strip binary 54 | run: >- 55 | docker run -v $PWD/:/work rustembedded/cross:${{ matrix.target }}-0.2.1 ${{ matrix.strip }} /work/target/${{ matrix.target }}/release/templar 56 | 57 | - name: Upload artifact 58 | uses: actions/upload-artifact@master 59 | with: 60 | name: ${{ matrix.file-tag }} 61 | path: target/${{ matrix.target }}/release/templar 62 | 63 | build-windows: 64 | name: Build Windows Artifacts 65 | 66 | strategy: 67 | matrix: 68 | include: 69 | - toolchain: stable-msvc 70 | target: x86_64-pc-windows-msvc 71 | file-tag: win-x86_64 72 | 73 | runs-on: 'windows-latest' 74 | 75 | steps: 76 | - name: Checkout code 77 | uses: actions/checkout@v2 78 | 79 | - name: Use stable toolchain 80 | uses: actions-rs/toolchain@v1 81 | with: 82 | toolchain: stable 83 | target: '${{ matrix.target }}' 84 | override: true 85 | default: true 86 | 87 | - name: 'Build binary for ${{ matrix.target }}' 88 | uses: actions-rs/cargo@v1 89 | with: 90 | command: build 91 | args: '--all-features --target ${{ matrix.target }} --release' 92 | 93 | - name: Upload Windows artifact 94 | uses: actions/upload-artifact@master 95 | with: 96 | name: '${{ matrix.file-tag }}' 97 | path: target/${{ matrix.target }}/release/*.exe 98 | 99 | build-macos: 100 | name: Build Mac Artifacts 101 | runs-on: 'macos-latest' 102 | 103 | steps: 104 | - name: Checkout code 105 | uses: actions/checkout@v2 106 | 107 | - name: Use stable toolchain 108 | uses: actions-rs/toolchain@v1 109 | with: 110 | toolchain: stable 111 | target: 'x86_64-apple-darwin' 112 | 113 | - name: 'Build binary for x86_64-apple-darwin' 114 | uses: actions-rs/cargo@v1 115 | with: 116 | command: build 117 | args: '--all-features --release' 118 | 119 | - name: Upload Mac artifact 120 | uses: actions/upload-artifact@master 121 | with: 122 | name: darwin-x86_64 123 | path: target/release/templar 124 | 125 | new-release: 126 | name: Create new release 127 | runs-on: 'ubuntu-latest' 128 | needs: ['build-linux', 'build-macos', 'build-windows'] 129 | outputs: 130 | upload_url: ${{ steps.create_release.outputs.upload_url }} 131 | steps: 132 | - name: Create Release 133 | id: create_release 134 | uses: actions/create-release@v1 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | with: 138 | tag_name: ${{ github.ref }} 139 | release_name: Release ${{ github.ref }} 140 | body: '' 141 | draft: false 142 | prerelease: false 143 | 144 | release-tar-xz: 145 | name: Upload Tarballs 146 | runs-on: 'ubuntu-latest' 147 | needs: ['new-release'] 148 | strategy: 149 | matrix: 150 | include: 151 | - target: x86_64 152 | - target: i586 153 | - target: i686 154 | - target: aarch64 155 | - target: armv7l 156 | - target: armv6 157 | - target: darwin-x86_64 158 | 159 | steps: 160 | - name: Pull ${{ matrix.target }} 161 | uses: actions/download-artifact@master 162 | with: 163 | name: ${{ matrix.target }} 164 | 165 | - name: Package tarball 166 | run: |- 167 | chmod +x templar 168 | tar -cJf 'templar-${{ matrix.target }}.tar.xz' templar 169 | 170 | - name: Upload Release Assets 171 | id: upload-release-asset 172 | uses: actions/upload-release-asset@v1 173 | env: 174 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 175 | with: 176 | upload_url: ${{ needs.new-release.outputs.upload_url }} 177 | asset_path: ./templar-${{ matrix.target }}.tar.xz 178 | asset_name: templar-${{ matrix.target }}.tar.xz 179 | asset_content_type: application/x-gtar 180 | 181 | release-zip: 182 | name: Upload Zipfiles 183 | runs-on: 'ubuntu-latest' 184 | needs: ['new-release'] 185 | strategy: 186 | matrix: 187 | include: 188 | - target: win-x86_64 189 | 190 | steps: 191 | - name: Pull ${{ matrix.target }} 192 | uses: actions/download-artifact@master 193 | with: 194 | name: ${{ matrix.target }} 195 | 196 | - name: Zip it up 197 | run: zip 'templar-${{ matrix.target }}.zip' templar.exe 198 | 199 | - name: Upload Release Assets 200 | id: upload-release-asset 201 | uses: actions/upload-release-asset@v1 202 | env: 203 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 204 | with: 205 | upload_url: ${{ needs.new-release.outputs.upload_url }} 206 | asset_path: ./templar-${{ matrix.target }}.zip 207 | asset_name: templar-${{ matrix.target }}.zip 208 | asset_content_type: application/zip 209 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Build 2 | on: push 3 | 4 | jobs: 5 | build: 6 | name: Build Templar 7 | 8 | strategy: 9 | matrix: 10 | include: 11 | - os: ubuntu-latest 12 | toolchain: stable 13 | target: x86_64-unknown-linux-musl 14 | use-cross: true 15 | run-tests: false 16 | ext: '' 17 | - os: ubuntu-latest 18 | toolchain: stable 19 | target: aarch64-unknown-linux-musl 20 | use-cross: true 21 | run-tests: false 22 | ext: '' 23 | - os: ubuntu-latest 24 | toolchain: stable 25 | target: armv7-unknown-linux-musleabihf 26 | use-cross: true 27 | run-tests: false 28 | ext: '' 29 | - os: windows-latest 30 | toolchain: stable-msvc 31 | target: x86_64-pc-windows-msvc 32 | use-cross: false 33 | run-tests: false 34 | ext: '.exe' 35 | - os: macos-latest 36 | toolchain: stable 37 | target: x86_64-apple-darwin 38 | use-cross: false 39 | run-tests: false 40 | ext: '' 41 | 42 | runs-on: '${{ matrix.os }}' 43 | 44 | steps: 45 | - name: Checkout code 46 | uses: actions/checkout@v2 47 | 48 | - name: Use stable toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: stable 52 | target: '${{ matrix.target }}' 53 | override: true 54 | default: true 55 | 56 | - name: 'Build binary for ${{ matrix.target }}' 57 | uses: actions-rs/cargo@v1 58 | with: 59 | use-cross: ${{ matrix.use-cross }} 60 | command: build 61 | args: '--all-features --target ${{ matrix.target }}' 62 | 63 | - name: 'Run tests for ${{ matrix.target }}' 64 | uses: actions-rs/cargo@v1 65 | if: matrix.run-tests 66 | with: 67 | use-cross: ${{ matrix.use-cross }} 68 | command: test 69 | args: '--all-features --target ${{ matrix.target }}' 70 | -------------------------------------------------------------------------------- /.github/workflows/ci-rust.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | push: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | check: 15 | name: Cargo Check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | override: true 24 | - uses: actions-rs/cargo@v1 25 | with: 26 | command: check 27 | 28 | test: 29 | name: Cargo Test 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | 42 | format: 43 | name: Cargo Format 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | override: true 52 | - run: rustup component add rustfmt 53 | - uses: actions-rs/cargo@v1 54 | with: 55 | command: fmt 56 | args: --all -- --check 57 | 58 | lint: 59 | name: Clippy Lint 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | profile: minimal 66 | toolchain: stable 67 | override: true 68 | - run: rustup component add clippy 69 | - uses: actions-rs/cargo@v1 70 | with: 71 | command: clippy 72 | args: -- -D warnings 73 | -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: '0.4.1' 18 | 19 | - run: |- 20 | mdbook build docs 21 | 22 | - name: Publish gh-pages 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./docs/book 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust files 2 | Cargo.lock 3 | **/*.rs.bk 4 | /target 5 | /templar/target 6 | 7 | # Other misc files 8 | .cache 9 | .config 10 | .cargo/credentials 11 | 12 | .env 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["templar", "templar_macros"] 3 | 4 | [profile.release] 5 | opt-level = 3 6 | lto = true 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Phil Proctor 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Phil Proctor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Templar 2 | 3 | [![Continuous Build](https://github.com/proctorlabs/templar/workflows/Continuous%20Build/badge.svg)](https://github.com/proctorlabs/templar/actions) 4 | [![GitHub release](https://img.shields.io/github/v/release/proctorlabs/templar)](https://github.com/proctorlabs/templar/releases) 5 | [![Crate](https://img.shields.io/crates/v/templar.svg?color=%230b7cbc)](https://crates.io/crates/templar) 6 | [![Book](https://img.shields.io/badge/book-current-important.svg)](https://proctorlabs.github.io/templar/) 7 | [![API Docs](https://img.shields.io/badge/docs-current-important.svg)](https://docs.rs/templar/) 8 | [![Crates.io](https://img.shields.io/crates/l/templar)](LICENSE-MIT) 9 | 10 | Templar is both a Rust library and a CLI tool for working with templates. The usage and style is 11 | inspired by both Jinja2 and Ansible, though it is not intended to be a clone of either of these. 12 | 13 | The goal of the project is to provide fast and flexible dynamic templating, particularly for use with 14 | configurations and local tooling. Despite this, it likely can be adapted for HTML and front end rendering 15 | as well. 16 | 17 | ## Examples 18 | 19 | The templating syntax is likely familiar considering the frameworks that it is based on. For instance, a 20 | simple template may look like this: 21 | 22 | ```properties 23 | user_name={{ user.name }} {# Replace with the context property 'name' in 'user' #} 24 | full_context={{ . | json("pretty") }} {# Dump the entire context as JSON, '.' is the root node #} 25 | password={{ script('echo hunter2 | md5sum') }} {# Execute a shell command and calculate the MD5 sum #} 26 | ``` 27 | 28 | In addition to simple replacements, more complex expressions can be used. 29 | 30 | ```markdown 31 | The calculated result is {{ 100 * 5 / 10 }} {#- Prints '50' #} 32 | 33 | Today's guest list: 34 | {%- for person in ['Bob', 'Joe', 'Jen', 'Amy')] %} 35 | * {{ person }} will come to the party! 36 | {%- endfor %} {#- This will loop everyone in the inline array above, but they array could also come from the context #} 37 | ``` 38 | 39 | ## Another templating framework? 40 | 41 | Well... yes. 42 | 43 | There are many great templating frameworks out there, however they are mostly intended for web or HTML rendering. This leads 44 | to a few drawbacks when used for other purposes. 45 | 46 | * Templar has first class support for parsed configuration files. You can create a context directly from a config that is parsed with 47 | serde or, alternatively, use it for templating serde sub-elements. 48 | * You can opt to parse expressions directly instead of an entire template. 49 | * Context values are lazily processed on access. 50 | * Context values can refer to other context values. 51 | * Extending the base functionality is easy. 52 | * Support for dynamic context nodes that are recalculated on every access. e.g. repeated calls to a template with this content 53 | `{% if user.isRoot %} {{ do_something() }} {% end if %}` would change if the `user.isRoot` value changes. 54 | 55 | ## Template Syntax 56 | 57 | Much of the syntax is based on the wonderful [Jinja2](https://jinja.palletsprojects.com/en/2.10.x/) project. Here are some 58 | of the currently supported features. 59 | 60 | * Value replacement can be done using the `{{ }}` syntax. 61 | * Literals supported are strings (single, double, or backtick quoted), boolean, numbers (currently parsed as i64), null, arrays, and maps 62 | * Identifiers that start with an alphabetic character can be referred to directly e.g. `{{ some.value.path }}` 63 | * The root node can be referred to with `.` allowing things like `{{ . | json }}` to be used to dump the entire context as JSON 64 | * Identifiers of non-standard type, e.g. starting with a non-alphabetic character, spaces, etc. can be referred to using the 65 | bracket syntax. e.g. `{{ .['565'] }}`. This also allows array access and identifier of non-standard types (such as boolean). 66 | * Inline arrays: `{{ [1,2,3,4] }}` and complex nesting also possible e.g. `{{ [1,2, script("echo 'hello world!'"), (5 + 5 | base64)] }}` 67 | * Inline maps: `{{ {'key': 'value', 'otherKey': { 'nested': 'map' } } }}` 68 | * Control flow can be done using the `{% %}` syntax 69 | * If/else if: `{% if 10/2 == 5 %}The world is sane!{% else if false %}What universe are we in?{% end if %}` 70 | * Scoping can be done manually: `{% scope %}I'm in a scope!{% end scope %}` 71 | * For loops: `{% for thing in lots.of.stuff %} {{ thing['name'] }} {% end for %}`. For loops always enter a new scope. 72 | * Comments use the `{# #}` syntax and will be remitted from the output. 73 | * Whitespace control can be accomplished by adding a `-` to any of the above blocks e.g. `{{- 'no whitespace! -}}`. 74 | * Whitespace control can be added to one or both sides of the tags. All spaces, new lines, or other whitespace on the side with the `-` 75 | on it will be removed as if the block is immediately next to the other element. 76 | 77 | As documentation is still in progress, see the [kitchen sink](./examples/kitchen_sink.tmpl) for examples of template usage. 78 | 79 | ## Expression syntax 80 | 81 | Everything inside the standard `{{ }}` block is an expression. Each block holds exactly one expression, but that expression can be chained with 82 | many individual operations. A quick overview: 83 | 84 | * Math operations: `+ - * / %` these operations are only valid with numeric types 85 | * Equality: `== != < <= > >= && ||` 86 | * Value setting: `=` the left side of this operation must be some identifier e.g. `{{ some.val.path = 'hello world!' }}` 87 | * String concatenation: `~` e.g. `{{ 'Hello' ~ ' ' ~ 'world!' }}` prints "Hello world!" 88 | * Functions: `ident()` e.g. `{{ env('USER') }}` would retrieve the value of the environment variable "USER". 89 | * Filters: `|` e.g. `{{ 'hello world' | upper }}` would use the 'upper' filter to print "HELLO WORLD" 90 | 91 | As documentation is still in progress, see the [expression tests](./src/test/expressions.rs) for examples of expression usage. 92 | 93 | ## Performance 94 | 95 | Templar prefers rendering performance over parsing performance. While you should take most benchmarks with a grain of salt, simple templates 96 | render in a few microseconds. On my AMD Ryzen 2700U processor, I can render a simple template about 300,000 times a second on a single thread. 97 | 98 | Templates vary a lot though and templates that call out to shell commands or do other complex things will get less performance. 99 | 100 | ## API 101 | 102 | Full API documentation can be found on [docs.rs](https://docs.rs/templar/) 103 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Phil Proctor"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Templar" 7 | 8 | [rust] 9 | edition = "2018" 10 | 11 | [output.html] 12 | preferred-dark-theme = "coal" 13 | git-repository-url = "https://github.com/proctorlabs/templar" 14 | site-url = "https://proctorlabs.github.io/templar/" 15 | 16 | [output.html.fold] 17 | enable = true 18 | level = 1 19 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./intro.md) 4 | - [Installation](./install.md) 5 | - [Quick Start]() 6 | - [Command Line]() 7 | - [Usage](./cli/usage.md) 8 | - [Templating](./templates/intro.md) 9 | - [Syntax](./templates/syntax.md) 10 | - [Expressions](./templates/expressions.md) 11 | - [Filters](./templates/filters.md) 12 | - [Functions](./templates/functions.md) 13 | - [API Documentation](./api.md) 14 | -------------------------------------------------------------------------------- /docs/src/api.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | Complete documentation for the development API is available at [docs.rs](https://docs.rs/templar/). 4 | -------------------------------------------------------------------------------- /docs/src/cli/intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /docs/src/cli/usage.md: -------------------------------------------------------------------------------- 1 | # Templar CLI 2 | 3 | The CLI can be used to run expressions or execute templates to STDOUT or an output file. 4 | 5 | ## Usage 6 | 7 | Tu process a template, `templar template ` 8 | 9 | ```bash 10 | templar-template 0.3.0 11 | Execute a template and render the output 12 | 13 | USAGE: 14 | templar template 15 | 16 | OPTIONS: 17 | -d, --dynamic ... File to parse and load into the templating context as a dynamic input 18 | -h, --help Prints help information 19 | -i, --input ... File to parse and load into the templating context 20 | -o, --output Output to send the result to, defaults to stdout 21 | -s, --set ... Directly set a variable on the context 22 | 23 | ARGS: 24 | Template file(s) to open 25 | ``` 26 | 27 | To run an expression directly, `templar expression ` 28 | 29 | ```bash 30 | templar-expression 0.3.0 31 | Execute an expression and render the output 32 | 33 | USAGE: 34 | templar expression 35 | 36 | OPTIONS: 37 | -d, --dynamic ... File to parse and load into the templating context as a dynamic input 38 | -h, --help Prints help information 39 | -i, --input ... File to parse and load into the templating context 40 | -o, --output Output to send the result to, defaults to stdout 41 | -s, --set ... Directly set a variable on the context 42 | 43 | ARGS: 44 | The expression to run 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/src/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | We try to supply easy install methods for different platforms on a best-effort basis. 4 | 5 | ## Binary Install 6 | 7 | The primary way Templar is distributed is in binary form from [Github](https://github.com/proctorlabs/templar/releases). 8 | Binaries are available for a number of platforms and can be quickly installed via script. A simple script install can 9 | be like this: 10 | 11 | ```bash 12 | # Make sure to pick the architecture and variant for your platform 13 | curl -sL https://github.com/proctorlabs/templar/releases/download/v0.3.0/templar-x86_64-unknown-linux-gnu.tar.xz | 14 | tar -xJ -C /usr/local/bin && chmod +x /usr/local/bin/templar 15 | ``` 16 | 17 | ## Cargo (source) 18 | 19 | If Cargo is available, templar can be quickly installed with this command: 20 | 21 | ```bash 22 | cargo install --all-features templar 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Templar is a templating tool for the command line or available as a library for Rust. Templar 4 | attempts to be a lightweight tool that runs with very few dependencies, distributed in binary form 5 | for a number of architectures. 6 | -------------------------------------------------------------------------------- /docs/src/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | TBD 4 | 5 | ## Load data 6 | 7 | TBD 8 | 9 | ## Use template 10 | 11 | TBD 12 | -------------------------------------------------------------------------------- /docs/src/templates/expressions.md: -------------------------------------------------------------------------------- 1 | # Expressions 2 | 3 | Everything inside the standard `{{ }}` block is an expression. Each block holds exactly one expression, but that expression can be chained with 4 | many individual operations. A quick overview: 5 | 6 | * Math operations: `+ - * / %` these operations are only valid with numeric types 7 | * Equality: `== != < <= > >= && ||` 8 | * Value setting: `=` the left side of this operation must be some identifier e.g. `{{ some.val.path = 'hello world!' }}` 9 | * String concatenation: `~` e.g. `{{ 'Hello' ~ ' ' ~ 'world!' }}` prints "Hello world!" 10 | * Functions: `ident()` e.g. `{{ env('USER') }}` would retrieve the value of the environment variable "USER". 11 | * Filters: `|` e.g. `{{ 'hello world' | upper }}` would use the 'upper' filter to print "HELLO WORLD" 12 | 13 | As documentation is still in progress, see the [expression tests](./src/test/expressions.rs) for examples of expression usage. 14 | -------------------------------------------------------------------------------- /docs/src/templates/filters.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | Filters are used to process the result of an expression in a template. 4 | 5 | ## Overview 6 | 7 | As an example, the expression `{{ 'hello' | upper }}` uses the "upper" filter to create 8 | the upper case result "HELLO". 9 | 10 | ## Built in filters 11 | 12 | - require: Will throw an error if the result is empty or null 13 | - default(any): Replaces empty, null, or error types with the default value from the args 14 | - length: Returns the length of a string or array 15 | - lower: Lowercase the rendered result 16 | - upper: Uppercase the rendered result 17 | - trim: Trim whitespace off the rendered result 18 | - split(str?): Split a string into an array. Delimited by newline, but an arg can be used to override the delimiter. 19 | - index(int): Retrieve the int index from the array 20 | - join(str?): Join an array with the provided string. Defaults to newline 21 | - string: Forces the result into a string type, usually by rendering it 22 | - key(str): Retrieve the value of the specified key from the dictionary 23 | - escape_html: (alias 'e') Render the result and escape HTML characters 24 | - yaml: (alias yml) Serialize the data into a YAML string. Requires"yaml-extension" feature (default on) 25 | - json(str?): Serialize the data into a JSON string. Set str to 'pretty' to print with indentation. Requires the "json-extension" feature (default on) 26 | - base64(str?): Encode the result as Base64. If the optional string parameter is set to "decode" then it will try to decode instead. Requires "base64-extension" feature (default on) 27 | -------------------------------------------------------------------------------- /docs/src/templates/functions.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | Functions are used for pulling or creating data from other sources or using other methods. 4 | 5 | ## Overview 6 | 7 | As an example, the file() function will open a file and return the contents as a string. Functions do not require 8 | any data to work, though most need arguments. Functions can be used as an argument to other functions, 9 | to filters, or as the base operation. For example, this is a valid expression: 10 | 11 | ```template 12 | { 'filename': 'settings.json', 'content': json(file('settings.json')) } | yml 13 | ``` 14 | 15 | The above will creat a map with two fields "filename" and "content" with content containing the parsed 16 | contents the file `settings.json`. Then we pass this map to the filter `yml` to then render 17 | that map into a serialized YML string. 18 | 19 | ## Built in functions 20 | 21 | - file(str): Open file and read contents to a string 22 | - env(str): Read the named environment variable 23 | - script(str): Execute the string as a shell script. Returns a map with keys "stdout", "stderr", "status" 24 | - command(str, str[]?): Execute the supplied command with the supplied arguments. Returns a map with keys "stdout", "stderr", "status" 25 | - json(str): Parse the supplied JSON string into a map. Requires "json-extension" feature (default on) 26 | - yaml(str): (alias yml) Parse the supplied YML string into a map. Requires "yaml-extension" feature (default on) 27 | -------------------------------------------------------------------------------- /docs/src/templates/intro.md: -------------------------------------------------------------------------------- 1 | # Templating 2 | 3 | Templar's templating syntax is inspired by Jinja2 and Ansible. 4 | 5 | ## Examples 6 | 7 | The templating syntax is likely familiar considering the frameworks that it is based on. For instance, a 8 | simple template may look like this: 9 | 10 | ```properties 11 | user_name={{ user.name }} {# Replace with the context property 'name' in 'user' #} 12 | full_context={{ . | json("pretty") }} {# Dump the entire context as JSON, '.' is the root node #} 13 | password={{ script('echo hunter2 | md5sum') }} {# Execute a shell command and calculate the MD5 sum #} 14 | ``` 15 | 16 | In addition to simple replacements, more complex expressions can be used. 17 | 18 | ```markdown 19 | The calculated result is {{ 100 * 5 / 10 }} {#- Prints '50' #} 20 | 21 | Today's guest list: 22 | {%- for person in ['Bob', 'Joe', 'Jen', 'Amy')] %} 23 | * {{ person }} will come to the party! 24 | {%- endfor %} {#- This will loop everyone in the inline array above, but they array could also come from the context #} 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/src/templates/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | Much of the syntax is based on the wonderful [Jinja2](https://jinja.palletsprojects.com/en/2.10.x/) project. Here are some 4 | of the currently supported features. 5 | 6 | * Value replacement can be done using the `{{ }}` syntax. 7 | * Literals supported are strings (single, double, or backtick quoted), boolean, numbers (currently parsed as i64), null, arrays, and maps 8 | * Identifiers that start with an alphabetic character can be referred to directly e.g. `{{ some.value.path }}` 9 | * The root node can be referred to with `.` allowing things like `{{ . | json }}` to be used to dump the entire context as JSON 10 | * Identifiers of non-standard type, e.g. starting with a non-alphabetic character, spaces, etc. can be referred to using the 11 | bracket syntax. e.g. `{{ .['565'] }}`. This also allows array access and identifier of non-standard types (such as boolean). 12 | * Inline arrays: `{{ [1,2,3,4] }}` and complex nesting also possible e.g. `{{ [1,2, script("echo 'hello world!'"), (5 + 5 | base64)] }}` 13 | * Inline maps: `{{ {'key': 'value', 'otherKey': { 'nested': 'map' } } }}` 14 | * Control flow can be done using the `{% %}` syntax 15 | * If/else if: `{% if 10/2 == 5 %}The world is sane!{% else if false %}What universe are we in?{% end if %}` 16 | * Scoping can be done manually: `{% scope %}I'm in a scope!{% end scope %}` 17 | * For loops: `{% for thing in lots.of.stuff %} {{ thing['name'] }} {% end for %}`. For loops always enter a new scope. 18 | * Comments use the `{# #}` syntax and will be remitted from the output. 19 | * Whitespace control can be accomplished by adding a `-` to any of the above blocks e.g. `{{- 'no whitespace! -}}`. 20 | * Whitespace control can be added to one or both sides of the tags. All spaces, new lines, or other whitespace on the side with the `-` 21 | on it will be removed as if the block is immediately next to the other element. 22 | 23 | As documentation is still in progress, see the [kitchen sink](./examples/kitchen_sink.tmpl) for examples of template usage. 24 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use templar::*; 2 | 3 | const TEMPLATE: &str = r#" 4 | {#- This is a comment, the '-' on either side will eat whitespace too -#} 5 | The value of some.string is: {{ some.string }} 6 | {#- using '-' on only the left side will eat the newline prior to this so that there isn't a blank line in between #} 7 | The value of some.number is: {{ some.number -}} 8 | "#; 9 | 10 | fn main() -> Result<(), TemplarError> { 11 | // Parse the template using the global instance, this is suitable for most purposes 12 | let template = Templar::global().parse(TEMPLATE)?; 13 | 14 | // Create a context and set some data 15 | let mut data: Document = Document::default(); 16 | data["some"]["string"] = "Hello World!".into(); 17 | data["some"]["number"] = 42i64.into(); 18 | let context = StandardContext::new(); 19 | context.set(data)?; 20 | 21 | // Render a template using the context creates 22 | println!("{}", template.render(&context)?); 23 | 24 | // -- The output of this looks like below 25 | // The value of some.string is: Hello World! 26 | // The value of some.number is: 42 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/custom_filter.rs: -------------------------------------------------------------------------------- 1 | use templar::*; 2 | 3 | const TEMPLATE: &str = r#" 4 | {{- `I'm CRAZY!!!` | repeater(5) -}} 5 | "#; 6 | 7 | // This macro isn't necessary, but it makes it easy if your method is expecting specific data types. 8 | // The extra identifier after the '|' is the unstructured::Document variant we are expecting. 9 | // This macro will also throw sane errors if incoming data types are not what is expected. 10 | // If you need to throw an error inside your macro definition, you can return TemplarError::RenderFailure(format!("Message")).into() 11 | // and these errors will propagate up to the render() or exec() calls to the template. 12 | // See the `Filter` type in lib.rs if you can't or don't want to use the macro. 13 | templar_filter! { 14 | fn repeater(inc: String | String, count: i64 | I64) -> String { 15 | inc.push('\n'); 16 | inc.repeat(count as usize).trim() 17 | } 18 | } 19 | 20 | fn main() -> Result<(), TemplarError> { 21 | // Since we need to customize our instance, we can't use the global instance. We'll use a builder. 22 | let mut builder = TemplarBuilder::default(); 23 | builder.add_filter("repeater", repeater); 24 | let templar = builder.build(); 25 | 26 | // Parse the template using our customized instance 27 | let template = templar.parse(TEMPLATE)?; 28 | 29 | // Create an empty context 30 | let context = StandardContext::new(); 31 | 32 | // Render a template using the context creates 33 | println!("{}", template.render(&context)?); 34 | 35 | // -- The output of this looks like below 36 | // I'm CRAZY!!! 37 | // I'm CRAZY!!! 38 | // I'm CRAZY!!! 39 | // I'm CRAZY!!! 40 | // I'm CRAZY!!! 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Somekey": "Somevalue", 3 | "Array": [1, 2, 3], 4 | "Dict": { 5 | "key": "val", 6 | "another": "val" 7 | } 8 | } -------------------------------------------------------------------------------- /examples/kitchen_sink.tmpl: -------------------------------------------------------------------------------- 1 | The kitchen sink. 2 | Basically just a bunch of random stuff thrown into a file to demonstrate templates. 3 | 4 | --- Comment example 5 | {#- <-- The '-' here removes the whitespace between this comment and the previous line in output 6 | 7 | Try running this command with Templar: 8 | templar -i Cargo.toml -i examples/example.json template examples/template.tmpl 9 | 10 | #} 11 | 12 | --- Value replacements 13 | This template was run with the user {{ env('USER') }} 14 | 15 | The value 'Somekey' from example.json is {{ Somekey | default('NONE') }} 16 | The package name from Cargo.toml is {{ package.name }} 17 | 18 | --- For loop examples 19 | Iterate over an array 20 | {%- for item in Array %} 21 | {{ item }} 22 | {%- end for %} 23 | 24 | Iterate over a dictionary 25 | {%- for item in Dict %} 26 | Key: {{ item.key }} -> Value: {{ item.value }} 27 | {%- end for %} 28 | 29 | --- Setting values 30 | {{- val = 'test' }} 31 | {{ val }} 32 | 33 | --- Scoping example 34 | {{ 'scoped_val' = ('Not in a scope...' | upper) -}} 35 | {{ scoped_val }} 36 | {%- scope -%} 37 | {{ 'scoped_val' = 'I\'m in a scope!' }} 38 | {{ scoped_val }} 39 | {%- end scope %} 40 | {{ scoped_val }} 41 | 42 | --- If/Else Example 43 | {%- if env('USER') | lower == 'root' %} 44 | {{ 'don\'t run as root!!' | upper }} 45 | {%- else if env('USER') == 'phil' %} 46 | Hi phil! 47 | {%- else %} 48 | Good, you aren't running as root 49 | {%- end if %} 50 | 51 | {{ `hello` }} 52 | 53 | {#- 54 | The rendered output of this template: 55 | ----- 56 | 57 | The kitchen sink. 58 | Basically just a bunch of random stuff thrown into a file to demonstrate templates. 59 | 60 | --- Comment example 61 | 62 | --- Value replacements 63 | This template was run with the user phil 64 | 65 | The value 'Somekey' from example.json is Somevalue 66 | The package name from Cargo.toml is 67 | 68 | --- For loop examples 69 | Iterate over an array 70 | 1 71 | 2 72 | 3 73 | 74 | Iterate over a dictionary 75 | Key: another -> Value: val 76 | Key: key -> Value: val 77 | 78 | --- Setting values 79 | test 80 | 81 | --- Scoping example 82 | NOT IN A SCOPE... 83 | I'm in a scope! 84 | NOT IN A SCOPE... 85 | 86 | --- If/Else Example 87 | Hi phil! 88 | 89 | hello 90 | #} 91 | -------------------------------------------------------------------------------- /examples/self_referring.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Self Referring Demo 3 | name_upper: "{{ name | upper }}" 4 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | target := `echo -n "${TARGET:-x86_64-unknown-linux-gnu}"` 2 | build_dir := `echo -n $PWD/target/${TARGET:-x86_64-unknown-linux-gnu}/release` 3 | package_dir := `echo -n $PWD/target/package` 4 | cargo := `echo -n "${CARGO:-cargo}"` 5 | bin_name := 'templar' 6 | 7 | release: tag publish 8 | default: build 9 | 10 | _readme: setup-cargo 11 | 12 | _validate: 13 | #!/usr/bin/env bash 14 | set -Eeou pipefail 15 | 16 | echo 'Making sure all changes have been committed...' 17 | if [[ $(git diff --stat) != '' ]]; then 18 | echo 'Working tree dirty, not allowing publish until all changes have been committed.' 19 | #exit 1 20 | fi 21 | 22 | echo 'Running "cargo check"' 23 | cargo check --all-features --tests --examples --bins --benches 24 | 25 | echo 'Running unit tests' 26 | cargo test --all-features 27 | 28 | @setup-cargo: 29 | rustup toolchain install stable 30 | rustup target add '{{ target }}' 31 | 32 | # DOGFOODING 33 | cargo install templar --all-features 34 | 35 | # Other stuff 36 | cargo install cargo-deb 37 | cargo install cargo-readme 38 | cargo install cargo-strip 39 | cargo install mdbook 40 | 41 | build: 42 | cargo build --all-features 43 | 44 | changelog: 45 | #!/usr/bin/env bash 46 | set -Eeou pipefail 47 | git log --pretty=format:'%d %s' --no-merges | grep -E '(tag:|#chg)' | sed 's/.*#chg /- /g' | sed 's/ (tag:/\n## Release/g' | sed 's/) .*/\n/g' 48 | 49 | run +args="": 50 | cargo run --all-features -- {{args}} 51 | 52 | watch +args="": 53 | watchexec -w src just run -- {{args}} 54 | 55 | build-release: 56 | #!/usr/bin/env bash 57 | set -Eeou pipefail 58 | echo 'Building for {{ target }}' 59 | {{cargo}} build --all-features --release --target '{{ target }}' 60 | 61 | package-tar: build-release 62 | #!/usr/bin/env bash 63 | set -Eeou pipefail 64 | mkdir -p '{{ package_dir }}' 65 | cargo strip --target '{{ target }}' || true 66 | tar -C '{{ build_dir }}' -cvJf '{{ package_dir }}/{{ bin_name }}-{{ target }}.tar.xz' '{{ bin_name }}' 67 | 68 | package-deb: build-release 69 | cp -f target/{{ target }}/release/templar target/release/templar 70 | cargo deb --no-build --no-strip -o "{{ package_dir }}/{{ bin_name }}-{{ target }}.deb" 71 | 72 | book: 73 | mdbook build docs 74 | 75 | serve-book: 76 | mdbook serve docs 77 | 78 | package: package-tar package-deb 79 | 80 | dry-run: _validate 81 | cargo publish --all-features --dry-run 82 | 83 | tag: _validate 84 | #!/usr/bin/env bash 85 | set -Eeou pipefail 86 | git tag "v$(templar -i Cargo.toml -e '.[`package`][`version`]')" 87 | git push --tags 88 | 89 | publish: _validate 90 | #!/usr/bin/env bash 91 | set -Eeou pipefail 92 | cargo publish --all-features 93 | -------------------------------------------------------------------------------- /templar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "templar" 3 | authors = ["Phil Proctor "] 4 | description = "Lightweight, fast, and powerful templating engine" 5 | documentation = "https://docs.rs/templar" 6 | version = "0.5.0" 7 | edition = "2018" 8 | repository = "https://github.com/proctorlabs/templar" 9 | keywords = ["cli", "template", "templating", "handlebars", "jinja"] 10 | categories = ["template-engine"] 11 | license = "MIT/Apache-2.0" 12 | readme = "../README.md" 13 | exclude = [".github/**", "docs/**"] 14 | workspace = "../" 15 | 16 | [package.metadata.deb] 17 | maintainer = "Phil Proctor " 18 | copyright = "2020, templar development team" 19 | license-file = ["LICENSE-MIT", "5"] 20 | extended-description = """\ 21 | Templar is a command for processing template files with \ 22 | a Jinja2 inspired syntax and using a variety of data sources \ 23 | as an input.""" 24 | depends = "$auto" 25 | section = "utility" 26 | priority = "optional" 27 | 28 | [dependencies] 29 | templar_macros = { path = "../templar_macros", version = "0.5.0" } 30 | 31 | # General dependencies 32 | unstructured = { version = "0.5.1", default_features = false, features = [] } 33 | lazy_static = "1.4" 34 | pest = "2.1" 35 | pest_derive = "2.1" 36 | derive_more = "0.99" 37 | 38 | # Optional serde deps 39 | serde = { version = "1.0", optional = true, features = ["derive"] } 40 | serde_yaml = { version ="0.8", optional = true } 41 | serde_json = { version ="1.0", optional = true } 42 | toml = { version = "0.5", optional = true } 43 | serde-xml-rs = { version = "0.4", optional = true } 44 | 45 | # Misc optionals 46 | parking_lot = { version = "0.11", optional = true } 47 | base64 = { version = "0.13", optional = true } 48 | 49 | # Binary deps 50 | clap = { version = "2.33", optional = true } 51 | structopt = { version = "0.3", optional = true } 52 | 53 | [dev-dependencies] 54 | criterion = "0.3" 55 | 56 | [features] 57 | default = ["shared-context", "yaml-extension", "json-extension", "base64-extension"] 58 | 59 | #ser/deser extensions 60 | toml-extension = ["serde", "toml"] 61 | xml-extension = ["serde", "serde-xml-rs"] 62 | json-extension = ["serde", "serde_json"] 63 | yaml-extension = ["serde", "serde_yaml"] 64 | 65 | #other 66 | shared-context = ["parking_lot"] 67 | base64-extension = ["base64"] 68 | 69 | #groups 70 | serde-extensions = ["toml-extension", "xml-extension", "json-extension", "yaml-extension"] 71 | common-extensions = ["serde-extensions", "base64-extension"] 72 | full = ["serde-extensions", "base64-extension", "shared-context"] 73 | 74 | #binary 75 | bin = ["clap", "structopt", "common-extensions"] 76 | 77 | experimental = [] 78 | 79 | [lib] 80 | name = "templar" 81 | path = "src/lib.rs" 82 | 83 | [[bin]] 84 | name = "templar" 85 | path = "src/main.rs" 86 | required-features = ["bin"] 87 | 88 | [[bench]] 89 | name = "templar_benchmark" 90 | harness = false 91 | path = "benches/lib.rs" 92 | -------------------------------------------------------------------------------- /templar/benches/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::black_box; 5 | use criterion::Criterion; 6 | use templar::error::*; 7 | use templar::*; 8 | 9 | static EXPR: &str = r#" 10 | This is a template. 11 | 12 | {# It includes comments #} 13 | {{ `and expressions` }} 14 | {{ "THAT CAN CALL FILTERS" | lower }} 15 | "#; 16 | 17 | fn exec_expression(template: &Template, context: &impl Context) -> Result<()> { 18 | template.exec(context); 19 | Ok(()) 20 | } 21 | 22 | fn criterion_benchmark(c: &mut Criterion) { 23 | let template = Templar::global().parse_template(EXPR).unwrap(); 24 | let context = StandardContext::new(); 25 | c.bench_function("Execute a simple expression", |b| { 26 | b.iter(|| exec_expression(black_box(&template), black_box(&context))) 27 | }); 28 | } 29 | 30 | criterion_group!(benches, criterion_benchmark); 31 | criterion_main!(benches); 32 | -------------------------------------------------------------------------------- /templar/src/cli/command.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::error::Error; 3 | use std::path::PathBuf; 4 | use structopt::clap::AppSettings::*; 5 | use structopt::StructOpt; 6 | 7 | impl Command { 8 | pub fn parse() -> Result { 9 | Ok(Self::from_args()) 10 | } 11 | } 12 | 13 | #[derive(StructOpt, Debug)] 14 | #[structopt( 15 | name = "templar", 16 | rename_all = "kebab_case", 17 | author, 18 | about, 19 | settings = &[ArgRequiredElseHelp, DeriveDisplayOrder, DisableHelpSubcommand, UnifiedHelpMessage] 20 | )] 21 | pub struct Command { 22 | /// Output to send the result to, defaults to stdout. 23 | #[structopt(short = "o", long, parse(from_os_str))] 24 | pub destination: Option, 25 | 26 | /// Template file or directory to process 27 | #[structopt(short, long, conflicts_with = "expr")] 28 | pub template: Option, 29 | 30 | /// Evaluate a single expression instead of a full template 31 | #[structopt( 32 | short, 33 | long = "expression", 34 | name = "expression", 35 | conflicts_with = "template" 36 | )] 37 | pub expr: Option, 38 | 39 | /// File to parse and load into the templating context as a dynamic input 40 | #[structopt( 41 | short, 42 | long = "dynamic", 43 | name = "dynamic", 44 | number_of_values = 1, 45 | multiple = true 46 | )] 47 | pub dynamic_input: Vec, 48 | 49 | /// File to parse and load into the templating context 50 | #[structopt(short, long, number_of_values = 1, multiple = true)] 51 | pub input: Vec, 52 | 53 | /// Directly set a variable on the context 54 | #[structopt(short, long, parse(try_from_str = parse_key_val), number_of_values = 1)] 55 | pub set: Vec<(String, String)>, 56 | 57 | /// Allow directories to be recursively processed 58 | #[structopt(short, long)] 59 | pub recursive: bool, 60 | 61 | /// Overwrite target if it already exists 62 | #[structopt(short, long)] 63 | pub force: bool, 64 | } 65 | 66 | /// Parse a single key-value pair 67 | fn parse_key_val(s: &str) -> std::result::Result<(T, U), Box> 68 | where 69 | T: std::str::FromStr, 70 | T::Err: Error + 'static, 71 | U: std::str::FromStr, 72 | U::Err: Error + 'static, 73 | { 74 | let pos = s 75 | .find('=') 76 | .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; 77 | Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) 78 | } 79 | -------------------------------------------------------------------------------- /templar/src/cli/context.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::path::PathBuf; 3 | use templar::{InnerData, Templar}; 4 | // use unstructured::Document; 5 | 6 | pub fn build_context(options: &Command) -> Result { 7 | let ctx = StandardContext::new(); 8 | for file in options.dynamic_input.iter() { 9 | let doc = parse_data(file)?; 10 | let tree: TemplateTree = Templar::global().parse(&doc)?; 11 | ctx.set(tree)?; 12 | } 13 | for file in options.input.iter() { 14 | ctx.merge(parse_data(file)?)?; 15 | } 16 | for setter in options.set.iter() { 17 | ctx.set_path(&[&setter.0.to_string().into()], setter.1.to_string())?; 18 | } 19 | Ok(ctx) 20 | } 21 | 22 | fn parse_data(path: &PathBuf) -> Result { 23 | let contents = read_file(path)?; 24 | let ext: String = path 25 | .extension() 26 | .map(|ext| ext.to_string_lossy().to_lowercase()) 27 | .unwrap_or_default(); 28 | Ok(match &ext as &str { 29 | "js" | "json" => serde_json::from_str(&contents).wrap()?, 30 | "yml" | "yaml" => serde_yaml::from_str(&contents).wrap()?, 31 | "xml" => serde_xml_rs::from_str(&contents) 32 | .map_err(|e| TemplarError::RenderFailure(format!("{:?}", e)))?, 33 | "toml" => toml::from_str(&contents).wrap()?, 34 | _ => serde_json::from_str(&contents).wrap()?, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /templar/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use command::*; 3 | use context::build_context; 4 | use std::fs::{create_dir_all, remove_file}; 5 | use std::io::prelude::*; 6 | use std::path::PathBuf; 7 | use templar::Templar; 8 | use util::*; 9 | 10 | mod command; 11 | mod context; 12 | mod util; 13 | 14 | pub fn run() -> Result<()> { 15 | CommandContext::new(Command::parse()?)?.run() 16 | } 17 | 18 | #[derive(Debug)] 19 | struct CommandContext { 20 | cmd: Command, 21 | ctx: StandardContext, 22 | } 23 | 24 | impl CommandContext { 25 | fn new(cmd: Command) -> Result { 26 | let ctx = build_context(&cmd)?; 27 | Ok(CommandContext { cmd, ctx }) 28 | } 29 | 30 | fn run(&self) -> Result<()> { 31 | match (&self.cmd.expr, &self.cmd.template) { 32 | (Some(ref text), None) => self.exec_expression(text), 33 | (None, Some(ref file)) => self.exec_path(file), 34 | (None, None) => self.exec_stdin(), 35 | _ => unreachable!(), //Command:parse() has these as mutually exclusive 36 | } 37 | } 38 | 39 | fn exec_path(&self, file: &PathBuf) -> Result<()> { 40 | if file.is_file() { 41 | let template_contents = read_file(file)?; 42 | self.render_file(Templar::global().parse_template(&template_contents)?) 43 | } else if file.is_dir() { 44 | match ( 45 | self.cmd.recursive, 46 | self.cmd.destination.as_ref(), 47 | self.cmd.force, 48 | ) { 49 | (false, _, _) => Err(TemplarError::RenderFailure( 50 | "Recursive flag must be used to template a directory.".into(), 51 | )), 52 | (true, None, _) => Err(TemplarError::RenderFailure( 53 | "Destination path required when templating into a directory".into(), 54 | )), 55 | (true, Some(d), true) => self.render_recursive(file, d), 56 | (true, Some(d), false) => { 57 | if !d.exists() || d.is_dir() { 58 | self.render_recursive(file, d) 59 | } else { 60 | Err(TemplarError::RenderFailure( 61 | "Destination must be new path or existing directory. Use --force to allow overwriting existing content.".into(), 62 | )) 63 | } 64 | } 65 | } 66 | } else { 67 | Err(TemplarError::RenderFailure("Template not found!".into())) 68 | } 69 | } 70 | 71 | fn exec_expression(&self, text: &str) -> Result<()> { 72 | self.render_file(Templar::global().parse_expression(text)?) 73 | } 74 | 75 | fn exec_stdin(&self) -> Result<()> { 76 | let template_contents = read_stdin()?; 77 | self.render_file(Templar::global().parse_template(&template_contents)?) 78 | } 79 | 80 | fn render_recursive(&self, src: &PathBuf, dst: &PathBuf) -> Result<()> { 81 | if src.is_dir() { 82 | if dst.is_file() && self.cmd.force { 83 | remove_file(dst)?; 84 | } else if !dst.exists() { 85 | create_dir_all(dst)?; 86 | } 87 | for entry in src.read_dir()? { 88 | let p = entry?.path(); 89 | let filename = p.file_name().unwrap(); 90 | let mut newsrc = src.clone(); 91 | let mut newdst = dst.clone(); 92 | newsrc.push(filename); 93 | newdst.push(filename); 94 | self.render_recursive(&newsrc, &newdst)?; 95 | } 96 | Ok(()) 97 | } else { 98 | let template_contents = read_file(src)?; 99 | let tpl = Templar::global().parse_template(&template_contents)?; 100 | let output = tpl.render(&self.ctx)?; 101 | if dst.is_file() { 102 | if self.cmd.force { 103 | remove_file(dst)?; 104 | } else { 105 | return Err(TemplarError::RenderFailure(format!( 106 | "Destination file '{}' exists!", 107 | dst.file_name().unwrap_or_default().to_string_lossy() 108 | ))); 109 | } 110 | } 111 | write_file(dst, &output) 112 | } 113 | } 114 | 115 | fn render_file(&self, tpl: Template) -> Result<()> { 116 | let output = tpl.render(&self.ctx)?; 117 | match self.cmd.destination { 118 | Some(ref file) => write_file(file, &output), 119 | None => write_stdout(&output), 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /templar/src/cli/util.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::fs::File; 3 | use std::path::PathBuf; 4 | 5 | pub fn read_stdin() -> Result { 6 | let mut result = String::new(); 7 | std::io::stdin().read_to_string(&mut result)?; 8 | Ok(result) 9 | } 10 | 11 | pub fn read_file(path: &PathBuf) -> Result { 12 | let mut file = File::open(path)?; 13 | let mut result = String::new(); 14 | file.read_to_string(&mut result)?; 15 | Ok(result) 16 | } 17 | 18 | pub fn write_stdout(contents: &str) -> Result<()> { 19 | print!("{}", contents); 20 | Ok(()) 21 | } 22 | 23 | pub fn write_file(file: &PathBuf, contents: &str) -> Result<()> { 24 | let mut f = File::create(file)?; 25 | f.write_all(contents.as_bytes())?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /templar/src/context/dynamic/context_map.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default)] 4 | pub struct ContextMap { 5 | root: BTreeMap, 6 | } 7 | 8 | impl ContextMap { 9 | pub fn new>(doc: T) -> Self { 10 | let mut result = ContextMap::default(); 11 | result.set(doc, &[]).unwrap_or_default(); 12 | result 13 | } 14 | 15 | pub fn set>(&mut self, value: T, path: &[&InnerData]) -> Result<()> { 16 | if path.is_empty() { 17 | let val: ContextMapValue = value.into(); 18 | if let ContextMapValue::Map(map) = val { 19 | for (k, v) in map.into_iter() { 20 | self.root.insert(k, v); 21 | } 22 | } 23 | return Ok(()); 24 | } 25 | if path.len() == 1 { 26 | self.root.insert(path[0].clone(), value.into()); 27 | return Ok(()); 28 | } 29 | let mut target: &mut ContextMapValue = self 30 | .root 31 | .entry(path[0].clone()) 32 | .or_insert_with(ContextMapValue::new_map); 33 | for p in path.iter().skip(1).take(path.len() - 1) { 34 | target = target.get_or_add_key(*p); 35 | } 36 | target.set(value.into()); 37 | Ok(()) 38 | } 39 | 40 | pub fn exec(&self, ctx: &impl Context, path: &[&InnerData]) -> Data { 41 | if path.is_empty() { 42 | let copy = ContextMapValue::Map(self.root.clone()); 43 | return copy.exec(ctx); 44 | } 45 | let walker = ContextWalk::from(self.root.get(&path[0])); 46 | for p in path.iter().skip(1) { 47 | walker.walk(ctx, p); 48 | } 49 | walker.exec(ctx) 50 | } 51 | } 52 | 53 | impl From for ContextMapValue { 54 | fn from(val: TemplateTree) -> Self { 55 | match val { 56 | TemplateTree::Template(t) => t.into(), 57 | TemplateTree::Sequence(s) => { 58 | let result: Vec = s.iter().map(|t| t.clone().into()).collect(); 59 | ContextMapValue::Seq(result) 60 | } 61 | TemplateTree::Mapping(m) => { 62 | let result: BTreeMap = m 63 | .iter() 64 | .map(|(k, v)| (k.clone(), v.clone().into())) 65 | .collect(); 66 | ContextMapValue::Map(result) 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl ContextMapValue { 73 | #[inline] 74 | fn new_map() -> Self { 75 | ContextMapValue::Map(BTreeMap::new()) 76 | } 77 | 78 | fn set>(&mut self, val: T) { 79 | drop(replace(self, val.into())); 80 | } 81 | 82 | fn get_or_add_key(&mut self, key: &InnerData) -> &mut ContextMapValue { 83 | match self { 84 | ContextMapValue::Map(ref mut map) => map 85 | .entry(key.clone()) 86 | .or_insert_with(ContextMapValue::new_map), 87 | _ => { 88 | let new_val = ContextMapValue::new_map(); 89 | drop(replace(self, new_val)); 90 | self.get_or_add_key(key) 91 | } 92 | } 93 | } 94 | 95 | pub fn exec(&self, ctx: &impl Context) -> Data { 96 | match self { 97 | ContextMapValue::Node(node) => node.exec(ctx), 98 | ContextMapValue::Map(map) => { 99 | let mut result: BTreeMap = BTreeMap::new(); 100 | for (k, v) in map.iter() { 101 | match v.exec(ctx).into_result() { 102 | Ok(d) => result.insert(k.clone(), d.into_inner()), 103 | Err(e) => return e.into(), 104 | }; 105 | } 106 | result.into() 107 | } 108 | ContextMapValue::Seq(s) => { 109 | let result: Result> = s 110 | .iter() 111 | .map(|v| Ok(v.exec(ctx).into_result()?.into_inner())) 112 | .collect(); 113 | match result { 114 | Ok(s) => Data::new(s), 115 | Err(e) => e.into(), 116 | } 117 | } 118 | _ => Data::empty(), 119 | } 120 | } 121 | } 122 | 123 | #[derive(Clone, Debug)] 124 | pub enum ContextMapValue { 125 | Seq(Vec), 126 | Map(BTreeMap), 127 | Node(Arc), 128 | Empty, 129 | } 130 | 131 | impl Default for ContextMapValue { 132 | fn default() -> Self { 133 | ContextMapValue::Empty 134 | } 135 | } 136 | 137 | impl> From for ContextMapValue { 138 | fn from(val: T) -> Self { 139 | match val.into() { 140 | InnerData::Map(m) => { 141 | let mut new_val = BTreeMap::new(); 142 | for (k, v) in m.into_iter() { 143 | new_val.insert(k, v.into()); 144 | } 145 | ContextMapValue::Map(new_val) 146 | } 147 | InnerData::Seq(s) => { 148 | let new_val: Vec = s.into_iter().map(|i| i.into()).collect(); 149 | ContextMapValue::Seq(new_val) 150 | } 151 | InnerData::Newtype(mut d) => d.take().into(), 152 | other => ContextMapValue::Node(Arc::new(Data::from(other).into())), 153 | } 154 | } 155 | } 156 | 157 | impl From for ContextMapValue { 158 | fn from(val: Node) -> Self { 159 | ContextMapValue::Node(Arc::new(val)) 160 | } 161 | } 162 | 163 | impl From