├── .gitattributes ├── .github ├── tim-look.png ├── timengine.png └── workflows │ ├── docs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bindings └── node │ └── tim │ ├── README.md │ ├── package.json │ └── src │ ├── bin │ ├── .gitkeep │ ├── tim-linux.node │ └── tim-macos.node │ └── tim.js ├── editors └── tim.sublime-syntax ├── example ├── example.js ├── example_httpbeast.nim ├── example_mummy.nim ├── example_prologue.nim ├── initializer.nim ├── preview.html ├── storage │ └── .gitkeep └── templates │ ├── layouts │ ├── base.timl │ └── secondary.timl │ ├── partials │ ├── foot.timl │ ├── meta │ │ └── head.timl │ └── ws.timl │ └── views │ ├── about.timl │ ├── error.timl │ └── index.timl ├── src ├── tim.nim ├── tim.nims └── timpkg │ ├── app │ ├── manage.nim │ ├── microservice.nim │ └── source.nim │ ├── engine │ ├── ast.nim │ ├── compilers │ │ ├── html.nim │ │ ├── nimc.nim │ │ └── tim.nim │ ├── logging.nim │ ├── meta.nim │ ├── package │ │ ├── manager.nim │ │ └── remote.nim │ ├── parser.nim │ ├── stdlib.nim │ └── tokens.nim │ └── server │ ├── app.nim │ ├── config.nim │ └── dynloader.nim ├── tests ├── app │ ├── storage │ │ └── .gitkeep │ └── templates │ │ ├── layouts │ │ └── base.timl │ │ ├── partials │ │ └── btn.timl │ │ └── views │ │ └── index.timl ├── config.nims ├── snippets │ ├── cli_data.timl │ ├── html.timl │ ├── invalid.timl │ ├── loops.timl │ ├── std_arrays.timl │ ├── std_objects.timl │ └── std_strings.timl └── test1.nim └── tim.nimble /.gitattributes: -------------------------------------------------------------------------------- 1 | *.node binary 2 | *.node -text -diff -delta -------------------------------------------------------------------------------- /.github/tim-look.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/.github/tim-look.png -------------------------------------------------------------------------------- /.github/timengine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/.github/timengine.png -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' 6 | # branches: 7 | # - main 8 | paths-ignore: 9 | - LICENSE 10 | - README.* 11 | - examples 12 | - editors 13 | # pull_request: 14 | # paths-ignore: 15 | # - LICENSE 16 | # - README.* 17 | # - examples 18 | # - editors 19 | env: 20 | nim-version: 'stable' 21 | nim-src: src/${{ github.event.repository.name }}.nim 22 | deploy-dir: .gh-pages 23 | jobs: 24 | docs: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: jiro4989/setup-nim-action@v1 29 | with: 30 | nim-version: ${{ env.nim-version }} 31 | - run: nimble install -Y 32 | - run: nim doc --index:on --project --path:. --out:${{ env.deploy-dir }} ${{ env.nim-src }} 33 | 34 | - name: "Rename to index.html" 35 | run: mv ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 36 | 37 | - name: "Find and replace (index.html)" 38 | run: sed -i 's/${{ github.event.repository.name }}.html/index.html/g' ${{ env.deploy-dir }}/index.html 39 | 40 | - name: "Find and replace (theindex.html)" 41 | run: sed -i 's/${{ github.event.repository.name }}.html/index.html/g' ${{ env.deploy-dir }}/theindex.html 42 | 43 | - name: Deploy documents 44 | uses: peaceiris/actions-gh-pages@v3 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | publish_dir: ${{ env.deploy-dir }} 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' 6 | env: 7 | APP_NAME: 'tim' 8 | NIM_VERSION: '2.0.0' 9 | MAINTAINER: 'OpenPeeps' 10 | jobs: 11 | build-artifact: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - windows-latest 18 | - macOS-latest 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: jiro4989/setup-nim-action@v1 22 | with: 23 | nim-version: ${{ env.NIM_VERSION }} 24 | - run: choosenim show path -y 25 | - run: nimble build -Y -d:release 26 | - name: Create artifact 27 | run: | 28 | os="${{ runner.os }}" 29 | assets="${{ env.APP_NAME }}_$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')" 30 | echo "$assets" 31 | mkdir -p "dist/$assets" 32 | cp -r bin LICENSE README.* "dist/$assets/" 33 | ( 34 | cd dist 35 | if [[ "${{ runner.os }}" == Windows ]]; then 36 | 7z a "$assets.zip" "$assets" 37 | else 38 | tar czf "$assets.tar.gz" "$assets" 39 | fi 40 | ls -lah *.* 41 | ) 42 | shell: bash 43 | - uses: actions/upload-artifact@v2 44 | with: 45 | name: artifact-${{ matrix.os }} 46 | path: | 47 | dist/*.tar.gz 48 | dist/*.zip 49 | 50 | create-release: 51 | runs-on: ubuntu-latest 52 | needs: 53 | - build-artifact 54 | steps: 55 | - uses: actions/checkout@v1 56 | - name: Create Release 57 | id: create-release 58 | uses: actions/create-release@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tag_name: ${{ github.ref }} 63 | release_name: ${{ github.ref }} 64 | body: Release 65 | draft: false 66 | prerelease: false 67 | 68 | - name: Write upload_url to file 69 | run: echo '${{ steps.create-release.outputs.upload_url }}' > upload_url.txt 70 | 71 | - uses: actions/upload-artifact@v2 72 | with: 73 | name: create-release 74 | path: upload_url.txt 75 | 76 | upload-release: 77 | runs-on: ubuntu-latest 78 | needs: create-release 79 | strategy: 80 | matrix: 81 | include: 82 | - os: ubuntu-latest 83 | asset_name_suffix: linux.tar.gz 84 | asset_content_type: application/gzip 85 | - os: windows-latest 86 | asset_name_suffix: windows.zip 87 | asset_content_type: application/zip 88 | - os: macOS-latest 89 | asset_name_suffix: macos.tar.gz 90 | asset_content_type: application/gzip 91 | steps: 92 | - uses: actions/download-artifact@v2 93 | with: 94 | name: artifact-${{ matrix.os }} 95 | 96 | - uses: actions/download-artifact@v2 97 | with: 98 | name: create-release 99 | 100 | - id: vars 101 | run: | 102 | echo "::set-output name=upload_url::$(cat upload_url.txt)" 103 | 104 | - name: Upload Release Asset 105 | id: upload-release-asset 106 | uses: actions/upload-release-asset@v1 107 | env: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | with: 110 | upload_url: ${{ steps.vars.outputs.upload_url }} 111 | asset_path: ${{ env.APP_NAME }}_${{ matrix.asset_name_suffix }} 112 | asset_name: ${{ env.APP_NAME }}_${{ matrix.asset_name_suffix }} 113 | asset_content_type: ${{ matrix.asset_content_type }} 114 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - LICENSE 7 | - README.* 8 | - examples 9 | - editors 10 | - package.json 11 | pull_request: 12 | paths-ignore: 13 | - LICENSE 14 | - README.* 15 | - examples 16 | - editors 17 | - package.json 18 | 19 | jobs: 20 | test: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | nim-version: 25 | - '2.0.0' 26 | os: 27 | - ubuntu-latest 28 | # - windows-latest 29 | # - macOS-latest 30 | # - macos-13 # building on arm64 fails 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: jiro4989/setup-nim-action@v1 34 | with: 35 | nim-version: ${{ matrix.nim-version }} 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | # - run: sudo apt-get -y install libsass-dev 38 | - run: 'sudo apt-get install -y libpcre3-dev' 39 | - run: "npm install cmake-js -g" 40 | - run: "choosenim show path -y" 41 | - run: nimble install -Y 42 | - run: nimble test 43 | - run: denim build src/${{ github.event.repository.name }}.nim -r -y --cmake 44 | - name: "update tim.node" 45 | run: | 46 | git config --local user.name "github-actions[bot]" 47 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 48 | timNodeName="tim-$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]').node" 49 | git pull origin main 50 | git checkout . 51 | if test -f ./bindings/node/tim/src/bin/$timNodeName; then 52 | rm -f ./bindings/node/tim/src/bin/$timNodeName 53 | git add ./bindings/node/tim/src/bin/$timNodeName 54 | git commit -m "cleanup previous tim.node" 55 | fi 56 | cp ./bin/tim.node ./bindings/node/tim/src/bin/$timNodeName 57 | git status 58 | git add ./bindings/node/tim/src/bin/$timNodeName 59 | git commit -m "update tim for node on ${{ runner.os }}" 60 | - name: Push changes # push the output folder to your repo 61 | uses: ad-m/github-push-action@master 62 | with: 63 | github_token: ${{ secrets.GITHUB_TOKEN }} 64 | branch: 'main' 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /bin 3 | !tim-*.node 4 | nimcache/ 5 | nimblecache/ 6 | htmldocs/ 7 | /pkginfo.json 8 | /tests/app/storage/* 9 | /example/storage/ast/* 10 | /example/storage/html/* 11 | /bindings/tim/src/bin/* 12 | /init.sh 13 | /CMakeLists.txt 14 | denim_build/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Tim - Template Engine
3 | ⚡️ A high-performance template engine & markup language
4 | FastCompiled • Written in Nim 👑 5 |

6 | 7 |

8 | nimble install tim / npm install @openpeeps/tim 9 |

10 | 11 |

12 | API reference | Download

13 | Github Actions Github Actions 14 |

15 | 16 | ## 😍 Key Features 17 | or more like a _todo list_ 18 | - Fast & easy to code! 19 | - Caching & Pre-compilation 20 | - Transpiles to **JavaScript** for **Client-Side Rendering** 21 | - Supports embeddable code `json`, `js`, `yaml`, `css` 22 | - Built-in **Browser Sync & Reload** 23 | - Output Minifier 24 | - Written in Nim language 👑 25 | 26 | > [!NOTE] 27 | > We are currently rewriting big parts of Tim Engine to make it more performant and easier to use. Check out the [rewrite](https://github.com/openpeeps/tim/tree/rewrite) branch for the latest changes and improvements. 28 | 29 | 30 | ### Syntax Extensions 31 | - VSCode Extension available in [VS Marketplace](https://marketplace.visualstudio.com/items?itemName=CletusIgwe.timextension) (Thanks to [Cletus Igwe](https://github.com/Uzo2005)) 32 | - Sublime Syntax package available in [/editors](https://github.com/openpeeps/tim/blob/main/editors/tim.sublime-syntax) 33 | 34 | ### ❤ Contributions & Support 35 | - 🐛 Found a bug? [Create a new Issue](https://github.com/openpeeps/tim/issues) 36 | - 👋 Wanna help? [Fork it!](https://github.com/openpeeps/tim/fork) 37 | - 🎉 Spread the word! **Tell your friends about Tim Engine** 38 | - ⚽️ Play with Tim Engine in your next web-project 39 | - 😎 [Get €20 in cloud credits from Hetzner](https://hetzner.cloud/?ref=Hm0mYGM9NxZ4) 40 | - 🥰 [Donate via PayPal address](https://www.paypal.com/donate/?hosted_button_id=RJK3ZTDWPL55C) 41 | 42 | ### 🎩 License 43 | Tim Engine | `LGPLv3` license. [Made by Humans from OpenPeeps](https://github.com/openpeeps).
44 | Copyright © 2025 OpenPeeps & Contributors — All rights reserved. 45 | -------------------------------------------------------------------------------- /bindings/node/tim/README.md: -------------------------------------------------------------------------------- 1 |

2 | Tim - Template Engine
3 | ⚡️ A high-performance template engine & markup language
4 | FastCompiled • Written in Nim 👑 5 |

6 | 7 |

8 | npm install @openpeeps/tim 9 |

10 | 11 |

12 | API reference | Check Tim on GitHub

13 | Github Actions Github Actions 14 |

15 | 16 | ### Quick example 17 | ```timl 18 | div.container > div.row > div.col-lg-7.mx-auto 19 | h1.display-3.fw-bold: "Tim is Awesome" 20 | a href="https://github.com/openpeeps/tim" title="This is hot!": "Check Tim on GitHub" 21 | ``` 22 | 23 | ## Key features 24 | - Fast, compiled, easy to code 25 | - Caching & Pre-compilation 26 | - Transpiles to JavaScript for Client-Side Rendering 27 | - Supports embeddable code `json`, `js`, `yaml`, `css` 28 | - Built-in Browser Sync & Reload 29 | - Output Minifier 30 | - Written in Nim language 👑 31 | 32 | ### Tim in action 33 | [Here is an example web app](https://github.com/openpeeps/tim/blob/main/example/example.js) rendered by Tim Engine. 34 | 35 | ### Syntax Highlighting 36 | - VSCode Extension available in [VS Marketplace](https://marketplace.visualstudio.com/items?itemName=CletusIgwe.timextension) (Thanks to [Cletus Igwe](https://github.com/Uzo2005)) 37 | - Sublime Syntax package available in [/editors](https://github.com/openpeeps/tim/blob/main/editors/tim.sublime-syntax) 38 | 39 | ### ❤ Contributions & Support 40 | - 🐛 Found a bug? [Create a new Issue](https://github.com/openpeeps/tim/issues) 41 | - 👋 Wanna help? [Fork it!](https://github.com/openpeeps/tim/fork) 42 | - 🎉 Spread the word! **Tell your friends about Tim Engine** 43 | - ⚽️ Play with Tim Engine in your next web-project 44 | - 😎 [Get €20 in cloud credits from Hetzner](https://hetzner.cloud/?ref=Hm0mYGM9NxZ4) 45 | - 🥰 [Donate via PayPal address](https://www.paypal.com/donate/?hosted_button_id=RJK3ZTDWPL55C) 46 | 47 | ### 🎩 License 48 | Tim Engine | `LGPLv3` license. [Made by Humans from OpenPeeps](https://github.com/openpeeps).
49 | Copyright © 2024 OpenPeeps & Contributors — All rights reserved. -------------------------------------------------------------------------------- /bindings/node/tim/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openpeeps/tim", 3 | "version": "0.1.3", 4 | "description": "This is Tim ⚡️ A high-performance template engine & markup language written in Nim", 5 | "main": "src/tim.js", 6 | "files": ["README.md", "./src/bin"], 7 | "keywords": [ 8 | "template-engine", 9 | "markup-language", 10 | "template", 11 | "engine", 12 | "napi", 13 | "c++", 14 | "tim-language", 15 | "bindings", 16 | "frontend", 17 | "nim-language", 18 | "nim" 19 | ], 20 | "author": "OpenPeeps", 21 | "license": "LGPL-3.0", 22 | "homepage": "https://openpeeps.dev", 23 | "repository": { 24 | "directory": "github", 25 | "url": "https://github.com/openpeeps/tim" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/openpeeps/tim/issues" 29 | } 30 | } -------------------------------------------------------------------------------- /bindings/node/tim/src/bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/bindings/node/tim/src/bin/.gitkeep -------------------------------------------------------------------------------- /bindings/node/tim/src/bin/tim-linux.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/bindings/node/tim/src/bin/tim-linux.node -------------------------------------------------------------------------------- /bindings/node/tim/src/bin/tim-macos.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/bindings/node/tim/src/bin/tim-macos.node -------------------------------------------------------------------------------- /bindings/node/tim/src/tim.js: -------------------------------------------------------------------------------- 1 | module.exports = (() => { 2 | if(process.platform === 'darwin') { 3 | return require('./bin/tim-macos.node'); 4 | } else if(process.platform === 'linux') { 5 | return require('./bin/tim-linux.node'); 6 | } else { 7 | throw new Error('Tim Engine - Unsupported platform ' + process.platform) 8 | } 9 | })(); -------------------------------------------------------------------------------- /editors/tim.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # See http://www.sublimetext.com/docs/syntax.html 4 | file_extensions: 5 | - timl 6 | scope: source.tim 7 | variables: 8 | ident: '[A-Za-z_][A-Za-z_0-9]*' 9 | html_id: '[a-zA-Z_-][a-zA-Z0-9_-]*' 10 | end_block: '^(\s*?)@\b(end)\b' 11 | contexts: 12 | 13 | main: 14 | - include: cssSnippet 15 | - include: scriptSnippet 16 | - include: styleSnippet 17 | - include: jsSnippet 18 | - include: json 19 | - include: yaml 20 | - include: identCall 21 | 22 | - match: '\b(echo)\b|@\b(include|import|end|client|placeholder)\b' 23 | scope: keyword.control.import.timl 24 | 25 | - match: '@view' 26 | scope: entity.name.function.timl 27 | 28 | - match: '(@[\w+-]*)' 29 | scope: entity.name.function.tim 30 | 31 | - match: '(@[\w+-]*)(\((.*)?\))' 32 | captures: 33 | 1: entity.name.function.tim 34 | 35 | - match: '\b(true|false)\b' 36 | scope: constant.language.timl 37 | 38 | - match: '\$(app|this)\b' 39 | scope: constant.language.timl 40 | 41 | - match: '\b(if|elif|else|for|while|in|and|or|fn|func|block|component|return|discard|break|type|typeof)\b' 42 | scope: keyword.control.timl 43 | 44 | - match: '\b(string|int|float|bool|array|object|stream)\b' 45 | scope: keyword.control.timl 46 | 47 | - match: '\b(var|const)\b' 48 | captures: 49 | 1: keyword.control.bro 50 | push: varIdent 51 | 52 | - match: (\$)([a-zA-Z_][a-zA-Z0-9_]*)\b 53 | scope: variable.member.timl 54 | 55 | - match: '([\w-]*)(=)' 56 | captures: 57 | 1: entity.other.attribute-name 58 | 59 | - match: (\.)({{html_id}})\b 60 | captures: 61 | 1: markup.bold entity.name.function 62 | # 2: entity.name.function 63 | 64 | - match: "'" 65 | scope: punctuation.definition.string.begin.timl 66 | push: single_quoted_string 67 | 68 | - match: '"""' 69 | scope: punctuation.definition.string.begin.timl 70 | push: triple_quoted_string 71 | 72 | - match: '"' 73 | scope: punctuation.definition.string.begin.timl 74 | push: double_quoted_string 75 | 76 | # Comments begin with a '//' and finish at the end of the line 77 | - match: '//' 78 | scope: punctuation.definition.comment.tim 79 | push: line_comment 80 | 81 | # - match: '>' 82 | # scope: markup.bold 83 | 84 | - match: '(\?|\||\*|/|&|\-|\+)' 85 | scope: keyword.operator.logical 86 | 87 | - match: '(:|\.)' 88 | scope: markup.bold 89 | 90 | - match: '=' 91 | scope: markup.bold keyword.operator.assignment.timl 92 | 93 | - match: '\b(? { 88 | console.log(`Server is running on http://${host}:${port}`) 89 | }) 90 | -------------------------------------------------------------------------------- /example/example_httpbeast.nim: -------------------------------------------------------------------------------- 1 | import ../src/tim 2 | import std/[options, asyncdispatch, macros, 3 | os, strutils, times, json] 4 | import pkg/[httpbeast] 5 | 6 | from std/httpcore import HttpCode, Http200 7 | from std/net import Port 8 | 9 | include ./initializer 10 | 11 | proc resp(req: Request, view: string, layout = "base", code = Http200, 12 | headers = "Content-Type: text/html", local = newJObject()) = 13 | local["path"] = %*(req.path.get()) 14 | let htmlOutput = timl.render(view, layout, local = local) 15 | req.send(code, htmlOutput, headers) 16 | 17 | proc onRequest(req: Request): Future[void] = 18 | {.gcsafe.}: 19 | let path = req.path.get() 20 | case req.httpMethod.get() 21 | of HttpGet: 22 | case path 23 | of "/": 24 | req.resp("index", 25 | local = %*{ 26 | "meta": { 27 | "title": "Tim Engine is Awesome!" 28 | } 29 | }) 30 | of "/about": 31 | req.resp("about", "secondary", 32 | local = %*{ 33 | "meta": { 34 | "title": "About Tim Engine" 35 | } 36 | }) 37 | else: 38 | req.resp("error", code = Http404, local = %*{ 39 | "meta": { 40 | "title": "Oh, you're a genius!", 41 | "msg": "Oh yes, yes. It's got action, it's got drama, it's got dance! Oh, it's going to be a hit hit hit!" 42 | } 43 | }) 44 | else: req.send(Http501) 45 | 46 | echo "Serving on http://localhost:8080" 47 | let serverSettings = initSettings(Port(8080), numThreads = 1) 48 | run(onRequest, serverSettings) 49 | -------------------------------------------------------------------------------- /example/example_mummy.nim: -------------------------------------------------------------------------------- 1 | import std/[times, os, strutils, json] 2 | import pkg/[mummy, mummy/routers] 3 | 4 | include ./initializer 5 | 6 | # 7 | # Example Mummy + Tim Engine 8 | # 9 | template initHeaders {.dirty.} = 10 | var headers: HttpHeaders 11 | headers["Content-Type"] = "text/html" 12 | 13 | proc resp(req: Request, view: string, layout = "base", 14 | local = newJObject(), code = 200) = 15 | initHeaders() 16 | {.gcsafe.}: 17 | local["path"] = %*(req.path) 18 | let output = timl.render(view, layout, local = local) 19 | req.respond(200, headers, output) 20 | 21 | proc indexHandler(req: Request) = 22 | req.resp("index", local = %*{ 23 | "meta": { 24 | "title": "Tim Engine is Awesome!" 25 | } 26 | } 27 | ) 28 | 29 | proc aboutHandler(req: Request) = 30 | req.resp("about", layout = "secondary", 31 | local = %*{ 32 | "meta": { 33 | "title": "About Tim Engine" 34 | } 35 | } 36 | ) 37 | 38 | proc e404(req: Request) = 39 | req.resp("error", code = 404, 40 | local = %*{ 41 | "meta": { 42 | "title": "Oh, you're a genius!", 43 | "msg": "Oh yes, yes. It's got action, it's got drama, it's got dance! Oh, it's going to be a hit hit hit!" 44 | } 45 | } 46 | ) 47 | 48 | var router: Router 49 | router.get("/", indexHandler) 50 | router.get("/about", aboutHandler) 51 | 52 | # Custom 404 handler 53 | router.notFoundHandler = e404 54 | 55 | let server = newServer(router) 56 | echo "Serving on http://localhost:8081" 57 | server.serve(Port(8081)) 58 | -------------------------------------------------------------------------------- /example/example_prologue.nim: -------------------------------------------------------------------------------- 1 | #This example demonstrates using Tim with Prologue 2 | import std/[strutils, times] 3 | import prologue 4 | include ./initializer 5 | 6 | #init Settings for prologue 7 | let 8 | settings = newSettings(port = Port(8082)) 9 | 10 | var app = newApp(settings = settings) 11 | 12 | #define your route handling callbacks 13 | proc indexPageHandler(ctx: Context) {.async, gcsafe.} = 14 | let localObjects = %*{ 15 | "meta": { 16 | "title": "Tim Engine is Awesome!" 17 | }, 18 | "path": "/" 19 | } 20 | 21 | {.cast(gcsafe).}: #timl is a global using GC'ed memory and prologue loves it's callbacks to be gc-safe 22 | let indexPage = timl.render(viewName = "index", layoutName = "base", local = localObjects) 23 | 24 | resp indexPage 25 | 26 | proc aboutPageHandler(ctx: Context) {.async, gcsafe.} = 27 | let localObjects = %*{ 28 | "meta": { 29 | "title": "About Tim Engine" 30 | }, 31 | "path": "/about" 32 | } 33 | {.cast(gcsafe).}: #timl is a global using GC'ed memory and prologue loves it's callbacks to be gc-safe 34 | let aboutPage = timl.render(viewName = "about", layoutName = "secondary", local = localObjects) 35 | 36 | resp aboutPage 37 | 38 | proc e404(ctx: Context) {.async, gcsafe.} = 39 | let localObjects = %*{ 40 | "meta": { 41 | "title": "Oh, you're a genius!", 42 | "msg": "Oh yes, yes. It's got action, it's got drama, it's got dance! Oh, it's going to be a hit hit hit!" 43 | }, 44 | "path": %*(ctx.request.path) 45 | } 46 | {.cast(gcsafe).}: #timl is a global using GC'ed memory and prologue loves it's callbacks to be gc-safe 47 | let e404Page = timl.render(viewName = "error", layoutName = "base", local = localObjects) 48 | 49 | resp e404Page 50 | 51 | #tell prologue how to handle routes 52 | app.addRoute("/", indexPageHandler) 53 | app.addRoute("/about", aboutPageHandler) 54 | app.registerErrorHandler(Http404, e404) 55 | 56 | app.run() -------------------------------------------------------------------------------- /example/initializer.nim: -------------------------------------------------------------------------------- 1 | import ../src/tim 2 | import ../src/timpkg/engine/meta 3 | import std/critbits 4 | 5 | # 6 | # Setup Tim Engine 7 | # 8 | var 9 | timl = 10 | newTim( 11 | src = "templates", 12 | output = "storage", 13 | basepath = currentSourcePath(), 14 | minify = true, 15 | indent = 2, 16 | # showHtmlError = true 17 | ) 18 | 19 | tim.initModule: 20 | # initialize local module 21 | block: 22 | proc sayHello(x: string): Node[ntLitString] = 23 | result = ast.newNode(ntLitString) 24 | result.sVal = args[0].value.sVal 25 | 26 | # some read-only data to expose inside templates 27 | # using the built-in `$app` constant 28 | let globalData = %*{ 29 | "year": parseInt(now().format("yyyy")), 30 | "watchout": { 31 | "enable": true, 32 | "port": 6502, 33 | "delay": 300, 34 | }, 35 | "stylesheets": [ 36 | { 37 | "type": "stylesheet", 38 | "src": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" 39 | }, 40 | { 41 | "type": "preconnect", 42 | "src": "https://fonts.googleapis.com" 43 | }, 44 | { 45 | "type": "preconnect", 46 | "src": "https://fonts.gstatic.com" 47 | }, 48 | { 49 | "type": "stylesheet", 50 | "src": "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" 51 | } 52 | ] 53 | } 54 | # 2. Pre-compile discovered templates 55 | # before booting your web app. 56 | # 57 | # Note that `waitThread` will keep thread alive. 58 | # This is required while in dev mode 59 | # by the built-in file system monitor 60 | # in order to rebuild templates. 61 | # 62 | # Don't forget to enable hot code reload 63 | # using `-d:timHotCode` 64 | var timThread: Thread[void] 65 | proc precompileEngine() {.thread.} = 66 | {.gcsafe.}: 67 | # let's add some placeholders 68 | # const snippetCode2 = """ 69 | # div.alert.aler.dark.rounded-0.border-0.mb-0 > p.mb-0: "Alright, I'm the second snippet loaded by #topbar placeholder." 70 | # """ 71 | # let snippetParser = parseSnippet("mysnippet", readFile("./mysnippet.timl")) 72 | # # timl.addPlaceholder("topbar", snippetParser.getAst) 73 | 74 | # let snippetParser2 = parseSnippet("mysnippet2", snippetCode2) 75 | # timl.addPlaceholder("topbar", snippetParser2.getAst) 76 | 77 | timl.precompile( 78 | waitThread = true, 79 | global = globalData, 80 | flush = true, # flush old cache on reboot 81 | ) 82 | 83 | createThread(timThread, precompileEngine) 84 | -------------------------------------------------------------------------------- /example/preview.html: -------------------------------------------------------------------------------- 1 | Tim Engine is Awesome
Tim Engine

This is Tim 👋 A super fast template engine for cool kids!

Build sleek, dynamic websites and apps in a breeze with Tim Engine's intuitive syntax and powerful features. It's the template engine that keeps up with your creativity.

Other projects

© 2024 — Made by Humans from OpenPeeps

Open Source | LGPL-3.0 license

-------------------------------------------------------------------------------- /example/storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/example/storage/.gitkeep -------------------------------------------------------------------------------- /example/templates/layouts/base.timl: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | @include "meta/head" 4 | style: """ 5 | body { 6 | background-color: #212121; 7 | color: whitesmoke 8 | } 9 | 10 | body, h1, h2, h3, h4, h5, h6, 11 | .h1, .h2, .h3, .h4, .h5, .h6{ 12 | font-family: 'Inter', sans-serif; 13 | } 14 | 15 | .btn-primary { 16 | --bs-btn-color: #fff; 17 | --bs-btn-bg: #ea4444; 18 | --bs-btn-border-color: #ea4444; 19 | --bs-btn-hover-color: #fff; 20 | --bs-btn-hover-bg: #c73434; 21 | --bs-btn-hover-border-color: #c73434; 22 | --bs-btn-focus-shadow-rgb: 49,132,253; 23 | --bs-btn-active-color: #fff; 24 | --bs-btn-active-bg: #b62929; 25 | --bs-btn-active-border-color: #b62929; 26 | --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 27 | --bs-btn-disabled-color: #fff; 28 | --bs-btn-disabled-bg: #0d6efd; 29 | --bs-btn-disabled-border-color: #0d6efd; 30 | } 31 | """ 32 | body 33 | @view 34 | @include "ws" -------------------------------------------------------------------------------- /example/templates/layouts/secondary.timl: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | @include "meta/head" 4 | style: """ 5 | body { 6 | background-color: #1b0f5b; 7 | } 8 | 9 | body, .text-light { 10 | color: #eeeedc !important 11 | } 12 | 13 | body, h1, h2, h3, h4, h5, h6, 14 | .h1, .h2, .h3, .h4, .h5, .h6{ 15 | font-family: 'Inter', sans-serif; 16 | } 17 | 18 | .btn-primary { 19 | --bs-btn-color: #fff; 20 | --bs-btn-bg: #ea4444; 21 | --bs-btn-border-color: #ea4444; 22 | --bs-btn-hover-color: #fff; 23 | --bs-btn-hover-bg: #c73434; 24 | --bs-btn-hover-border-color: #c73434; 25 | --bs-btn-focus-shadow-rgb: 49,132,253; 26 | --bs-btn-active-color: #fff; 27 | --bs-btn-active-bg: #b62929; 28 | --bs-btn-active-border-color: #b62929; 29 | --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 30 | --bs-btn-disabled-color: #fff; 31 | --bs-btn-disabled-bg: #0d6efd; 32 | --bs-btn-disabled-border-color: #0d6efd; 33 | } 34 | """ 35 | body 36 | @view 37 | @include "ws" -------------------------------------------------------------------------------- /example/templates/partials/foot.timl: -------------------------------------------------------------------------------- 1 | div.row > div.col-12.text-center 2 | div.my-3#clickable 3 | a.btn.btn-primary.btn-lg.rounded-pill.px-4.py-2 4 | href="https://github.com/openpeeps/tim" target="_blank": 5 | svg viewBox="0 0 24 24" width="24" height="24" 6 | stroke="currentColor" stroke-width="2" 7 | fill="none" stroke-linecap="round" 8 | stroke-linejoin="round" class="css-i6dzq1" 9 | path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 10 | 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 11 | 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 12 | 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 13 | 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" 14 | span.fw-bold.ms-2: "Check it on GitHub" 15 | if $this.path == "/about": 16 | a.btn.text-light.btn-lg.rounded-pill.px-4.py-2 href="/": 17 | "Go back to Homepage" 18 | else: 19 | div.mt-2 > a.text-light.text-decoration-none href="/about": 20 | span.me-2 "Curious about" 21 | em: "\"Forgotten Professions & Historical Oddities\"?" 22 | div.text-center 23 | p.mb-0: "© " & $app.year & " — Made by Humans from OpenPeeps" 24 | p: "Open Source | LGPL-3.0 license" 25 | 26 | @client target="#clickable" 27 | // transpile tim code to javascript for client-side rendering 28 | div.mt-3 29 | a.text-secondary.text-decoration-none href="https://hetzner.cloud/?ref=Hm0mYGM9NxZ4" 30 | style="border: 2px dotted; display: inline-block; padding: 10px; border-radius: 15px;" 31 | small 32 | span: "👉 Create a VPS using our link and 👇 " 33 | br 34 | span: "Get €20 in cloud credits from Hetzner" 35 | @end 36 | -------------------------------------------------------------------------------- /example/templates/partials/meta/head.timl: -------------------------------------------------------------------------------- 1 | meta charset="UTF-8" 2 | meta name="viewport" content="width=device-width, initial-scale=1" 3 | title: $this.meta.title 4 | 5 | for $style in $app.stylesheets: 6 | link rel=$style.type href=$style.src -------------------------------------------------------------------------------- /example/templates/partials/ws.timl: -------------------------------------------------------------------------------- 1 | if $app.watchout.enable: 2 | const watchoutPort = $app.watchout.port 3 | @js 4 | // use to pass data from Tim to Javascript 5 | let watchoutSyncPort = %*watchoutPort 6 | { 7 | function connectWatchoutServer() { 8 | const watchout = new WebSocket(`ws://127.0.0.1:${watchoutSyncPort}/ws`); 9 | watchout.addEventListener('message', (e) => { 10 | if(e.data == '1') location.reload() 11 | }); 12 | watchout.addEventListener('close', () => { 13 | setTimeout(() => { 14 | console.log('Watchout WebSocket is closed. Try again...') 15 | connectWatchoutServer() 16 | }, 300) 17 | }) 18 | } 19 | connectWatchoutServer() 20 | } 21 | @end 22 | -------------------------------------------------------------------------------- /example/templates/views/about.timl: -------------------------------------------------------------------------------- 1 | @placeholder#topbar 2 | var boxes = [ 3 | { 4 | title: "Chimney Sweep" 5 | description: "Once feared for the soot they carried, 6 | these skilled climbers cleaned fireplaces to prevent 7 | fires and improve indoor air quality" 8 | } 9 | { 10 | title: "Town Crier" 11 | description: "With booming voices and ringing bells, 12 | they delivered news and announcements in the days 13 | before mass media" 14 | } 15 | { 16 | title: "Ratcatcher" 17 | description: "These pest controllers faced smelly 18 | challenges, but their work helped prevent the 19 | spread of diseases like the plague" 20 | } 21 | { 22 | title: "Ancient Rome" 23 | description: "In ancient Rome, gladiators sometimes 24 | fought wild animals while wearing costumes of mythological figures" 25 | } 26 | { 27 | title: "The first traffic light" 28 | description: "Was installed in London in 1868 and used gas 29 | lanterns to signal stop and go." 30 | } 31 | { 32 | title: "The Great Wall at once?" 33 | description: "Nope. It wasn't built all at once, but over 34 | centuries by different dynasties." 35 | } 36 | ] 37 | 38 | section.pt-5 > div.container 39 | div.row > div#content-zone.col-lg-7.mx-auto 40 | div.text-center > img src="https://raw.githubusercontent.com/openpeeps/tim/main/.github/timengine.png" alt="Tim Engine" width="200px" 41 | h1.display-4.fw-bold: 42 | "Random Forgotten Professions & Historical Oddities 🤯" 43 | div.row.my-3.g-4 44 | for $box in $boxes: 45 | div.col-lg-4.d-flex.align-items-stretch > div.card.bg-transparent.text-light.border-0 style="border-radius: 18px" > div.card-body.p-4 46 | div.card-title.fw-bold.h3: $box.title 47 | p.card-text.fw-normal.h5.lh-base: $box.description 48 | @include "foot" 49 | -------------------------------------------------------------------------------- /example/templates/views/error.timl: -------------------------------------------------------------------------------- 1 | section.container > div.row.vh-100 > div.col-lg-7.mx-auto.align-self-center.text-center 2 | h1.display-5.fw-bold: "😅 " & $this.meta.title 3 | p.mb-4.h4.fw-normal.px-4 style="line-height: 1.8em": $this.meta.msg 4 | a href="/" class="btn btn-outline-secondary text-light btn-lg px-4 rounded-pill": "👉 Go back to Pantzini" -------------------------------------------------------------------------------- /example/templates/views/index.timl: -------------------------------------------------------------------------------- 1 | // tips: variables declared at template level 2 | // with default value are known at compile-time 3 | const logo = "https://raw.githubusercontent.com/openpeeps/tim/main/.github/timengine.png" 4 | const heading = "This is Tim 👋 A super fast template engine for cool kids!" 5 | const lead = // double quote in multi-line strings 6 | "Build sleek, dynamic websites and apps in a breeze with 7 | Tim Engine's intuitive syntax and powerful features. 8 | It's the template engine that keeps up with your creativity." 9 | 10 | section.pt-5 > div.container > div.row > div#content-zone.col-lg-7.mx-auto 11 | div.text-center 12 | img src=$logo alt="Tim Engine" width="200px" 13 | h1.display-4.fw-bold: $heading 14 | p.mb-4.h4.fw-normal.px-4 style="line-height: 1.8em": $lead 15 | @include "foot" // include footer 16 | -------------------------------------------------------------------------------- /src/tim.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/json except `%*` 8 | import std/[times, asyncdispatch, 9 | sequtils, macros, macrocache, strutils, os] 10 | 11 | import pkg/watchout 12 | import pkg/importer/resolver 13 | import pkg/kapsis/cli 14 | 15 | import timpkg/engine/[meta, parser, logging] 16 | import timpkg/engine/compilers/html 17 | 18 | from timpkg/engine/ast import `$` 19 | 20 | when not isMainModule: 21 | import timpkg/engine/stdlib 22 | 23 | const 24 | DOCKTYPE = "" 25 | defaultLayout = "base" 26 | localStorage* = CacheSeq"LocalStorage" 27 | # Compile-time Cache seq to handle local data 28 | 29 | macro initCommonStorage*(x: untyped) = 30 | ## Initializes a common localStorage that can be 31 | ## shared between controllers 32 | if x.kind == nnkStmtList: 33 | add localStorage, x[0] 34 | elif x.kind == nnkTableConstr: 35 | add localStorage, x 36 | else: error("Invalid common storage initializer. Use `{}`, or `do` block") 37 | 38 | proc toLocalStorage*(x: NimNode): NimNode = 39 | if x.kind in {nnkTableConstr, nnkCurly}: 40 | var shareLocalNode: NimNode 41 | if localStorage.len > 0: 42 | shareLocalNode = localStorage[0] 43 | if x.len > 0: 44 | shareLocalNode.copyChildrenTo(x) 45 | return newCall(ident("%*"), x) 46 | if shareLocalNode != nil: 47 | return newCall(ident("%*"), shareLocalNode) 48 | result = newCall(ident"newJObject") 49 | # error("Local storage requires either `nnkTableConstr` or `nnkCurly`") 50 | 51 | macro `&*`*(n: untyped): untyped = 52 | ## Compile-time localStorage initializer 53 | ## that helps reusing shareable data. 54 | ## 55 | ## Once merged it calls `%*` macro from `std/json` 56 | ## for converting NimNode to JsonNode 57 | result = toLocalStorage(n) 58 | 59 | proc jitCompiler(engine: TimEngine, 60 | tpl: TimTemplate, data: JsonNode, 61 | placeholders: TimEngineSnippets = nil): HtmlCompiler = 62 | ## Compiles `tpl` AST at runtime 63 | engine.newCompiler( 64 | ast = engine.readAst(tpl), 65 | tpl = tpl, 66 | minify = engine.isMinified, 67 | indent = engine.getIndentSize, 68 | data = data, 69 | placeholders = placeholders 70 | ) 71 | 72 | proc toHtml*(name, code: string, local = newJObject(), minify = true): string = 73 | ## Read timl from `code` string 74 | let p = parseSnippet(name, code) 75 | if likely(not p.hasErrors): 76 | var data = newJObject() 77 | data["local"] = local 78 | let c = newCompiler( 79 | ast = parser.getAst(p), 80 | minify, 81 | data = data 82 | ) 83 | if likely(not c.hasErrors): 84 | return c.getHtml() 85 | raise newException(TimError, "c.logger.errors.toSeq[0]") # todo 86 | raise newException(TimError, "p.logger.errors.toSeq[0]") # todo 87 | 88 | proc toAst*(name, code: string): string = 89 | let p = parseSnippet(name, code) 90 | if likely(not p.hasErrors): 91 | return ast.printAstNodes(parser.getAst(p)) 92 | 93 | template displayErrors(l: Logger) = 94 | for err in l.errors: 95 | display(err) 96 | display(l.filePath) 97 | 98 | proc compileCode(engine: TimEngine, tpl: TimTemplate, 99 | refreshAst = false) = 100 | # Compiles `tpl` TimTemplate to either `.html` or binary `.ast` 101 | var p: Parser = engine.newParser(tpl, refreshAst = refreshAst) 102 | if likely(not p.hasError): 103 | if tpl.jitEnabled(): 104 | # if marked as JIT will save the produced 105 | # binary AST on disk for runtime computation 106 | engine.writeAst(tpl, parser.getAst(p)) 107 | else: 108 | # otherwise, compiles the generated AST and save 109 | # a pre-compiled HTML version on disk 110 | var c = engine.newCompiler(parser.getAst(p), 111 | tpl, engine.isMinified, engine.getIndentSize) 112 | if likely(not c.hasError): 113 | case tpl.getType: 114 | of ttView: 115 | engine.writeHtml(tpl, c.getHtml) 116 | of ttLayout: 117 | engine.writeHtml(tpl, c.getHead) 118 | engine.writeHtmlTail(tpl, c.getTail) 119 | else: discard 120 | else: 121 | c.logger.displayErrors() 122 | else: p.logger.displayErrors() 123 | 124 | proc resolveDependants(engine: TimEngine, x: seq[string]) = 125 | for path in x: 126 | let tpl = engine.getTemplateByPath(path) 127 | case tpl.getType 128 | of ttPartial: 129 | echo tpl.getDeps.toSeq 130 | engine.resolveDependants(tpl.getDeps.toSeq) 131 | else: 132 | engine.compileCode(tpl, refreshAst = true) 133 | 134 | # initialize Browser Sync & Reload using 135 | # libdatachannel WebSocket server and Watchout 136 | # for handling file monitoring and changes 137 | import pkg/libdatachannel/bindings 138 | import pkg/libdatachannel/websockets 139 | 140 | # needs to be global 141 | var 142 | watcher: Watchout 143 | wsServerConfig = initWebSocketConfig() 144 | hasChanges: bool 145 | 146 | proc connectionCallback(wsserver: cint, ws: cint, userPtr: pointer) {.cdecl.} = 147 | proc wsMessageCallback(ws: cint, msg: cstring, size: cint, userPtr: pointer) = 148 | if hasChanges: 149 | ws.message("1") 150 | hasChanges = false 151 | else: 152 | ws.message("0") 153 | 154 | discard rtcSetMessageCallback(ws, wsMessageCallback) 155 | 156 | proc precompile*(engine: TimEngine, flush = true, 157 | waitThread = false, browserSyncPort = Port(6502), 158 | browserSyncDelay = 100, global: JsonNode = newJObject(), 159 | watchoutNotify = true) = 160 | ## Precompiles available templates inside `layouts` and `views` 161 | ## directories to either static `.html` or binary `.ast`. 162 | ## 163 | ## Enable `flush` option to delete outdated generated 164 | ## files (enabled by default). 165 | ## 166 | ## Enable filesystem monitor by compiling with `-d:timHotCode` flag. 167 | ## You can create a separate thread for precompiling templates 168 | ## (use `waitThread` to keep the thread alive) 169 | if flush: engine.flush() 170 | engine.setGlobalData(global) 171 | engine.importsHandle = resolver.initResolver() 172 | when defined timHotCode: 173 | # Define callback procs for pkg/watchout 174 | proc notify(label, fname: string) = 175 | if watchoutNotify: 176 | echo label 177 | echo indent(fname & "\n", 3) 178 | 179 | # Callback `onFound` 180 | proc onFound(file: watchout.File) = 181 | # Runs when detecting a new template. 182 | let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) 183 | case tpl.getType 184 | of ttView, ttLayout: 185 | engine.compileCode(tpl) 186 | else: discard 187 | 188 | # Callback `onChange` 189 | proc onChange(file: watchout.File) = 190 | # Runs when detecting changes 191 | let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) 192 | notify("✨ Changes detected", file.getName()) 193 | case tpl.getType() 194 | of ttView, ttLayout: 195 | # engine.importsHandle.excl(file.getPath()) 196 | engine.compileCode(tpl) 197 | else: 198 | # engine.importsHandle.excl(file.getPath()) 199 | engine.resolveDependants(engine.importsHandle.dependencies(file.getPath).toSeq) 200 | hasChanges = true 201 | 202 | # Callback `onDelete` 203 | proc onDelete(file: watchout.File) = 204 | # Runs when deleting a file 205 | notify("✨ Deleted", file.getName()) 206 | engine.clearTemplateByPath(file.getPath()) 207 | 208 | wsServerConfig.port = browserSyncPort.uint16 209 | websockets.startServer(addr(wsServerConfig), connectionCallback) 210 | sleep(100) # give some time for the web socket server to start 211 | 212 | let basepath = engine.getSourcePath() 213 | # Setup the filesystem monitor 214 | watcher = 215 | newWatchout(@[ 216 | basepath / "layouts" / "*", 217 | basepath / "views" / "*", 218 | basepath / "partials" / "*" 219 | ], "*.timl") 220 | watcher.onChange = onChange 221 | watcher.onFound = onFound 222 | watcher.onDelete = onDelete 223 | watcher.start() 224 | else: 225 | for tpl in engine.getViews(): 226 | engine.compileCode(tpl) 227 | for tpl in engine.getLayouts(): 228 | engine.compileCode(tpl) 229 | 230 | template layoutWrapper(getViewBlock) {.dirty.} = 231 | result = DOCKTYPE 232 | var layoutTail: string 233 | var hasError: bool 234 | if not layout.jitEnabled: 235 | # when requested layout is pre-rendered 236 | # will use the static HTML version from disk 237 | add result, layout.getHtml() 238 | getViewBlock 239 | layoutTail = layout.getTail() 240 | else: 241 | var jitLayout = engine.jitCompiler(layout, data, placeholders) 242 | if likely(not jitLayout.hasError): 243 | add result, jitLayout.getHead() 244 | getViewBlock 245 | layoutTail = jitLayout.getTail() 246 | else: 247 | hasError = true 248 | jitLayout.logger.displayErrors() 249 | add result, layoutTail 250 | 251 | proc render*(engine: TimEngine, viewName: string, 252 | layoutName = defaultLayout, local = newJObject(), 253 | placeholders: TimEngineSnippets = nil): string = 254 | ## Renders a view based on `viewName` and `layoutName`. 255 | ## Exposing data from controller to the current template is possible 256 | ## using the `local` object. 257 | if engine.hasView(viewName): 258 | var 259 | view: TimTemplate = engine.getView(viewName) 260 | data: JsonNode = newJObject() 261 | data["local"] = local 262 | if likely(engine.hasLayout(layoutName)): 263 | var layout: TimTemplate = engine.getLayout(layoutName) 264 | # echo view.jitEnabled 265 | if not view.jitEnabled: 266 | # render a pre-compiled HTML 267 | layoutWrapper: 268 | # add result, view.getHtml() 269 | add result, 270 | if engine.isMinified: 271 | view.getHtml() 272 | else: 273 | indent(view.getHtml(), layout.getViewIndent) 274 | else: 275 | # compile and render template at runtime 276 | layoutWrapper: 277 | var jitView = engine.jitCompiler(view, data, placeholders) 278 | if likely(not jitView.hasError): 279 | # add result, jitView.getHtml 280 | add result, 281 | if engine.isMinified: 282 | jitView.getHtml() 283 | else: 284 | indent(jitView.getHtml(), layout.getViewIndent) 285 | else: 286 | jitView.logger.displayErrors() 287 | hasError = true 288 | else: 289 | raise newException(TimError, "Trying to wrap `" & viewName & "` view using non-existing layout " & layoutName) 290 | else: 291 | raise newException(TimError, "View not found " & viewName) 292 | 293 | when defined napibuild: 294 | # Setup for building TimEngine as a node addon via NAPI 295 | import pkg/[denim, jsony] 296 | # import std/os 297 | # from std/sequtils import toSeq 298 | 299 | var timjs: TimEngine 300 | init proc(module: Module) = 301 | proc init(src: string, output: string, 302 | basepath: string, minify: bool, indent: int) {.export_napi.} = 303 | ## Initialize TimEngine Engine 304 | timjs = newTim( 305 | args.get("src").getStr, 306 | args.get("output").getStr, 307 | args.get("basepath").getStr, 308 | args.get("minify").getBool, 309 | args.get("indent").getInt 310 | ) 311 | 312 | proc precompile(opts: object) {.export_napi.} = 313 | ## Precompile TimEngine templates 314 | var opts: JsonNode = jsony.fromJson($(args.get("opts"))) 315 | var globals: JsonNode 316 | if opts.hasKey"data": 317 | globals = opts["data"] 318 | var browserSync: JsonNode 319 | if opts.hasKey"watchout": 320 | browserSync = opts["watchout"] 321 | let browserSyncPort = browserSync["port"].getInt 322 | timjs.flush() # each precompilation will flush old files 323 | timjs.setGlobalData(globals) 324 | timjs.importsHandle = initResolver() 325 | if browserSync["enable"].getBool: 326 | # Define callback procs for pkg/watchout 327 | proc notify(label, fname: string) = 328 | echo label 329 | echo indent(fname & "\n", 3) 330 | 331 | # Callback `onFound` 332 | proc onFound(file: watchout.File) = 333 | # Runs when detecting a new template. 334 | let tpl: TimTemplate = timjs.getTemplateByPath(file.getPath()) 335 | case tpl.getType 336 | of ttView, ttLayout: 337 | timjs.compileCode(tpl) 338 | else: discard 339 | 340 | # Callback `onChange` 341 | proc onChange(file: watchout.File) = 342 | # Runs when detecting changes 343 | let tpl: TimTemplate = timjs.getTemplateByPath(file.getPath()) 344 | notify("✨ Changes detected", file.getName()) 345 | case tpl.getType() 346 | of ttView, ttLayout: 347 | timjs.compileCode(tpl) 348 | else: 349 | timjs.resolveDependants(timjs.importsHandle.dependencies(file.getPath).toSeq) 350 | 351 | # Callback `onDelete` 352 | proc onDelete(file: watchout.File) = 353 | # Runs when deleting a file 354 | notify("✨ Deleted", file.getName()) 355 | timjs.clearTemplateByPath(file.getPath()) 356 | 357 | let basepath = timjs.getSourcePath() 358 | var w = 359 | newWatchout( 360 | dirs = @[basepath / "layouts" / "*", 361 | basepath / "views" / "*", 362 | basepath / "partials" / "*"], 363 | onChange, onFound, onDelete, 364 | recursive = true, 365 | ext = @[".timl"], delay = 200, 366 | browserSync = 367 | WatchoutBrowserSync(port: Port(browserSyncPort), 368 | delay: browserSync["delay"].getInt) 369 | ) 370 | # start filesystem monitor in a separate thread 371 | w.start() 372 | else: 373 | for tpl in timjs.getViews(): 374 | timjs.compileCode(tpl) 375 | for tpl in timjs.getLayouts(): 376 | timjs.compileCode(tpl) 377 | 378 | proc render(view: string, layout: string, local: object) {.export_napi.} = 379 | ## Render a `view` by name 380 | var local: JsonNode = jsony.fromJson($(args.get("local"))) 381 | let x = timjs.render( 382 | args.get("view").getStr, 383 | args.get("layout").getStr, 384 | local 385 | ) 386 | return %*(x) 387 | 388 | proc fromHtml(path: string) {.export_napi.} = 389 | ## Read Tim code from `path` and output minified HTML 390 | let path = $args.get("path") 391 | let p = parseSnippet(path.extractFilename, readFile(path)) 392 | if likely(not p.hasErrors): 393 | let c = newCompiler(parser.getAst(p), true) 394 | return %*c.getHtml() 395 | 396 | proc toHtml(name: string, code: string) {.export_napi.} = 397 | ## Transpile `code` to minified HTML 398 | let 399 | name = $args.get("name") 400 | code = $args.get("code") 401 | p = parseSnippet(name, code) 402 | if likely(not p.hasErrors): 403 | let c = newCompiler(parser.getAst(p), true) 404 | return %*c.getHtml() 405 | 406 | elif defined timSwig: 407 | # Generate C API for generating SWIG wrappers 408 | # import pkg/genny 409 | 410 | # proc init*(src, output: string; minifyOutput = false; indentOutput = 2): TimEngine = 411 | # ## Initialize TimEngine 412 | # result = newTim(src, output, "", minifyOutput, indentOutput) 413 | 414 | # exportRefObject TimEngine: 415 | # procs: 416 | # init 417 | # precompile 418 | 419 | # writeFiles("bindings/generated", "tim") 420 | # include genny/internal 421 | # todo 422 | discard 423 | 424 | elif not isMainModule: 425 | # Expose Tim Engine API for Nim development 426 | # as a Nimble library 427 | import std/[hashes, enumutils] 428 | import timpkg/engine/ast 429 | 430 | export ast, parser, html, json, stdlib 431 | export meta except TimEngine 432 | export localModule, SourceCode, Arg, NodeType 433 | 434 | proc initLocalModule(modules: NimNode): NimNode = 435 | result = newStmtList() 436 | var functions: seq[string] 437 | modules.expectKind nnkArgList 438 | for mblock in modules[0]: 439 | mblock.expectKind nnkBlockStmt 440 | for m in mblock[1]: 441 | case m.kind 442 | of nnkProcDef: 443 | let id = m[0] 444 | var hashKey = stdlib.getHashedIdent(id.strVal) 445 | var fn = "fn " & $m[0] & "*(" 446 | var fnReturnType: NodeType 447 | var params: seq[string] 448 | var paramsType: seq[DataType] 449 | if m[3][0].kind != nnkEmpty: 450 | for p in m[3][1..^1]: 451 | add params, $p[0] & ":" & $p[1] 452 | hashKey = hashKey !& hashIdentity(parseEnum[DataType]($p[1])) 453 | add fn, params.join(",") 454 | add fn, "): " 455 | fnReturnType = ast.getType(m[3][0]) 456 | add fn, $fnReturnType 457 | else: 458 | add fn, ")" 459 | add functions, fn 460 | var lambda = nnkLambda.newTree(newEmptyNode(), newEmptyNode(), newEmptyNode()) 461 | var procParams = newNimNode(nnkFormalParams) 462 | procParams.add( 463 | ident("Node"), 464 | nnkIdentDefs.newTree( 465 | ident("args"), 466 | nnkBracketExpr.newTree( 467 | ident("openarray"), 468 | ident("Arg") 469 | ), 470 | newEmptyNode() 471 | ), 472 | nnkIdentDefs.newTree( 473 | ident("returnType"), 474 | ident("NodeType"), 475 | ident(symbolName(ntLitString)) 476 | ) 477 | ) 478 | add lambda, procParams 479 | add lambda, newEmptyNode() 480 | add lambda, newEmptyNode() 481 | add lambda, m[6] 482 | add result, 483 | newAssignment( 484 | nnkBracketExpr.newTree( 485 | ident"localModule", 486 | newLit hashKey 487 | ), 488 | lambda 489 | ) 490 | else: 491 | add result, m 492 | add result, 493 | newAssignment( 494 | nnkBracketExpr.newTree( 495 | ident("stdlib"), 496 | newLit("*") 497 | ), 498 | nnkTupleConstr.newTree( 499 | ident("localModule"), 500 | newCall(ident("SourceCode"), newLit(functions.join("\n"))) 501 | ) 502 | ) 503 | 504 | macro initModule*(x: varargs[untyped]): untyped = 505 | initLocalModule(x) 506 | 507 | else: 508 | # Build Tim Engine as a standalone CLI application 509 | import pkg/kapsis 510 | import pkg/kapsis/[runtime, cli] 511 | import timpkg/app/[source, microservice, manage] 512 | 513 | commands: 514 | -- "Source-to-Source" 515 | # Transpile timl code to a specific target source. 516 | # For now only `-t:html` works. S2S targets planned: 517 | # JavaScript, Nim, Python, Ruby and more 518 | src path(`timl`), 519 | string(-t), # choose a target (default target `html`) 520 | string(-o), # save output to file 521 | ?json(--data), # pass data to global/local scope 522 | bool(--pretty), # pretty print output HTML (still buggy) 523 | bool(--nocache), # tells Tim to import modules and rebuild cache 524 | bool(--bench), # benchmark operations 525 | bool("--json-errors"): 526 | ## Transpile `timl` to HTML 527 | 528 | ast path(`timl`), filename(`output`): 529 | ## Serialize template to binary AST 530 | 531 | repr path(`ast`), string(`ext`), bool(--pretty): 532 | ## Deserialize binary AST to target source 533 | 534 | html path(`html_file`): 535 | ## Transpile HTML to Tim code 536 | 537 | -- "Microservice" 538 | new path(`config`): 539 | ## Initialize a new config file 540 | 541 | run path(`config`): 542 | ## Run Tim as a Microservice application 543 | 544 | build path(`ast`): 545 | ## Build pluggable templates `dll` from `.timl` files. Requires Nim 546 | 547 | bundle path(`config`): 548 | ## Bundle a standalone front-end app from project. Requires Nim 549 | 550 | -- "Development" 551 | # The built-in package manager store installed packages 552 | init: 553 | ## Initializes a new Tim Engine package 554 | 555 | install url(`pkg`): 556 | ## Install a package from remote source 557 | 558 | remove string(`pkg`): 559 | ## Remove an installed package@0.1.0 by name and version 560 | -------------------------------------------------------------------------------- /src/tim.nims: -------------------------------------------------------------------------------- 1 | --mm:arc 2 | --define:timHotCode 3 | --threads:on 4 | --deepcopy:on 5 | --define:nimPreviewHashRef 6 | --define:ssl 7 | --define:"ThreadPoolSize=1" 8 | --define:"FixedChanSize=2" 9 | 10 | when defined napibuild: 11 | --define:napiOrWasm 12 | --define:watchoutBrowserSync 13 | --noMain:on 14 | --passC:"-I/usr/include/node -I/usr/local/include/node" 15 | 16 | when isMainModule: 17 | --define:timStandalone 18 | --define:watchoutBrowserSync 19 | when defined release: 20 | --opt:speed 21 | --define:danger 22 | --passC:"-flto" 23 | --passL:"-flto" 24 | -------------------------------------------------------------------------------- /src/timpkg/app/manage.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2025 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[os, osproc, strutils, sequtils, uri, httpclient] 8 | import ../engine/package/[manager, remote] 9 | import ../server/config 10 | 11 | import pkg/kapsis/[cli, runtime] 12 | import pkg/[nyml, semver] 13 | 14 | proc initCommand*(v: Values) = 15 | ## Initialize a Tim Engine package 16 | let currDir = getCurrentDir() 17 | if walkDir(currDir).toSeq.len > 0: 18 | displayError("Could not init a package. Directory is not empty") 19 | let pkgname = currDir.extractFilename 20 | let configPath = currDir / pkgname & ".config.yaml" 21 | let username = execCmdEx("git config user.name") 22 | let pkglicense = "" 23 | let config = """ 24 | name: $1 25 | type: package 26 | version: 0.1.0 27 | author: $2 28 | license: $3 29 | description: "A cool package for Tim Engine" 30 | 31 | requires: 32 | - tim >= 0.1.3 33 | """ % [pkgname, username.output, pkglicense] 34 | createDir(currDir / "src") 35 | writeFile(configPath, config) 36 | 37 | proc aliasCommand*(v: Values) = 38 | ## Creates an alias of a local package. 39 | ## This command allows to import the local project 40 | ## using the pkg prefix `@import 'pkg/mypackage'` 41 | # if walkFiles(getCurrentDir() / "*.config.yaml"): 42 | # echo "?" 43 | # execCmdEx("ln -s") 44 | discard 45 | 46 | proc installCommand*(v: Values) = 47 | ## Install a package from remote GIT sources 48 | let pkgr = manager.initPackageRemote() 49 | pkgr.loadPackages() # load database of existing packages 50 | let pkgUrl = v.get("pkg").getUrl() 51 | if pkgUrl.scheme.len > 0: 52 | if pkgUrl.hostname == "github.com": 53 | let pkgPath = pkgUrl.path[1..^1].split("/") 54 | # Connect to the remote source and try find a `tim.config.yaml`, 55 | # Check the `yaml` config file and download the package 56 | let orgName = pkgPath[0] 57 | let pkgName = pkgPath[1] 58 | let res = pkgr.remote.httpGet("repo_contents_path", @[orgName, pkgName, "tim.config.yaml"]) 59 | case res.code 60 | of Http200: 61 | let remoteYaml: GithubFileResponse = pkgr.remote.getFileContent(res) # this is base64 encoded 62 | let pkgConfig: TimConfig = fromYaml(remoteYaml.content.decode(), TimConfig) 63 | case pkgConfig.`type`: 64 | of typePackage: 65 | if not pkgr.hasPackage(pkgConfig.name): 66 | display(("Installing $1@$2" % [pkgConfig.name, pkgConfig.version])) 67 | if pkgr.createPackage(orgName, pkgName, pkgConfig): 68 | displayInfo("Updating Packager DB") 69 | pkgr.updatePackages() 70 | displaySuccess("Done!") 71 | else: 72 | displayInfo("Package $1@$2 is already installed" % [pkgConfig.name, pkgConfig.version]) 73 | else: 74 | displayError("Tim projects cannot be installed via Packager. Use git instead") 75 | else: discard # todo prompt error 76 | 77 | proc removeCommand*(v: Values) = 78 | ## Removes an installed package by name and version (if provided) 79 | let input = v.get("pkg").getStr.split("@") 80 | var hasVersion: bool 81 | let pkgName = input[0] 82 | let pkgVersion = 83 | if input.len == 2: 84 | hasVersion = true; parseVersion(input[1]) 85 | else: newVersion(0,1,0) 86 | displayInfo("Finding package `" & pkgName & "`") 87 | let pkgr = manager.initPackageRemote() 88 | pkgr.loadPackages() # load database of existing packages 89 | if pkgr.hasPackage(pkgName): 90 | displaySuccess("Delete package `" & pkgName & "`") 91 | pkgr.deletePackage(pkgName) 92 | else: 93 | displayError("Package `" & pkgName & "` not found") 94 | -------------------------------------------------------------------------------- /src/timpkg/app/microservice.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL-v3 License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[os, osproc, strutils] 8 | import pkg/[nyml, flatty] 9 | import pkg/kapsis/[cli, runtime] 10 | 11 | import ../server/[app, config] 12 | import ../engine/meta 13 | 14 | proc newCommand*(v: Values) = 15 | ## Initialize a new Tim Engine configuration 16 | ## using current working directory 17 | discard 18 | 19 | proc runCommand*(v: Values) = 20 | ## Runs Tim Engine as a microservice front-end application. 21 | let path = absolutePath(v.get("config").getPath.path) 22 | let config = fromYaml(path.readFile, TimConfig) 23 | var timEngine = 24 | newTim( 25 | config.compilation.source, 26 | config.compilation.output, 27 | path.parentDir 28 | ) 29 | app.run(timEngine, config) 30 | 31 | proc buildCommand*(v: Values) = 32 | ## Initialize a new Tim Engine configuration 33 | ## using current working directory 34 | discard 35 | 36 | import ../engine/[parser, ast] 37 | import ../engine/compilers/nimc 38 | import ../server/dynloader 39 | proc bundleCommand*(v: Values) = 40 | ## Bundle Tim templates to shared libraries 41 | ## for fast plug & serve. 42 | let 43 | cachedPath = v.get("ast").getPath.path 44 | cachedAst = readFile(cachedPath) 45 | c = nimc.newCompiler(fromFlatty(cachedAst, Ast), true) 46 | var 47 | genFilepath = cachedPath.changeFileExt(".nim") 48 | genFilepathTuple = genFilepath.splitFile() 49 | # nim requires that module name starts with a letter 50 | genFilepathTuple.name = "r_" & genFilepathTuple.name 51 | genFilepath = genFilepathTuple.dir / genFilepathTuple.name & genFilepathTuple.ext 52 | let dynlibPath = cachedPath.changeFileExt(".dylib") 53 | writeFile(genFilepath, c.exportCode()) 54 | # if not dynlibPath.fileExists: 55 | let status = execCmdEx("nim c --mm:arc -d:danger --opt:speed --app:lib --noMain -o:" & dynlibPath & " " & genFilePath) 56 | if status.exitCode > 0: 57 | return # nim compilation error 58 | removeFile(genFilepath) 59 | var collection = DynamicTemplates() 60 | let hashedName = cachedPath.splitFile.name 61 | collection.load(hashedName) 62 | echo collection.render(hashedName) 63 | collection.unload(hashedName) -------------------------------------------------------------------------------- /src/timpkg/app/source.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL-v3 License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[os, json, monotimes, times, strutils] 8 | 9 | import pkg/[jsony, flatty] 10 | import pkg/kapsis/[cli, runtime] 11 | 12 | import ../engine/[parser, ast] 13 | import ../engine/logging 14 | import ../engine/compilers/[html, nimc] 15 | 16 | proc srcCommand*(v: Values) = 17 | ## Transpiles a `.timl` file to a target source 18 | let 19 | fpath = $(v.get("timl").getPath) 20 | ext = v.get("-t").getStr 21 | pretty = v.has("--pretty") 22 | flagNoCache = v.has("--nocache") 23 | flagRecache = v.has("--recache") 24 | hasDataFlag = v.has("--data") 25 | hasJsonFlag = v.has("--json-errors") 26 | outputPath = if v.has("-o"): v.get("-o").getStr else: "" 27 | enableBenchmark = v.has("--bench") 28 | # enableWatcher = v.has("w") 29 | let jsonData: JsonNode = 30 | if hasDataFlag: v.get("--data").getJson 31 | else: nil 32 | let name = fpath 33 | let timlCode = readFile(getCurrentDir() / fpath) 34 | let t = getMonotime() 35 | let p = parseSnippet(name, timlCode, flagNoCache, flagRecache) 36 | if likely(not p.hasErrors): 37 | if ext == "html": 38 | let c = html.newCompiler( 39 | ast = parser.getAst(p), 40 | minify = (pretty == false), 41 | data = jsonData 42 | ) 43 | if likely(not c.hasErrors): 44 | let benchTime = getMonotime() - t 45 | if outputPath.len > 0: 46 | writeFile(outputPath, c.getHtml.strip) 47 | else: 48 | display c.getHtml().strip 49 | if enableBenchmark: 50 | displayInfo("Done in " & $benchTime) 51 | else: 52 | if not hasJsonFlag: 53 | for err in c.logger.errors: 54 | display err 55 | displayInfo c.logger.filePath 56 | else: 57 | let outputJsonErrors = newJArray() 58 | for err in c.logger.errorsStr: 59 | add outputJsonErrors, err 60 | display jsony.toJson(outputJsonErrors) 61 | if enableBenchmark: 62 | displayInfo("Done in " & $(getMonotime() - t)) 63 | quit(1) 64 | elif ext == "nim": 65 | let c = nimc.newCompiler(parser.getAst(p)) 66 | display c.exportCode() 67 | if enableBenchmark: 68 | displayInfo("Done in " & $(getMonotime() - t)) 69 | else: 70 | displayError("Unknown target `" & ext & "`") 71 | if enableBenchmark: 72 | displayInfo("Done in " & $(getMonotime() - t)) 73 | quit(1) 74 | else: 75 | for err in p.logger.errors: 76 | display(err) 77 | displayInfo p.logger.filePath 78 | if enableBenchmark: 79 | displayInfo("Done in " & $(getMonotime() - t)) 80 | quit(1) 81 | 82 | proc astCommand*(v: Values) = 83 | ## Build binary AST from a `timl` file 84 | let fpath = v.get("timl").getPath.path 85 | let opath = normalizedPath(getCurrentDir() / v.get("output").getFilename) 86 | let p = parseSnippet(fpath, readFile(getCurrentDir() / fpath)) 87 | if likely(not p.hasErrors): 88 | writeFile(opath, flatty.toFlatty(parser.getAst(p))) 89 | 90 | proc reprCommand*(v: Values) = 91 | ## Read a binary AST to target source 92 | let fpath = v.get("ast").getPath.path 93 | let ext = v.get("ext").getStr 94 | let pretty = v.has("pretty") 95 | if ext == "html": 96 | let c = html.newCompiler(flatty.fromFlatty(readFile(fpath), Ast), pretty == false) 97 | display c.getHtml().strip 98 | elif ext == "nim": 99 | let c = nimc.newCompiler(flatty.fromFlatty(readFile(fpath), Ast)) 100 | display c.exportCode() 101 | else: 102 | displayError("Unknown target `" & ext & "`") 103 | quit(1) 104 | 105 | import std/[xmltree, ropes, strtabs, sequtils] 106 | import pkg/htmlparser 107 | 108 | proc htmlCommand*(v: Values) = 109 | ## Transpile HTML code to Tim code 110 | let filepath = $(v.get("html_file").getPath) 111 | displayWarning("Work in progress. Unstable results") 112 | var indentSize = 0 113 | var timldoc: Rope 114 | var inlineNest: bool 115 | proc parseHtmlNode(node: XmlNode, toInlineNest: var bool = inlineNest) = 116 | var isEmbeddable: bool 117 | case node.kind 118 | of xnElement: 119 | let tag: HtmlTag = node.htmlTag() 120 | if not toInlineNest: 121 | add timldoc, indent(ast.getHtmlTag(tag), 2 * indentSize) 122 | else: 123 | add timldoc, " > " & ast.getHtmlTag(tag) 124 | inlineNest = false 125 | isEmbeddable = 126 | if tag in {tagScript, tagStyle}: true 127 | else: false 128 | # handle node attributes 129 | if node.attrsLen > 0: 130 | var attrs: Rope 131 | for k, v in node.attrs(): 132 | if k == "class": 133 | add attrs, rope("." & join(v.split(), ".")) 134 | elif k == "id": 135 | add attrs, rope("#" & v.strip) 136 | else: 137 | add attrs, rope(" " & k & "=\"" & v & "\"") 138 | add timldoc, attrs 139 | # handle child nodes 140 | let subNodes = node.items.toSeq() 141 | if subNodes.len > 1: 142 | if subNodes[0].kind == xnText: 143 | if subNodes[0].innerText.strip().len == 0: 144 | if subNodes[1].kind != xnText: 145 | if subNodes.len == 3: 146 | inlineNest = true 147 | for subNode in subNodes: 148 | parseHtmlNode(subNode, inlineNest) 149 | return 150 | else: 151 | add timldoc, "\n" 152 | else: 153 | add timldoc, "\n" 154 | inc indentSize 155 | for subNode in subNodes: 156 | parseHtmlNode(subNode) 157 | dec indentSize 158 | elif subNodes.len == 1: 159 | let subNode = subNodes[0] 160 | case subNode.kind 161 | of xnText: 162 | if not isEmbeddable: 163 | add timlDoc, ": \"" & subNode.innerText.strip() & "\"\n" 164 | else: 165 | add timlDoc, ": \"\"\"" & subNode.innerText.strip() & "\"\"\"\n" 166 | else: discard 167 | else: 168 | add timldoc, "\n" # self-closing tags requires new line at the end 169 | inlineNest = false 170 | of xnText: 171 | let innerText = node.innerText 172 | if innerText.strip().len > 0: 173 | if not isEmbeddable: 174 | add timlDoc, ": \"" & innerText.strip() & "\"\n" 175 | else: 176 | add timlDoc, ": \"\"\"" & innerText.strip() & "\"\"\"\n" 177 | else: discard 178 | 179 | let htmldoc = htmlparser.loadHtml(getCurrentDir() / filepath) 180 | for node in htmldoc: 181 | case node.kind 182 | of xnElement: 183 | parseHtmlNode(node) 184 | else: discard 185 | 186 | echo timldoc 187 | -------------------------------------------------------------------------------- /src/timpkg/engine/compilers/nimc.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[macros, os, tables, strutils] 8 | import pkg/jsony 9 | import ./tim 10 | 11 | from ../meta import TimEngine, TimTemplate, TimTemplateType, 12 | getType, getSourcePath, getGlobalData 13 | 14 | type 15 | NimCompiler* = object of TimCompiler 16 | Code = distinct string 17 | NimNodeSymbol = distinct string 18 | 19 | template genNewResult: NimNodeSymbol = 20 | let x = newAssignment(ident"result", newLit("")) 21 | NimNodeSymbol x.repr 22 | 23 | template genNewVar: NimNodeSymbol = 24 | let x = newVarStmt(ident"$1", ident"$2") 25 | NimNodeSymbol x.repr 26 | 27 | template genNewConst: NimNodeSymbol = 28 | let x = newConstStmt(ident"$1", ident"$2") 29 | NimNodeSymbol x.repr 30 | 31 | template genNewAddResult: NimNodeSymbol = 32 | let x = nnkCommand.newTree(ident"add", ident"result", newLit"$1") 33 | NimNodeSymbol x.repr 34 | 35 | template genNewAddResultUnquote: NimNodeSymbol = 36 | let x = nnkCommand.newTree(ident"add", ident"result", ident"$1") 37 | NimNodeSymbol x.repr 38 | 39 | template genViewProc: NimNodeSymbol = 40 | let x = newProc( 41 | nnkPostfix.newTree(ident"*", ident"$1"), 42 | params = [ 43 | ident"string", 44 | nnkIdentDefs.newTree( 45 | ident"app", 46 | ident"this", 47 | ident"JsonNode", 48 | newCall(ident"newJObject") 49 | ) 50 | ], 51 | body = newStmtList( 52 | newCommentStmtNode("Render homepage") 53 | ) 54 | ) 55 | NimNodeSymbol x.repr 56 | 57 | template genIfStmt: NimNodeSymbol = 58 | let x = newIfStmt( 59 | ( 60 | cond: ident"$1", 61 | body: newStmtList().add(ident"$2") 62 | ) 63 | ) 64 | NimNodeSymbol x.repr 65 | 66 | template genElifStmt: NimNodeSymbol = 67 | let x = nnkElifBranch.newTree(ident"$1", newStmtList().add(ident"$2")) 68 | NimNodeSymbol x.repr 69 | 70 | template genElseStmt: NimNodeSymbol = 71 | let x = nnkElse.newTree(newStmtList(ident"$1")) 72 | NimNodeSymbol x.repr 73 | 74 | template genCall: NimNodeSymbol = 75 | let x = nnkCall.newTree(ident"$1") 76 | NimNodeSymbol x.repr 77 | 78 | template genCommand: NimNodeSymbol = 79 | NimNodeSymbol("$1 $2") 80 | 81 | template genForItemsStmt: NimNodeSymbol = 82 | let x = nnkForStmt.newTree( 83 | ident"$1", 84 | ident"$2", 85 | newStmtList().add(ident"$3") 86 | ) 87 | NimNodeSymbol x.repr 88 | 89 | const 90 | ctrl = genViewProc() 91 | newResult* = genNewResult() 92 | addResult* = genNewAddResult() 93 | addResultUnquote* = genNewAddResultUnquote() 94 | newVar* = genNewVar() 95 | newConst* = genNewConst() 96 | newIf* = genIfStmt() 97 | newElif* = genElifStmt() 98 | newElse* = genElseStmt() 99 | newCallNode* = genCall() 100 | newCommandNode* = genCommand() 101 | newForItems* = genForItemsStmt() 102 | voidElements = [tagArea, tagBase, tagBr, tagCol, 103 | tagEmbed, tagHr, tagImg, tagInput, tagLink, tagMeta, 104 | tagParam, tagSource, tagTrack, tagWbr, tagCommand, 105 | tagKeygen, tagFrame] 106 | # 107 | # forward declarations 108 | # 109 | proc getValue(c: NimCompiler, node: Node, needEscaping = true, quotes = "\""): string 110 | proc walkNodes(c: var NimCompiler, nodes: seq[Node]) 111 | 112 | proc fmt*(nns: NimNodeSymbol, arg: varargs[string]): string = 113 | result = nns.string % arg 114 | 115 | template toCode(nns: NimNodeSymbol, args: varargs[string]): untyped = 116 | indent(fmt(nns, args), 2) 117 | 118 | template write(nns: NimNodeSymbol, args: varargs[string]) = 119 | add c.output, indent(fmt(nns, args), 2) & c.nl 120 | 121 | template writeToResult(nns: NimNodeSymbol, isize = 2, addNewLine = true, args: varargs[string]) = 122 | add result, indent(fmt(nns, args), isize) 123 | if addNewLine: add result, c.nl 124 | 125 | proc writeVar(c: var NimCompiler, node: Node) = 126 | case node.varImmutable: 127 | of false: 128 | write(newVar, node.varName, c.getValue(node.varValue)) 129 | of true: 130 | write(newConst, node.varName, c.getValue(node.varValue)) 131 | 132 | proc getVar(c: var NimCompiler, node: Node): string = 133 | case node.varImmutable: 134 | of false: 135 | writeToResult(newVar, 0, args = [node.varName, c.getValue(node.varValue)]) 136 | of true: 137 | writeToResult(newConst, 0, args = [node.varName, c.getValue(node.varValue)]) 138 | 139 | proc getAttrs(c: var NimCompiler, attrs: HtmlAttributes): string = 140 | ## Write HTMLAttributes 141 | var i = 0 142 | var skipQuote: bool 143 | let len = attrs.len 144 | for k, attrNodes in attrs: 145 | var attrStr: seq[string] 146 | if not c.isClientSide: 147 | add result, indent("$1=\\\"" % k, 1) 148 | for attrNode in attrNodes: 149 | case attrNode.nt 150 | of ntLitString, ntLitInt, ntLitFloat, ntLitBool: 151 | add attrStr, c.getValue(attrNode, true, "") 152 | else: discard # todo 153 | if not c.isClientSide: 154 | add result, attrStr.join(" ") 155 | if not skipQuote and i != len: 156 | add result, "\\\"" 157 | else: 158 | skipQuote = false 159 | inc i 160 | # else: 161 | # add result, domSetAttribute % [xel, k, attrStr.join(" ")] 162 | 163 | proc htmlElement(c: var NimCompiler, x: Node): string = 164 | block: 165 | case c.minify: 166 | of false: 167 | if c.stickytail == true: 168 | c.stickytail = false 169 | else: discard 170 | let t = x.getTag() 171 | add result, "<" 172 | add result, t 173 | if x.attrs != nil: 174 | if x.attrs.len > 0: 175 | add result, c.getAttrs(x.attrs) 176 | add result, ">" 177 | for i in 0..x.nodes.high: 178 | let node = x.nodes[i] 179 | case node.nt 180 | of ntLitString, ntLitInt, ntLitFloat, ntLitBool: 181 | add result, c.getValue(node, false) 182 | of ntIdent: 183 | add result, "\" & " & c.getValue(node, false) & " & \"" 184 | of ntVariableDef: 185 | add result, "\"" & c.nl 186 | add result, c.getVar(node) 187 | # kinda hack, `add result, $1` unquoted for inserting remaining tails 188 | writeToResult(addResultUnquote, 0, false, args = "\"") 189 | of ntHtmlElement: 190 | add result, c.htmlElement(node) 191 | else: discard 192 | case x.tag 193 | of voidElements: 194 | discard 195 | else: 196 | case c.minify: 197 | of false: 198 | discard 199 | # add c.output, c.getIndent(node.meta) 200 | else: discard 201 | add result, "" 204 | c.stickytail = false 205 | 206 | proc writeElement(c: var NimCompiler, node: Node) = 207 | write(addResult, c.htmlElement(node)) 208 | 209 | proc getInfixExpr(c: var NimCompiler, node: Node): string = 210 | case node.nt 211 | of ntInfixExpr: 212 | result = c.getValue(node.infixLeft) 213 | add result, indent($(node.infixOp), 1) 214 | add result, c.getValue(node.infixRight).indent(1) 215 | else: discard 216 | 217 | proc writeCondition(c: var NimCompiler, node: Node) = 218 | let ifexpr = c.getInfixExpr(node.condIfBranch.expr) 219 | var cond = toCode(newIf, ifexpr, "discard") 220 | for elifnode in node.condElifBranch: 221 | let elifexpr = c.getInfixExpr(elifnode.expr) 222 | add cond, toCode(newElif, elifexpr, "discard") 223 | if node.condElseBranch.stmtList.len > 0: 224 | add cond, toCode(newElse, "") 225 | add c.output, cond & "\n" 226 | 227 | proc writeCommand(c: var NimCompiler, node: Node) = 228 | case node.cmdType 229 | of cmdEcho: 230 | write newCommandNode, $cmdEcho, c.getValue(node.cmdValue) 231 | of cmdReturn: 232 | write newCommandNode, $cmdReturn, c.getValue(node.cmdValue) 233 | else: discard 234 | 235 | proc writeLoop(c: var NimCompiler, node: Node) = 236 | write newForItems, "keys", "fruits", "echo aaa" 237 | 238 | proc getValue(c: NimCompiler, node: Node, 239 | needEscaping = true, quotes = "\""): string = 240 | result = 241 | case node.nt 242 | of ntLitString: 243 | if needEscaping: 244 | escape(node.sVal, quotes, quotes) 245 | else: 246 | node.sVal 247 | of ntLitInt: 248 | $node.iVal 249 | of ntLitFloat: 250 | $node.iVal 251 | of ntLitBool: 252 | $node.bVal 253 | of ntIdent: 254 | node.identName 255 | else: "" 256 | 257 | proc walkNodes(c: var NimCompiler, nodes: seq[Node]) = 258 | for i in 0..nodes.high: 259 | let node = nodes[i] 260 | case node.nt 261 | of ntVariableDef: 262 | c.writeVar node 263 | of ntHtmlElement: 264 | c.writeElement node 265 | of ntConditionStmt: 266 | c.writeCondition node 267 | of ntCommandStmt: 268 | c.writeCommand node 269 | of ntLoopStmt: 270 | c.writeLoop node 271 | else: discard 272 | 273 | proc genProcName(path: string): string = 274 | # Generate view proc name based on `path` 275 | let path = path.splitFile 276 | result = "render" 277 | var i = 0 278 | var viewName: string 279 | while i < path.name.len: 280 | case path.name[i] 281 | of '-', ' ', '_': 282 | while path.name[i] in {'-', ' ', '_'}: 283 | inc i 284 | add viewName, path.name[i].toUpperAscii # todo convert unicode to ascii 285 | else: 286 | add viewName, path.name[i] 287 | inc i 288 | add result, viewName.capitalizeAscii 289 | 290 | proc newCompiler*(ast: Ast, makelib = false): NimCompiler = 291 | var c = NimCompiler(ast: ast) 292 | if makelib: 293 | add c.output, "import std/[json, dynlib]" & c.nl 294 | add c.output, "proc NimMain {.cdecl, importc.}" & c.nl 295 | add c.output, "{.push exportc, dynlib, cdecl.}" & c.nl 296 | add c.output, "proc library_init = NimMain()" & c.nl 297 | else: 298 | add c.output, "import std/json" & c.nl 299 | let procName = genProcName(ast.src) 300 | add c.output, ctrl.string % "renderTemplate" 301 | add c.output, fmt(newResult) & c.nl 302 | c.walkNodes(c.ast.nodes) 303 | # if makelib: 304 | # add c.output, "echo " & procName & "()" & c.nl 305 | if makelib: 306 | add c.output, "proc library_deinit = GC_FullCollect()" & c.nl 307 | add c.output, "{.pop.}" & c.nl 308 | result = c 309 | 310 | proc exportCode*(c: NimCompiler): string = 311 | c.output 312 | -------------------------------------------------------------------------------- /src/timpkg/engine/compilers/tim.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import ../meta, ../ast, ../logging 8 | export meta, ast, logging 9 | 10 | type 11 | TimCompiler* = object of RootObj 12 | ast*: Ast 13 | tpl*: TimTemplate 14 | engine*: TimEngine 15 | nl*: string = "\n" 16 | output*, jsOutput*, jsonOutput*, 17 | yamlOutput*, cssOutput*: string 18 | start*, isClientSide*: bool 19 | case tplType*: TimTemplateType 20 | of ttLayout: 21 | head*: string 22 | else: discard 23 | logger*: Logger 24 | indent*: int = 2 25 | partialIndent* : int = 0 26 | minify*, hasErrors*: bool 27 | stickytail*: bool 28 | # when `false` inserts a `\n` char 29 | # before closing the HTML element tag. 30 | # Does not apply to `textarea`, `button` and other 31 | # self closing tags (such as `submit`, `img` and so on) -------------------------------------------------------------------------------- /src/timpkg/engine/logging.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | from ./tokens import TokenTuple 8 | from ./ast import Meta 9 | 10 | import std/[sequtils, json, strutils] 11 | 12 | when compileOption("app", "console"): 13 | import pkg/kapsis/cli 14 | 15 | type 16 | Message* = enum 17 | invalidIndentation = "Invalid indentation [InvalidIndentation]" 18 | unexpectedToken = "Unexpected token $ [UnexpectedToken]" 19 | undeclaredIdentifier = "Undeclared identifier $ [UndeclaredIdentifier]" 20 | invalidAccessorStorage = "Invalid accessor storage $ for $ [InvalidAccessorStorage]" 21 | identRedefine = "Attempt to redefine $ [IdentRedefine]" 22 | varImmutable = "Attempt to reassign value to immutable variable $ [VarImmutable]" 23 | varImmutableValue = "Immutable variable $ requires initialization [VarImmutableValue]" 24 | typeMismatchMutable = "Type mismatch. Got $ expected a mutable $ [TypeMismatchMutable]" 25 | fnUndeclared = "Undeclared function $ [UndeclaredFunction]" 26 | fnReturnMissingCommand = "Expression $ is of type $ and has to be used or discarded [UseOrDiscard]" 27 | fnReturnVoid = "Function $ has no return type [VoidFunction]" 28 | fnExtraArg = "Extra arguments given. Got $ expected $ [ExtraArgs]" 29 | unimplementedForwardDeclaration = "Unimplemented forward declaration $ [UnimplementedForwardDeclaration]" 30 | badIndentation = "Nestable statement requires indentation [BadIndentation]" 31 | invalidContext = "Invalid $ in this context [InvalidContext]" 32 | invalidViewLoader = "Invalid use of `@view` in this context. Use a layout instead [InvalidViewLoader]" 33 | duplicateViewLoader = "Duplicate `@view` loader [DuplicateViewLoaded]" 34 | typeMismatch = "Type mismatch. Got $ expected $ [TypeMismatch]" 35 | typeMismatchObject = "Type mismatch. Expected an instance of $ [TypeMismatch]" 36 | duplicateAttribute = "Duplicate HTML attribute $ [DuplicateAttribute]" 37 | duplicateField = "Duplicate field $ [DuplicateField]" 38 | undeclaredField = "Undeclared field $ [UndeclaredField]" 39 | invalidIterator = "Invalid iterator [InvalidIterator]" 40 | indexDefect = "Index $ not in $ [IndexDefect]" 41 | importError = "Cannot open file: $ [ImportError]" 42 | importCircularError = "Circular import detected: $ [CircularImport]" 43 | invalidComponentName = "Invalid component name $ [InvalidComponentName]" 44 | assertionFailed = "Assertion failed" 45 | eof = "EOF reached before closing $ [EOF]" 46 | internalError = "$" 47 | 48 | Level* = enum 49 | lvlInfo 50 | lvlNotice 51 | lvlWarn 52 | lvlError 53 | 54 | Log* = ref object 55 | msg: Message 56 | extraLabel: string 57 | line, col: int 58 | useFmt: bool 59 | args, extraLines: seq[string] 60 | 61 | Logger* = ref object 62 | filePath*: string 63 | infoLogs*, noticeLogs*, warnLogs*, errorLogs*: seq[Log] 64 | 65 | proc add(logger: Logger, lvl: Level, msg: Message, line, col: int, 66 | useFmt: bool, args: varargs[string]) = 67 | let log = Log(msg: msg, args: args.toSeq(), 68 | line: line, col: col, useFmt: useFmt) 69 | case lvl 70 | of lvlInfo: 71 | logger.infoLogs.add(log) 72 | of lvlNotice: 73 | logger.noticeLogs.add(log) 74 | of lvlWarn: 75 | logger.warnLogs.add(log) 76 | of lvlError: 77 | logger.errorLogs.add(log) 78 | 79 | proc add(logger: Logger, lvl: Level, msg: Message, line, col: int, useFmt: bool, 80 | extraLines: seq[string], extraLabel: string, args: varargs[string]) = 81 | let log = Log( 82 | msg: msg, 83 | args: args.toSeq(), 84 | line: line, 85 | col: col + 1, 86 | useFmt: useFmt, 87 | extraLines: extraLines, 88 | extraLabel: extraLabel 89 | ) 90 | case lvl: 91 | of lvlInfo: 92 | logger.infoLogs.add(log) 93 | of lvlNotice: 94 | logger.noticeLogs.add(log) 95 | of lvlWarn: 96 | logger.warnLogs.add(log) 97 | of lvlError: 98 | logger.errorLogs.add(log) 99 | 100 | proc getMessage*(log: Log): Message = 101 | result = log.msg 102 | 103 | proc newInfo*(logger: Logger, msg: Message, line, col: int, 104 | useFmt: bool, args:varargs[string]) = 105 | logger.add(lvlInfo, msg, line, col, useFmt, args) 106 | 107 | proc newNotice*(logger: Logger, msg: Message, line, col: int, 108 | useFmt: bool, args:varargs[string]) = 109 | logger.add(lvlNotice, msg, line, col, useFmt, args) 110 | 111 | proc newWarn*(logger: Logger, msg: Message, line, col: int, 112 | useFmt: bool, args:varargs[string]) = 113 | logger.add(lvlWarn, msg, line, col, useFmt, args) 114 | 115 | proc newError*(logger: Logger, msg: Message, line, col: int, useFmt: bool, args:varargs[string]) = 116 | logger.add(lvlError, msg, line, col, useFmt, args) 117 | 118 | proc newErrorMultiLines*(logger: Logger, msg: Message, line, col: int, 119 | useFmt: bool, extraLines: seq[string], extraLabel: string, args:varargs[string]) = 120 | logger.add(lvlError, msg, line, col, useFmt, extraLines, extraLabel, args) 121 | 122 | proc newWarningMultiLines*(logger: Logger, msg: Message, line, col: int, 123 | useFmt: bool, extraLines: seq[string], extraLabel: string, args:varargs[string]) = 124 | logger.add(lvlWarn, msg, line, col, useFmt, extraLines, extraLabel, args) 125 | 126 | template warn*(msg: Message, tk: TokenTuple, args: varargs[string]) = 127 | p.logger.newWarn(msg, tk.line, tk.pos, false, args) 128 | 129 | template warn*(msg: Message, tk: TokenTuple, strFmt: bool, args: varargs[string]) = 130 | p.logger.newWarn(msg, tk.line, tk.pos, true, args) 131 | 132 | proc warn*(logger: Logger, msg: Message, line, col: int, args: varargs[string]) = 133 | logger.add(lvlWarn, msg, line, col, false, args) 134 | 135 | proc warn*(logger: Logger, msg: Message, line, col: int, strFmt: bool, args: varargs[string]) = 136 | logger.add(lvlWarn, msg, line, col, true, args) 137 | 138 | template warnWithArgs*(msg: Message, tk: TokenTuple, args: openarray[string]) = 139 | if not p.hasErrors: 140 | p.logger.newWarn(msg, tk.line, tk.pos, true, args) 141 | 142 | template error*(msg: Message, tk: TokenTuple) = 143 | if not p.hasErrors: 144 | p.logger.newError(msg, tk.line, tk.pos, false) 145 | p.hasErrors = true 146 | return # block code execution 147 | 148 | template error*(msg: Message, tk: TokenTuple, args: openarray[string]) = 149 | if not p.hasErrors: 150 | p.logger.newError(msg, tk.line, tk.pos, false, args) 151 | p.hasErrors = true 152 | return # block code execution 153 | 154 | template error*(msg: Message, tk: TokenTuple, strFmt: bool, 155 | extraLines: seq[string], extraLabel: string, args: varargs[string]) = 156 | if not p.hasErrors: 157 | newErrorMultiLines(p.logger, msg, tk.line, tk.pos, strFmt, extraLines, extraLabel, args) 158 | p.hasErrors = true 159 | return # block code execution 160 | 161 | template error*(msg: Message, meta: Meta, args: varargs[string]) = 162 | if not p.hasErrors: 163 | p.logger.newError(msg, meta[0], meta[2], true, args) 164 | p.hasErrors = true 165 | return # block code execution 166 | 167 | template errorWithArgs*(msg: Message, tk: TokenTuple, args: openarray[string]) = 168 | if not p.hasErrors: 169 | p.logger.newError(msg, tk.line, tk.pos, true, args) 170 | p.hasErrors = true 171 | return # block code execution 172 | 173 | template compileErrorWithArgs*(msg: Message, args: openarray[string]) = 174 | c.logger.newError(msg, node.meta[0], node.meta[1], true, args) 175 | c.hasErrors = true 176 | return 177 | 178 | template compileErrorWithArgs*(msg: Message, args: openarray[string], meta: Meta) = 179 | c.logger.newError(msg, meta[0], meta[1], true, args) 180 | c.hasErrors = true 181 | return 182 | 183 | template compileErrorWithArgs*(msg: Message) = 184 | c.logger.newError(msg, node.meta[0], node.meta[1], true, []) 185 | c.hasErrors = true 186 | return 187 | 188 | template compileErrorWithArgs*(msg: Message, meta: Meta, args: openarray[string]) = 189 | c.logger.newError(msg, meta[0], meta[1], true, args) 190 | c.hasErrors = true 191 | return 192 | 193 | proc error*(logger: Logger, msg: Message, line, col: int, args: openarray[string]) = 194 | logger.add(lvlError, msg, line, col, false, args) 195 | 196 | when defined napiOrWasm: 197 | proc runIterator(i: Log, label = ""): string = 198 | if label.len != 0: 199 | add result, label 200 | add result, "(" & $i.line & ":" & $i.col & ")" & spaces(1) 201 | if i.useFmt: 202 | var x: int 203 | var str = split($i.msg, "$") 204 | let length = count($i.msg, "$") - 1 205 | for s in str: 206 | add result, s.strip() 207 | if length >= x: 208 | add result, indent(i.args[x], 1) 209 | inc x 210 | else: 211 | add result, $i.msg 212 | for a in i.args: 213 | add result, a 214 | 215 | proc `$`*(i: Log): string = 216 | runIterator(i) 217 | 218 | iterator warnings*(logger: Logger): string = 219 | for i in logger.warnLogs: 220 | yield runIterator(i, "Warning") 221 | 222 | iterator errors*(logger: Logger): string = 223 | for i in logger.errorLogs: 224 | yield runIterator(i) 225 | if i.extraLines.len != 0: 226 | if i.extraLabel.len != 0: 227 | var extraLabel = "\n" 228 | add extraLabel, indent(i.extraLabel, 6) 229 | yield extraLabel 230 | for extraLine in i.extraLines: 231 | var extra = "\n" 232 | add extra, indent(extraLine, 12) 233 | yield extra 234 | 235 | else: 236 | proc runIterator(i: Log, label: string, fgColor: ForegroundColor): Row = 237 | add result, span(label, fgColor, indentSize = 0) 238 | add result, span("(" & $i.line & ":" & $i.col & ")") 239 | if i.useFmt: 240 | var x: int 241 | var str = split($i.msg, "$") 242 | let length = count($i.msg, "$") - 1 243 | for s in str: 244 | add result, span(s.strip()) 245 | if length >= x: 246 | add result, span(i.args[x], fgBlue) 247 | inc x 248 | else: 249 | add result, span($i.msg) 250 | for a in i.args: 251 | add result, span(a, fgBlue) 252 | 253 | iterator warnings*(logger: Logger): Row = 254 | for i in logger.warnLogs: 255 | yield runIterator(i, "Warning", fgYellow) 256 | 257 | iterator errors*(logger: Logger): Row = 258 | for i in logger.errorLogs: 259 | yield runIterator(i, "Error", fgRed) 260 | if i.extraLines.len != 0: 261 | if i.extraLabel.len != 0: 262 | var extraLabel: Row 263 | extraLabel.add(span(i.extraLabel, indentSize = 6)) 264 | yield extraLabel 265 | for extraLine in i.extraLines: 266 | var extra: Row 267 | extra.add(span(extraLine, indentSize = 12)) 268 | yield extra 269 | 270 | proc runIteratorStr(i: Log, label = ""): JsonNode = 271 | result = newJObject() 272 | result["line"] = newJInt(i.line) 273 | result["col"] = newJInt(i.col) 274 | result["code"] = newJInt(i.msg.ord) 275 | if i.useFmt: 276 | var x: int 277 | var str = split($i.msg, "$") 278 | let length = count($i.msg, "$") - 1 279 | var msg: string 280 | for s in str: 281 | add msg, s 282 | if length >= x: 283 | add msg, i.args[x] 284 | inc x 285 | result["msg"] = newJString(msg) 286 | else: 287 | var str = $i.msg 288 | for a in i.args: 289 | add str, a 290 | result["msg"] = newJString(str) 291 | 292 | # iterator warningsStr*(logger: Logger): string = 293 | # for i in logger.warnLogs: 294 | # yield runIteratorStr(i, "Warning") 295 | 296 | iterator errorsStr*(logger: Logger): JsonNode = 297 | for i in logger.errorLogs: 298 | yield runIteratorStr(i) 299 | # if i.extraLines.len != 0: 300 | # if i.extraLabel.len != 0: 301 | # var extraLabel = "\n" 302 | # add extraLabel, indent(i.extraLabel, 6) 303 | # yield extraLabel 304 | # for extraLine in i.extraLines: 305 | # var extra = "\n" 306 | # add extra, indent(extraLine, 12) 307 | # yield extra -------------------------------------------------------------------------------- /src/timpkg/engine/meta.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[macros, os, json, strutils, 8 | sequtils, locks, tables] 9 | 10 | import pkg/[checksums/md5, flatty] 11 | import pkg/importer/resolver 12 | 13 | export getProjectPath 14 | 15 | from ./ast import Ast 16 | from ./package/manager import Packager, loadPackages 17 | 18 | var placeholderLocker*: Lock 19 | 20 | type 21 | TimTemplateType* = enum 22 | ttInvalid 23 | ttLayout = "layouts" 24 | ttView = "views" 25 | ttPartial = "partials" 26 | 27 | TemplateSourcePaths = tuple[src, ast, html: string] 28 | TimTemplate* = ref object 29 | jit, inUse: bool 30 | templateId: string 31 | templateName: string 32 | case templateType: TimTemplateType 33 | of ttPartial: 34 | discard 35 | of ttLayout: 36 | viewIndent: uint 37 | else: discard 38 | sources*: TemplateSourcePaths 39 | dependents: Table[string, string] 40 | 41 | TemplateTable = TableRef[string, TimTemplate] 42 | 43 | TimPolicy* = ref object 44 | # todo 45 | 46 | TimEngineRuntime* = enum 47 | runtimeLiveAndRun 48 | runtimeLiveAndRunCli 49 | runtimePassAndExit 50 | 51 | TimEngine* = ref object 52 | `type`*: TimEngineRuntime 53 | base, src, output: string 54 | minify, htmlErrors: bool 55 | indentSize: int 56 | layouts, views, partials: TemplateTable = TemplateTable() 57 | errors*: seq[string] 58 | policy: TimPolicy 59 | globals: JsonNode = newJObject() 60 | importsHandle*: Resolver 61 | packager*: Packager 62 | 63 | TimEngineSnippets* = TableRef[string, seq[Ast]] 64 | TimError* = object of CatchableError 65 | 66 | when defined timStaticBundle: 67 | import pkg/supersnappy 68 | type 69 | StaticFilesystemTable = TableRef[string, string] 70 | 71 | var StaticFilesystem* = StaticFilesystemTable() 72 | 73 | # 74 | # Placeholders API 75 | # 76 | proc toPlaceholder*(placeholders: TimEngineSnippets, key: string, snippetTree: Ast) = 77 | ## Insert a snippet to a specific placeholder 78 | withLock placeholderLocker: 79 | if placeholders.hasKey(key): 80 | placeholders[key].add(snippetTree) 81 | else: 82 | placeholders[key] = @[snippetTree] 83 | deinitLock placeholderLocker 84 | 85 | proc hasPlaceholder*(placeholders: TimEngineSnippets, key: string): bool = 86 | ## Determine if a placholder exists by `key` 87 | withLock placeholderLocker: 88 | result = placeholders.hasKey(key) 89 | deinitLock placeholderLocker 90 | 91 | iterator listPlaceholders*(placeholders: TimEngineSnippets): (string, seq[Ast]) = 92 | ## List registered placeholders 93 | withLock placeholderLocker: 94 | for k, v in placeholders.mpairs: 95 | yield (k, v) 96 | deinitLock placeholderLocker 97 | 98 | iterator snippets*(placeholders: TimEngineSnippets, key: string): Ast = 99 | ## List all snippets attached from a placeholder 100 | withLock placeholderLocker: 101 | for x in placeholders[key]: 102 | yield x 103 | deinitLock placeholderLocker 104 | 105 | proc deleteSnippet*(placeholders: TimEngineSnippets, key: string, i: int) = 106 | ## Delete a snippet from a placeholder by `key` and `i` order 107 | withLock placeholderLocker: 108 | placeholders[key].del(i) 109 | deinitLock placeholderLocker 110 | 111 | proc getPath*(engine: TimEngine, key: string, templateType: TimTemplateType): string = 112 | ## Get absolute path of `key` view, partial or layout 113 | var k: string 114 | var tree: seq[string] 115 | result = engine.src & "/" & $templateType & "/$1" 116 | if key.endsWith(".timl"): 117 | k = key[0 .. ^6] 118 | else: 119 | k = key 120 | if key.contains("."): 121 | tree = k.split(".") 122 | result = result % [tree.join("/")] 123 | else: 124 | result = result % [k] 125 | result &= ".timl" 126 | result = normalizedPath(result) # normalize path for Windows 127 | 128 | proc setGlobalData*(engine: TimEngine, data: JsonNode) = 129 | engine.globals = data 130 | 131 | proc getGlobalData*(engine: TimEngine): JsonNode = 132 | engine.globals 133 | 134 | proc hashid(path: string): string = 135 | # Creates an MD5 hashed version of `path` 136 | result = getMD5(path) 137 | 138 | proc getHtmlPath(engine: TimEngine, path: string): string = 139 | engine.output / "html" / hashid(path) & ".html" 140 | 141 | proc getAstPath(engine: TimEngine, path: string): string = 142 | engine.output / "ast" / hashid(path) & ".ast" 143 | 144 | proc getHtmlStoragePath*(engine: TimEngine): string = 145 | ## Returns the `html` directory path used for 146 | ## storing static HTML files 147 | result = engine.output / "html" 148 | 149 | proc getAstStoragePath*(engine: TimEngine): string = 150 | ## Returns the `ast` directory path used for 151 | ## storing binary AST files. 152 | result = engine.output / "ast" 153 | 154 | # 155 | # TimTemplate API 156 | # 157 | proc newTemplate(id: string, tplType: TimTemplateType, 158 | sources: TemplateSourcePaths): TimTemplate = 159 | TimTemplate( 160 | templateId: id, 161 | templateType: tplType, 162 | templateName: sources.src.extractFilename, 163 | sources: sources 164 | ) 165 | 166 | proc getType*(t: TimTemplate): TimTemplateType = 167 | ## Get template type of `t` 168 | t.templateType 169 | 170 | proc getHash*(t: TimTemplate): string = 171 | ## Returns the hashed path of `t` 172 | hashid(t.sources.src) 173 | 174 | proc getName*(t: TimTemplate): string = 175 | ## Get template name of `t` 176 | t.templateName 177 | 178 | proc getTemplateId*(t: TimTemplate): string = 179 | ## Get template id of `t` 180 | t.templateId 181 | 182 | proc setViewIndent*(t: TimTemplate, i: uint) = 183 | assert t.templateType == ttLayout 184 | t.viewIndent = i 185 | 186 | proc getViewIndent*(t: TimTemplate): uint = 187 | assert t.templateType == ttLayout 188 | t.viewIndent 189 | 190 | when defined timStandalone: 191 | proc getTargetSourcePath*(engine: TimEngine, t: TimTemplate, targetSourcePath, ext: string): string = 192 | result = t.sources.src.replace(engine.src, targetSourcePath).changeFileExt(ext) 193 | 194 | proc hasDep*(t: TimTemplate, path: string): bool = 195 | t.dependents.hasKey(path) 196 | 197 | proc addDep*(t: TimTemplate, path: string) = 198 | ## Add a new dependent 199 | t.dependents[path] = path 200 | 201 | proc getDeps*(t: TimTemplate): seq[string] = 202 | t.dependents.keys.toSeq() 203 | 204 | proc writeHtml*(engine: TimEngine, tpl: TimTemplate, htmlCode: string) = 205 | ## Writes `htmlCode` on disk using `tpl` info 206 | when defined timStaticBundle: 207 | let id = splitFile(tpl.sources.html).name 208 | StaticFilesystem[id] = compress(htmlCode) 209 | else: 210 | writeFile(tpl.sources.html, htmlCode) 211 | 212 | proc writeHtmlTail*(engine: TimEngine, tpl: TimTemplate, htmlCode: string) = 213 | ## Writes `htmlCode` tails on disk using `tpl` info 214 | when defined timStaticBundle: 215 | let id = splitFile(tpl.sources.html).name 216 | StaticFilesystem[id & "_tail"] = compress(htmlCode) 217 | else: 218 | writeFile(tpl.sources.html.changeFileExt("tail"), htmlCode) 219 | 220 | proc writeAst*(engine: TimEngine, tpl: TimTemplate, astCode: Ast) = 221 | ## Writes `astCode` on disk using `tpl` info 222 | when defined timStaticBundle: 223 | let id = splitFile(tpl.sources.ast).name 224 | StaticFilesystem[id] = toFlatty(astCode).compress 225 | else: 226 | writeFile(tpl.sources.ast, flatty.toFlatty(astCode)) 227 | 228 | proc readAst*(engine: TimEngine, tpl: TimTemplate): Ast = 229 | ## Get `AST` of `tpl` TimTemplate from storage 230 | when defined timStaticBundle: 231 | let id = splitFile(tpl.sources.ast).name 232 | result = fromFlatty(uncompress(StaticFilesystem[id]), Ast) 233 | else: 234 | try: 235 | let binAst = readFile(tpl.sources.ast) 236 | result = flatty.fromFlatty(binAst, Ast) 237 | except IOError: 238 | discard 239 | 240 | proc getSourcePath*(t: TimTemplate): string = 241 | ## Returns the absolute source path of `t` TimTemplate 242 | result = t.sources.src 243 | 244 | proc getAstPath*(t: TimTemplate): string = 245 | ## Returns the absolute `html` path of `t` TimTemplate 246 | result = t.sources.ast 247 | 248 | proc getHtmlPath*(t: TimTemplate): string = 249 | ## Returns the absolute `ast` path of `t` TimTemplate 250 | result = t.sources.html 251 | 252 | proc jitEnable*(t: TimTemplate) = 253 | if not t.jit: t.jit = true 254 | 255 | proc jitEnabled*(t: TimTemplate): bool = t.jit 256 | 257 | proc getHtml*(t: TimTemplate): string = 258 | ## Returns precompiled static HTML of `t` TimTemplate 259 | when defined timStaticBundle: 260 | let id = splitFile(t.sources.html).name 261 | result = uncompress(StaticFilesystem[id]) 262 | else: 263 | try: 264 | result = readFile(t.getHtmlPath) 265 | except IOError: 266 | result = "" 267 | 268 | proc getTail*(t: TimTemplate): string = 269 | ## Returns the tail of a split layout 270 | when defined timStaticBundle: 271 | let id = splitFile(t.sources.html).name 272 | result = uncompress(StaticFilesystem[id & "_tail"]) 273 | else: 274 | try: 275 | result = readFile(t.getHtmlPath.changeFileExt("tail")) 276 | except IOError as e: 277 | raise newException(TimError, e.msg & "\nSource: " & t.sources.src) 278 | 279 | iterator getViews*(engine: TimEngine): TimTemplate = 280 | for id, tpl in engine.views: 281 | yield tpl 282 | 283 | iterator getLayouts*(engine: TimEngine): TimTemplate = 284 | for id, tpl in engine.layouts: 285 | yield tpl 286 | 287 | # 288 | # TimEngine Engine API 289 | # 290 | 291 | proc getTemplateByPath*(engine: TimEngine, path: string): TimTemplate = 292 | ## Search for `path` in `layouts` or `views` table 293 | let id = hashid(path) # todo extract parent dir from path? 294 | if engine.views.hasKey(path): 295 | return engine.views[path] 296 | if engine.layouts.hasKey(path): 297 | return engine.layouts[path] 298 | if engine.partials.hasKey(path): 299 | return engine.partials[path] 300 | let 301 | astPath = engine.output / "ast" / id & ".ast" 302 | htmlPath = engine.output / "html" / id & ".html" 303 | sources = (src: path, ast: astPath, html: htmlPath) 304 | if engine.src / $ttLayout in path: 305 | engine.layouts[path] = newTemplate(id, ttLayout, sources) 306 | return engine.layouts[path] 307 | if engine.src / $ttView in path: 308 | engine.views[path] = newTemplate(id, ttView, sources) 309 | return engine.views[path] 310 | if engine.src / $ttPartial in path: 311 | engine.partials[path] = newTemplate(id, ttPartial, sources) 312 | return engine.partials[path] 313 | 314 | proc hasLayout*(engine: TimEngine, key: string): bool = 315 | ## Determine if `key` exists in `layouts` table 316 | result = engine.layouts.hasKey(engine.getPath(key, ttLayout)) 317 | 318 | proc getLayout*(engine: TimEngine, key: string): TimTemplate = 319 | ## Get a `TimTemplate` from `layouts` by `key` 320 | result = engine.layouts[engine.getPath(key, ttLayout)] 321 | result.inUse = true 322 | 323 | proc hasView*(engine: TimEngine, key: string): bool = 324 | ## Determine if `key` exists in `views` table 325 | result = engine.views.hasKey(engine.getPath(key, ttView)) 326 | 327 | proc getView*(engine: TimEngine, key: string): TimTemplate = 328 | ## Get a `TimTemplate` from `views` by `key` 329 | result = engine.views[engine.getPath(key, ttView)] 330 | result.inUse = true 331 | 332 | proc getBasePath*(engine: TimEngine): string = 333 | engine.base 334 | 335 | proc getTemplatePath*(engine: TimEngine, path: string): string = 336 | path.replace(engine.base, "") 337 | 338 | proc isUsed*(t: TimTemplate): bool = t.inUse 339 | proc showHtmlErrors*(engine: TimEngine): bool = engine.htmlErrors 340 | 341 | proc newTim*(src, output, basepath: string, minify = true, 342 | indent = 2, showHtmlError, enableBinaryCompilation = false): TimEngine = 343 | ## Initializes `TimEngine` engine 344 | ## 345 | ## Use `src` to specify the source target. `output` path 346 | ## will be used to save pre-compiled files on disk. 347 | ## 348 | ## `basepath` is the root path of your project. You can 349 | ## use `currentSourcePath()` 350 | ## 351 | ## Optionally, you can disable HTML minification using 352 | ## `minify` and `indent`. 353 | ## 354 | ## By enabling `enableBinaryCompilation` will compile 355 | ## all binary `.ast` files found in the `/ast` directory 356 | ## to dynamic library using Nim. 357 | ## 358 | ## **Note, this feature is not available for Source-to-Source 359 | ## transpilation.** Also, binary compilation requires having Nim installed. 360 | var basepath = 361 | if basepath.fileExists: 362 | basepath.parentDir # if comes from `currentSourcePath()` 363 | else: 364 | if not basepath.dirExists: 365 | raise newException(TimError, "Invalid basepath directory") 366 | basepath 367 | if src.isAbsolute or output.isAbsolute: 368 | raise newException(TimError, 369 | "Expecting a relative path for `src` and `output`") 370 | result = 371 | TimEngine( 372 | src: normalizedPath(basepath / src), 373 | output: normalizedPath(basepath / output), 374 | base: basepath, 375 | minify: minify, 376 | indentSize: indent, 377 | htmlErrors: showHtmlError, 378 | packager: Packager() 379 | ) 380 | result.packager.loadPackages() 381 | for sourceDir in [ttLayout, ttView, ttPartial]: 382 | if not dirExists(result.src / $sourceDir): 383 | raise newException(TimError, "Missing $1 directory: \n$2" % [$sourceDir, result.src / $sourceDir]) 384 | for fpath in walkDirRec(result.src / $sourceDir): 385 | let 386 | id = hashid(fpath) 387 | astPath = result.output / "ast" / id & ".ast" 388 | htmlPath = result.output / "html" / id & ".html" 389 | sources = (src: fpath, ast: astPath, html: htmlPath) 390 | case sourceDir: 391 | of ttLayout: 392 | result.layouts[fpath] = id.newTemplate(ttLayout, sources) 393 | of ttView: 394 | result.views[fpath] = id.newTemplate(ttView, sources) 395 | of ttPartial: 396 | result.partials[fpath] = id.newTemplate(ttPartial, sources) 397 | else: discard 398 | when not defined timStaticBundle: 399 | discard existsOrCreateDir(result.output / "ast") 400 | discard existsOrCreateDir(result.output / "html") 401 | if enableBinaryCompilation: 402 | discard existsOrCreateDir(result.output / "html") 403 | 404 | proc isMinified*(engine: TimEngine): bool = 405 | result = engine.minify 406 | 407 | proc getIndentSize*(engine: TimEngine): int = 408 | result = engine.indentSize 409 | 410 | proc flush*(engine: TimEngine) = 411 | ## Flush precompiled files 412 | for f in walkDir(engine.getAstStoragePath): 413 | if f.path.endsWith(".ast"): 414 | f.path.removeFile() 415 | 416 | for f in walkDir(engine.getHtmlStoragePath): 417 | if f.path.endsWith(".html"): 418 | f.path.removeFile() 419 | 420 | proc getSourcePath*(engine: TimEngine): string = 421 | result = engine.src 422 | 423 | proc getTemplateType*(engine: TimEngine, path: string): TimTemplateType = 424 | ## Returns `TimTemplateType` by `path` 425 | let basepath = engine.getSourcePath() 426 | for xdir in ["layouts", "views", "partials"]: 427 | if path.startsWith(basepath / xdir): 428 | return parseEnum[TimTemplateType](xdir) 429 | 430 | proc clearTemplateByPath*(engine: TimEngine, path: string) = 431 | ## Clear a template from `TemplateTable` by `path` 432 | case engine.getTemplateType(path): 433 | of ttLayout: 434 | engine.layouts.del(path) 435 | of ttView: 436 | engine.views.del(path) 437 | of ttPartial: 438 | engine.partials.del(path) 439 | else: discard 440 | -------------------------------------------------------------------------------- /src/timpkg/engine/package/manager.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2025 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[tables, strutils, os, osproc, options, sequtils] 8 | import pkg/[jsony, flatty, nyml, semver, checksums/md5] 9 | import ../ast 10 | 11 | import ./remote 12 | import ../../server/config 13 | 14 | type 15 | VersionedPackage = OrderedTableRef[string, TimConfig] 16 | PackagesTable* = OrderedTableRef[string, VersionedPackage] 17 | Packager* = ref object 18 | remote*: RemoteSource 19 | packages: PackagesTable 20 | # An ordered table containing a versioned 21 | # table of `Package` 22 | flagNoCache*: bool 23 | flagRecache*: bool 24 | 25 | const 26 | pkgrHomeDir* = getHomeDir() / ".tim" 27 | pkgrHomeDirTemp* = pkgrHomeDir / "tmp" 28 | pkgrPackagesDir* = pkgrHomeDir / "packages" 29 | pkgrTokenPath* = pkgrHomeDir / ".env" 30 | pkgrPackageCachedDir* = pkgrPackagesDir / "$1" / "$2" / ".cache" 31 | pkgrPackageSourceDir* = pkgrPackagesDir / "$1" / "$2" / "src" 32 | pkgrIndexPath* = pkgrPackagesDir / "index.json" 33 | 34 | # when not defined release: 35 | # proc `$`*(pkgr: Packager): string = 36 | # # for debug purposes 37 | # pretty(jsony.fromJson(jsony.toJson(pkgr)), 2) 38 | 39 | proc initPackager*: Packager = 40 | discard existsOrCreateDir(pkgrHomeDir) 41 | discard existsOrCreateDir(pkgrHomeDirTemp) 42 | result = Packager() 43 | 44 | proc initPackageRemote*: Packager = 45 | ## Initialize Tim Engine Packager with Remote Source 46 | result = initPackager() 47 | result.remote = initRemoteSource(pkgrHomeDir) 48 | 49 | proc hasPackage*(pkgr: Packager, pkgName: string): bool = 50 | ## Determine if a `pkgName` is installed 51 | result = pkgr.packages.hasKey(pkgName) 52 | if result: 53 | result = dirExists(pkgrPackagesDir / pkgName) 54 | result = dirExists(pkgrPackageSourceDir % [pkgName, "0.1.0"]) 55 | 56 | proc updatePackages*(pkgr: Packager) = 57 | ## Update packages index 58 | writeFile(pkgrIndexPath, toJson(pkgr.packages)) 59 | 60 | proc createPackage*(pkgr: Packager, orgName, pkgName: string, pkgConfig: TimConfig): bool = 61 | ## Create package directory for `pkgConfig` 62 | ## Returns `true` if succeed. 63 | let v = pkgConfig.version 64 | discard existsOrCreateDir(pkgrPackagesDir / pkgConfig.name) 65 | let tempPath = pkgrHomeDirTemp / pkgConfig.name & "@" & v & ".tar" 66 | let pkgPath = pkgrPackagesDir / pkgConfig.name / v 67 | if not existsOrCreateDir(pkgPath): 68 | if not fileExists(tempPath): 69 | if pkgr.remote.download("repo_tarball_ref", tempPath, @[orgName, pkgName, "main"]): 70 | discard execProcess("tar", args = ["-xzf", tempPath, "-C", pkgPath, "--strip-components=1"], 71 | options = {poStdErrToStdOut, poUsePath}) 72 | result = true 73 | else: 74 | discard execProcess("tar", args = ["-xzf", tempPath, "-C", pkgPath, "--strip-components=1"], 75 | options = {poStdErrToStdOut, poUsePath}) 76 | result = true 77 | if result: 78 | if not pkgr.packages.hasKey(pkgConfig.name): 79 | pkgr.packages[pkgConfig.name] = VersionedPackage() 80 | pkgr.packages[pkgConfig.name][v] = pkgConfig 81 | 82 | proc deletePackage*(pkgr: Packager, pkgName: string, pkgVersion: Option[Version] = none(Version)) = 83 | ## Delete a package by name and semantic version (when provided). 84 | ## Running the `remove` command over an aliased package 85 | ## will delete de alias and keep the original package folder in place 86 | let pkgConfig = pkgr.packages[pkgName] 87 | let version = 88 | if pkgVersion.isSome: 89 | # use the specified version 90 | $(pkgVersion.get()) 91 | else: 92 | # always choose the latest version 93 | let versions = pkgConfig.keys.toSeq 94 | pkgConfig[versions[versions.high]].version 95 | echo pkgrPackagesDir / pkgConfig[version].name / version 96 | 97 | proc loadModule*(pkgr: Packager, pkgName: string): string = 98 | ## Load a Tim Engine module from a specific package 99 | let pkgName = pkgName[4..^1].split("/") 100 | let pkgPath = pkgrPackageSourceDir % [pkgName[0], "0.1.0"] 101 | result = readFile(normalizedPath(pkgPath / pkgName[1..^1].join("/") & ".timl")) 102 | 103 | proc cacheModule*(pkgr: Packager, pkgName: string, ast: Ast) = 104 | ## Cache a Tim Engine module to binary AST 105 | let pkgName = pkgName[4..^1].split("/") 106 | let cachePath = pkgrPackageCachedDir % [pkgName[0], "0.1.0"] 107 | let cacheAstPath = cachePath / getMD5(pkgName[1..^1].join("/")) & ".ast" 108 | discard existsOrCreateDir(cachePath) 109 | writeFile(cacheAstPath, toFlatty(ast)) 110 | 111 | proc getCachedModule*(pkgr: Packager, pkgName: string): Ast = 112 | ## Retrieve a cached binary AST 113 | let pkgName = pkgName[4..^1].split("/") 114 | let cachePath = pkgrPackageCachedDir % [pkgName[0], "0.1.0"] 115 | let cacheAstPath = cachePath / getMD5(pkgName[1..^1].join("/")) & ".ast" 116 | if fileExists(cacheAstPath): 117 | result = fromFlatty(readFile(cacheAstPath), Ast) 118 | 119 | proc hasLoadedPackages*(pkgr: Packager): bool = 120 | ## Determine if packager has loaded the local database in memory 121 | pkgr.packages != nil 122 | 123 | proc loadPackages*(pkgr: Packager) = 124 | ## Load the local database of packages in memory 125 | if pkgrIndexPath.fileExists: 126 | let db = readFile(pkgrIndexPath) 127 | if db.len > 0: 128 | pkgr.packages = fromJson(readFile(pkgrIndexPath), PackagesTable) 129 | return 130 | new(pkgr.packages) 131 | -------------------------------------------------------------------------------- /src/timpkg/engine/package/remote.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2025 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[tables, httpcore, httpclient, strutils, base64] 8 | import pkg/[jsony, dotenv] 9 | 10 | from std/os import existsEnv, getEnv 11 | export base64 12 | 13 | type 14 | SourceType* = enum 15 | Github = "github_com" 16 | Gitlab = "gitlab_com" 17 | 18 | GithubFileResponse* = object 19 | name*, path*, sha*: string 20 | size*: int64 21 | url*, html_url*, git_url*, 22 | download_url*: string 23 | `type`*: string 24 | content*: string 25 | 26 | RemoteSource* = object 27 | apikey*: string 28 | source: SourceType 29 | client: HttpClient 30 | 31 | let GitHubRemoteEndpoints = newTable({ 32 | "base": "https://api.github.com", 33 | "repo": "/repos/$1/$2", 34 | "repo_contents": "/repos/$1/$2/contents", 35 | "repo_contents_path": "/repos/$1/$2/contents/$3", 36 | "repo_tags": "/repos/$1/$2/tags", 37 | "repo_tag_zip": "/repos/$1/$2/zipball/refs/tags/$3", 38 | "repo_tag_tar": "/repos/$1/$2/tarball/refs/tags/$3", 39 | "repo_tarball_ref": "/repos/$1/$2/tarball/$3", 40 | }) 41 | 42 | # 43 | # JSONY hooks 44 | # 45 | # proc parseHook*(s: string, i: var int, v: var Time) = 46 | # var str: string 47 | # parseHook(s, i, str) 48 | # v = parseTime(str, "yyyy-MM-dd'T'hh:mm:ss'.'ffffffz", local()) 49 | 50 | # proc dumpHook*(s: var string, v: Time) = 51 | # add s, '"' 52 | # add s, v.format("yyyy-MM-dd'T'hh:mm:ss'.'ffffffz", local()) 53 | # add s, '"' 54 | 55 | proc getRemoteEndpoints*(src: SourceType): TableRef[string, string] = 56 | case src 57 | of Github: GitHubRemoteEndpoints 58 | else: nil 59 | 60 | proc getRemotePath*(rs: RemoteSource, path: string, 61 | args: varargs[string]): string = 62 | case rs.source 63 | of Github: 64 | return GitHubRemoteEndpoints[path] 65 | else: discard 66 | 67 | 68 | proc httpGet*(client: RemoteSource, 69 | path: string, args: seq[string] = @[] 70 | ): Response = 71 | let endpoints = getRemoteEndpoints(client.source) 72 | let uri = endpoints["base"] & (endpoints[path] % args) 73 | result = client.client.request(uri, HttpGet) 74 | 75 | proc getFileContent*(client: RemoteSource, res: Response): GithubFileResponse = 76 | jsony.fromJson(res.body, GithubFileResponse) 77 | 78 | proc download*(client: RemoteSource, 79 | path, tmpPath: string, args: seq[string] = @[]): bool = 80 | ## Download a file from remote `path` and returns the 81 | ## local path to tmp file 82 | let endpoints = getRemoteEndpoints(client.source) 83 | let uri = endpoints["base"] & (endpoints[path] % args) 84 | client.client.downloadFile(uri, tmpPath) 85 | result = true 86 | 87 | proc initRemoteSource*(pkgrHomeDir: string, source: SourceType = Github): RemoteSource = 88 | result.source = source 89 | let key = "timengine_" & $source & "_apikey" 90 | dotenv.load(pkgrHomeDir, ".tokens") 91 | for x in SourceType: 92 | if existsEnv($x) and source == x: 93 | result.apikey = getEnv($x) 94 | result.client = newHttpClient() 95 | result.client.headers = newHttpheaders({ 96 | "Authorization": "Bearer " & result.apikey 97 | }) 98 | 99 | # proc getRemoteSourceFile*(rs: RemoteSource) 100 | # echo getRemotePath(RemoteSource(), "base") -------------------------------------------------------------------------------- /src/timpkg/engine/stdlib.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[macros, macrocache, enumutils, hashes, 8 | os, math, strutils, re, sequtils, critbits, 9 | random, unicode, json, tables, base64, 10 | httpclient, oids] 11 | 12 | import pkg/[jsony, nyml, urlly] 13 | import pkg/checksums/md5 14 | import ./ast 15 | 16 | type 17 | Arg* = tuple[name: string, value: Node] 18 | NimCallableHandle* = proc(args: openarray[Arg], returnType: NodeType = ntLitVoid): Node 19 | 20 | Module = OrderedTable[Hash, NimCallableHandle] 21 | SourceCode* = distinct string 22 | Stdlib = CritBitTree[(Module, SourceCode)] 23 | 24 | StringsModule* = object of CatchableError 25 | ArraysModule* = object of CatchableError 26 | OSModule* = object of CatchableError 27 | ColorsModule* = object of CatchableError 28 | SystemModule* = object of CatchableError 29 | ObjectsModule* = object of CatchableError 30 | 31 | var 32 | stdlib*: Stdlib 33 | strutilsModule {.threadvar.}, 34 | sequtilsModule {.threadvar.}, 35 | osModule {.threadvar.}, 36 | critbitsModule {.threadvar.}, 37 | systemModule {.threadvar.}, 38 | mathModule {.threadvar.}, 39 | objectsModule {.threadvar.}, 40 | urlModule {.threadvar.}, 41 | localModule* {.threadvar.}: Module 42 | 43 | const NimblePkgVersion {.strdefine.} = "Unknown" 44 | const version = NimblePkgVersion 45 | 46 | proc toNimSeq*(node: Node): seq[string] = 47 | for item in node.arrayItems: 48 | result.add(item.sVal) 49 | 50 | proc getHashedIdent*(key: string): Hash = 51 | if key.len > 1: 52 | hash(key[0] & key[1..^1].toLowerAscii) 53 | else: 54 | hash(key) 55 | 56 | macro initStandardLibrary() = 57 | type 58 | Forward = object 59 | id: string 60 | # function identifier (nim side) 61 | alias: string 62 | # if not provided, it will use the `id` 63 | # for the bass function name 64 | returns: NodeType 65 | # the return type, one of: `ntLitString`, `ntLitInt`, 66 | # `ntLitBool`, `ntLitFloat`, `ntLitArray`, `ntLitObject` 67 | args: seq[(NodeType, string)] 68 | # a seq of NodeType for type matching 69 | wrapper: NimNode 70 | # wraps nim function 71 | hasWrapper: bool 72 | src: string 73 | 74 | proc registerFunction(id: string, args: openarray[(NodeType, string)], nt: NodeType): string = 75 | var p = args.map do: 76 | proc(x: (NodeType, string)): string = 77 | "$1: $2" % [x[1], $(x[0])] 78 | result = "fn $1*($2): $3\n" % [id, p.join(", "), $nt] 79 | 80 | # proc registerVariable(id: string, dataType: DataType, varValue: Node) {.compileTime.} = 81 | # # discard 82 | 83 | proc fwd(id: string, returns: NodeType, args: openarray[(NodeType, string)] = [], 84 | alias = "", wrapper: NimNode = nil, src = ""): Forward = 85 | Forward( 86 | id: id, 87 | returns: returns, 88 | args: args.toSeq, 89 | alias: alias, 90 | wrapper: wrapper, 91 | hasWrapper: wrapper != nil, 92 | src: src 93 | ) 94 | 95 | proc argToSeq[T](arg: Arg): T = 96 | toNimSeq(arg.value) 97 | 98 | template formatWrapper: untyped = 99 | try: 100 | ast.newString(format(args[0].value.sVal, argToSeq[seq[string]](args[1]))) 101 | except ValueError as e: 102 | raise newException(StringsModule, e.msg) 103 | 104 | template systemStreamFunction: untyped = 105 | try: 106 | let src = 107 | if not isAbsolute(args[0].value.sVal): 108 | absolutePath(args[0].value.sVal) 109 | else: args[0].value.sVal 110 | let str = readFile(src) 111 | let ext = src.splitFile.ext 112 | if ext == ".json": 113 | return ast.newStream(jsony.fromJson(str, JsonNode)) 114 | elif ext in [".yml", ".yaml"]: 115 | return ast.newStream(yaml(str).toJson.get) 116 | else: 117 | echo "error" 118 | except IOError as e: 119 | raise newException(SystemModule, e.msg) 120 | except JsonParsingError as e: 121 | raise newException(SystemModule, e.msg) 122 | 123 | template systemStreamString: untyped = 124 | var res: Node 125 | if args[0].value.nt == ntLitString: 126 | res = ast.newStream(jsony.fromJson(args[0].value.sVal, JsonNode)) 127 | elif args[0].value.nt == ntStream: 128 | if args[0].value.streamContent.kind == JString: 129 | res = ast.newStream(jsony.fromJson(args[0].value.streamContent.str, JsonNode)) 130 | else: discard # todo conversion error 131 | res 132 | 133 | template systemJsonUrlStream = 134 | # retrieve JSON content from remote source 135 | # parse and return it as a Stream node 136 | var httpClient: HttpClient = 137 | if args.len == 1: 138 | newHttpClient(userAgent = "Tim Engine v" & version) 139 | else: 140 | let httpHeaders = newHttpHeaders() 141 | for k, v in args[1].value.objectItems: 142 | httpHeaders[k] = v.toString() 143 | newHttpClient(userAgent = "Tim Engine v" & version, headers = httpHeaders) 144 | let streamNode: Node = ast.newNode(ntStream) 145 | try: 146 | let contents = httpClient.getContent(args[0].value.sVal) 147 | streamNode.streamContent = fromJson(contents) 148 | streamNode 149 | finally: 150 | httpClient.close() 151 | 152 | template systemRandomize: untyped = 153 | randomize() 154 | ast.newInteger(rand(args[0].value.iVal)) 155 | 156 | template systemInc: untyped = 157 | inc args[0].value.iVal 158 | 159 | template convertToString: untyped = 160 | var str: ast.Node 161 | var val = args[0].value 162 | case val.nt: 163 | of ntLitInt: 164 | str = ast.newString($(val.iVal)) 165 | of ntLitFloat: 166 | str = ast.newString($(val.fVal)) 167 | of ntLitBool: 168 | str = ast.newString($(val.bVal)) 169 | of ntStream: 170 | if likely(val.streamContent != nil): 171 | case val.streamContent.kind: 172 | of JString: 173 | str = ast.newString($(val.streamContent.str)) 174 | of JBool: 175 | str = ast.newString($(val.streamContent.bval)) 176 | of JInt: 177 | str = ast.newString($(val.streamContent.num)) 178 | of JFloat: 179 | str = ast.newString($(val.streamContent.fnum)) 180 | of JNull: 181 | str = ast.newString("null") 182 | else: discard # should dump Object/Array too? 183 | else: discard 184 | str 185 | 186 | template parseCode: untyped = 187 | var xast: Node = ast.newNode(ntRuntimeCode) 188 | xast.runtimeCode = args[0].value.sVal 189 | xast 190 | 191 | template systemArrayLen = 192 | let x = ast.newNode(ntLitInt) 193 | x.iVal = args[0].value.arrayItems.len 194 | x 195 | 196 | template systemStreamLen = 197 | let x = ast.newNode(ntLitInt) 198 | x.iVal = 199 | case args[0].value.streamContent.kind 200 | of JString: 201 | len(args[0].value.streamContent.getStr) 202 | else: 0 # todo error 203 | x 204 | 205 | template generateId = 206 | ast.newString($genOid()) 207 | 208 | template generateUuid4 = 209 | ast.newString("todo") 210 | 211 | template genBase64 = 212 | let base64Obj = ast.newObject(ObjectStorage()) 213 | # let encodeFn = ast.newFunction(returnType = ntLitString) 214 | base64Obj.objectItems["encode"] = 215 | createFunction: 216 | returnType = typeString 217 | params = [("str", typeString, nil)] 218 | base64Obj 219 | 220 | let 221 | fnSystem = @[ 222 | fwd("json", ntStream, [(ntLitString, "path")], wrapper = getAst(systemStreamFunction())), 223 | fwd("parseJsonString", ntStream, [(ntLitString, "path")], wrapper = getAst(systemStreamString())), 224 | fwd("parseJsonString", ntStream, [(ntStream, "path")], wrapper = getAst(systemStreamString())), 225 | fwd("remoteJson", ntStream, [(ntLitString, "path")], wrapper = getAst(systemJsonUrlStream())), 226 | fwd("remoteJson", ntStream, [(ntLitString, "path"), (ntLitObject, "headers")], wrapper = getAst(systemJsonUrlStream())), 227 | fwd("yaml", ntStream, [(ntLitString, "path")], wrapper = getAst(systemStreamFunction())), 228 | fwd("rand", ntLitInt, [(ntLitInt, "max")], "random", wrapper = getAst(systemRandomize())), 229 | fwd("len", ntLitInt, [(ntLitString, "x")]), 230 | fwd("len", ntLitInt, [(ntLitArray, "x")], wrapper = getAst(systemArrayLen())), 231 | fwd("len", ntLitInt, [(ntStream, "x")], wrapper = getAst(systemStreamLen())), 232 | fwd("encode", ntLitString, [(ntLitString, "x")], src = "base64"), 233 | fwd("decode", ntLitString, [(ntLitString, "x")], src = "base64"), 234 | fwd("toString", ntLitString, [(ntLitInt, "x")], wrapper = getAst(convertToString())), 235 | fwd("toString", ntLitString, [(ntLitBool, "x")], wrapper = getAst(convertToString())), 236 | fwd("toString", ntLitString, [(ntStream, "x")], wrapper = getAst(convertToString())), 237 | fwd("timl", ntLitString, [(ntLitString, "x")], wrapper = getAst(parseCode())), 238 | fwd("inc", ntLitVoid, [(ntLitInt, "x")], wrapper = getAst(systemInc())), 239 | fwd("dec", ntLitVoid, [(ntLitInt, "x")]), 240 | fwd("genid", ntLitString, wrapper = getAst(generateId())), 241 | fwd("uuid4", ntLitString, wrapper = getAst(generateUuid4())), 242 | fwd("base64", ntLitObject, wrapper = getAst(genBase64())) 243 | ] 244 | 245 | let 246 | # std/math 247 | # implements basic math functions 248 | fnMath = @[ 249 | fwd("ceil", ntLitFloat, [(ntLitFloat, "x")]), 250 | fwd("floor", ntLitFloat, [(ntLitFloat, "x")]), 251 | fwd("max", ntLitInt, [(ntLitInt, "x"), (ntLitInt, "y")], src = "system"), 252 | fwd("min", ntLitInt, [(ntLitInt, "x"), (ntLitInt, "y")], src = "system"), 253 | fwd("round", ntLitFloat, [(ntLitFloat, "x")]), 254 | ] 255 | # std/strings 256 | # implements common functions for working with strings 257 | # https://nim-lang.github.io/Nim/strutils.html 258 | 259 | template strRegexFind = 260 | let arrayNode = ast.newNode(ntLitArray) 261 | for res in re.findAll(args[0].value.sVal, re(args[1].value.sVal)): 262 | let strNode = ast.newNode(ntLitString) 263 | strNode.sVal = res 264 | add arrayNode.arrayItems, strNode 265 | arrayNode 266 | 267 | template strRegexMatch = 268 | let boolNode = ast.newNode(ntLitBool) 269 | boolNode.bVal = re.match(args[0].value.sVal, re(args[1].value.sVal)) 270 | boolNode 271 | 272 | template strStartsWithStream = 273 | let boolNode = ast.newNode(ntLitBool) 274 | if args[1].value.streamContent.kind == JString: 275 | boolNode.bVal = strutils.startsWith(args[0].value.sVal, args[1].value.streamContent.str) 276 | boolNode 277 | 278 | template strStreamStartsWith = 279 | let boolNode = ast.newNode(ntLitBool) 280 | if args[0].value.streamContent.kind == JString: 281 | boolNode.bVal = strutils.startsWith(args[0].value.streamContent.str, args[1].value.sVal) 282 | boolNode 283 | 284 | let 285 | fnStrings = @[ 286 | fwd("endsWith", ntLitBool, [(ntLitString, "s"), (ntLitString, "suffix")]), 287 | fwd("startsWith", ntLitBool, [(ntLitString, "s"), (ntLitString, "prefix")]), 288 | fwd("startsWith", ntLitBool, [(ntLitString, "s"), (ntStream, "prefix")], wrapper = getAst(strStartsWithStream())), 289 | fwd("startsWith", ntLitBool, [(ntStream, "s"), (ntLitString, "prefix")], wrapper = getAst(strStreamStartsWith())), 290 | fwd("capitalizeAscii", ntLitString, [(ntLitString, "s")], "capitalize"), 291 | fwd("replace", ntLitString, [(ntLitString, "s"), (ntLitString, "sub"), (ntLitString, "by")]), 292 | fwd("toLowerAscii", ntLitString, [(ntLitString, "s")], "toLower"), 293 | fwd("toUpperAscii", ntLitString, [(ntLitString, "s")], "toUpper"), 294 | fwd("contains", ntLitBool, [(ntLitString, "s"), (ntLitString, "sub")]), 295 | fwd("parseBool", ntLitBool, [(ntLitString, "s")]), 296 | fwd("parseInt", ntLitInt, [(ntLitString, "s")]), 297 | fwd("parseFloat", ntLitFloat, [(ntLitString, "s")]), 298 | fwd("format", ntLitString, [(ntLitString, "s"), (ntLitArray, "a")], wrapper = getAst(formatWrapper())), 299 | fwd("find", ntLitArray, [(ntLitString, "s"), (ntLitString, "pattern")], wrapper = getAst(strRegexFind())), 300 | fwd("match", ntLitBool, [(ntLitString, "s"), (ntLitString, "pattern")], wrapper = getAst(strRegexMatch())) 301 | ] 302 | 303 | # std/arrays 304 | # implements common functions for working with arrays (sequences) 305 | # https://nim-lang.github.io/Nim/sequtils.html 306 | 307 | template arraysContains: untyped = 308 | ast.newBool(system.contains(toNimSeq(args[0].value), args[1].value.sVal)) 309 | 310 | template arraysAdd: untyped = 311 | add(args[0].value.arrayItems, args[1].value) 312 | 313 | template arraysShift: untyped = 314 | try: 315 | delete(args[0].value.arrayItems, 0) 316 | except IndexDefect as e: 317 | raise newException(ArraysModule, e.msg) 318 | 319 | template arraysPop: untyped = 320 | try: 321 | delete(args[0].value.arrayItems, 322 | args[0].value.arrayItems.high) 323 | except IndexDefect as e: 324 | raise newException(ArraysModule, e.msg) 325 | 326 | template arraysShuffle = 327 | randomize() 328 | shuffle(args[0].value.arrayItems) 329 | 330 | template arraysJoin = 331 | if args.len == 2: 332 | ast.newString(strutils.join( 333 | toNimSeq(args[0].value), args[1].value.sVal)) 334 | else: 335 | ast.newString(strutils.join(toNimSeq(args[0].value))) 336 | 337 | template arraysDelete = 338 | delete(args[0].value.arrayItems, args[1].value.iVal) 339 | 340 | template arraysFind = 341 | for i in 0..args[0].value.arrayItems.high: 342 | if args[0].value.arrayItems[i].sVal == args[1].value.sVal: 343 | return ast.newInteger(i) 344 | 345 | template arrayHigh = 346 | ast.newInteger(args[0].value.arrayItems.high) 347 | 348 | template arraySplit = 349 | let arr = ast.newArray() 350 | for x in strutils.split(args[0].value.sVal, args[1].value.sVal): 351 | add arr.arrayItems, ast.newString(x) 352 | arr 353 | 354 | template arrayCountdown = 355 | let arr = ast.newArray() 356 | for i in countdown(args[0].value.arrayItems.high, 0): 357 | add arr.arrayItems, args[0].value.arrayItems[i] 358 | arr 359 | 360 | let 361 | fnArrays = @[ 362 | fwd("contains", ntLitBool, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysContains()), 363 | fwd("add", ntLitVoid, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysAdd()), 364 | fwd("add", ntLitVoid, [(ntLitArray, "x"), (ntLitInt, "item")], wrapper = getAst arraysAdd()), 365 | fwd("add", ntLitVoid, [(ntLitArray, "x"), (ntLitBool, "item")], wrapper = getAst arraysAdd()), 366 | fwd("shift", ntLitVoid, [(ntLitArray, "x")], wrapper = getAst arraysShift()), 367 | fwd("pop", ntLitVoid, [(ntLitArray, "x")], wrapper = getAst arraysPop()), 368 | fwd("shuffle", ntLitVoid, [(ntLitArray, "x")], wrapper = getAst arraysShuffle()), 369 | fwd("join", ntLitString, [(ntLitArray, "x"), (ntLitString, "sep")], wrapper = getAst arraysJoin()), 370 | fwd("join", ntLitString, [(ntLitArray, "x")], wrapper = getAst arraysJoin()), 371 | fwd("delete", ntLitVoid, [(ntLitArray, "x"), (ntLitInt, "pos")], wrapper = getAst arraysDelete()), 372 | fwd("find", ntLitInt, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysFind()), 373 | fwd("high", ntLitInt, [(ntLitArray, "x")], wrapper = getAst(arrayHigh())), 374 | fwd("split", ntLitArray, [(ntLitString, "s"), (ntLitString, "sep")], wrapper = getAst(arraySplit())), 375 | fwd("countdown", ntLitArray, [(ntLitArray, "x")], wrapper = getAst(arrayCountdown())), 376 | ] 377 | 378 | template objectHasKey: untyped = 379 | ast.newBool(args[0].value.objectItems.hasKey(args[1].value.sVal)) 380 | 381 | template objectAddValue: untyped = 382 | args[0].value.objectItems[args[1].value.sVal] = args[2].value 383 | 384 | template objectDeleteValue: untyped = 385 | if args[0].value.objectItems.hasKey(args[1].value.sVal): 386 | args[0].value.objectItems.del(args[1].value.sVal) 387 | 388 | template objectClearValues: untyped = 389 | args[0].value.objectItems.clear() 390 | 391 | template objectLength: untyped = 392 | ast.newInteger(args[0].value.objectItems.len) 393 | 394 | template objectGetOrDefault: untyped = 395 | getOrDefault(args[0].value.objectItems, args[1].value.sVal) 396 | 397 | proc convertObjectCss(node: Node, isNested = false): string = 398 | var x: seq[string] 399 | for k, v in node.objectItems: 400 | case v.nt: 401 | of ntLitInt: 402 | add x, k & ":" 403 | add x[^1], $(v.iVal) 404 | of ntLitFloat: 405 | add x, k & ":" 406 | add x[^1], $(v.fVal) 407 | of ntLitString: 408 | add x, k & ":" 409 | add x[^1], v.sVal 410 | of ntLitObject: 411 | if isNested: 412 | raise newException(ObjectsModule, "Cannot converted nested objects to CSS") 413 | add x, k & "{" 414 | add x[^1], convertObjectCss(v, true) 415 | add x[^1], "}" 416 | else: discard 417 | result = x.join(";") 418 | 419 | template objectInlineCss: untyped = 420 | ast.newString(convertObjectCss(args[0].value)) 421 | 422 | let 423 | fnObjects = @[ 424 | fwd("hasKey", ntLitBool, [(ntLitObject, "x"), (ntLitString, "key")], wrapper = getAst(objectHasKey())), 425 | fwd("add", ntLitVoid, [(ntLitObject, "x"), (ntLitString, "key"), (ntLitString, "value")], wrapper = getAst(objectAddValue())), 426 | fwd("add", ntLitVoid, [(ntLitObject, "x"), (ntLitString, "key"), (ntLitInt, "value")], wrapper = getAst(objectAddValue())), 427 | fwd("add", ntLitVoid, [(ntLitObject, "x"), (ntLitString, "key"), (ntLitFloat, "value")], wrapper = getAst(objectAddValue())), 428 | fwd("add", ntLitVoid, [(ntLitObject, "x"), (ntLitString, "key"), (ntLitBool, "value")], wrapper = getAst(objectAddValue())), 429 | fwd("del", ntLitVoid, [(ntLitObject, "x"), (ntLitString, "key")], wrapper = getAst(objectDeleteValue())), 430 | fwd("len", ntLitInt, [(ntLitObject, "x")], wrapper = getAst(objectLength())), 431 | fwd("clear", ntLitVoid, [(ntLitObject, "x")], wrapper = getAst(objectClearValues())), 432 | fwd("toCSS", ntLitString, [(ntLitObject, "x")], wrapper = getAst(objectInlineCss())), 433 | ] 434 | 435 | # std/os 436 | # implements some basic read-only operating system functions 437 | # https://nim-lang.org/docs/os.html 438 | template osWalkFiles: untyped = 439 | let x = toSeq(walkPattern(args[0].value.sVal)) 440 | var a = ast.newArray() 441 | a.arrayType = ntLitString 442 | a.arrayItems = 443 | x.map do: 444 | proc(xpath: string): Node = ast.newString(xpath) 445 | a 446 | let 447 | fnOs = @[ 448 | fwd("absolutePath", ntLitString, [(ntLitString, "path")]), 449 | fwd("dirExists", ntLitBool, [(ntLitString, "path")]), 450 | fwd("fileExists", ntLitBool, [(ntLitString, "path")]), 451 | fwd("normalizedPath", ntLitString, [(ntLitString, "path")], "normalize"), 452 | # fwd("splitFile", ntTuple, [ntLitString]), 453 | fwd("extractFilename", ntLitString, [(ntLitString, "path")], "getFilename"), 454 | fwd("isAbsolute", ntLitBool, [(ntLitString, "path")]), 455 | fwd("readFile", ntLitString, [(ntLitString, "path")], src="system"), 456 | fwd("isRelativeTo", ntLitBool, [(ntLitString, "path"), (ntLitString, "base")], "isRelative"), 457 | fwd("getCurrentDir", ntLitString), 458 | fwd("joinPath", ntLitString, [(ntLitString, "head"), (ntLitString, "tail")], "join"), 459 | fwd("parentDir", ntLitString, [(ntLitString, "path")]), 460 | fwd("walkFiles", ntLitArray, [(ntLitString, "path")], wrapper = getAst osWalkFiles()), 461 | ] 462 | 463 | # 464 | # std/url 465 | # https://treeform.github.io/urlly/urlly.html 466 | template urlParse = 467 | let address = 468 | if args[0].value.nt == ntLitString: 469 | args[0].value.sVal 470 | else: 471 | ast.toString(args[0].value.streamContent) 472 | let someUrl: Url = parseUrl(address) 473 | let paths = ast.newArray() 474 | for somePath in someUrl.paths: 475 | add paths.arrayItems, ast.newString(somePath) 476 | let objectResult = ast.newObject(newOrderedTable({ 477 | "scheme": ast.newString(someUrl.scheme), 478 | "username": ast.newString(someUrl.username), 479 | "password": ast.newString(someUrl.password), 480 | "hostname": ast.newString(someUrl.hostname), 481 | "port": ast.newString(someUrl.port), 482 | "fragment": ast.newString(someUrl.fragment), 483 | "paths": paths, 484 | "secured": ast.newBool(someUrl.scheme in ["https", "ftps"]) 485 | })) 486 | 487 | let queryTable = ast.newObject(ObjectStorage()) 488 | for query in someUrl.query: 489 | queryTable.objectItems[query[0]] = ast.newString(query[1]) 490 | objectResult.objectItems["query"] = queryTable 491 | objectResult 492 | 493 | let 494 | fnUrl = @[ 495 | fwd("parseUrl", ntLitObject, [(ntLitString, "s")], wrapper = getAst(urlParse())), 496 | fwd("parseUrl", ntLitObject, [(ntStream, "s")], wrapper = getAst(urlParse())), 497 | ] 498 | 499 | # 500 | # Times 501 | # 502 | # template timesParseDate = 503 | # let obj = ast.newNode(ntLitObject) 504 | # # obj.objectItems[""] 505 | 506 | # let 507 | # fnTimes = @[ 508 | # fwd("parseDate", ntLitObject, [(ntLitString, "input"), (ntLitString, "format")], wrapper = getAst(timesParseDate())]) 509 | # ] 510 | 511 | result = newStmtList() 512 | let libs = [ 513 | ("system", fnSystem, "system"), 514 | ("math", fnMath, "math"), 515 | ("strutils", fnStrings, "strings"), 516 | ("sequtils", fnArrays, "arrays"), 517 | ("objects", fnObjects, "objects"), 518 | ("os", fnOs, "os"), 519 | ("url", fnUrl, "url"), 520 | # ("times", fnTimes, "times"), 521 | ] 522 | for lib in libs: 523 | var sourceCode: string 524 | for fn in lib[1]: 525 | var 526 | lambda = nnkLambda.newTree(newEmptyNode(), newEmptyNode(), newEmptyNode()) 527 | params = newNimNode(nnkFormalParams) 528 | params.add( 529 | ident("Node"), 530 | nnkIdentDefs.newTree( 531 | ident("args"), 532 | nnkBracketExpr.newTree( 533 | ident("openarray"), 534 | ident("Arg") 535 | ), 536 | newEmptyNode() 537 | ), 538 | nnkIdentDefs.newTree( 539 | ident("returnType"), 540 | ident("NodeType"), 541 | ident(symbolName(fn.returns)) 542 | ) 543 | ) 544 | lambda.add(params) 545 | lambda.add(newEmptyNode()) 546 | lambda.add(newEmptyNode()) 547 | var valNode = 548 | case fn.returns: 549 | of ntLitBool: "newBool" 550 | of ntLitString: "newString" 551 | of ntLitInt: "newInteger" 552 | of ntLitFloat: "newFloat" 553 | of ntLitArray: "newArray" # todo implement toArray 554 | of ntLitObject: "newObject" # todo implement toObject 555 | else: "getVoidNode" 556 | var i = 0 557 | var fnIdent = 558 | if fn.alias.len != 0: fn.alias 559 | else: fn.id 560 | add sourceCode, 561 | registerFunction(fnIdent, fn.args, fn.returns) 562 | var callNode: NimNode 563 | var hashKey = getHashedIdent(fnIdent) 564 | if not fn.hasWrapper: 565 | var callableNode = 566 | if lib[0] != "system": 567 | if fn.src.len == 0: 568 | newCall(newDotExpr(ident(lib[0]), ident(fn.id))) 569 | else: 570 | newCall(newDotExpr(ident(fn.src), ident(fn.id))) 571 | else: 572 | if fn.src.len == 0: 573 | newCall(newDotExpr(ident("system"), ident(fn.id))) 574 | else: 575 | newCall(newDotExpr(ident(fn.src), ident(fn.id))) 576 | for arg in fn.args: 577 | hashKey = hashKey !& hashIdentity(arg[0].getDataType()) 578 | let fieldName = 579 | case arg[0] 580 | of ntLitBool: "bVal" 581 | of ntLitString: "sVal" 582 | of ntLitInt: "iVal" 583 | of ntLitFloat: "fVal" 584 | of ntLitArray: "arrayItems" 585 | of ntLitObject: "pairsVal" 586 | else: "None" 587 | if fieldName.len != 0: 588 | callableNode.add( 589 | newDotExpr( 590 | newDotExpr( 591 | nnkBracketExpr.newTree( 592 | ident("args"), 593 | newLit(i) 594 | ), 595 | ident("value") 596 | ), 597 | ident(fieldName) 598 | ) 599 | ) 600 | else: 601 | callableNode.add( 602 | newDotExpr( 603 | nnkBracketExpr.newTree(ident"args", newLit(i)), 604 | ident"value" 605 | ), 606 | ) 607 | inc i 608 | if fn.returns != ntLitVoid: 609 | callNode = newCall(ident(valNode), callableNode) 610 | else: 611 | callNode = 612 | nnkStmtList.newTree( 613 | callableNode, 614 | newCall(ident"getVoidNode") 615 | ) 616 | else: 617 | for arg in fn.args: 618 | hashKey = hashKey !& hashIdentity(arg[0].getDataType()) 619 | if fn.returns != ntLitVoid: 620 | callNode = fn.wrapper 621 | else: 622 | callNode = 623 | nnkStmtList.newTree( 624 | fn.wrapper, 625 | newCall(ident"getVoidNode") 626 | ) 627 | lambda.add(newStmtList(callNode)) 628 | # let fnName = fnIdent[0] & fnIdent[1..^1].toLowerAscii 629 | add result, 630 | newAssignment( 631 | nnkBracketExpr.newTree( 632 | ident(lib[0] & "Module"), 633 | newLit hashKey 634 | ), 635 | lambda 636 | ) 637 | add result, 638 | newAssignment( 639 | nnkBracketExpr.newTree( 640 | ident("stdlib"), 641 | newLit("std/" & lib[2]) 642 | ), 643 | nnkTupleConstr.newTree( 644 | ident(lib[0] & "Module"), 645 | newCall(ident("SourceCode"), newLit(sourceCode)) 646 | ) 647 | ) 648 | # when not defined release: 649 | # echo result.repr 650 | # echo "std/" & lib[2] 651 | # echo sourceCode 652 | 653 | proc initModuleSystem* = 654 | {.gcsafe.}: 655 | initStandardLibrary() 656 | 657 | proc exists*(lib: string): bool = 658 | ## Checks if `lib` exists in `stdlib` 659 | result = stdlib.hasKey(lib) 660 | 661 | proc std*(lib: string): (Module, SourceCode) {.raises: KeyError.} = 662 | ## Retrieves a module from `stdlib` 663 | result = stdlib[lib] 664 | 665 | proc call*(lib: string, hashKey: Hash, args: seq[Arg]): Node = 666 | ## Retrieves a Nim proc from `module` 667 | # let key = fnName[0] & fnName[1..^1].toLowerAscii 668 | result = stdlib[lib][0][hashKey](args) 669 | -------------------------------------------------------------------------------- /src/timpkg/engine/tokens.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | import std/oids 7 | import pkg/toktok 8 | 9 | handlers: 10 | proc handleDocBlock(lex: var Lexer, kind: TokenKind) = 11 | while true: 12 | case lex.buf[lex.bufpos] 13 | of '*': 14 | add lex 15 | if lex.current == '/': 16 | add lex 17 | break 18 | of NewLines: 19 | inc lex.lineNumber 20 | add lex 21 | of EndOfFile: break 22 | else: add lex 23 | lex.kind = kind 24 | 25 | proc handleInlineComment(lex: var Lexer, kind: TokenKind) = 26 | inc lex.bufpos 27 | while true: 28 | case lex.buf[lex.bufpos]: 29 | of NewLines, EndOfFile: break 30 | else: 31 | inc lex.bufpos 32 | lex.kind = kind 33 | 34 | proc handleVar(lex: var Lexer, kind: TokenKind) = 35 | lexReady lex 36 | inc lex.bufpos 37 | var isSafe: bool 38 | if lex.current == '$': 39 | isSafe = true 40 | inc lex.bufpos 41 | case lex.buf[lex.bufpos] 42 | of IdentStartChars: 43 | add lex 44 | while true: 45 | case lex.buf[lex.bufpos] 46 | of IdentChars: 47 | add lex 48 | of Whitespace, EndOfFile: 49 | break 50 | else: 51 | break 52 | else: discard 53 | if not isSafe: 54 | lex.kind = kind 55 | else: 56 | lex.kind = tkIdentVarSafe 57 | if lex.token.len > 255: 58 | lex.setError("Identifier name is longer than 255 characters") 59 | 60 | proc handleMagics(lex: var Lexer, kind: TokenKind) = 61 | template collectSnippet(tKind: TokenKind) = 62 | if tKind in {tkSnippetJson, tkSnippetJs, tkSnippetYaml, tkSnippetMarkdown}: 63 | # first, check if snippet has an 64 | # identifier prefixed by `#` tag 65 | if lex.buf[lex.bufpos] == '#': 66 | inc lex.bufpos 67 | var scriptName = "#" 68 | while true: 69 | case lex.buf[lex.bufpos]: 70 | of EndOfFile: 71 | lex.setError("EOF reached before closing @end") 72 | return 73 | of IdentStartChars: 74 | add scriptName, lex.buf[lex.bufpos] 75 | inc lex.bufpos 76 | while true: 77 | case lex.buf[lex.bufpos]: 78 | of IdentChars + {'-'}: 79 | add scriptName, lex.buf[lex.bufpos] 80 | inc lex.bufpos 81 | else: break 82 | else: break 83 | add lex.attr, scriptName 84 | while true: 85 | try: 86 | case lex.buf[lex.bufpos] 87 | of EndOfFile: 88 | lex.setError("EOF reached before closing @end") 89 | return 90 | of '@': 91 | if lex.next("end"): 92 | lex.kind = tKind 93 | lex.token = lex.token.unindent(pos + 2) 94 | inc lex.bufpos, 4 95 | break 96 | else: 97 | add lex 98 | of NewLines: 99 | add lex.token, "\n" 100 | lex.handleNewLine() 101 | else: 102 | if lex.buf[lex.bufpos] == '%' and lex.next("*"): 103 | case lex.buf[lex.bufpos + 2] 104 | of IdentStartChars: 105 | inc lex.bufpos, 2 106 | var attr = $(genOid()) & "_" 107 | add attr, lex.buf[lex.bufpos] 108 | inc lex.bufpos 109 | while true: 110 | case lex.buf[lex.bufpos] 111 | of IdentChars: 112 | add attr, lex.buf[lex.bufpos] 113 | inc lex.bufpos 114 | else: 115 | add lex.attr, attr 116 | add lex.token, "%*" & attr 117 | break 118 | else: discard 119 | else: add lex 120 | except: 121 | lex.bufpos = lex.handleRefillChar(lex.bufpos) 122 | lexReady lex 123 | if lex.next("json"): 124 | let pos = lex.getColNumber(lex.bufpos) 125 | inc lex.bufpos, 5 126 | collectSnippet(tkSnippetJson) 127 | elif lex.next("js"): 128 | let pos = lex.getColNumber(lex.bufpos) 129 | inc lex.bufpos, 3 130 | collectSnippet(tkSnippetJs) 131 | elif lex.next("do"): 132 | let pos = lex.getColNumber(lex.bufpos) 133 | inc lex.bufpos, 3 134 | collectSnippet(tkDo) 135 | elif lex.next("yaml"): 136 | let pos = lex.getColNumber(lex.bufpos) 137 | inc lex.bufpos, 5 138 | collectSnippet(tkSnippetYaml) 139 | elif lex.next("md"): 140 | let pos = lex.getColNumber(lex.bufpos) 141 | inc lex.bufpos, 3 142 | collectSnippet(tkSnippetMarkdown) 143 | elif lex.next("placeholder"): 144 | let pos = lex.getColNumber(lex.bufpos) 145 | inc lex.bufpos, 12 146 | lex.kind = tkPlaceholder 147 | lex.token = "@placeholder" 148 | elif lex.next("include"): 149 | lex.setToken tkInclude, 8 150 | elif lex.next("import"): 151 | lex.setToken tkImport, 7 152 | elif lex.next("view"): 153 | lex.setToken tkViewLoader, 5 154 | elif lex.next("client"): 155 | lex.setToken tkClient, 7 156 | elif lex.next("end"): 157 | lex.setToken tkEnd, 4 158 | else: 159 | lex.setToken tkAt, 1 160 | 161 | proc handleBackticks(lex: var Lexer, kind: TokenKind) = 162 | lex.startPos = lex.getColNumber(lex.bufpos) 163 | setLen(lex.token, 0) 164 | let lineno = lex.lineNumber 165 | inc lex.bufpos 166 | while true: 167 | case lex.buf[lex.bufpos] 168 | of '\\': 169 | lex.handleSpecial() 170 | if lex.hasError: return 171 | of '`': 172 | lex.kind = kind 173 | inc lex.bufpos 174 | break 175 | of NewLines: 176 | if lex.multiLineStr: 177 | inc lex.bufpos 178 | else: 179 | lex.setError("EOL reached before end of string") 180 | return 181 | of EndOfFile: 182 | lex.setError("EOF reached before end of string") 183 | return 184 | else: 185 | add lex.token, lex.buf[lex.bufpos] 186 | inc lex.bufpos 187 | if lex.multiLineStr: 188 | lex.lineNumber = lineno 189 | 190 | proc handleSingleQuoteStr(lex: var Lexer, kind: TokenKind) = 191 | setLen(lex.token, 0) 192 | inc lex.bufpos # ' 193 | while true: 194 | case lex.buf[lex.bufpos] 195 | of '\\': 196 | add lex.token, "\\" 197 | if lex.next("'"): 198 | add lex.token, '\'' 199 | inc lex.bufpos 200 | else: 201 | add lex.token, lex.buf[lex.bufpos] 202 | inc lex.bufpos 203 | of '\'': 204 | lex.kind = tkString # marks token kind as tkString 205 | inc lex.bufpos # ' 206 | break 207 | of NewLines: 208 | lex.setError("EOL reached before end of single-quote string") 209 | return 210 | of EndOfFile: 211 | lex.setError("EOF reached before end of single-qute string") 212 | else: 213 | add lex.token, lex.buf[lex.bufpos] 214 | inc lex.bufpos 215 | 216 | const toktokSettings = 217 | toktok.Settings( 218 | tkPrefix: "tk", 219 | lexerName: "Lexer", 220 | lexerTuple: "TokenTuple", 221 | lexerTokenKind: "TokenKind", 222 | tkModifier: defaultTokenModifier, 223 | useDefaultIdent: true, 224 | keepUnknown: true, 225 | keepChar: true, 226 | ) 227 | 228 | registerTokens toktokSettings: 229 | plus = '+' 230 | minus = '-' 231 | asterisk = '*' 232 | divide = '/': 233 | doc = tokenize(handleDocBlock, '*') 234 | comment = tokenize(handleInlineComment, '/') 235 | `mod` = '%' 236 | caret = '^' 237 | lc = '{' 238 | rc = '}' 239 | lp = '(' 240 | rp = ')' 241 | lb = '[' 242 | rb = ']' 243 | dot = '.' 244 | id = '#' 245 | ternary = '?' 246 | exc = '!': 247 | ne = '=' 248 | assign = '=': 249 | eq = '=' 250 | colon = ':' 251 | comma = ',' 252 | scolon = ';' 253 | gt = '>': 254 | gte = '=' 255 | lt = '<': 256 | lte = '=' 257 | amp = '&': 258 | andAnd = '&' 259 | pipe = '|': 260 | orOr = '|' 261 | backtick = tokenize(handleBackticks, '`') 262 | sqString = tokenize(handleSingleQuoteStr, '\'') 263 | `case` = "case" 264 | `of` = "of" 265 | `if` = "if" 266 | `elif` = "elif" 267 | `else` = "else" 268 | `and` = "and" 269 | `for` = "for" 270 | `while` = "while" 271 | `in` = "in" 272 | `or` = "or" 273 | `bool` = ["true", "false"] 274 | 275 | # literals 276 | litBool = "bool" 277 | litInt = "int" 278 | litString = "string" 279 | litFloat = "float" 280 | litObject = "object" 281 | litArray = "array" 282 | litFunction = "function" 283 | litStream = "stream" 284 | litVoid = "void" 285 | 286 | # magics 287 | at = tokenize(handleMagics, '@') 288 | `import` 289 | snippetJs 290 | snippetYaml 291 | snippetJson 292 | snippetMarkdown 293 | placeholder 294 | viewLoader 295 | client 296 | `end` 297 | `include` 298 | `do` = "do" 299 | fn = "fn" 300 | `func` = "func" # alias `fn` 301 | `block` = "block" 302 | component = "component" 303 | `var` = "var" 304 | `const` = "const" 305 | `type` = "type" 306 | returnCmd = "return" 307 | echoCmd = "echo" 308 | discardCmd = "discard" 309 | breakCmd = "break" 310 | continueCmd = "continue" 311 | assertCmd = "assert" 312 | identVar = tokenize(handleVar, '$') 313 | identVarSafe 314 | `static` = "static" 315 | -------------------------------------------------------------------------------- /src/timpkg/server/app.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL-v3 License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[os, asyncdispatch, strutils, 8 | sequtils, json, critbits, options] 9 | 10 | import pkg/[httpbeast, watchout, jsony] 11 | import pkg/importer/resolver 12 | import pkg/kapsis/cli 13 | 14 | import ./config 15 | import ../engine/[meta, parser, logging] 16 | import ../engine/compilers/[html, nimc] 17 | 18 | # 19 | # Tim Engine Setup 20 | # 21 | type 22 | CacheTable = CritBitTree[string] 23 | 24 | var Cache = CacheTable() 25 | const 26 | address = "tcp://127.0.0.1:5559" 27 | DOCKTYPE = "" 28 | defaultLayout = "base" 29 | 30 | template displayErrors(l: Logger) = 31 | for err in l.errors: 32 | display(err) 33 | display(l.filePath) 34 | 35 | proc transpileCode(engine: TimEngine, tpl: TimTemplate, 36 | config: TimConfig, refreshAst = false) = 37 | ## Transpile `tpl` TimTemplate to a specific target source 38 | var p: Parser = engine.newParser(tpl, refreshAst = refreshAst) 39 | if likely(not p.hasError): 40 | if tpl.jitEnabled(): 41 | # if marked as JIT will save the produced 42 | # binary AST on disk for runtime computation 43 | engine.writeAst(tpl, parser.getAst(p)) 44 | else: 45 | # otherwise, compiles AST to static HTML 46 | var c = html.newCompiler(engine, parser.getAst(p), tpl, 47 | engine.isMinified, engine.getIndentSize) 48 | if likely(not c.hasError): 49 | case tpl.getType: 50 | of ttView: 51 | engine.writeHtml(tpl, c.getHtml) 52 | of ttLayout: 53 | engine.writeHtml(tpl, c.getHead) 54 | else: discard 55 | else: displayErrors c.logger 56 | else: displayErrors p.logger 57 | 58 | proc resolveDependants(engine: TimEngine, 59 | deps: seq[string], config: TimConfig) = 60 | for path in deps: 61 | let tpl = engine.getTemplateByPath(path) 62 | case tpl.getType 63 | of ttPartial: 64 | echo tpl.getDeps.toSeq 65 | engine.resolveDependants(tpl.getDeps.toSeq, config) 66 | else: 67 | engine.transpileCode(tpl, config, true) 68 | 69 | proc precompile(engine: TimEngine, config: TimConfig, globals: JsonNode = newJObject()) = 70 | ## Pre-compiles available templates 71 | engine.setGlobalData(globals) 72 | engine.importsHandle = resolver.initResolver() 73 | if not config.compilation.release: 74 | proc onFound(file: watchout.File) = 75 | # Callback `onFound` 76 | # Runs when detecting a new template. 77 | let tpl: TimTemplate = 78 | engine.getTemplateByPath(file.getPath()) 79 | case tpl.getType 80 | of ttView, ttLayout: 81 | engine.transpileCode(tpl, config) 82 | else: discard 83 | 84 | proc onChange(file: watchout.File) = 85 | # Callback `onChange` 86 | # Runs when detecting changes 87 | let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) 88 | displayInfo("✨ Changes detected\n " & file.getName()) 89 | case tpl.getType() 90 | of ttView, ttLayout: 91 | engine.transpileCode(tpl, config) 92 | else: 93 | engine.resolveDependants(tpl.getDeps.toSeq, config) 94 | 95 | proc onDelete(file: watchout.File) = 96 | # Callback `onDelete` 97 | # Runs when deleting a file 98 | displayInfo("Deleted a template\n " & file.getName()) 99 | engine.clearTemplateByPath(file.getPath()) 100 | 101 | var watcher = 102 | newWatchout( 103 | @[engine.getSourcePath() / "*"], 104 | onChange, onFound, onDelete, 105 | recursive = true, 106 | ext = @[".timl"], 107 | delay = config.browser_sync.delay, 108 | browserSync = 109 | WatchoutBrowserSync( 110 | port: config.browser_sync.port, 111 | delay: config.browser_sync.delay 112 | ) 113 | ) 114 | # watch for file changes in a separate thread 115 | watcher.start() # config.target != tsHtml 116 | else: 117 | discard 118 | 119 | proc jitCompiler(engine: TimEngine, 120 | tpl: TimTemplate, data: JsonNode): HtmlCompiler = 121 | ## Compiles `tpl` AST at runtime 122 | html.newCompiler( 123 | engine, 124 | engine.readAst(tpl), 125 | tpl, 126 | engine.isMinified, 127 | engine.getIndentSize, 128 | data 129 | ) 130 | 131 | template layoutWrapper(getViewBlock) {.dirty.} = 132 | result = DOCKTYPE 133 | var layoutTail: string 134 | var hasError: bool 135 | if not layout.jitEnabled: 136 | # when requested layout is pre-rendered 137 | # will use the static HTML version from disk 138 | add result, layout.getHtml() 139 | getViewBlock 140 | layoutTail = layout.getTail() 141 | else: 142 | var jitLayout = engine.jitCompiler(layout, data) 143 | if likely(not jitLayout.hasError): 144 | add result, jitLayout.getHead() 145 | getViewBlock 146 | layoutTail = jitLayout.getTail() 147 | else: 148 | hasError = true 149 | jitLayout.logger.displayErrors() 150 | add result, layoutTail 151 | 152 | proc render*(engine: TimEngine, viewName: string, layoutName = "base", local = newJObject()): string = 153 | # Renders a `viewName` 154 | if likely(engine.hasView(viewName)): 155 | var 156 | view: TimTemplate = engine.getView(viewName) 157 | data: JsonNode = newJObject() 158 | data["local"] = local 159 | if likely(engine.hasLayout(layoutName)): 160 | var layout: TimTemplate = engine.getLayout(layoutName) 161 | if not view.jitEnabled: 162 | # render a pre-compiled HTML 163 | layoutWrapper: 164 | add result, indent(view.getHtml(), layout.getViewIndent) 165 | else: 166 | # compile and render template at runtime 167 | layoutWrapper: 168 | var jitView = engine.jitCompiler(view, data) 169 | if likely(not jitView.hasError): 170 | add result, indent(jitView.getHtml(), layout.getViewIndent) 171 | else: 172 | jitView.logger.displayErrors() 173 | hasError = true 174 | else: 175 | raise newException(TimError, "View not found") 176 | 177 | # 178 | # Tim Engine - Server handle 179 | # 180 | from std/httpcore import HttpCode 181 | proc startServer(engine: TimEngine) = 182 | proc onRequest(req: Request): Future[void] = 183 | {.gcsafe.}: 184 | req.send(200.HttpCode, engine.render("index"), "Content-Type: text/html") 185 | 186 | httpbeast.run(onRequest, initSettings(numThreads = 1)) 187 | 188 | proc run*(engine: var TimEngine, config: TimConfig) = 189 | ## Tim can serve the HTTP service with TCP or Unix socket. 190 | ## 191 | ## **Note** By default, Unix socket would only be available to same user. 192 | ## If you want access it from Nginx, you need to loosen permissions. 193 | displayInfo("Preloading templates...") 194 | let globals = %*{} # todo 195 | engine.precompile(config, globals) 196 | displayInfo("Tim Engine Server is up & running") 197 | engine.startServer() -------------------------------------------------------------------------------- /src/timpkg/server/config.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL-v3 License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | from std/net import Port, `$` 7 | import pkg/[nyml, semver] 8 | 9 | export `$` 10 | 11 | type 12 | TargetSource* = enum 13 | tsNim = "nim" 14 | tsJS = "js" 15 | tsHtml = "html" 16 | tsRuby = "rb" 17 | tsPython = "py" 18 | 19 | BrowserSync* = ref object 20 | port*: Port 21 | delay*: uint # ms todo use jsony hooks + std/times 22 | 23 | ConfigType* = enum 24 | typeProject = "project" 25 | typePackage = "package" 26 | 27 | Requirement* = object 28 | id: string 29 | version: Version 30 | 31 | PolicyName* = enum 32 | policyAny = "any" 33 | policyStdlib = "stdlib" 34 | policyPackages = "packages" 35 | policyImports = "imports" 36 | policyLoops = "loops" 37 | policyConditionals = "conditionals" 38 | policyAssignments = "assignments" 39 | 40 | CompilationPolicy* = object 41 | allow: set[PolicyName] 42 | 43 | CompilationSettings* = object 44 | target*: TargetSource 45 | source*, output*: string 46 | policy*: CompilationPolicy 47 | release*: bool 48 | 49 | TimConfig* = ref object 50 | name*: string 51 | version*: string 52 | license*, description*: string 53 | requires*: seq[string] 54 | case `type`*: ConfigType 55 | of typeProject: 56 | compilation*: CompilationSettings 57 | else: discard 58 | browser_sync*: BrowserSync 59 | 60 | proc `$`*(c: TimConfig): string = 61 | jsony.toJson(c) 62 | 63 | # when isMainModule: 64 | # const sample = """ 65 | # name: "bootstrap" 66 | # type: package 67 | # version: 0.1.0 68 | # author: OpenPeeps 69 | # license: MIT 70 | # description: "Bootstrap v5.x components for Tim Engine" 71 | # git: "https://github.com/openpeeps/bootstrap.timl" 72 | 73 | # requires: 74 | # - tim >= 0.1.4 75 | # """ 76 | # echo fromYaml(sample, TimConfig) 77 | -------------------------------------------------------------------------------- /src/timpkg/server/dynloader.nim: -------------------------------------------------------------------------------- 1 | # A super fast template engine for cool kids 2 | # 3 | # (c) 2024 George Lemon | LGPL-v3 License 4 | # Made by Humans from OpenPeeps 5 | # https://github.com/openpeeps/tim 6 | 7 | import std/[tables, dynlib, json] 8 | type 9 | DynamicTemplate = object 10 | name: string 11 | lib: LibHandle 12 | function: Renderer 13 | Renderer = proc(app, this: JsonNode = newJObject()): string {.gcsafe, stdcall.} 14 | DynamicTemplates* = ref object 15 | templates: OrderedTableRef[string, DynamicTemplate] = newOrderedTable[string, Dynamictemplate]() 16 | 17 | when defined macosx: 18 | const ext = ".dylib" 19 | elif defined windows: 20 | const ext = ".dll" 21 | else: 22 | const ext = ".so" 23 | 24 | proc load*(collection: DynamicTemplates, t: string) = 25 | ## Load a dynamic template 26 | var tpl = DynamicTemplate(lib: loadLib(t & ext)) 27 | tpl.function = cast[Renderer](tpl.lib.symAddr("renderTemplate")) 28 | collection.templates[t] = tpl 29 | 30 | proc reload*(collection: DynamicTemplates, t: string) = 31 | ## Reload a dynamic template 32 | discard 33 | 34 | proc unload*(collection: DynamicTemplates, t: string) = 35 | ## Unload a dynamic template 36 | dynlib.unloadLib(collection.templates[t].lib) 37 | reset(collection.templates[t]) 38 | collection.templates.del(t) 39 | 40 | proc render*(collection: DynamicTemplates, t: string): string = 41 | ## Render a dynamic template 42 | if likely(collection.templates.hasKey(t)): 43 | return collection.templates[t].function(this = %*{"x": "ola!"}) 44 | -------------------------------------------------------------------------------- /tests/app/storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpeeps/tim/f80ddb6e3fcb08ea467939b60d494dd3df75305a/tests/app/storage/.gitkeep -------------------------------------------------------------------------------- /tests/app/templates/layouts/base.timl: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | meta charset="utf-8" 4 | meta name="viewport" content="width=device-width, initial-scale=1" 5 | title: "Tim Engine is Awesome!" 6 | body 7 | @view -------------------------------------------------------------------------------- /tests/app/templates/partials/btn.timl: -------------------------------------------------------------------------------- 1 | button.btn: "Click me!" -------------------------------------------------------------------------------- /tests/app/templates/views/index.timl: -------------------------------------------------------------------------------- 1 | div.container > div.row > div.col-12 2 | h1: "Hello, Hello, Hello!" 3 | p: "It's me, The Red Guy!" 4 | @include "btn" 5 | 6 | fn say(guessWho: string): string = 7 | return "Hello, " & $guessWho & " is here!" 8 | 9 | echo "✨ " & say("The Red Guy") & " ✨" 10 | 11 | // passing data form Tim to JavaScript 12 | var x = "Hello from JavaScript" 13 | @js 14 | console.log("%*x") 15 | @end 16 | 17 | // using `client` block tells Tim to transpile the 18 | // given timl code to JavaScript for client-side rendering 19 | // use `do` block to insert additional 20 | // js code after `client` block 21 | @client target="div.container" 22 | button.btn: "Hello" 23 | @do 24 | el0.addEventListener('click', (e) => console.log(e.currentTarget)) 25 | @end -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | switch("deepcopy", "on") -------------------------------------------------------------------------------- /tests/snippets/cli_data.timl: -------------------------------------------------------------------------------- 1 | echo $app 2 | echo $this 3 | 4 | for $it in $app.items: 5 | li > span: $it -------------------------------------------------------------------------------- /tests/snippets/html.timl: -------------------------------------------------------------------------------- 1 | const stream = json("./data/sample.json") 2 | 3 | echo $stream[0].payload.commits[0].distinct 4 | if $stream[0].payload.commits[0].distinct == true: 5 | echo "?" 6 | 7 | if $this.asdassa: 8 | echo "???" -------------------------------------------------------------------------------- /tests/snippets/invalid.timl: -------------------------------------------------------------------------------- 1 | const x = 123 2 | h1: $x 3 | $x = 321 // invalid -------------------------------------------------------------------------------- /tests/snippets/loops.timl: -------------------------------------------------------------------------------- 1 | // todo -------------------------------------------------------------------------------- /tests/snippets/std_arrays.timl: -------------------------------------------------------------------------------- 1 | @import "std/arrays" 2 | 3 | var x = ["one", "two", "three"] 4 | assert $x.contains("two") 5 | assert $x.contains("four") == false 6 | $x.add("four") 7 | assert $x.contains("four") -------------------------------------------------------------------------------- /tests/snippets/std_objects.timl: -------------------------------------------------------------------------------- 1 | @import "std/objects" 2 | @import "std/strings" 3 | 4 | var x = { 5 | name: "Steven S. Hughes", 6 | address: "3703 Snyder Avenue Charlotte, NC 28208", 7 | birthday: "07-10-1956", 8 | } 9 | 10 | assert $x.hasKey("name") 11 | assert $x.hasKey("address") 12 | assert $x.hasKey("birthday") 13 | assert $x.hasKey("age") == false 14 | 15 | // copy an object by assignment 16 | var x2 = $x 17 | $x["age"] = 48 18 | assert $x.hasKey("age") 19 | assert $x2.hasKey("age") == false 20 | 21 | var say = { 22 | getHello: 23 | fn(x: string): string { 24 | return toUpper($x & " World") 25 | } 26 | } 27 | 28 | assert $say.getHello("Yellow") == "YELLOW WORLD" -------------------------------------------------------------------------------- /tests/snippets/std_strings.timl: -------------------------------------------------------------------------------- 1 | @import "std/strings" 2 | var x = "Tim is Awesome" 3 | 4 | assert $x.toUpper == "TIM IS AWESOME" 5 | assert $x.toLower == "tim is awesome" 6 | assert $x.startsWith("Tim") 7 | assert $x.endsWith("some") 8 | assert $x.replace("Awesome", "Great!") == "Tim is Great!" 9 | assert $x.contains("Awesome") 10 | 11 | assert parseInt("100") == 100 12 | assert parseFloat("100.99") == 100.99 13 | assert parseBool("true") 14 | assert parseBool("false") == false 15 | assert format("$1 is $2", ["Tim", "Awesome"]) == $x -------------------------------------------------------------------------------- /tests/test1.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, os, htmlparser, xmltree, strtabs, sequtils] 2 | import ../src/tim 3 | 4 | var t = newTim("./app/templates", "./app/storage", 5 | currentSourcePath(), minify = false, indent = 2) 6 | 7 | test "precompile": 8 | t.precompile(flush = true, waitThread = false) 9 | 10 | test "render index": 11 | echo t.render("index") 12 | 13 | test "check layout": 14 | let html = t.render("index").parseHtml 15 | # check `meta` tags 16 | let meta = html.findAll("meta").toSeq 17 | check meta.len == 2 18 | check meta[0].attrs["charset"] == "utf-8" 19 | 20 | check meta[1].attrsLen == 2 21 | check meta[1].attrs.hasKey("name") 22 | check meta[1].attrs.hasKey("content") 23 | 24 | let title = html.findAll("title").toSeq 25 | check title.len == 1 26 | check title[0].innerText == "Tim Engine is Awesome!" 27 | 28 | import std/sequtils 29 | import ../src/timpkg/engine/[logging, parser, compilers/html] 30 | 31 | proc toHtml(id, code: string): (Parser, HtmlCompiler) = 32 | result[0] = parseSnippet(id, code) 33 | result[1] = newCompiler(result[0].getAst, false) 34 | 35 | proc load(x: string): string = 36 | readFile(currentSourcePath().parentDir / "snippets" / x & ".timl") 37 | 38 | test "assignment var": 39 | const code = """ 40 | var a = 123 41 | h1: $a 42 | var b = {} 43 | """ 44 | let x = toHtml("test_var", code) 45 | check x[0].hasErrors == false 46 | check x[1].hasErrors == false 47 | 48 | test "invalid timl code": 49 | let x = toHtml("invalid", load("invalid")) 50 | check x[0].hasErrors == false 51 | check x[1].hasErrors == true 52 | 53 | test "conditions if": 54 | let code = """ 55 | if 0 == 0: 56 | span: "looks true to me"""" 57 | assert tim.toHtml("test_if", code) == 58 | """looks true to me""" 59 | 60 | test "conditions if/else": 61 | let code = """ 62 | if 1 != 1: 63 | span: "looks true to me" 64 | else: 65 | span.just-some-basic-stuff: "this is basic"""" 66 | assert tim.toHtml("test_if", code) == 67 | """this is basic""" 68 | 69 | test "conditions if/elif": 70 | let code = """ 71 | if 1 != 1: 72 | span: "looks true to me" 73 | elif 1 == 1: 74 | span.just-some-basic-stuff: "this is basic"""" 75 | assert tim.toHtml("test_if", code) == 76 | """this is basic""" 77 | 78 | test "conditions if/elif/else": 79 | let code = """ 80 | if 1 != 1: 81 | span: "looks true to me" 82 | elif 1 > 1: 83 | span.just-some-basic-stuff: "this is basic" 84 | else: 85 | span.none 86 | """ 87 | assert tim.toHtml("test_if", code) == 88 | """""" 89 | 90 | test "loops for": 91 | let code = """ 92 | var fruits = ["satsuma", "watermelon", "orange"] 93 | for $fruit in $fruits: 94 | span data-fruit=$fruit: $fruit 95 | """ 96 | assert tim.toHtml("test_loops", code) == 97 | """satsumawatermelonorange""" 98 | 99 | test "loops for + nested elements": 100 | let code = """ 101 | section#main > div.my-4 > ul.text-center 102 | for $x in ["barberbeats", "vaporwave", "aesthetic"]: 103 | li.d-block > span.fw-bold: $x""" 104 | assert tim.toHtml("test_loops_nested", code) == 105 | """
  • barberbeats
  • vaporwave
  • aesthetic
""" 106 | 107 | test "loops for in range": 108 | let code = """ 109 | for $i in 0..4: 110 | i: $i""" 111 | assert tim.toHtml("for_inrange", code) == 112 | """01234""" 113 | 114 | test "loops using * multiplier": 115 | let code = """ 116 | const items = ["keyboard", "speakers", "mug"] 117 | li * 3: $i + 1 & " - " & $items[$i]""" 118 | assert tim.toHtml("test_multiplier", code) == 119 | """
  • 1 - keyboard
  • 2 - speakers
  • 3 - mug
  • """ 120 | 121 | test "loops using * var multiplier": 122 | let code = """ 123 | const x = 3 124 | const items = ["keyboard", "speakers", "mug"] 125 | li * $x: $items[$i]""" 126 | assert tim.toHtml("test_multiplier", code) == 127 | """
  • keyboard
  • speakers
  • mug
  • """ 128 | 129 | test "loops while block + inc break": 130 | let code = """ 131 | var i = 0 132 | while true: 133 | if $i == 100: 134 | break 135 | inc($i) 136 | span: "Total: " & $i.toString""" 137 | assert tim.toHtml("test_while_inc", code) == 138 | """Total: 100""" 139 | 140 | test "loops while block + dec break": 141 | let code = """ 142 | var i = 100 143 | while true: 144 | if $i == 0: 145 | break 146 | dec($i) 147 | span: "Remained: " & $i.toString""" 148 | assert tim.toHtml("test_while_dec", code) == 149 | """Remained: 0""" 150 | 151 | test "loops while block + dec": 152 | let code = """ 153 | var i = 100 154 | while $i != 0: 155 | dec($i) 156 | span: "Remained: " & $i.toString""" 157 | assert tim.toHtml("test_while_dec", code) == 158 | """Remained: 0""" 159 | 160 | test "function return string": 161 | let code = """ 162 | fn hello(x: string): string = 163 | return $x 164 | h1: hello("Tim is awesome!") 165 | """ 166 | assert tim.toHtml("test_function", code) == 167 | """

    Tim is awesome!

    """ 168 | 169 | test "function return int": 170 | let code = """ 171 | fn hello(x: int): int = 172 | return $x + 10 173 | h1: hello(7) 174 | """ 175 | assert tim.toHtml("test_function", code) == 176 | """

    17

    """ 177 | 178 | test "objects anonymous function": 179 | let code = """ 180 | @import "std/strings" 181 | @import "std/os" 182 | 183 | var x = { 184 | getHello: 185 | fn(x: string): string { 186 | return toUpper($x & " World") 187 | } 188 | } 189 | h1: $x.getHello("Hello") 190 | """ 191 | assert tim.toHtml("anonymous_function", code) == 192 | """

    HELLO WORLD

    """ 193 | 194 | test "std/strings": 195 | let x = toHtml("std_strings", load("std_strings")) 196 | assert x[1].hasErrors == false 197 | 198 | test "std/arrays": 199 | let x = toHtml("std_arrays", load("std_arrays")) 200 | assert x[1].hasErrors == false 201 | 202 | test "std/objects": 203 | let x = toHtml("std_objects", load("std_objects")) 204 | assert x[1].hasErrors == false -------------------------------------------------------------------------------- /tim.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.3" 4 | author = "OpenPeeps" 5 | description = "A super fast template engine for cool kids!" 6 | license = "LGPLv3" 7 | srcDir = "src" 8 | skipDirs = @["example", "editors", "bindings"] 9 | installExt = @["nim"] 10 | bin = @["tim"] 11 | binDir = "bin" 12 | 13 | # Dependencies 14 | 15 | requires "nim >= 2.0.0" 16 | requires "toktok#head" 17 | requires "https://github.com/openpeeps/importer" 18 | # requires "importer#head" 19 | requires "watchout#head" 20 | requires "kapsis#head" 21 | requires "denim#head" 22 | requires "checksums" 23 | requires "jsony" 24 | requires "flatty#head" 25 | requires "nyml >= 0.1.8" 26 | # requires "marvdown#head" 27 | requires "urlly >= 1.1.1" 28 | requires "semver >= 1.2.2" 29 | requires "dotenv" 30 | requires "genny >= 0.1.0" 31 | requires "htmlparser" 32 | 33 | # Required for running Tim Engine as a 34 | # microservice frontend application 35 | requires "httpbeast#head" 36 | requires "libdatachannel" 37 | 38 | task node, "Build a NODE addon": 39 | exec "denim build src/tim.nim --cmake --yes" 40 | 41 | import std/os 42 | 43 | task examples, "build all examples": 44 | for e in walkDir(currentSourcePath().parentDir / "example"): 45 | let x = e.path.splitFile 46 | if x.name.startsWith("example_") and x.ext == ".nim": 47 | exec "nim c -d:timHotCode --threads:on -d:watchoutBrowserSync --deepcopy:on --mm:arc -o:./bin/" & x.name & " example/" & x.name & x.ext 48 | 49 | task example, "example httpbeast + tim": 50 | exec "nim c -d:timHotCode -d:watchoutBrowserSync --deepcopy:on --threads:on --mm:arc -o:./bin/example_httpbeast example/example_httpbeast.nim" 51 | 52 | task examplep, "example httpbeast + tim release": 53 | exec "nim c -d:timStaticBundle -d:release --threads:on --mm:arc -o:./bin/example_httpbeast example/example_httpbeast.nim" 54 | 55 | task dev, "build a dev cli": 56 | exec "nimble build -d:timStandalone" 57 | 58 | task prod, "build a prod cli": 59 | exec "nimble build -d:release -d:timStandalone" 60 | 61 | task staticlib, "Build Tim Engine as Static Library": 62 | exec "nimble c --app:staticlib -d:release" 63 | 64 | task swig, "Build C sources from Nim": 65 | exec "nimble --noMain --noLinking -d:timHotCode --threads:on -d:watchoutBrowserSync -d:timSwig --deepcopy:on --mm:arc --header:tim.h --nimcache:./bindings/_source cc -c src/tim.nim" --------------------------------------------------------------------------------