From 28975736cdca1554efcff29ebffa001237ace156 Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:17:42 -0400 Subject: [PATCH] refactor: Added parent support to mustache so templates can work right. --- .envrc | 2 +- mustache/mustache.odin | 152 +++++++++++++++++++++++++++++++++++++---- render.odin | 29 ++++---- 3 files changed, 152 insertions(+), 31 deletions(-) diff --git a/.envrc b/.envrc index 48a5693..64fa5d2 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,3 @@ use flake -export PATH="$PATH:./vendor/bin" \ No newline at end of file +export PATH="$PATH:./" diff --git a/mustache/mustache.odin b/mustache/mustache.odin index c12ab9b..f4e85d8 100644 --- a/mustache/mustache.odin +++ b/mustache/mustache.odin @@ -48,6 +48,8 @@ Token_Delimiters :: struct { otag_comment: string, otag_inverted: string, otag_partial: string, + otag_parent: string, + otag_block: string, otag_delim: string, ctag_delim: string, } @@ -63,6 +65,8 @@ CORE_DEF :: Token_Delimiters { otag_comment = "{{!", otag_inverted = "{{^", otag_partial = "{{>", + otag_parent = "{{<", + otag_block = "{{$", otag_delim = "{{=", ctag_delim = "=}}", } @@ -85,6 +89,8 @@ Token_Type :: enum { Section_Close, Comment, Partial, + Parent, + Block_Open, Newline, Skip, EOF, // The last token parsed, caller should not call again. @@ -123,6 +129,7 @@ Template :: struct { partials: any, context_stack: [dynamic]Context_Stack_Entry, layout: string, + block_overrides: map[string][]Token, } Context_Stack_Entry :: struct { @@ -485,6 +492,10 @@ lexer_start :: proc(l: ^Lexer, new_type: Token_Type) { l.cur_token_start_pos = l.cursor + len(l.delim.otag_inverted) case .Partial: l.cur_token_start_pos = l.cursor + len(l.delim.otag_partial) + case .Parent: + l.cur_token_start_pos = l.cursor + len(l.delim.otag_parent) + case .Block_Open: + l.cur_token_start_pos = l.cursor + len(l.delim.otag_block) case .Comment: l.cur_token_start_pos = l.cursor + len(l.delim.otag_comment) case .Tag_Literal: @@ -500,7 +511,7 @@ lexer_start :: proc(l: ^Lexer, new_type: Token_Type) { switch cur_type { case .Newline: l.cur_token_start_pos = l.cursor + len("\n") - case .Tag, .Section_Open_Inverted, .Tag_Literal, .Section_Close, .Section_Open, .Comment, .Partial: + case .Tag, .Section_Open_Inverted, .Tag_Literal, .Section_Close, .Section_Open, .Comment, .Partial, .Parent, .Block_Open: l.cur_token_start_pos = l.cursor + len(l.delim.ctag) case .Tag_Literal_Triple: l.cur_token_start_pos = l.cursor + len(l.delim.ctag_lit) @@ -519,7 +530,7 @@ lexer_append :: proc(l: ^Lexer, allocator := context.allocator) { lexer_append_text(l) case .Newline: lexer_append_newline(l) - case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Comment, .Partial, .Section_Open, .Section_Open_Inverted, .Section_Close: + case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Comment, .Partial, .Section_Open, .Section_Open_Inverted, .Section_Close, .Parent, .Block_Open: lexer_append_tag(l, l.cur_token_type, allocator) case .EOF, .Skip: } @@ -601,6 +612,12 @@ lexer_parse :: proc(l: ^Lexer, allocator := context.allocator) -> (err: Lexer_Er case lexer_peek(l, l.delim.otag_partial): lexer_append(l, allocator = allocator) lexer_start(l, .Partial) + case lexer_peek(l, l.delim.otag_parent): + lexer_append(l, allocator = allocator) + lexer_start(l, .Parent) + case lexer_peek(l, l.delim.otag_block): + lexer_append(l, allocator = allocator) + lexer_start(l, .Block_Open) case lexer_peek(l, l.delim.otag_literal): lexer_append(l, allocator = allocator) lexer_start(l, .Tag_Literal) @@ -635,7 +652,7 @@ lexer_token_should_skip :: proc(l: ^Lexer, t: Token) -> (skip: bool) { skip = lexer_should_skip_newline_token(l, t) case .Text: skip = lexer_should_skip_text_token(l, t) - case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Partial, .Section_Open, .Section_Close, .Section_Open_Inverted: + case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Partial, .Section_Open, .Section_Close, .Section_Open_Inverted, .Parent, .Block_Open: skip = false case .EOF, .Skip, .Comment: skip = true @@ -692,7 +709,7 @@ lexer_should_skip_newline_token :: proc(l: ^Lexer, token: Token) -> bool { case .Tag, .Tag_Literal, .Tag_Literal_Triple: return false case .Section_Open, .Section_Close, .Section_Open_Inverted, .Comment, - .Partial, .Newline, .Skip, .EOF: + .Partial, .Parent, .Block_Open, .Newline, .Skip, .EOF: } } @@ -714,7 +731,7 @@ lexer_should_skip_text_token :: proc(l: ^Lexer, token: Token) -> bool { } case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Partial: return false - case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment: + case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment, .Parent, .Block_Open: standalone_tag_count += 1 case .Newline, .Skip, .EOF: } @@ -739,7 +756,7 @@ lexer_token_is_standalone_partial :: proc(l: ^Lexer, token: Token) -> bool { } case .Tag, .Tag_Literal, .Tag_Literal_Triple: return false - case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment, .Partial: + case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment, .Partial, .Parent, .Block_Open: standalone_tag_count += 1 case .Newline, .Skip, .EOF: } @@ -962,7 +979,7 @@ token_content :: proc(tmpl: ^Template, t: Token, allocator := context.allocator) case .Newline: s = "\n" case .Section_Open, .Section_Open_Inverted, .Section_Close, - .Comment, .Skip, .EOF, .Partial: + .Comment, .Skip, .EOF, .Partial, .Parent, .Block_Open: } return s @@ -973,7 +990,7 @@ token_is_tag :: proc(t: Token) -> bool { case .Tag, .Tag_Literal, .Tag_Literal_Triple: return true case .Text, .Newline, .Section_Open, .Section_Open_Inverted, .Section_Close, - .Comment, .Skip, .EOF, .Partial: + .Comment, .Skip, .EOF, .Partial, .Parent, .Block_Open: return false } @@ -1109,6 +1126,112 @@ template_insert_content_into_layout :: proc( return nil } +// Find the index of a Section_Close token matching the given name, +// respecting nesting depth across all open tag types. +find_matching_close :: proc(tokens: []Token, start: int, name: string) -> int { + depth := 1 + for i in start+1.. j + 1 { + overrides[t.value] = tmpl.lexer.tokens[j+1:block_close] + } else { + overrides[t.value] = {} + } + j = block_close + 1 + } else { + j += 1 + } + } + + // Load parent template from partials + partials_map, ok := tmpl.partials.(map[string]string) + if !ok { + return + } + parent_src, has_parent := partials_map[parent_name] + if !has_parent { + return + } + + // Lex parent template + parent_lexer := lexer_make(allocator) + parent_lexer.src = parent_src + parent_lexer.delim = CORE_DEF + if lexer_parse(parent_lexer, allocator = allocator) != nil { + return + } + + // Build resolved token list: replace blocks with overrides where applicable + resolved := make([dynamic]Token, allocator) + defer delete(resolved) + pi := 0 + for pi < len(parent_lexer.tokens) { + pt := parent_lexer.tokens[pi] + if pt.type == .Block_Open { + block_close := find_matching_close(parent_lexer.tokens[:], pi, pt.value) + if override, has_override := overrides[pt.value]; has_override { + for ot in override { + append(&resolved, ot) + } + } else { + for di in pi+1.. string { return string(data) } -render_with_layout :: proc( +render_template :: proc( content_tpl: string, data: any, - base_tpl: string, partials: map[string]string, ) -> string { - result, err := mustache.render_in_layout(content_tpl, data, base_tpl, partials) + result, err := mustache.render(content_tpl, data, partials) if err != nil { fmt.eprintfln("thor: mustache error: %v", err) return "" @@ -131,7 +129,7 @@ render_site :: proc(pages: []Page, config: Site) { // Load shared resources once partials := load_partials(config.layouts_dir) - base_tpl := load_template(config.layouts_dir, "base.html") + partials["base"] = load_template(config.layouts_dir, "base.html") post_tpl := load_template(config.layouts_dir, "post.html") now, ok := time.time_to_datetime(time.now()) @@ -163,7 +161,7 @@ render_site :: proc(pages: []Page, config: Site) { if page.type == .Home { continue } - html := render_page_html(page, config, post_tpl, base_tpl, partials, base) + html := render_page_html(page, config, post_tpl, partials, base) if .Minify in config.features { html = minify_html(html) } @@ -173,7 +171,7 @@ render_site :: proc(pages: []Page, config: Site) { // Render home page if has_home { home_tpl := load_template(config.layouts_dir, "home.html") - home_html := render_home_html(home, pages, config, home_tpl, base_tpl, partials, base) + home_html := render_home_html(home, pages, config, home_tpl, partials, base) if .Minify in config.features { home_html = minify_html(home_html) } @@ -182,7 +180,7 @@ render_site :: proc(pages: []Page, config: Site) { // Render posts list page posts_tpl := load_template(config.layouts_dir, "posts_list.html") - posts_html := render_posts_html(pages, config, posts_tpl, base_tpl, partials, base) + posts_html := render_posts_html(pages, config, posts_tpl, partials, base) if .Minify in config.features { posts_html = minify_html(posts_html) } @@ -214,7 +212,6 @@ render_page_html :: proc( page: Page, config: Site, content_tpl: string, - base_tpl: string, partials: map[string]string, base: Base_Data, ) -> string { @@ -224,7 +221,7 @@ render_page_html :: proc( } data.title = fmt.tprintf("%s | %s", page.title, config.title) data.page_title = page.title - data.body_html = page.body_html + data.body = page.body_html data.has_date = page.date != "" data.date_iso = page.date data.date_display = format_date(page.date) @@ -235,7 +232,7 @@ render_page_html :: proc( data.is_article = is_article data.og_section = "posts" data.og_published = page.date - return render_with_layout(content_tpl, data, base_tpl, partials) + return render_template(content_tpl, data, partials) } render_home_html :: proc( @@ -243,7 +240,6 @@ render_home_html :: proc( pages: []Page, config: Site, content_tpl: string, - base_tpl: string, partials: map[string]string, base: Base_Data, ) -> string { @@ -260,20 +256,19 @@ render_home_html :: proc( base = base, } data.title = config.title - data.home_body = home.body_html + data.body = home.body_html data.list_pages = list_pages data.og_url = fmt.tprintf("%s/", config.base_url) data.og_title = config.title data.og_type = "website" data.is_article = false - return render_with_layout(content_tpl, data, base_tpl, partials) + return render_template(content_tpl, data, partials) } render_posts_html :: proc( pages: []Page, config: Site, content_tpl: string, - base_tpl: string, partials: map[string]string, base: Base_Data, ) -> string { @@ -301,7 +296,7 @@ render_posts_html :: proc( data.og_title = "Posts" data.og_type = "website" data.is_article = false - return render_with_layout(content_tpl, data, base_tpl, partials) + return render_template(content_tpl, data, partials) } load_partials :: proc(layouts_dir: string) -> map[string]string {