Compare commits

...

6 Commits

Author SHA1 Message Date
Spencer Brower 0abac89d88 feat: Warns the user if they exceed 16 context frames. 2026-07-28 16:58:46 -04:00
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
14 changed files with 356 additions and 224 deletions
+3 -3
View File
@@ -220,7 +220,7 @@ Templates use Mustache with template inheritance (`{{<base}}` / `{{$block}}`):
<!-- page.html (content layout) --> <!-- page.html (content layout) -->
{{<base}} {{<base}}
{{$main}} {{$main}}
<main><article><h1>{{page_title}}</h1>{{&content}}</article></main> <main><article><h1>{{page.title}}</h1>{{&content}}</article></main>
{{/main}} {{/main}}
{{/base}} {{/base}}
``` ```
@@ -240,7 +240,7 @@ Base_Data :: struct {
} }
Page_Data :: struct { Page_Data :: struct {
using base: Base_Data, // fields promoted via reflection fallback 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 date: string, // raw ISO 8601; formatted via `| format` in templates
} }
Home_Data :: struct { Home_Data :: struct {
@@ -249,7 +249,7 @@ Home_Data :: struct {
} }
Section_Data :: struct { Section_Data :: struct {
using base: Base_Data, using base: Base_Data,
page_title: string, page.title: string,
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
} }
``` ```
+8 -9
View File
@@ -1,8 +1,6 @@
# Thor # Thor
[TOC] Thor is a simple Static Site Generator designed for personal blogs and other small websites.
Thor is a simple Static Sire 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. Its core principals are simplicity and minimal configuration, so you can get started as quickly as possible.
@@ -16,14 +14,14 @@ It is based on Hugo, and gingerbill's SSG. Templating is done with (extended?) M
- Menus (WIP) - Menus (WIP)
- Extended Markdown ([See below](#extended-markdown)) - Extended Markdown ([See below](#extended-markdown))
- Basic (whitespace) minification. - Basic (whitespace) minification.
- Union File System (Modules)
## What it doesn't do ## What it doesn't do
- Internationalization - Internationalization
- Pagination (Yet) - Pagination (Yet)
- Themes - Themes
- Union File System (Yet) - Image Manipulation
- Image Manipulation - TailwindCSS integration
- TailwindCSS integration
## Getting Started ## Getting Started
@@ -34,7 +32,8 @@ Then follow [The Guide]()
For a more complete setup, run `thor new site`. For a more complete setup, run `thor new site`.
## Extended Markdown ## Extended Markdown
- Emoji expansion - Emoji expansion
- margin style footnotes - margin style footnotes
- Guthub style alerts - Github style alerts
- [and more] - [and more]
+23 -7
View File
@@ -2,12 +2,25 @@
- Polish existing features before moving on to new ones. - Polish existing features before moving on to new ones.
- [ ] Improve diagnostics - [ ] Improve diagnostics
- [ ] Simplify / unify template context stack. Come up with a name for it. - [ ] show "stack traces" in template error diagnostics
- [ ] `render_template` should accept `Template_Context`, not `any` - [ ] better diagnostics for syntax errors in treesitter.
- [ ] Ensure diagnostics for MAX_CONTEXT_DEPTH are good.
- [ ] show a proper diagnostic for timezones
- currently "unable to load timezone 'America/New_Yorkskie'"
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
- [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 - [ ] Load grammars dynamically
- [ ] consider adding a limit to the context stack in mustache. - [x] 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. - [x] Add heading ids as a default on extension.
- [ ] starred must be a param.
- [x] Add a `#config(MAX_CONTEXT_DEPTH, 16?)` to `mustache`.
- [ ] Documentation
- [ ] talk about the context stack (and its limit).
- [ ] 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.
- [ ] Don't show annoying log output in tests.
## Performance ## Performance
@@ -32,6 +45,11 @@
- [ ] can markdown extensions run in parallel? - [ ] can markdown extensions run in parallel?
- [ ] enforce MAX_SLUG_LENGTH - [ ] 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 ## Memory Management
@@ -58,9 +76,6 @@
- [ ] display an error when no part of the date appears in the output. - [ ] display an error when no part of the date appears in the output.
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2" - [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime? - [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
- [ ] show a proper diagnostic for timezones
- currently "unable to load timezone 'America/New_Yorkskie'"
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
## General ## General
- [ ] get rid of the global variables in the `treesitter` package. - [ ] get rid of the global variables in the `treesitter` package.
@@ -145,6 +160,7 @@ main :: proc () {
- [ ] `new site` set up new project - [ ] `new site` set up new project
- [ ] warn/error when unknown key used in mustache. - [ ] warn/error when unknown key used in mustache.
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc. - [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
- [ ] opt-in "strict_keys" mode. in this mode, key lookups may not view parent objects.
## Notes ## Notes
View File
+13 -2
View File
@@ -7,6 +7,7 @@ import "core:fmt"
import "core:log" import "core:log"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
import "core:time"
// Fields with underscores should never be set by the user. // Fields with underscores should never be set by the user.
Page :: struct { Page :: struct {
@@ -18,12 +19,13 @@ Page :: struct {
title: string, title: string,
description: string, description: string,
date: string, date: string,
year: string,
lastmod: string, lastmod: string,
menu: string, menu: string,
content: string, content: string,
og: Open_Graph, og: Open_Graph,
draft: bool, draft: bool,
is_starred: bool, starred: bool,
_is_index: bool `private`, _is_index: bool `private`,
} }
@@ -231,9 +233,18 @@ load_page :: proc(
page.title = fm.title page.title = fm.title
page.description = fm.description page.description = fm.description
page.date = fm.date 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.lastmod = fm.lastmod
page.draft = fm.draft page.draft = fm.draft
page.is_starred = fm.isStarred page.starred = fm.isStarred
page.menu = fm.menu page.menu = fm.menu
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index) page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
page.og = fm.og page.og = fm.og
+1 -1
View File
@@ -2,7 +2,7 @@
{{$main}} {{$main}}
<main> <main>
<article> <article>
<h1>{{page_title}}</h1> <h1>{{page.title}}</h1>
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time> {{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
{{/date_iso}} {{&content}} {{/date_iso}} {{&content}}
</article> </article>
+1 -1
View File
@@ -1,7 +1,7 @@
{{<base}} {{<base}}
{{$main}} {{$main}}
<main> <main>
<h1>{{page_title}}</h1> <h1>{{page.title}}</h1>
{{&content}} {{&content}}
{{#posts | group_by year}} {{#posts | group_by year}}
<section> <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)
}
+198 -138
View File
@@ -1,17 +1,28 @@
package mustache package mustache
import "base:runtime"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:reflect"
import "core:strings" import "core:strings"
// A hypothetical maximum context depth. Trying to pass more than this many items
// to render(tpl, data), or nesting templates further than this depth would be
// an error.
// May be enforced in a later version (for performance)
MAX_CONTEXT_DEPTH :: #config(MAX_CONTEXT_DEPTH, 16)
// Context_Stack is the growable stack of data frames walked top-to-bottom by
// resolve_name. Frames are pushed on section descent and popped on exit; the
// root data (or each element of a root []any) forms the base frames.
Context_Stack :: [dynamic]any
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Error types // Error types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Error_Kind :: enum { Error_Kind :: enum {
Syntax, // parse-time: malformed template Syntax, // parse-time: malformed template
Data, // render-time: template fine, data wrong (e.g. filter misuse) Data, // render-time: template fine, data wrong (e.g. filter misuse)
} }
Error_Body :: struct { Error_Body :: struct {
@@ -21,14 +32,18 @@ Error_Body :: struct {
} }
// Error is nil when no error occurred. // Error is nil when no error occurred.
Error :: union { Error_Body } Error :: union {
Error_Body,
}
// body unwraps the Error_Body from a non-nil Error. // body unwraps the Error_Body from a non-nil Error.
// Precondition: err != nil. // Precondition: err != nil.
body :: proc(err: Error) -> Error_Body { body :: proc(err: Error) -> Error_Body {
switch e in err { switch e in err {
case Error_Body: return e case Error_Body:
case: return {} return e
case:
return {}
} }
} }
@@ -142,12 +157,24 @@ render :: proc(
err: Error, err: Error,
) { ) {
builder: strings.Builder builder: strings.Builder
strings.builder_init(&builder, allocator) strings.builder_init(&builder, context.temp_allocator)
defer strings.builder_destroy(&builder)
ctx := make([dynamic]any, 0, 4, allocator) ctx := make(Context_Stack, 0, 4, context.temp_allocator)
defer delete(ctx)
append(&ctx, data) // 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[:] all_nodes := tmpl.nodes[:]
err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder) err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder)
@@ -272,14 +299,14 @@ parse_section :: proc(
case .Section_Close: case .Section_Close:
if strings.contains(tok.value, "|") { if strings.contains(tok.value, "|") {
return Error_Body { return Error_Body {
msg = fmt.tprintf( msg = fmt.tprintf(
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key", "pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
tok.value, tok.value,
), ),
pos = tok.pos, pos = tok.pos,
kind = .Syntax, kind = .Syntax,
} }
} }
if end_tag != "" && tok.value == end_tag { if end_tag != "" && tok.value == end_tag {
pos^ += 1 pos^ += 1
@@ -316,12 +343,7 @@ parse_section :: proc(
idx := len(nodes) idx := len(nodes)
append( append(
nodes, nodes,
Node { Node{kind = .Parent, key = tok.value, indent = tok.indent, pos = tok.pos},
kind = .Parent,
key = tok.value,
indent = tok.indent,
pos = tok.pos,
},
) )
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
nodes[idx].children = nodes[idx + 1:len(nodes)] nodes[idx].children = nodes[idx + 1:len(nodes)]
@@ -329,15 +351,7 @@ parse_section :: proc(
case .Block_Open: case .Block_Open:
pos^ += 1 pos^ += 1
idx := len(nodes) idx := len(nodes)
append( append(nodes, Node{kind = .Block, key = tok.value, indent = tok.indent, pos = tok.pos})
nodes,
Node {
kind = .Block,
key = tok.value,
indent = tok.indent,
pos = tok.pos,
},
)
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
nodes[idx].children = nodes[idx + 1:len(nodes)] nodes[idx].children = nodes[idx + 1:len(nodes)]
} }
@@ -484,21 +498,29 @@ remove_line_indent :: proc(s: string, indent: string, allocator := context.alloc
render_template :: proc( render_template :: proc(
pt: Template, pt: Template,
ctx: ^[dynamic]any, ctx: ^Context_Stack,
partials: map[string]Template, partials: map[string]Template,
b: ^strings.Builder, b: ^strings.Builder,
blocks: map[string]Block_Override, blocks: map[string]Block_Override,
indent: string, indent: string,
) -> Error { ) -> Error {
if len(indent) > 0 { if len(indent) > 0 {
state := Indent_State{indent = indent, at_line_start = false} state := Indent_State {
indent = indent,
at_line_start = false,
}
strings.write_string(b, indent) // first line always gets indent strings.write_string(b, indent) // first line always gets indent
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state) return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state)
} }
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil) return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
} }
write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_line_start: ^bool) { write_indented :: proc(
b: ^strings.Builder,
indent: string,
content: string,
at_line_start: ^bool,
) {
if len(indent) == 0 || len(content) == 0 { if len(indent) == 0 || len(content) == 0 {
strings.write_string(b, content) strings.write_string(b, content)
return return
@@ -529,7 +551,7 @@ write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_
render_nodes :: proc( render_nodes :: proc(
current: Template, current: Template,
nodes: []Node, nodes: []Node,
ctx: ^[dynamic]any, ctx: ^Context_Stack,
partials: map[string]Template, partials: map[string]Template,
b: ^strings.Builder, b: ^strings.Builder,
blocks: map[string]Block_Override = nil, blocks: map[string]Block_Override = nil,
@@ -573,16 +595,16 @@ render_nodes :: proc(
if perr == nil { if perr == nil {
temp: strings.Builder temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator) strings.builder_init(&temp, context.temp_allocator)
render_nodes( render_nodes(
sub_tpl, sub_tpl,
sub_tpl.nodes[:], sub_tpl.nodes[:],
ctx, ctx,
partials, partials,
&temp, &temp,
blocks, blocks,
nil, nil,
) or_return ) or_return
write_value(b, strings.to_string(temp), escape = true) write_value(b, strings.to_string(temp), escape = true)
} }
} else { } else {
write_value(b, val, escape = true) write_value(b, val, escape = true)
@@ -615,16 +637,16 @@ render_nodes :: proc(
if perr == nil { if perr == nil {
temp: strings.Builder temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator) strings.builder_init(&temp, context.temp_allocator)
render_nodes( render_nodes(
sub_tpl, sub_tpl,
sub_tpl.nodes[:], sub_tpl.nodes[:],
ctx, ctx,
partials, partials,
&temp, &temp,
blocks, blocks,
nil, nil,
) or_return ) or_return
write_value(b, strings.to_string(temp), escape = false) write_value(b, strings.to_string(temp), escape = false)
} }
} else { } else {
write_value(b, val, escape = false) write_value(b, val, escape = false)
@@ -651,23 +673,36 @@ render_nodes :: proc(
context.temp_allocator, context.temp_allocator,
) )
if perr == nil { if perr == nil {
render_nodes( render_nodes(
sub_tpl, sub_tpl,
sub_tpl.nodes[:], sub_tpl.nodes[:],
ctx, ctx,
partials, partials,
b, b,
blocks, blocks,
nil, nil,
) or_return ) or_return
} }
} else if is_truthy(val) { } else if is_truthy(val) {
children := node.children children := node.children
elem_info, count, data := list_info(val) elem_info, count, data := list_info(val)
if elem_info != nil { if elem_info != nil {
for j in 0 ..< count { for j in 0 ..< count {
elem := extract_list_element(elem_info, data, j) elem := extract_list_element(elem_info, data, j)
append(ctx, elem) context_push(ctx, elem, current, node)
defer pop(ctx)
render_nodes(
current,
children,
ctx,
partials,
b,
blocks,
indent_state,
) or_return
}
} else {
context_push(ctx, val, current, node)
defer pop(ctx) defer pop(ctx)
render_nodes( render_nodes(
current, current,
@@ -679,13 +714,8 @@ render_nodes :: proc(
indent_state, indent_state,
) or_return ) or_return
} }
} else {
append(ctx, val)
defer pop(ctx)
render_nodes(current, children, ctx, partials, b, blocks, indent_state) or_return
} }
} i += 1 + len(node.children)
i += 1 + len(node.children)
case .Inverted: case .Inverted:
val := resolve_name(node.key, ctx[:]) val := resolve_name(node.key, ctx[:])
@@ -699,10 +729,18 @@ render_nodes :: proc(
} }
val = transformed val = transformed
} }
if !is_truthy(val) { if !is_truthy(val) {
render_nodes(current, node.children, ctx, partials, b, blocks, indent_state) or_return render_nodes(
} current,
i += 1 + len(node.children) node.children,
ctx,
partials,
b,
blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Partial: case .Partial:
name := node.key name := node.key
@@ -721,64 +759,64 @@ render_nodes :: proc(
} }
i += 1 i += 1
case .Block: case .Block:
content_nodes: []Node content_nodes: []Node
content_blocks := blocks content_blocks := blocks
render_current := current render_current := current
found_override := false found_override := false
if blocks != nil { if blocks != nil {
if o, ok := blocks[node.key]; ok { if o, ok := blocks[node.key]; ok {
content_nodes = o.nodes content_nodes = o.nodes
found_override = true found_override = true
render_current = o.source render_current = o.source
}
} }
} if !found_override {
if !found_override { content_nodes = node.children
content_nodes = node.children
}
if len(node.indent) > 0 {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
render_current,
content_nodes,
ctx,
partials,
&temp,
content_blocks,
nil,
) or_return
at_ls := true
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
} else {
render_nodes(
render_current,
content_nodes,
ctx,
partials,
b,
content_blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Parent:
parent_children := node.children
merged := merge_block_overrides(parent_children, blocks, current)
pt, found := partials[node.key]
if !found {
warn_missing_partial(current, partials, node, node.key)
} else {
warn_unmatched_block_overrides(current, pt, parent_children)
render_template(pt, ctx, partials, b, merged, node.indent) or_return
if indent_state != nil {
indent_state.at_line_start = false
} }
}
i += 1 + len(node.children) if len(node.indent) > 0 {
temp: strings.Builder
strings.builder_init(&temp, context.temp_allocator)
render_nodes(
render_current,
content_nodes,
ctx,
partials,
&temp,
content_blocks,
nil,
) or_return
at_ls := true
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
} else {
render_nodes(
render_current,
content_nodes,
ctx,
partials,
b,
content_blocks,
indent_state,
) or_return
}
i += 1 + len(node.children)
case .Parent:
parent_children := node.children
merged := merge_block_overrides(parent_children, blocks, current)
pt, found := partials[node.key]
if !found {
warn_missing_partial(current, partials, node, node.key)
} else {
warn_unmatched_block_overrides(current, pt, parent_children)
render_template(pt, ctx, partials, b, merged, node.indent) or_return
if indent_state != nil {
indent_state.at_line_start = false
}
}
i += 1 + len(node.children)
} }
} }
return nil return nil
@@ -922,3 +960,25 @@ warn_unmatched_block_overrides :: proc(
} }
} }
context_push :: proc(ctx: ^Context_Stack, val: any, current: Template, node: Node) {
append(ctx, val)
if len(ctx^) == MAX_CONTEXT_DEPTH + 1 {
warn_context_depth(current, node)
}
}
// warn_context_depth emits a diagnostic warning pointing at the section tag
// whose push carried the context stack past MAX_CONTEXT_DEPTH.
warn_context_depth :: proc(current: Template, node: Node) {
msg := fmt.tprintf(
"context stack depth exceeded %d (possible recursive section/partial)",
MAX_CONTEXT_DEPTH,
)
path := current.path
if path == "" {
path = "<input>"
}
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
log.warnf("%s", diag)
}
+38
View File
@@ -1,7 +1,9 @@
#+test #+test
package mustache package mustache
import "core:log"
import "core:mem" import "core:mem"
import "core:strings"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -124,3 +126,39 @@ leak_repeated_render :: proc(t: ^testing.T) {
} }
} }
// A deeply nested map pushes the context stack past MAX_CONTEXT_DEPTH (16).
// The depth warning must be non-fatal: rendering still succeeds.
@(test)
test_context_depth_warns :: proc(t: ^testing.T) {
context.logger = log.nil_logger()
AMT :: MAX_CONTEXT_DEPTH + 2
// Build AMT nested {x: {...}} levels; the innermost holds `leaf`.
data := make(map[string]any, context.temp_allocator)
data["leaf"] = "found"
for _ in 0 ..< AMT {
outer := make(map[string]any, context.temp_allocator)
outer["x"] = data
data = outer
}
// Template: AMT nested {{#x}} sections around {{leaf}}.
src: strings.Builder
strings.builder_init(&src, context.temp_allocator)
for _ in 0 ..< AMT do strings.write_string(&src, "{{#x}}")
strings.write_string(&src, "{{leaf}}")
for _ in 0 ..< AMT do strings.write_string(&src, "{{/x}}")
template := strings.to_string(src)
tmpl, perr := parse(template, "<depth-test>", context.temp_allocator)
testing.expect(t, perr == nil, "should parse")
if perr != nil {
return
}
result, rerr := render(tmpl, data, allocator = context.temp_allocator)
testing.expect(t, rerr == nil, "depth warning must be non-fatal")
testing.expect_value(t, result, "found")
}
+1 -4
View File
@@ -153,10 +153,7 @@ suggest_correction :: proc(available: []string, missing: string) -> string {
if len(available) == 0 || len(missing) == 0 { if len(available) == 0 || len(missing) == 0 {
return "" return ""
} }
threshold := 2 threshold := max(2, len(missing) / 3)
if len(missing) > 8 {
threshold = len(missing) / 4
}
best: string best: string
best_dist := threshold + 1 best_dist := threshold + 1
+10 -6
View File
@@ -10,11 +10,15 @@ Inner :: struct {
bar: int, bar: int,
} }
Page :: struct {
title: string,
}
Outer :: struct { Outer :: struct {
title: string, title: string,
page_title: string, page: Page,
inner: Inner, inner: Inner,
numbers: [3]int, numbers: [3]int,
} }
@(test) @(test)
@@ -150,8 +154,8 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
@(test) @(test)
test_suggest_correction_exact :: proc(t: ^testing.T) { test_suggest_correction_exact :: proc(t: ^testing.T) {
available := []string{"title", "page_title", "body"} available := []string{"title", "page.title", "body"}
testing.expect_value(t, suggest_correction(available, "page_titel"), "page_title") testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
} }
@(test) @(test)
+19 -47
View File
@@ -2,7 +2,6 @@ package main
import "mustache" import "mustache"
import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:os" import "core:os"
@@ -11,43 +10,20 @@ import "core:time"
import "core:time/datetime" import "core:time/datetime"
Template_Context :: struct { Template_Context :: struct {
params: json.Value,
now: string, now: string,
content: string,
title: string, title: string,
description: string,
og: Open_Graph,
date_format: string, date_format: string,
timezone: ^datetime.TZ_Region, timezone: ^datetime.TZ_Region,
og: Open_Graph,
// Page Data site: Site_Context,
page_title: string, page: Page,
date: string,
// Home data // Home data
pages: [dynamic]Page_Context, pages: [dynamic]Page,
// Section Data // Section Data
// TODO: Remove "posts" from the Odin code // TODO: Remove "posts" from the Odin code
posts: [dynamic]Page_Context, posts: [dynamic]Page,
}
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),
}
} }
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template { load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
@@ -125,10 +101,10 @@ capitalize :: proc(s: string) -> string {
render_template :: proc( render_template :: proc(
content_tpl: mustache.Template, content_tpl: mustache.Template,
data: Template_Context, ctx: Template_Context,
partials: map[string]mustache.Template, partials: map[string]mustache.Template,
) -> string { ) -> 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 { if err != nil {
log.errorf( log.errorf(
"%s", "%s",
@@ -157,9 +133,8 @@ render_site :: proc(site: ^Site) {
assert(ok2) assert(ok2)
ctx := Template_Context { ctx := Template_Context {
site = site.site_context,
now = now, now = now,
params = site.params,
description = site.description,
og = site.og, og = site.og,
date_format = site.date.format, date_format = site.date.format,
timezone = site.tz, timezone = site.tz,
@@ -175,6 +150,7 @@ render_site :: proc(site: ^Site) {
break break
} }
} }
ctx.page = home
// Collect sections // Collect sections
sections := make(map[string]bool) sections := make(map[string]bool)
@@ -268,9 +244,7 @@ render_page_html :: proc(
) -> string { ) -> string {
ctx := ctx ctx := ctx
ctx.title = fmt.tprintf("%s | %s", page.title, site.title) ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
ctx.page_title = page.title ctx.page = page
ctx.content = page.content
ctx.date = page.date
ctx.og = og_for_page(site.og, page) ctx.og = og_for_page(site.og, page)
return render_template(content_tpl, ctx, partials) return render_template(content_tpl, ctx, partials)
} }
@@ -282,18 +256,16 @@ render_home_html :: proc(
partials: map[string]mustache.Template, partials: map[string]mustache.Template,
ctx: Template_Context, ctx: Template_Context,
) -> string { ) -> string {
list_pages := make([dynamic]Page_Context) list_pages := make([dynamic]Page, 0, 8, context.temp_allocator)
defer delete(list_pages)
for page in site.pages { for page in site.pages {
if page._is_index { if page._is_index {
continue continue
} }
append(&list_pages, build_page_context(page)) append(&list_pages, page)
} }
ctx := ctx ctx := ctx
ctx.title = site.title ctx.title = site.title
ctx.content = home.content
ctx.pages = list_pages ctx.pages = list_pages
ctx.og = og_for_page(site.og, home) ctx.og = og_for_page(site.og, home)
@@ -309,24 +281,24 @@ render_section :: proc(
partials: map[string]mustache.Template, partials: map[string]mustache.Template,
ctx: Template_Context, ctx: Template_Context,
) -> string { ) -> string {
posts := make([dynamic]Page_Context) posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator)
defer delete(posts)
for page in site.pages { for page in site.pages {
if page.section != section || page._is_index { if page.section != section || page._is_index {
continue continue
} }
append(&posts, build_page_context(page)) append(&posts, page)
} }
ctx := ctx ctx := ctx
if has_index { if has_index {
ctx.content = section_index.content ctx.page = section_index
ctx.page_title = section_index.title
ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title) ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title)
ctx.og = og_for_page(site.og, section_index) ctx.og = og_for_page(site.og, section_index)
} else { } else {
ctx.page_title = capitalize(section) ctx.page = Page {
ctx.title = fmt.tprintf("%s | %s", capitalize(section), site.title) title = capitalize(section),
}
ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title)
ctx.og.title = capitalize(section) ctx.og.title = capitalize(section)
ctx.og.description = "" ctx.og.description = ""
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section) 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" 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 { Site :: struct {
using site_context: Site_Context,
arena: mem.Dynamic_Arena, arena: mem.Dynamic_Arena,
pages: #soa[dynamic]Page, pages: #soa[dynamic]Page,
modules: [dynamic]string, modules: [dynamic]string,
vfs: VFS, vfs: VFS,
title: string,
description: string,
base_url: string,
config_path: string, config_path: string,
content_dir: string, content_dir: string,
assets_dir: string, assets_dir: string,
output_dir: string, output_dir: string,
layouts_dir: string, layouts_dir: string,
params: json.Object,
features: bit_set[Feature], features: bit_set[Feature],
markdown_extensions: bit_set[md.Extension], markdown_extensions: bit_set[md.Extension],
og: Open_Graph,
date: Date_Preferences, date: Date_Preferences,
tz: ^datetime.TZ_Region, tz: ^datetime.TZ_Region,
grammars: string, grammars: string,