mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 19:33:32 -04:00
142 lines
4.2 KiB
Markdown
142 lines
4.2 KiB
Markdown
# Plan: Computed Properties in Mustache
|
|
|
|
## Problem
|
|
|
|
`Year_Section` is a privileged, hardcoded struct in render.odin. Every section listing gets pages grouped by year — no choice. The grouping dimension (year) is baked into the code. Templates can't request different views of the same data.
|
|
|
|
## Solution: Computed Properties
|
|
|
|
Struct fields that are procs get called during mustache data resolution. When `lookup_in` resolves a field via reflection and finds a proc, it calls it and uses the return value. This is computed properties (Vue/Ember pattern), not spec lambdas (text transformers).
|
|
|
|
No mustache syntax changes. `{{#posts.by_year}}` is vanilla mustache — the magic is in data resolution, not template syntax.
|
|
|
|
## Template Usage
|
|
|
|
```handlebars
|
|
{{#posts.by_year}}
|
|
<h2>{{year}}</h2>
|
|
{{#posts}}
|
|
<li>{{title}}</li>
|
|
{{/posts}}
|
|
{{/posts.by_year}}
|
|
|
|
{{#posts.all}}
|
|
<li>{{title}}</li>
|
|
{{/posts.all}}
|
|
```
|
|
|
|
Context resolution handles naming naturally — `posts` inside a Year_Group (the `.posts` field) shadows the outer `posts` view object.
|
|
|
|
## Data Model
|
|
|
|
```odin
|
|
Year_Group :: struct {
|
|
year: string,
|
|
posts: [dynamic]Page_Context,
|
|
}
|
|
|
|
Pages_View :: struct {
|
|
all: [dynamic]Page_Context,
|
|
by_year: proc() -> [dynamic]Year_Group,
|
|
}
|
|
|
|
Section_Data :: struct {
|
|
using base: Base_Data,
|
|
page_title: string,
|
|
posts: Pages_View,
|
|
}
|
|
```
|
|
|
|
`by_year` is a closure that captures `all` and groups lazily. Grouping only happens when the template requests it.
|
|
|
|
## Implementation Steps
|
|
|
|
### 1. Mustache data layer (`mustache/data.odin`)
|
|
|
|
In `lookup_in` (or `resolve_name`), after resolving a field value via reflection:
|
|
|
|
```odin
|
|
// If the resolved value is a proc, call it
|
|
if value, ok := resolved.(proc() -> any); ok {
|
|
return value()
|
|
}
|
|
```
|
|
|
|
Need to handle proc detection generically — Odin has many proc types (different signatures, calling conventions). The computed properties we use are all zero-argument procs returning `any`.
|
|
|
|
Alternative: check via `reflect.Type_Info` if the value is a proc type, then call it through `reflect.CallProcedure` or transmute.
|
|
|
|
### 2. Odin closure challenge
|
|
|
|
Contextless procs can't capture variables. Regular closure procs capture context by reference. We need the proc to access the page list when called later by mustache.
|
|
|
|
Options:
|
|
- Store the data alongside the proc in the struct (e.g., `all` field on `Pages_View`). The proc accesses it via the struct instance.
|
|
- Use `context.allocator` to store captured data that the closure references.
|
|
- Pass the struct as an implicit `self` parameter (method-style).
|
|
|
|
The simplest approach: the closure captures a pointer to the data, which is arena-allocated and alive during rendering.
|
|
|
|
### 3. Build `Pages_View` in `render_section`
|
|
|
|
```odin
|
|
all_pages := make([dynamic]Page_Context)
|
|
for page in site.pages {
|
|
if page.section != section || page._is_index { continue }
|
|
append(&all_pages, build_page_context(page))
|
|
}
|
|
|
|
view := Pages_View{
|
|
all = all_pages,
|
|
by_year = group_by_year_closure, // captures all_pages
|
|
}
|
|
```
|
|
|
|
### 4. Remove `Year_Section` and `year_sections`
|
|
|
|
- Delete `Year_Section` struct
|
|
- Remove `year_sections` from `Section_Data`
|
|
- Remove year grouping logic from `render_section`
|
|
- Delete `get_year` helper (or move it into the closure)
|
|
|
|
### 5. Update templates
|
|
|
|
`posts_index.html`:
|
|
```handlebars
|
|
{{#posts.by_year}}
|
|
<section>
|
|
<h2>{{year}}</h2>
|
|
<ul class="post-list">
|
|
{{#posts}} <li>...</li> {{/posts}}
|
|
</ul>
|
|
</section>
|
|
{{/posts.by_year}}
|
|
```
|
|
|
|
Home page could use the same pattern:
|
|
```handlebars
|
|
{{#pages.all}}
|
|
<li>...</li>
|
|
{{/pages.all}}
|
|
```
|
|
|
|
### 6. Tests
|
|
|
|
- Verify `by_year` proc is called lazily (only when template requests it)
|
|
- Verify year groups contain correct pages
|
|
- Verify flat list (`all`) works independently
|
|
|
|
## Prerequisite
|
|
|
|
**Step 1 (mustache data layer) must be done first.** This is the foundation — without proc-calling in data resolution, none of the rest works.
|
|
|
|
## Future Extensions
|
|
|
|
Once computed properties work, they're available everywhere:
|
|
- `posts.featured` — filter by starred
|
|
- `posts.recent(n)` — most recent N posts (if we support params)
|
|
- `site.tags` — all unique tags across posts
|
|
- `pages.by_section("posts")` — filter by section
|
|
|
|
These are all zero-argument procs on view structs. No mustache changes needed beyond step 1.
|