Files
thor/mustache/mustache.odin
T
2026-07-12 15:05:35 -04:00

1724 lines
43 KiB
Odin

package mustache
import "base:runtime"
import "core:encoding/json"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:reflect"
import "core:slice"
import "core:strings"
TRUE :: "true"
FALSEY :: "false"
// Special characters that will receive HTML-escaping
// treatment, if necessary.
HTML_LESS_THAN :: "<"
HTML_GREATER_THAN :: ">"
HTML_QUOTE :: """
HTML_AMPERSAND :: "&"
Render_Error :: union {
Lexer_Error,
Template_Error,
File_Not_Found_Error,
json.Error,
}
File_Not_Found_Error :: struct {
filename: string,
}
Lexer_Error :: union {
Unbalanced_Tags,
}
Unbalanced_Tags :: struct {}
Token_Delimiters :: struct {
otag: string,
ctag: string,
otag_lit: string,
ctag_lit: string,
otag_section_open: string,
otag_section_close: string,
otag_literal: string,
otag_comment: string,
otag_inverted: string,
otag_partial: string,
otag_delim: string,
ctag_delim: string,
}
CORE_DEF :: Token_Delimiters {
otag = "{{",
ctag = "}}",
otag_lit = "{{{",
ctag_lit = "}}}",
otag_section_open = "{{#",
otag_section_close = "{{/",
otag_literal = "{{&",
otag_comment = "{{!",
otag_inverted = "{{^",
otag_partial = "{{>",
otag_delim = "{{=",
ctag_delim = "=}}",
}
Token :: struct {
type: Token_Type,
value: string,
pos: Pos,
iters: int,
start_i: int,
}
Token_Type :: enum {
Text,
Tag,
Section_Open_Inverted,
Tag_Literal,
Tag_Literal_Triple,
Section_Open,
Section_Close,
Comment,
Partial,
Newline,
Skip,
EOF, // The last token parsed, caller should not call again.
}
Pos :: struct {
start: int,
end: int,
line: int,
}
Lexer :: struct {
src: string,
cursor: int,
line: int,
tokens: [dynamic]Token,
cur_token_type: Token_Type,
cur_token_start_pos: int,
tag_stack: [dynamic]rune,
delim: Token_Delimiters,
}
Data_Error :: enum {
None,
Unsupported_Type,
Map_Key_Not_Found,
}
Template_Error :: union {
Data_Error,
}
Template :: struct {
lexer: ^Lexer,
data: any,
partials: any,
context_stack: [dynamic]Context_Stack_Entry,
layout: string,
}
Context_Stack_Entry :: struct {
data: any,
label: string,
}
Data_Type :: enum {
Map,
Struct,
List,
Value,
Null,
}
// Returns true if the value is one of the "falsey" values
// for a context.
@(private)
_falsey_context: map[string]bool
// Returns true if the value is one of the "falsey" values
// for a context.
@(private)
_whitespace: map[rune]bool
/*
UTILITY PROCEDURES
*/
trim_decimal_string :: proc(s: string, allocator := context.allocator) -> string {
if len(s) == 0 || s[len(s)-1] != '0' {
return strings.clone(s[:], allocator)
}
// We have at least one trailing zero. Search backwards and find the rest.
trailing_start_idx := len(s)-1
for i := len(s) - 2; i >= 0 ; i -= 1 {
switch s[i] {
case '0':
if trailing_start_idx == i + 1 {
trailing_start_idx = i
}
case '.':
if trailing_start_idx == i + 1 {
// Removes point completely for numbers like 0.000
trailing_start_idx = i
}
return strings.clone(s[:trailing_start_idx], allocator)
}
}
return strings.clone(s[:], allocator)
}
escape_html_string :: proc(s: string, allocator := context.allocator) -> string {
escaped := s
// Ampersand escaping goes first.
escaped, _ = strings.replace_all(escaped, "&", HTML_AMPERSAND, allocator = allocator)
escaped, _ = strings.replace_all(escaped, "<", HTML_LESS_THAN, allocator = allocator)
escaped, _ = strings.replace_all(escaped, ">", HTML_GREATER_THAN, allocator = allocator)
escaped, _ = strings.replace_all(escaped, "\"", HTML_QUOTE, allocator = allocator)
return escaped
}
// Gets the value of a struct field.
struct_get :: proc(obj: any, key: string) -> any {
if !is_struct(obj) {
return nil
}
obj := obj
if is_union(obj) {
obj = reflect.get_union_variant(obj)
}
return reflect.struct_field_value_by_name(obj, key)
}
// Retrieves a value from a map. In mustache.odin, all map keys must be
// string values because we do not know the type of value inside a tag.
//
// Eg., {{name}} -- we assume "name" is either a string key to a map,
// or the name of a field on a struct.
map_get :: proc(v: any, map_key: string) -> (dug: any, err: Template_Error) {
if !is_map(v) {
return nil, .Unsupported_Type
}
m := (^mem.Raw_Map)(v.data)
if m == nil {
return nil, .Unsupported_Type
}
v := v
if is_union(v) {
v = reflect.get_union_variant(v)
}
// Use type_info_base to ensure we get the underlying data structure
// of a named type if we run into one. Like Map, List, etc.
base_tinfo := runtime.type_info_base(type_info_of(v.id))
tinfo := base_tinfo.variant.(runtime.Type_Info_Map)
map_info := tinfo.map_info
if map_info == nil {
return nil, .Unsupported_Type
}
map_cap := uintptr(runtime.map_cap(m^))
ks, vs, hs, _, _ := runtime.map_kvh_data_dynamic(m^, map_info)
for bucket_index in 0..<map_cap {
runtime.map_hash_is_valid(hs[bucket_index]) or_continue
// Accessing the map key.
key_ptr := rawptr(runtime.map_cell_index_dynamic(ks, map_info.ks, bucket_index))
key_any := any{key_ptr, tinfo.key.id}
key_info := runtime.type_info_base(type_info_of(key_any.id))
key_info_any := any{key_any.data, key_info.id}
key: string
// Keys can only be of a string type.
#partial switch tinfo in key_info.variant {
case runtime.Type_Info_String:
switch s in key_info_any {
case string:
key = s
case cstring:
key = string(s)
}
case:
return nil, .Unsupported_Type
}
// Access the value.
value_ptr := rawptr(runtime.map_cell_index_dynamic(vs, map_info.vs, bucket_index))
value_any := any{value_ptr, tinfo.value.id}
value_info := runtime.type_info_base(type_info_of(value_any.id))
value_info_any := any{value_any.data, value_info.id}
value := value_info_any
if map_key == key {
// Unwrap 'any' values stored in map[string]any to avoid double-wrapping
if tinfo.value.id == typeid_of(any) {
inner := (^any)(value_ptr)
return inner^, nil
}
return value, nil
}
}
return nil, .Map_Key_Not_Found
}
// Checks if an 'any' object is a struct of some kind.
is_struct :: proc(obj: any) -> bool {
tid: typeid
tinfo: ^runtime.Type_Info
if is_union(obj) {
tid = reflect.union_variant_typeid(obj)
} else {
tid = obj.id
}
tinfo = type_info_of(tid)
return reflect.is_struct(tinfo)
}
// Checks if an 'any' object is a union of some kind.
is_union :: proc(obj: any) -> bool {
tinfo: ^runtime.Type_Info
base_tinfo: ^runtime.Type_Info
tinfo = type_info_of(obj.id)
base_tinfo = runtime.type_info_base(tinfo)
return reflect.type_kind(base_tinfo.id) == reflect.Type_Kind.Union
}
// Checks if an 'any' object is a map of some kind.
is_map :: proc(obj: any) -> bool {
tinfo: ^runtime.Type_Info
id: typeid
if is_union(obj) {
id = reflect.union_variant_typeid(obj)
} else {
id = obj.id
}
tinfo = type_info_of(id)
return reflect.is_dynamic_map(tinfo)
}
// Checks if an 'any' object is a list of some kind.
is_list :: proc(obj: any) -> bool {
tinfo: ^runtime.Type_Info
id: typeid
if is_union(obj) {
id = reflect.union_variant_typeid(obj)
} else {
id = obj.id
}
tinfo = type_info_of(id)
return reflect.is_array(tinfo) || reflect.is_dynamic_array(tinfo) || reflect.is_slice(tinfo)
}
// Checks if a string is plain whitespace.
is_text_blank :: proc(s: string) -> (res: bool) {
for r in s {
if !_whitespace[r] {
return false
}
}
return true
}
// Retrieves an element from a list (can be of any type -- array,
// dynamic array, slice) at a given index.
list_at :: proc(obj: any, i: int) -> any {
obj := obj
if is_union(obj) {
obj = reflect.get_union_variant(obj)
}
if !is_list(obj) {
return nil
}
return reflect.index(obj, i)
}
// Checks if a map has a given key.
map_has_key :: proc(v: any, map_key: string) -> (has: bool) {
if !is_map(v) {
return false
}
m := (^mem.Raw_Map)(v.data)
if m == nil {
return false
}
v := v
if is_union(v) {
v = reflect.get_union_variant(v)
}
// Use type_info_base to ensure we get the underlying data structure
// of a named type if we run into one. Like Map, List, etc.
base_tinfo := runtime.type_info_base(type_info_of(v.id))
tinfo := base_tinfo.variant.(runtime.Type_Info_Map)
map_info := tinfo.map_info
if map_info == nil {
return false
}
map_cap := uintptr(runtime.map_cap(m^))
ks, _, hs, _, _ := runtime.map_kvh_data_dynamic(m^, map_info)
for bucket_index in 0..<map_cap {
runtime.map_hash_is_valid(hs[bucket_index]) or_continue
// Accessing string type in key
key_ptr := rawptr(runtime.map_cell_index_dynamic(ks, map_info.ks, bucket_index))
key_any := any{key_ptr, tinfo.key.id}
key_info := runtime.type_info_base(type_info_of(key_any.id))
key_info_any := any{key_any.data, key_info.id}
key: string
// Validate that the map keys must be of a string or cstring type.
#partial switch tinfo in key_info.variant {
case runtime.Type_Info_String:
switch s in key_info_any {
case string:
key = s
case cstring:
key = string(s)
}
case:
return false
}
if map_key == key {
return true
}
}
return false
}
/*
LEXER-RELATED PROCEDURES
*/
lexer_make :: proc(allocator := context.allocator) -> ^Lexer {
l := new(Lexer, allocator)
l.tokens = make([dynamic]Token, 0, allocator)
l.tag_stack = make([dynamic]rune, 0, allocator)
return l
}
lexer_peek :: proc(l: ^Lexer, s: string, offset := 0) -> (bool) {
peek_i: int
peeked: rune
if l.cursor + offset + len(s) >= len(l.src) {
return false
}
for i := 0; i < len(s); i += 1 {
peek_i = l.cursor + offset + i
peeked = rune(l.src[peek_i])
if peeked != rune(s[i]) {
return false
}
}
return true
}
// Used AFTER a new Token is inserted into the tokens dynamic
// array. In the case of a .Tag_Literal ('{{{...}}}'), we need
// to advance the next start position by three instead of two,
// to account for the additional brace.
lexer_start :: proc(l: ^Lexer, new_type: Token_Type) {
cur_type := l.cur_token_type
switch {
// Moving from text into a tag.
case cur_type == .Text:
switch new_type {
case .Section_Open:
l.cur_token_start_pos = l.cursor + len(l.delim.otag_section_open)
case .Section_Close:
l.cur_token_start_pos = l.cursor + len(l.delim.otag_section_close)
case .Section_Open_Inverted:
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 .Comment:
l.cur_token_start_pos = l.cursor + len(l.delim.otag_comment)
case .Tag_Literal:
l.cur_token_start_pos = l.cursor + len(l.delim.otag_literal)
case .Tag_Literal_Triple:
l.cur_token_start_pos = l.cursor + len(l.delim.otag_lit)
case .Tag:
l.cur_token_start_pos = l.cursor + len(l.delim.otag)
case .Text, .Newline, .EOF, .Skip:
}
// Moving from a tag back into text.
case new_type == .Text:
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:
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)
case .Text, .EOF, .Skip:
}
}
// Update the current type to the new type.
l.cur_token_type = new_type
}
// Adds a new token to our list.
lexer_append :: proc(l: ^Lexer, allocator := context.allocator) {
switch l.cur_token_type {
case .Text:
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:
lexer_append_tag(l, l.cur_token_type, allocator)
case .EOF, .Skip:
}
}
lexer_append_tag :: proc(
l: ^Lexer,
token_type: Token_Type,
allocator := context.allocator,
) {
pos := Pos {
start=l.cur_token_start_pos,
end=l.cursor,
line=l.line,
}
if pos.end > pos.start {
// Remove all empty whitespace inside a valid tag so that we don't
// mess up our access of the data.
token_text := l.src[pos.start:pos.end]
token_text, _ = strings.remove_all(token_text, " ", allocator = allocator)
token := Token{type=token_type, value=token_text, pos=pos}
append(&l.tokens, token)
}
}
lexer_append_text :: proc(l: ^Lexer) {
pos := Pos {
start=l.cur_token_start_pos,
end=l.cursor,
line=l.line,
}
if pos.end > pos.start {
text := l.src[pos.start:pos.end]
token := Token{type=.Text, value=text, pos=pos}
append(&l.tokens, token)
}
}
lexer_append_newline :: proc(l: ^Lexer) {
pos := Pos {
start=l.cur_token_start_pos,
end=l.cursor + 1,
line=l.line,
}
newline := Token{type=.Newline, value="\n", pos=pos}
append(&l.tokens, newline)
}
lexer_parse :: proc(l: ^Lexer, allocator := context.allocator) -> (err: Lexer_Error) {
for l.cursor < len(l.src) {
ch := rune(l.src[l.cursor])
defer { l.cursor += 1 }
switch {
// When we hit a newline (and we are not inside a .Comment, as multi-line
// comments are permitted), add the current chunk as a new Token, insert
// a special .Newline token, and then begin as a new .Text Token.
case ch == '\n' && l.cur_token_type != .Comment:
lexer_append(l, allocator = allocator)
lexer_start(l, .Newline)
lexer_append(l, allocator = allocator)
lexer_start(l, .Text)
l.line += 1
case lexer_peek(l, l.delim.otag_lit):
lexer_append(l, allocator = allocator)
lexer_start(l, .Tag_Literal_Triple)
case lexer_peek(l, l.delim.otag_section_open):
lexer_append(l, allocator = allocator)
lexer_start(l, .Section_Open)
case lexer_peek(l, l.delim.otag_section_close):
lexer_append(l, allocator = allocator)
lexer_start(l, .Section_Close)
case lexer_peek(l, l.delim.otag_inverted):
lexer_append(l, allocator = allocator)
lexer_start(l, .Section_Open_Inverted)
case lexer_peek(l, l.delim.otag_partial):
lexer_append(l, allocator = allocator)
lexer_start(l, .Partial)
case lexer_peek(l, l.delim.otag_literal):
lexer_append(l, allocator = allocator)
lexer_start(l, .Tag_Literal)
case lexer_peek(l, l.delim.otag_comment):
lexer_append(l, allocator = allocator)
lexer_start(l, .Comment)
// Be careful with checking for "{{" -- it could be a substring of "{{{"
case lexer_peek(l, l.delim.otag) && l.cur_token_type != .Tag_Literal_Triple:
lexer_append(l, allocator = allocator)
lexer_start(l, .Tag)
case lexer_peek(l, "}") && l.cur_token_type != .Text:
lexer_append(l, allocator = allocator)
lexer_start(l, .Text)
}
}
// Add the last tag and mark that we hit the end of the file.
lexer_append(l, allocator = allocator)
l.cur_token_type = .EOF
return nil
}
lexer_print_tokens :: proc(l: ^Lexer) {
for t, i in l.tokens {
fmt.println(i, " ", t)
}
}
lexer_token_should_skip :: proc(l: ^Lexer, t: Token) -> (skip: bool) {
switch t.type {
case .Newline:
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:
skip = false
case .EOF, .Skip, .Comment:
skip = true
}
return skip
}
// Retrieves all the tokens that are on a given line of the input text.
lexer_tokens_on_same_line :: proc(l: ^Lexer, line: int) -> (tokens: []Token) {
on_line := false
start_i: int
end_i: int
for t, i in l.tokens {
if t.pos.line == line && !on_line {
on_line = true
start_i = i
} else if t.pos.line != line && on_line {
on_line = false
end_i = i
break
}
}
if on_line {
end_i = len(l.tokens)
}
if start_i <= end_i {
return l.tokens[start_i:end_i]
} else {
return l.tokens[0:0]
}
}
// Skip a newline if we are on a line that has either a
// non-blank .Text token OR any valid tags.
lexer_should_skip_newline_token :: proc(l: ^Lexer, token: Token) -> bool {
on_line := lexer_tokens_on_same_line(l, token.pos.line)
// If the newline is the only token present, do not skip it.
if len(on_line) == 1 {
return false
}
for t in on_line {
switch t.type {
case .Text:
if !is_text_blank(t.value) {
return false
}
case .Tag, .Tag_Literal, .Tag_Literal_Triple:
return false
case .Section_Open, .Section_Close, .Section_Open_Inverted, .Comment,
.Partial, .Newline, .Skip, .EOF:
}
}
return true
}
// If we are rendering a .Text tag, we should NOT render it if it is:
// - On a line with one .Section tag, AND
// - comprised of only whitespace, along with all the other .Text tokens
lexer_should_skip_text_token :: proc(l: ^Lexer, token: Token) -> bool {
on_line := lexer_tokens_on_same_line(l, token.pos.line)
standalone_tag_count := 0
for t in on_line {
switch t.type {
case .Text:
if !is_text_blank(t.value) {
return false
}
case .Tag, .Tag_Literal, .Tag_Literal_Triple, .Partial:
return false
case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment:
standalone_tag_count += 1
case .Newline, .Skip, .EOF:
}
}
// If we have gotten to the end, that means all the .Text
// tags on this line are blank. If we also only have a single
// section or comment tag, that means that tag is standalone.
return standalone_tag_count == 1
}
// Checks if a given .Partial Token is "standalone."
lexer_token_is_standalone_partial :: proc(l: ^Lexer, token: Token) -> bool {
on_line := lexer_tokens_on_same_line(l, token.pos.line)
standalone_tag_count := 0
for t in on_line {
switch t.type {
case .Text:
if !is_text_blank(t.value) {
return false
}
case .Tag, .Tag_Literal, .Tag_Literal_Triple:
return false
case .Section_Open, .Section_Open_Inverted, .Section_Close, .Comment, .Partial:
standalone_tag_count += 1
case .Newline, .Skip, .EOF:
}
}
// If we have gotten to the end, that means all the .Text
// tags on this line are blank. If we also only have a single
// section or comment tag, that means that tag is standalone.
return standalone_tag_count == 1
}
/*
TEMPLATE-RELATED PROCEDURES
*/
template_make :: proc(l: ^Lexer, allocator := context.allocator) -> ^Template {
t := new(Template, allocator)
t.lexer = l
t.context_stack = make([dynamic]Context_Stack_Entry, 0, allocator)
return t
}
// Sections can have false-y values in their corresponding data. When this
// is the case, the section should not be rendered. Example:
// input := "\"{{#boolean}}This should not be rendered.{{/boolean}}\""
// data := Map {
// "boolean" = "false"
// }
// Valid contexts are:
// - Map with at least one key
// - List with at least one element
// - string NOT in the _falsey_context mapping
template_token_is_valid :: proc(tmpl: ^Template, token: Token) -> (bool) {
stack_entry := tmpl.context_stack[0]
// The root stack is always valid.
if stack_entry.label == "ROOT" {
return true
}
switch data_type(stack_entry.data) {
case .Map, .List, .Struct:
return data_len(stack_entry.data) > 0
case .Value:
s := fmt.tprintf("%v", stack_entry.data)
return !_falsey_context[s]
case .Null:
return false
}
return false
}
template_string_from_key :: proc(
tmpl: ^Template,
key: string,
allocator := context.allocator,
) -> (s: string) {
resolved: any
if key == "." {
resolved = tmpl.context_stack[0].data
} else {
// If the top of the stack is a string and we need to access a hash of data,
// dig from the layer beneath the top.
ids := strings.split(key, ".", allocator = allocator)
for ctx in tmpl.context_stack {
resolved = dig(ctx.data, ids[0:1])
if resolved != nil {
break
}
}
// Apply "dotted name resolution" if we have parts after the core ID.
if len(ids[1:]) > 0 {
last := slice.last(ids[:])
last_slice := ids[len(ids)-1:]
resolved = dig(resolved, ids[1:])
if is_map(resolved) || is_struct(resolved) && has_key(resolved, last) {
resolved = dig(resolved, last_slice)
}
}
}
s, _ = any_to_string(resolved)
return s
}
template_print_stack :: proc(tmpl: ^Template) {
fmt.println("Current stack")
for c, i in tmpl.context_stack {
fmt.printf("\t[%v] %v: %v\n", i, c.label, c.data)
}
}
// Retrieves data to place on the context stack.
template_get_data_for_stack :: proc(
tmpl: ^Template,
data_id: string,
allocator := context.allocator,
) -> (data: any) {
ids := strings.split(data_id, ".", allocator = allocator)
defer delete(ids)
// New stack entries always need to resolve against the current top
// of the stack entry.
data = dig(tmpl.context_stack[0].data, ids)
// If we couldn't resolve against the top of the stack, add from the root.
if data == nil {
root_stack_entry := tmpl.context_stack[len(tmpl.context_stack)-1]
data = dig(root_stack_entry.data, ids)
}
// If we still can't find anything, mark this section as false-y.
if reflect.is_nil(data) {
return runtime.new_clone(FALSEY, allocator = allocator)^
} else {
return data
}
}
// Adds a new entry to the Template's context_stack. This occurs
// when we encounter a .Section_Open tag.
template_add_to_context_stack :: proc(
tmpl: ^Template,
t: Token,
offset: int,
allocator := context.allocator,
) {
data_id := t.value
data := template_get_data_for_stack(tmpl, data_id, allocator)
if t.type == .Section_Open_Inverted {
stack_entry := Context_Stack_Entry{
data=invert_data(data, allocator),
label=data_id,
}
inject_at(&tmpl.context_stack, 0, stack_entry)
} else {
switch data_type(data) {
case .Map, .Struct, .Value:
stack_entry := Context_Stack_Entry{data=data, label=data_id}
inject_at(&tmpl.context_stack, 0, stack_entry)
case .List:
template_inject_list_into_context_stack(tmpl, data, offset)
case .Null:
stack_entry := Context_Stack_Entry{data=nil, label=data_id}
inject_at(&tmpl.context_stack, 0, stack_entry)
}
}
}
template_inject_list_into_context_stack :: proc(tmpl: ^Template, list: any, offset: int) {
section_open := tmpl.lexer.tokens[offset]
section_name := section_open.value
start_chunk := offset + 1
end_chunk := template_find_section_close_tag_index(tmpl, section_name, offset)
// Remove the original chunk from the token list if the list is empty.
// We treat empty lists as false-y values.
if data_len(list) == 0 {
for _ in start_chunk..<end_chunk {
ordered_remove(&tmpl.lexer.tokens, start_chunk)
}
return
}
// If we have a list with contents, update the closing tag with:
// 1. The number of iterations to perform
// 2. The position of the start of the loop (eg., .Section_Open tag)
section_close := tmpl.lexer.tokens[end_chunk]
section_close.iters = data_len(list) - 1
section_close.start_i = offset
tmpl.lexer.tokens[end_chunk] = section_close
// Add each element of the list to the context stack. Add the data in
// reverse order of the list, so that the first entry is at the top.
for i := section_close.iters; i >= 0; i -= 1 {
el := list_at(list, i)
stack_entry := Context_Stack_Entry{data=el, label="TEMP LIST"}
inject_at(&tmpl.context_stack, 0, stack_entry)
}
}
// Finds the closing tag with a given value after
// the given offset.
template_find_section_close_tag_index :: proc(
tmpl: ^Template,
label: string,
offset: int,
) -> (int) {
for t, i in tmpl.lexer.tokens[offset:] {
if t.type == .Section_Close && t.value == label {
return i + offset
}
}
return -1
}
template_pop_from_context_stack :: proc(tmpl: ^Template) {
if len(tmpl.context_stack) > 1 {
ordered_remove(&tmpl.context_stack, 0)
}
}
token_content :: proc(tmpl: ^Template, t: Token, allocator := context.allocator) -> (s: string) {
switch t.type {
case .Text:
// NOTE: Carriage returns causing some wonkiness with .concatenate.
if t.value != "\r" {
s = t.value
}
case .Tag:
s = template_string_from_key(tmpl, t.value, allocator)
s = escape_html_string(s, allocator = allocator)
case .Tag_Literal, .Tag_Literal_Triple:
s = template_string_from_key(tmpl, t.value, allocator)
case .Newline:
s = "\n"
case .Section_Open, .Section_Open_Inverted, .Section_Close,
.Comment, .Skip, .EOF, .Partial:
}
return s
}
token_is_tag :: proc(t: Token) -> bool {
switch t.type {
case .Tag, .Tag_Literal, .Tag_Literal_Triple:
return true
case .Text, .Newline, .Section_Open, .Section_Open_Inverted, .Section_Close,
.Comment, .Skip, .EOF, .Partial:
return false
}
return false
}
// When a .Partial token is encountered, we need to inject the contents
// of the partial into the current list of tokens.
template_insert_partial :: proc(
tmpl: ^Template,
token: Token,
offset: int,
allocator := context.allocator,
) -> (err: Lexer_Error) {
partial_name := token.value
partial_content := dig(tmpl.partials, []string{partial_name})
partial_str, _ := any_to_string(partial_content)
lexer := lexer_make(allocator)
lexer.src = partial_str
lexer.line = token.pos.line
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Performs any indentation on the .Partial that we are inserting.
//
// Example: use the first Token as the indentation for the .Partial Token.
// [Token{type=.Text, value=" "}, Token{type=.Partial, value="to_add"}]
//
standalone := lexer_token_is_standalone_partial(tmpl.lexer, token)
if offset > 0 && standalone {
prev_token := tmpl.lexer.tokens[offset-1]
if prev_token.type == .Text && is_text_blank(prev_token.value) {
cur_line := lexer.tokens[len(lexer.tokens)-1].pos.line
#reverse for t, i in lexer.tokens {
// Do not indent the top line.
if cur_line == 0 {
break
}
// When moving back up a line, insert the indentation.
if cur_line != t.pos.line {
inject_at(&lexer.tokens, i+1, prev_token)
}
cur_line = t.pos.line
}
}
}
// Inject tokens from the partial into the primary template.
#reverse for t in lexer.tokens {
inject_at(&tmpl.lexer.tokens, offset+1, t)
}
return nil
}
// Inject a chunk of text into the token list of the larger layout template.
template_insert_content_into_layout :: proc(
tmpl: ^Template,
token: Token,
offset: int,
content: string,
allocator := context.allocator,
) -> (err: Lexer_Error) {
lexer := lexer_make(allocator)
lexer.src = content
lexer.line = token.pos.line
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Performs indentation on the content.
if offset > 0 {
prev_token := tmpl.lexer.tokens[offset-1]
if prev_token.type == .Text && is_text_blank(prev_token.value) {
cur_line := lexer.tokens[len(lexer.tokens)-1].pos.line
#reverse for t, i in lexer.tokens {
// Do not indent the top line.
if cur_line == 0 {
break
}
// When moving back up a line, insert the indentation.
if cur_line != t.pos.line {
inject_at(&lexer.tokens, i+1, prev_token)
}
cur_line = t.pos.line
}
}
}
// Inject tokens from the partial into the primary template.
#reverse for t in lexer.tokens {
inject_at(&tmpl.lexer.tokens, offset+1, t)
}
return nil
}
template_eat_tokens :: proc(
tmpl: ^Template,
sb: ^strings.Builder,
allocator := context.allocator,
) {
root: Context_Stack_Entry
root.label = "ROOT"
root.data = tmpl.data
inject_at(&tmpl.context_stack, 0, root)
// First pass to find all the whitespace/newline elements that should be skipped.
// This is performed up-front due to partial templates -- we cannot check for the
// whitespace logic *after* the partials have been injected into the template.
for &t in tmpl.lexer.tokens {
if lexer_token_should_skip(tmpl.lexer, t) {
t.type = .Skip
}
}
// Second pass to render the template.
i := 0
for i < len(tmpl.lexer.tokens) {
defer { i += 1 }
t := tmpl.lexer.tokens[i]
switch t.type {
case .Newline, .Text, .Tag, .Tag_Literal, .Tag_Literal_Triple:
if template_token_is_valid(tmpl, t) {
strings.write_string(sb, token_content(tmpl, t, allocator))
}
case .Section_Open, .Section_Open_Inverted:
template_add_to_context_stack(tmpl, t, i, allocator)
case .Section_Close:
template_pop_from_context_stack(tmpl)
// If we are in a loop and have iterations remaining, jump back to
// the token at the start of the loop.
if t.iters > 0 {
t.iters -= 1
tmpl.lexer.tokens[i] = t
i = t.start_i
}
case .Partial:
template_insert_partial(tmpl, t, i, allocator)
// Do nothing for these tags.
case .Comment, .Skip, .EOF:
}
}
}
template_render :: proc(
tmpl: ^Template,
allocator := context.allocator,
) -> (output: string, err: Render_Error) {
sb := strings.builder_make(allocator)
template_eat_tokens(tmpl, &sb, allocator)
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)
}
}
template_eat_tokens(layout_template, &sbl, allocator)
rendered = strings.to_string(sbl)
}
return rendered, nil
}
/*
DATA-SPECIFIC PROCEDURES
*/
// Gets the length of a given object.
data_len :: proc(obj: any) -> (l: int) {
obj := obj
if is_union(obj) {
obj = reflect.get_union_variant(obj)
}
switch data_type(obj) {
case .Struct:
l = len(reflect.struct_field_names(obj.id))
case .Map, .List, .Value:
l = reflect.length(obj)
case .Null:
}
return l
}
// Given a list of keys, access nested data inside any combination of
// maps, structs, and lists.
dig :: proc(d: any, keys: []string) -> any {
d := d
if len(keys) == 0 {
return d
}
for key in keys {
switch data_type(d) {
case .Struct:
d = struct_get(d, key)
case .Map:
d, _ = map_get(d, key)
case .List:
d = d
case .Value:
if key == "." {
d = fmt.tprintf("%v", d)
} else {
return nil
}
case .Null:
return nil
}
}
return d
}
any_to_string :: proc(obj: any) -> (s: string, err: Render_Error) {
switch data_type(obj) {
case .Struct, .Map, .List:
fmt.println("Could not convert", obj, "to printable content.")
return s, Template_Error {}
case .Value:
s = fmt.tprintf("%v", obj)
case .Null:
s = ""
}
return s, nil
}
has_key :: proc(obj: any, key: string) -> (has: bool) {
obj := obj
switch data_type(obj) {
case .Map:
return map_has_key(obj, key)
case .Struct:
if is_union(obj) {
obj = reflect.get_union_variant(obj)
}
fields := reflect.struct_field_names(obj.id)
return slice.contains(fields, key)
case .List, .Value, .Null:
return false
}
return has
}
// Get the data type of an object.
data_type :: proc(obj: any) -> Data_Type {
// Unwrap nested 'any' values (e.g. from map[string]any)
if obj.id == typeid_of(any) && obj.data != nil {
inner := (^any)(obj.data)
return data_type(inner^)
}
if reflect.is_nil(obj) {
return .Null
} else if is_struct(obj) {
return .Struct
} else if is_map(obj) {
return .Map
} else if is_list(obj) {
return .List
} else {
return .Value
}
}
// Inverts a piece of data. If it has any content, then return a
// falsey value. Otherwise, a truthful value.
invert_data :: proc(data: any, allocator := context.allocator) -> any {
s: string
switch data_type(data) {
case .Struct, .Map, .List:
if data_len(data) > 0 {
s = FALSEY
} else {
s = TRUE
}
case .Value:
if _falsey_context[fmt.tprintf("%v", data)] {
s = TRUE
} else {
s = FALSEY
}
case .Null:
s = TRUE
}
if s == "" {
s = FALSEY
}
return runtime.new_clone(s, allocator = allocator)^
}
/*
PRIMARY RENDER PROCEDURES
*/
render :: proc(
template: string,
data: any,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Parse template.
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template
template := template_make(lexer, allocator)
template.data = data
template.partials = partials
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_in_layout :: proc(
template: string,
data: any,
layout: string,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = data
template.partials = partials
template.layout = layout
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_in_layout_file :: proc(
template: string,
data: any,
layout_filename: string,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read layout file.
layout, _ := os.read_entire_file_from_path(layout_filename, allocator)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template
tmpl := template_make(lexer, allocator)
tmpl.lexer = lexer
tmpl.data = data
tmpl.partials = partials
tmpl.layout = string(layout)
s = template_render(tmpl, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename :: proc(
filename: string,
data: any,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file.
src, _ := os.read_entire_file_from_path(filename, allocator)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.lexer = lexer
template.data = data
template.partials = partials
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename_in_layout :: proc(
filename: string,
data: any,
layout: string,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file and trim the trailing newline.
src, _ := os.read_entire_file_from_path(filename, allocator)
if rune(src[len(src)-1]) == '\n' {
src = src[0:len(src)-1]
}
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = data
template.partials = partials
template.layout = layout
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename_in_layout_file :: proc(
filename: string,
data: any,
layout_filename: string,
partials: any = map[string]string {},
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file and trim the trailing newline.
src, _ := os.read_entire_file_from_path(filename, allocator)
if rune(src[len(src)-1]) == '\n' {
src = src[0:len(src)-1]
}
// Read layout file.
layout, _ := os.read_entire_file_from_path(layout_filename, allocator)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template
template := template_make(lexer, allocator)
template.data = data
template.partials = partials
template.layout = string(layout)
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_with_json :: proc(
template: string,
json_filename: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src, allocator = allocator) or_return
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_with_json_in_layout :: proc(
template: string,
json_filename: string,
layout: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src, allocator = allocator) or_return
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
template.layout = layout
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_with_json_in_layout_file :: proc(
template: string,
json_filename: string,
layout_filename: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read layout file.
layout, _ := os.read_entire_file_from_path(layout_filename, allocator)
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src, allocator = allocator) or_return
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = template
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
template.layout = string(layout)
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename_with_json :: proc(
filename: string,
json_filename: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file.
src, _ := os.read_entire_file_from_path(filename, allocator)
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src) or_return
defer json.destroy_value(json_data)
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename_with_json_in_layout :: proc(
filename: string,
json_filename: string,
layout: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file and trim the trailing newline.
src, _ := os.read_entire_file_from_path(filename, allocator)
if rune(src[len(src)-1]) == '\n' {
src = src[0:len(src)-1]
}
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src, allocator = allocator) or_return
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
template.layout = layout
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
render_from_filename_with_json_in_layout_file :: proc(
filename: string,
json_filename: string,
layout_filename: string,
allocator := context.allocator,
) -> (s: string, err: Render_Error) {
// Read template file and trim the trailing newline.
src, _ := os.read_entire_file_from_path(filename, allocator)
if rune(src[len(src)-1]) == '\n' {
src = src[0:len(src)-1]
}
// Read layout file.
layout, _ := os.read_entire_file_from_path(layout_filename, allocator)
// Load JSON.
json_src, _ := os.read_entire_file_from_path(json_filename, allocator)
json_data := json.parse(json_src, allocator = allocator) or_return
json_root := json_data.(json.Object)
// Parse template.
lexer := lexer_make(allocator)
lexer.src = string(src)
lexer.delim = CORE_DEF
lexer_parse(lexer, allocator = allocator) or_return
// Render template.
template := template_make(lexer, allocator)
template.data = json_root["data"]
template.partials = json_root["partials"]
template.layout = string(layout)
s = template_render(template, allocator) or_return
return strings.clone(s, allocator), nil
}
error :: proc(msg: string, args: ..any) -> ! {
fmt.eprint("\x1b[0;31modin-mustache Error:\x1b[0m ")
fmt.eprintf(msg, ..args)
fmt.eprint("\n")
os.exit(1)
}
_main :: proc(
template_filename: string,
json_filename: string,
layout_filename: string = "",
) -> (output: string, err: Render_Error) {
if !os.is_file(template_filename) {
return "", File_Not_Found_Error{filename=template_filename}
}
if !os.is_file(json_filename) {
return "", File_Not_Found_Error{filename=json_filename}
}
if layout_filename != "" && !os.is_file(layout_filename) {
return "", File_Not_Found_Error{filename=layout_filename}
}
if layout_filename != "" {
output = render_from_filename_with_json_in_layout_file(
template_filename,
json_filename,
layout_filename,
context.temp_allocator,
) or_return
} else {
output = render_from_filename_with_json(
template_filename,
json_filename,
context.temp_allocator,
) or_return
}
return output, nil
}
/*
Setup global vars.
*/
@(init)
init :: proc "contextless" () {
// Returns true if the value is one of the "falsey" values
// for a context.
_falsey_context[FALSEY] = true
_falsey_context["null"] = true
_falsey_context[""] = true
// Returns true if the value is one of the "falsey" values
// for a context.
_whitespace[' '] = true
_whitespace['\t'] = true
_whitespace['\r'] = true
}
main :: proc() {
defer free_all(context.temp_allocator)
when ODIN_DEBUG {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
defer mem.tracking_allocator_destroy(&track)
context.allocator = mem.tracking_allocator(&track)
}
if len(os.args) < 3 {
error("You need to pass at least paths to the template and JSON data.")
}
// If a third argument was provided, this is the layout file.
layout_file: string
if len(os.args) == 4 {
layout_file = os.args[3]
}
if output, err := _main(os.args[1], os.args[2], layout_file); err != nil {
fmt.printf("Err: %v\n", err)
os.exit(1)
} else {
fmt.eprint(output)
}
when ODIN_DEBUG {
for _, entry in track.allocation_map {
fmt.eprintf("%m leaked at %v\n", entry.location, entry.size)
}
for entry in track.bad_free_array {
fmt.eprintf("%v allocation %p was freed badly\n", entry.location, entry.memory)
}
}
}