mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
refactor: Added parent support to mustache so templates can work right.
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
use flake
|
||||
|
||||
export PATH="$PATH:./vendor/bin"
|
||||
export PATH="$PATH:./"
|
||||
|
||||
+139
-13
@@ -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..<len(tokens) {
|
||||
t := tokens[i]
|
||||
if t.value == name && (t.type == .Section_Open || t.type == .Section_Open_Inverted ||
|
||||
t.type == .Block_Open || t.type == .Parent) {
|
||||
depth += 1
|
||||
}
|
||||
if t.type == .Section_Close && t.value == name {
|
||||
depth -= 1
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// Handle {{<parent}} inheritance: collect block overrides, load parent template,
|
||||
// resolve blocks, and render the parent's structure.
|
||||
template_handle_parent :: proc(
|
||||
tmpl: ^Template,
|
||||
parent_token: Token,
|
||||
start_i: int,
|
||||
sb: ^strings.Builder,
|
||||
allocator := context.allocator,
|
||||
) {
|
||||
parent_name := parent_token.value
|
||||
close_i := find_matching_close(tmpl.lexer.tokens[:], start_i, parent_name)
|
||||
|
||||
// Collect block overrides from child template
|
||||
overrides := make(map[string][]Token, allocator)
|
||||
j := start_i + 1
|
||||
for j < close_i {
|
||||
t := tmpl.lexer.tokens[j]
|
||||
if t.type == .Block_Open {
|
||||
block_close := find_matching_close(tmpl.lexer.tokens[:], j, t.value)
|
||||
if block_close > 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..<block_close {
|
||||
append(&resolved, parent_lexer.tokens[di])
|
||||
}
|
||||
}
|
||||
pi = block_close + 1
|
||||
} else {
|
||||
append(&resolved, pt)
|
||||
pi += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Create template with resolved tokens and render
|
||||
resolved_lexer := lexer_make(allocator)
|
||||
for t in resolved {
|
||||
append(&resolved_lexer.tokens, t)
|
||||
}
|
||||
resolved_lexer.src = parent_lexer.src
|
||||
|
||||
parent_tmpl := template_make(resolved_lexer, allocator)
|
||||
parent_tmpl.data = tmpl.data
|
||||
parent_tmpl.partials = tmpl.partials
|
||||
|
||||
template_eat_tokens(parent_tmpl, sb, allocator)
|
||||
}
|
||||
|
||||
template_eat_tokens :: proc(
|
||||
tmpl: ^Template,
|
||||
sb: ^strings.Builder,
|
||||
@@ -1157,6 +1280,13 @@ template_process_tokens :: proc(
|
||||
}
|
||||
case .Partial:
|
||||
template_render_partial(tmpl, t, i, sb, allocator)
|
||||
case .Parent:
|
||||
template_handle_parent(tmpl, t, i, sb, allocator)
|
||||
close_i := find_matching_close(tmpl.lexer.tokens[:], i, t.value)
|
||||
i = close_i
|
||||
case .Block_Open:
|
||||
close_i := find_matching_close(tmpl.lexer.tokens[:], i, t.value)
|
||||
i = close_i
|
||||
// Do nothing for these tags.
|
||||
case .Comment, .Skip, .EOF:
|
||||
}
|
||||
@@ -1170,24 +1300,20 @@ template_render :: proc(
|
||||
sb := strings.builder_make(allocator)
|
||||
|
||||
template_eat_tokens(tmpl, &sb, allocator)
|
||||
rendered := strings.to_string(sb)
|
||||
rendered := strings.to_string(sb)
|
||||
|
||||
if tmpl.layout != "" {
|
||||
sbl := strings.builder_make(allocator)
|
||||
|
||||
// Parse the layout
|
||||
layout_lexer := lexer_make(allocator)
|
||||
layout_lexer.src = tmpl.layout
|
||||
layout_lexer.delim = CORE_DEF
|
||||
lexer_parse(layout_lexer, allocator = allocator) or_return
|
||||
|
||||
// The Layout template shares data and partials with the main template.
|
||||
layout_template := template_make(layout_lexer, allocator)
|
||||
layout_template.data = tmpl.data
|
||||
layout_template.partials = tmpl.partials
|
||||
|
||||
// TODO: Could we directly index the special {{content}} tag so that
|
||||
// we don't need to search it here by iterating and just get it?
|
||||
for t, i in layout_lexer.tokens {
|
||||
if token_is_tag(t) && t.value == "content" {
|
||||
template_insert_content_into_layout(layout_template, t, i, rendered, allocator)
|
||||
|
||||
+12
-17
@@ -28,6 +28,7 @@ Base_Data :: struct {
|
||||
now: datetime.DateTime,
|
||||
author: string,
|
||||
params: json.Value,
|
||||
body: string,
|
||||
title: string,
|
||||
og_site_name: string,
|
||||
og_description: string,
|
||||
@@ -41,7 +42,6 @@ Base_Data :: struct {
|
||||
Page_Data :: struct {
|
||||
using base: Base_Data,
|
||||
page_title: string,
|
||||
body_html: string,
|
||||
has_date: bool,
|
||||
date_iso: string,
|
||||
date_display: string,
|
||||
@@ -52,7 +52,6 @@ Page_Data :: struct {
|
||||
|
||||
Home_Data :: struct {
|
||||
using base: Base_Data,
|
||||
home_body: string,
|
||||
list_pages: [dynamic]Page_Context,
|
||||
}
|
||||
|
||||
@@ -112,13 +111,12 @@ load_template :: proc(layouts_dir: string, name: string) -> 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 {
|
||||
|
||||
Reference in New Issue
Block a user