mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
3.2 KiB
3.2 KiB
Mustache Extensions
Thor's mustache engine implements the Mustache spec plus the following non-standard extensions. Extensions are opt-in via template syntax — vanilla mustache templates render identically to the spec.
Pipes
A pipe expression appears inside a section tag and transforms the resolved value before iteration. Pipes let templates request different views of the same data without privileged Go-side support.
Syntax
{{#<key> | <op> <arg> <arg>... | <op> <arg>... }} … {{/<key>}}
- The first whitespace-separated token after
|is the op name; remaining tokens are its arguments. - Whitespace around
|is optional:posts|group_by yearandposts | group_by yearare equivalent. - Multiple filters compose left-to-right.
- At most 8 filters may appear in a single tag (compile-time constant
MAX_PIPESinpipes.odin). Exceeding it is a parse error. - At most 2 args per filter (compile-time constant
MAX_PIPE_ARGS). Exceeding it is a parse error. - The close tag is the bare key only. Pipe expressions are not allowed in close tags —
{{/posts | group_by year}}is a parse error. - Pipes are only supported in section tags (
{{#…}}and{{^…}}) today. Variable interpolation ({{x | op}}) is a parse error.
Example
{{#posts | group_by year}}
<section>
<h2>{{key}}</h2>
<ul>{{#items}}<li>{{title}}</li>{{/items}}</ul>
</section>
{{/posts}}
Available ops
group_by <field>
Buckets each element of a list by the value of <field>. Returns a list of Group values, each shaped as:
Group :: struct {
key: string, // the distinct field value
items: [dynamic]any, // elements sharing that value
}
Group order preserves first-appearance order in the input list (not key-sorted).
Errors (returned as Data_Error at render time):
- Argument count is not 1.
- Input is not a list.
- Any element is missing the named field.
- Any element has an empty value for the named field.
Memory ownership
- Parsed pipe filters (
Pipe_Filtervalues) are stored inline on eachNodevia[dynamic; MAX_PIPES]Pipe_Filter, andargsis inline on eachPipe_Filtervia[dynamic; MAX_PIPE_ARGS]string. Both use Odin's fixed-capacity dynamic array type, so no per-tag heap allocations occur at parse time. The storage dies with theTemplatewhendelete_templateis called. - Filter results (e.g. the
[dynamic]Groupreturned bygroup_by) are render-scoped allocations incontext.temp_allocator. They die with the render call. No caller-side cleanup is needed. - The string data inside
Pipe_Filter(op names, args) is borrowed from the template source — no cloning.
Future ops (not yet implemented)
The pipe framework is general; additional ops are straightforward to add to apply_filter in pipes.odin:
sort,sort_by <field>— orderingfilter <field> <value>,where <field>— selectiontake <n>,take_last <n>,skip <n>— slicingreverse— order flip
To add a new op:
- Implement
apply_<op>(value: any, args: []string) -> (any, Render_Error)inpipes.odin. - Add a
casetoapply_filter. - Add tests to
pipes_test.odin.