From 8b2dceb679c69718ce84d66e783a5a439f4ba628 Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:38:32 -0400 Subject: [PATCH] feat: Added mustache templating. --- TODOS.md | 5 +- config.odin | 1 + main.odin | 3 + mustache/.gitignore | 8 + mustache/.gitmodules | 4 + mustache/Justfile | 15 + mustache/LICENSE | 19 + mustache/README.md | 162 +++ mustache/examples/hello/hello.mustache | 11 + mustache/examples/hello/hello.odin | 28 + mustache/mustache.odin | 1723 +++++++++++++++++++++++ mustache/mustache_test.odin | 960 +++++++++++++ mustache/spec/Changes | 31 + mustache/spec/LICENSE | 20 + mustache/spec/README.md | 63 + mustache/spec/Rakefile | 36 + mustache/spec/TESTING.md | 45 + mustache/spec/specs/comments.json | 106 ++ mustache/spec/specs/comments.yml | 109 ++ mustache/spec/specs/delimiters.json | 132 ++ mustache/spec/specs/delimiters.yml | 158 +++ mustache/spec/specs/interpolation.json | 422 ++++++ mustache/spec/specs/interpolation.yml | 317 +++++ mustache/spec/specs/inverted.json | 227 +++ mustache/spec/specs/inverted.yml | 199 +++ mustache/spec/specs/partials.json | 153 ++ mustache/spec/specs/partials.yml | 116 ++ mustache/spec/specs/sections.json | 423 ++++++ mustache/spec/specs/sections.yml | 331 +++++ mustache/spec/specs/~dynamic-names.json | 316 +++++ mustache/spec/specs/~dynamic-names.yml | 377 +++++ mustache/spec/specs/~inheritance.json | 306 ++++ mustache/spec/specs/~inheritance.yml | 326 +++++ mustache/spec/specs/~lambdas.json | 222 +++ mustache/spec/specs/~lambdas.yml | 189 +++ mustache/test/data.json | 5 + mustache/test/layout.txt | 3 + mustache/test/template.txt | 1 + mustache_test.odin | 106 ++ render.odin | 323 ++--- 40 files changed, 7838 insertions(+), 163 deletions(-) create mode 100644 mustache/.gitignore create mode 100644 mustache/.gitmodules create mode 100644 mustache/Justfile create mode 100644 mustache/LICENSE create mode 100644 mustache/README.md create mode 100644 mustache/examples/hello/hello.mustache create mode 100644 mustache/examples/hello/hello.odin create mode 100644 mustache/mustache.odin create mode 100644 mustache/mustache_test.odin create mode 100644 mustache/spec/Changes create mode 100644 mustache/spec/LICENSE create mode 100644 mustache/spec/README.md create mode 100644 mustache/spec/Rakefile create mode 100644 mustache/spec/TESTING.md create mode 100644 mustache/spec/specs/comments.json create mode 100644 mustache/spec/specs/comments.yml create mode 100644 mustache/spec/specs/delimiters.json create mode 100644 mustache/spec/specs/delimiters.yml create mode 100644 mustache/spec/specs/interpolation.json create mode 100644 mustache/spec/specs/interpolation.yml create mode 100644 mustache/spec/specs/inverted.json create mode 100644 mustache/spec/specs/inverted.yml create mode 100644 mustache/spec/specs/partials.json create mode 100644 mustache/spec/specs/partials.yml create mode 100644 mustache/spec/specs/sections.json create mode 100644 mustache/spec/specs/sections.yml create mode 100644 mustache/spec/specs/~dynamic-names.json create mode 100644 mustache/spec/specs/~dynamic-names.yml create mode 100644 mustache/spec/specs/~inheritance.json create mode 100644 mustache/spec/specs/~inheritance.yml create mode 100644 mustache/spec/specs/~lambdas.json create mode 100644 mustache/spec/specs/~lambdas.yml create mode 100644 mustache/test/data.json create mode 100644 mustache/test/layout.txt create mode 100644 mustache/test/template.txt create mode 100644 mustache_test.odin diff --git a/TODOS.md b/TODOS.md index 5fc85f3..dfe8f9f 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,8 +1,4 @@ - [ ] add content-hash fingerprinting for tailwind cache busting. -- [ ] create template language - - Look at the source for 3 template languages that support mustache syntax, - and see how they implement the tempaltes, then pick the best one. One of them - should be Hugo / go. - [ ] Evaluate Tufte CSS — borrow sidenote CSS or replace TailwindCSS entirely - Option B: Steal Tufte's sidenote/margin-note CSS (adapt for dark theme), keep TailwindCSS - Option C: Full Tufte CSS — drop TailwindCSS, no build step, semantic HTML, customize for dark theme + Roboto @@ -16,3 +12,4 @@ - [ ] Content-hash fingerprinting for CSS and JS cache busting - [ ] OpenGraph meta tags - [ ] Search up for `thor.json` files. +- [ ] Partials inside sections still produce duplicate items — a fundamental issue with the mustache library's token handling. diff --git a/config.odin b/config.odin index 07e5597..b3363ee 100644 --- a/config.odin +++ b/config.odin @@ -17,6 +17,7 @@ Site_Config :: struct { base_url: string `args:"name=base-url"`, content_dir: string `args:"name=content"`, output_dir: string `args:"name=output"`, + layouts_dir: string, author: string, social: []Social_Link, drafts: bool `args:"name=drafts"`, diff --git a/main.odin b/main.odin index baa3665..48cef1c 100644 --- a/main.odin +++ b/main.odin @@ -33,6 +33,9 @@ main :: proc() { if config.output_dir == "" { config.output_dir = fmt.tprintf("%s/public", config_dir) } + if config.layouts_dir == "" { + config.layouts_dir = fmt.tprintf("%s/layouts", config_dir) + } if config.base_url == "" { config.base_url = "http://localhost:8080" } diff --git a/mustache/.gitignore b/mustache/.gitignore new file mode 100644 index 0000000..9a7cb7b --- /dev/null +++ b/mustache/.gitignore @@ -0,0 +1,8 @@ +*/**/.DS_Store +.DS_Store +tmp/ +bin/ +*/**/odin-mustache.dSYM/ +*/**/odin-mustache +*/**/odin-mustache-test.dSYM/ +*/**/odin-mustache-test diff --git a/mustache/.gitmodules b/mustache/.gitmodules new file mode 100644 index 0000000..e032690 --- /dev/null +++ b/mustache/.gitmodules @@ -0,0 +1,4 @@ +[submodule "spec"] + path = spec + url = git@github.com:mustache/spec.git + branch = v1.4.1 diff --git a/mustache/Justfile b/mustache/Justfile new file mode 100644 index 0000000..b045f40 --- /dev/null +++ b/mustache/Justfile @@ -0,0 +1,15 @@ +flags := "-vet -show-timings -strict-style -vet-cast -vet-tabs -vet-using-param -disallow-do -vet-semicolon" +name := "odin-mustache" + +build: + @mkdir -p bin + odin build . -out:bin/{{name}} -debug {{flags}} + +test: build + odin test . -out:bin/{{name}} + +run: build + bin/{{name}} test/template.txt test/data.json test/layout.txt + +check: + odin check . {{flags}} diff --git a/mustache/LICENSE b/mustache/LICENSE new file mode 100644 index 0000000..367a29a --- /dev/null +++ b/mustache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025 Benjamin Block + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mustache/README.md b/mustache/README.md new file mode 100644 index 0000000..335c3ea --- /dev/null +++ b/mustache/README.md @@ -0,0 +1,162 @@ +# odin-mustache +Native implementation of {{mustache}} templates in [Odin](https://odin-lang.org). + +https://github.com/benjamindblock/odin-mustache/assets/1155805/7d794861-e308-4132-aaa0-f800392208a4 + +All features are implemented, except for the ability to change delimiters. + +All in tests in the [official mustache spec](https://github.com/mustache/spec) pass successfully (except for the delimiters spec suite). + +## Documentation + +For more information about mustache, see the [mustache project page](https://mustache.github.io) or the mustache [man](https://mustache.github.io/mustache.5.html) [pages](https://mustache.github.io/mustache.1.html). + +View some [example mustache files](https://github.com/mustache/mustache/tree/master/examples) to get an overview. + +## Spec + +The [mustache-spec](https://github.com/mustache/spec) repo is added as a git submodule for testing purposes. To run tests, ensure that you run `git clone --recursive` and/or `git submodule update` as needed. + +## Usage + +### CLI Usage +``` +Usage: + odin-mustache [path to template] [path to JSON] [OPTIONAL - layout] + +Examples: + $ odin-mustache template.txt data.json +``` + +### Odin Usage +#### 1. `render(template: string, data: any, partials: any, allocator := context.allocator)` + +Renders a template, provided as a `string`. `data` and `partials` should be *either* a `map[string]...` or a `struct`. + +All `map` arguments passed **must** be keyed with `string` type data. When parsing a Mustache template, the text inside a tag (eg. `name` inside `{{name}}`) will be parsed as a `string`. + +#### 2. `render_from_filename(filename: string, data: any, partials: any, allocator := context.allocator)` + +Renders a template stored in a text file using `data` and `partials` provided. + +#### 3. `render_with_json(template: string, json_filename: string, allocator := context.allocator)` + +Renders a template `string` using data and partials stored inside a JSON file. `odin-mustache` will handle loading the JSON into a usable format for Mustache to work with. + +**NOTE**: The JSON file leverages the following top-level keys: +``` +"data": [required] +"partials": [optional] +``` + +#### 4. `render_from_filename_with_json(filename: string, json_filename: string, allocator := context.allocator)` + +Renders a template stored in a text file using data and partials stored inside a JSON file. `odin-mustache` will handle loading the JSON into a usable format for Mustache to work with. + +#### Example +```odin +input := "Hello, {{name}}!" +data: map[string]string = { + "name" = "St. Charles", +} +output, err := render(input, data, context.temp_allocator) +// => "Hello, St. Charles!" +``` + +## Escaping +`odin-mustache` follows the official mustache HTML escaping rules. That is, if you enclose a variable with two curly brackets, `{{var}}`, the contents are HTML-escaped. For instance, strings like `5 > 2` are converted to `5 > 2`. To use raw characters, use three curly brackets `{{{var}}}`. + +## Layouts +`odin-mustache` supports rendering templates with layouts. + +A layout is a regular template with a special function. It accepts **a `{{content}}` tag**. This is where the output of a child template will be inserted. Layouts have access to the same data provided to the regular template. + +Layouts can render content in both `{{normal}}` and `{{{literal}}}` tags. + +This is helpful for rendering scenarios like websites where the same elements (``, `