Compare commits

...

5 Commits

Author SHA1 Message Date
Spencer Brower 1824bae26c docs: Updated README.md 2026-07-28 13:14:30 -04:00
Spencer Brower 5627215d6f feat: Added site to the template context stack. 2026-07-28 13:07:14 -04:00
Spencer Brower 9d1954d70a feat: page prop now falls through (implicit access). 2026-07-28 12:04:54 -04:00
Spencer Brower fcf349eb51 fix: Removed content from Template_Context.
now accessible only through page.content.
2026-07-28 11:45:55 -04:00
Spencer Brower d51172c836 feat: Added page to template context. 2026-07-28 11:41:21 -04:00
12 changed files with 119 additions and 75 deletions
+3 -3
View File
@@ -220,7 +220,7 @@ Templates use Mustache with template inheritance (`{{<base}}` / `{{$block}}`):
<!-- page.html (content layout) -->
{{<base}}
{{$main}}
<main><article><h1>{{page_title}}</h1>{{&content}}</article></main>
<main><article><h1>{{page.title}}</h1>{{&content}}</article></main>
{{/main}}
{{/base}}
```
@@ -240,7 +240,7 @@ Base_Data :: struct {
}
Page_Data :: struct {
using base: Base_Data, // fields promoted via reflection fallback
page_title: string,
page.title: string,
date: string, // raw ISO 8601; formatted via `| format` in templates
}
Home_Data :: struct {
@@ -249,7 +249,7 @@ Home_Data :: struct {
}
Section_Data :: struct {
using base: Base_Data,
page_title: string,
page.title: string,
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
}
```
+4 -5
View File
@@ -1,8 +1,6 @@
# Thor
[TOC]
Thor is a simple Static Sire Generator designed for personal blogs and other small websites.
Thor is a simple Static Site Generator designed for personal blogs and other small websites.
Its core principals are simplicity and minimal configuration, so you can get started as quickly as possible.
@@ -16,12 +14,12 @@ It is based on Hugo, and gingerbill's SSG. Templating is done with (extended?) M
- Menus (WIP)
- Extended Markdown ([See below](#extended-markdown))
- Basic (whitespace) minification.
- Union File System (Modules)
## What it doesn't do
- Internationalization
- Pagination (Yet)
- Themes
- Union File System (Yet)
- Image Manipulation
- TailwindCSS integration
@@ -34,7 +32,8 @@ Then follow [The Guide]()
For a more complete setup, run `thor new site`.
## Extended Markdown
- Emoji expansion
- margin style footnotes
- Guthub style alerts
- Github style alerts
- [and more]
+14 -2
View File
@@ -2,12 +2,18 @@
- Polish existing features before moving on to new ones.
- [ ] Improve diagnostics
- [ ] Simplify / unify template context stack. Come up with a name for it.
- [ ] `render_template` should accept `Template_Context`, not `any`
- [x] Simplify / unify template context stack. Come up with a name for it.
- [x] `render_template` should accept `Template_Context`, not `any`
- [ ] Load grammars dynamically
- [ ] consider adding a limit to the context stack in mustache.
- [ ] better diagnostics for syntax errors in treesitter.
- [x] Add heading ids as a default on extension.
- [ ] show "stack traces" in template error diagnostics
- [ ] starred must be a param.
- [ ] Add a `#config(MAX_CONTEXT_DEPTH, 16?)` to `mustache`.
- [ ] menu system
- [ ] like Hugo's, but warn(/fail?) if menus are defined in the config *and* pages.
- i.e. force the user to choose one or the other.
## Performance
@@ -32,6 +38,11 @@
- [ ] can markdown extensions run in parallel?
- [ ] enforce MAX_SLUG_LENGTH
## Remove Privileged content
- [ ] `group_by` currently requires a computed `year` field on the page.
- We should replace this with `{{ pages | group_by (date | "2006") }}` or similar
## Memory Management
@@ -145,6 +156,7 @@ main :: proc () {
- [ ] `new site` set up new project
- [ ] warn/error when unknown key used in mustache.
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
- [ ] opt-in "strict_keys" mode. in this mode, key lookups may not view parent objects.
## Notes
View File
+13 -2
View File
@@ -7,6 +7,7 @@ import "core:fmt"
import "core:log"
import "core:os"
import "core:strings"
import "core:time"
// Fields with underscores should never be set by the user.
Page :: struct {
@@ -18,12 +19,13 @@ Page :: struct {
title: string,
description: string,
date: string,
year: string,
lastmod: string,
menu: string,
content: string,
og: Open_Graph,
draft: bool,
is_starred: bool,
starred: bool,
_is_index: bool `private`,
}
@@ -231,9 +233,18 @@ load_page :: proc(
page.title = fm.title
page.description = fm.description
page.date = fm.date
if page.date == "" {
info, stat_err := os.stat(file_path, context.allocator)
if stat_err == nil {
page.date, _ = time.time_to_rfc3339(info.modification_time, 0, false, context.allocator)
os.file_info_delete(info, context.allocator)
log.warnf("no date in frontmatter for %s, using file modification time: %s", file_path, page.date)
}
}
page.year = get_year(page.date)
page.lastmod = fm.lastmod
page.draft = fm.draft
page.is_starred = fm.isStarred
page.starred = fm.isStarred
page.menu = fm.menu
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
page.og = fm.og
+1 -1
View File
@@ -2,7 +2,7 @@
{{$main}}
<main>
<article>
<h1>{{page_title}}</h1>
<h1>{{page.title}}</h1>
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
{{/date_iso}} {{&content}}
</article>
+1 -1
View File
@@ -1,7 +1,7 @@
{{<base}}
{{$main}}
<main>
<h1>{{page_title}}</h1>
<h1>{{page.title}}</h1>
{{&content}}
{{#posts | group_by year}}
<section>
+28
View File
@@ -0,0 +1,28 @@
package main
import "core:fmt"
Ctx :: struct {
title: string,
using page: Page,
site: Site,
}
Page :: struct {
title: string,
}
Site :: struct {
title: string,
}
main :: proc() {
site := Ctx {
site = Site{title = "foo"},
page = Page{title = "bar"},
}
fmt.printfln("%+v", site)
fmt.printf("%+v", site)
}
+15
View File
@@ -1,5 +1,6 @@
package mustache
import "base:runtime"
import "core:fmt"
import "core:log"
import "core:reflect"
@@ -147,7 +148,21 @@ render :: proc(
ctx := make([dynamic]any, 0, 4, allocator)
defer delete(ctx)
// If data is a []any, expand into individual context frames.
// Otherwise, push as a single frame.
elem_info, count, slice_data := list_info(data)
if elem_info != nil {
if _, is_any := elem_info.variant.(runtime.Type_Info_Any); is_any {
for j in 0 ..< count {
append(&ctx, extract_list_element(elem_info, slice_data, j))
}
} else {
append(&ctx, data)
}
} else {
append(&ctx, data)
}
all_nodes := tmpl.nodes[:]
err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder)
+3 -3
View File
@@ -12,7 +12,7 @@ Inner :: struct {
Outer :: struct {
title: string,
page_title: string,
page.title: string,
inner: Inner,
numbers: [3]int,
}
@@ -150,8 +150,8 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
@(test)
test_suggest_correction_exact :: proc(t: ^testing.T) {
available := []string{"title", "page_title", "body"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "page_title")
available := []string{"title", "page.title", "body"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
}
@(test)
+19 -47
View File
@@ -2,7 +2,6 @@ package main
import "mustache"
import "core:encoding/json"
import "core:fmt"
import "core:log"
import "core:os"
@@ -11,43 +10,20 @@ import "core:time"
import "core:time/datetime"
Template_Context :: struct {
params: json.Value,
now: string,
content: string,
title: string,
description: string,
og: Open_Graph,
date_format: string,
timezone: ^datetime.TZ_Region,
// Page Data
page_title: string,
date: string,
og: Open_Graph,
site: Site_Context,
page: Page,
// Home data
pages: [dynamic]Page_Context,
pages: [dynamic]Page,
// Section Data
// TODO: Remove "posts" from the Odin code
posts: [dynamic]Page_Context,
}
Page_Context :: struct {
permalink: string,
title: string,
starred: bool,
date: string,
year: string,
}
build_page_context :: proc(page: Page) -> Page_Context {
return Page_Context {
permalink = page.permalink,
title = page.title,
starred = page.is_starred,
date = page.date,
year = get_year(page.date),
}
posts: [dynamic]Page,
}
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
@@ -125,10 +101,10 @@ capitalize :: proc(s: string) -> string {
render_template :: proc(
content_tpl: mustache.Template,
data: Template_Context,
ctx: Template_Context,
partials: map[string]mustache.Template,
) -> string {
result, err := mustache.render(content_tpl, data, partials)
result, err := mustache.render(content_tpl, []any{ctx.site, ctx.page, ctx}, partials)
if err != nil {
log.errorf(
"%s",
@@ -157,9 +133,8 @@ render_site :: proc(site: ^Site) {
assert(ok2)
ctx := Template_Context {
site = site.site_context,
now = now,
params = site.params,
description = site.description,
og = site.og,
date_format = site.date.format,
timezone = site.tz,
@@ -175,6 +150,7 @@ render_site :: proc(site: ^Site) {
break
}
}
ctx.page = home
// Collect sections
sections := make(map[string]bool)
@@ -268,9 +244,7 @@ render_page_html :: proc(
) -> string {
ctx := ctx
ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
ctx.page_title = page.title
ctx.content = page.content
ctx.date = page.date
ctx.page = page
ctx.og = og_for_page(site.og, page)
return render_template(content_tpl, ctx, partials)
}
@@ -282,18 +256,16 @@ render_home_html :: proc(
partials: map[string]mustache.Template,
ctx: Template_Context,
) -> string {
list_pages := make([dynamic]Page_Context)
defer delete(list_pages)
list_pages := make([dynamic]Page, 0, 8, context.temp_allocator)
for page in site.pages {
if page._is_index {
continue
}
append(&list_pages, build_page_context(page))
append(&list_pages, page)
}
ctx := ctx
ctx.title = site.title
ctx.content = home.content
ctx.pages = list_pages
ctx.og = og_for_page(site.og, home)
@@ -309,24 +281,24 @@ render_section :: proc(
partials: map[string]mustache.Template,
ctx: Template_Context,
) -> string {
posts := make([dynamic]Page_Context)
defer delete(posts)
posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator)
for page in site.pages {
if page.section != section || page._is_index {
continue
}
append(&posts, build_page_context(page))
append(&posts, page)
}
ctx := ctx
if has_index {
ctx.content = section_index.content
ctx.page_title = section_index.title
ctx.page = section_index
ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title)
ctx.og = og_for_page(site.og, section_index)
} else {
ctx.page_title = capitalize(section)
ctx.title = fmt.tprintf("%s | %s", capitalize(section), site.title)
ctx.page = Page {
title = capitalize(section),
}
ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title)
ctx.og.title = capitalize(section)
ctx.og.description = ""
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section)
+13 -6
View File
@@ -13,24 +13,31 @@ import "core:time/timezone"
import md "markdown"
// Site is the primary workhorse.
// Site_Context holds the site date that is accessible in templates.
Site_Context :: struct {
title: string,
description: string,
base_url: string,
params: json.Object,
og: Open_Graph,
}
// Site is the primary workhorse, containing everything needed to build the site,
// including an arena allocator, the pages, and all the various directories and
// enabled features.
Site :: struct {
using site_context: Site_Context,
arena: mem.Dynamic_Arena,
pages: #soa[dynamic]Page,
modules: [dynamic]string,
vfs: VFS,
title: string,
description: string,
base_url: string,
config_path: string,
content_dir: string,
assets_dir: string,
output_dir: string,
layouts_dir: string,
params: json.Object,
features: bit_set[Feature],
markdown_extensions: bit_set[md.Extension],
og: Open_Graph,
date: Date_Preferences,
tz: ^datetime.TZ_Region,
grammars: string,