├── .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 |
3 | ⚡️ A high-performance template engine & markup language
4 | Fast • Compiled • Written in Nim 👑
5 |
6 |
7 |
8 | nimble install tim
/ npm install @openpeeps/tim
9 |
10 |
11 |
12 | API reference | Download
13 |
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 |
3 | ⚡️ A high-performance template engine & markup language
4 | Fast • Compiled • Written in Nim 👑
5 |
6 |
7 |
8 | npm install @openpeeps/tim
9 |
10 |
11 |
12 | API reference | Check Tim on GitHub
13 |
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 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.
© 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, ""
202 | add result, t
203 | 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 | """satsuma watermelon orange """
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 | """0 1 2 3 4 """
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"
--------------------------------------------------------------------------------