fix: Fixed an issue with stars not showing up.

This commit is contained in:
Spencer Brower
2026-07-17 17:45:49 -04:00
parent 6e66709f05
commit c0ff59513f
2 changed files with 143 additions and 2 deletions
+141
View File
@@ -0,0 +1,141 @@
# 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.
+2 -2
View File
@@ -14,7 +14,7 @@
- [ ] ensure sidenote numbers render in display order and not in declaration order. - [ ] ensure sidenote numbers render in display order and not in declaration order.
- [ ] OpenGraph meta tags — verify all fields match production site - [ ] OpenGraph meta tags — verify all fields match production site
- [ ] Add Opt-in deflist support. - [ ] Add Opt-in deflist support.
- [ ] We need to be able to do `Year_Section` in a non-magical, unpriviledged way. - [ ] We need to be able to do `Year_Section` in a non-magical, unprivileged way. See [PLAN.md](PLAN.md) for computed properties approach.
- [ ] set opengraph tags / description automatically if unset. (Like hugo does) - [ ] set opengraph tags / description automatically if unset. (Like hugo does)
- [ ] Table of contents support. - [ ] Table of contents support.
- [ ] prefixing every log message with `thor: ` is silly - [ ] prefixing every log message with `thor: ` is silly
@@ -46,7 +46,7 @@
- [ ] Markdown - [ ] Markdown
- [ ] Add overloads for every extension - accept ^strings.Builder. - [ ] Add overloads for every extension - accept ^strings.Builder.
- [ ] Add conventional (Hugo style) footnotes option. - [ ] Add conventional (Hugo style) footnotes option.
- [ ] Add header ids - [ ] Add heading ids as a default on extension.
- [ ] Review every file in thor - [ ] Review every file in thor
- [ ] Review assets.odin - [ ] Review assets.odin
- [ ] Review content.odin - [ ] Review content.odin