package template
import "core:bytes"
import "core:mem"
import "core:os"
import "core:reflect"
import "core:strings"
@(private)
Field_Kind :: enum {
Text,
Value,
Block_Start,
Block_End,
}
@(private)
Field :: struct {
kind: Field_Kind,
content: string,
}
Template :: struct {
_template: []u8,
fields: [dynamic]Field,
}
@(private)
tpl_name_from_tag_value :: proc(value: string) -> (tpl_name, extra: string) {
tpl_name = value
if comma_index := strings.index_byte(tpl_name, ','); comma_index >= 0 {
tpl_name = tpl_name[:comma_index]
extra = value[1 + comma_index:]
}
return
}
Write_Error :: enum {
None = 0,
Not_A_Struct,
Not_A_Slice,
Unsupported_Type,
}
@(private)
next_key :: proc(template: ^[]u8) -> (key: string, offset: [2]int, ok: bool) {
key_start_idx := bytes.index(template^, {'{', '{'})
if key_start_idx > -1 {
key_end_idx := key_start_idx + bytes.index(template[key_start_idx:], {'}', '}'})
if key_end_idx > -1 {
key = string(template[key_start_idx + 2:key_end_idx])
offset = {key_start_idx + 2, key_end_idx}
ok = true
template^ = template[key_end_idx + 2:]
return
}
}
return string(template[:]), {}, false
}
compile :: proc {
compile_file,
compile_bytes,
}
compile_file :: proc(tpl: ^Template, filepath: string) -> bool {
raw_tpl, err := os.read_entire_file_from_path(filepath, context.allocator)
if err != nil {
return false
}
defer delete(raw_tpl)
compile_bytes(tpl, raw_tpl)
return true
}
compile_bytes :: proc(tpl: ^Template, template: []u8) {
tpl._template = bytes.clone(template)
tpl.fields = make([dynamic]Field, 0, 10)
offset := 0
current := tpl._template
for key, key_offsets in next_key(¤t) {
key_start, key_end := offset + key_offsets[0], offset + key_offsets[1]
if key_offsets[0] - 2 != 0 {
append(&tpl.fields, Field{kind = .Text, content = string(tpl._template[offset:key_start - 2])})
}
if key[0] == '#' {
append(&tpl.fields, Field{kind = .Block_Start, content = key[1:]})
} else if key[0] == '/' {
append(&tpl.fields, Field{kind = .Block_End, content = key[1:]})
} else {
append(&tpl.fields, Field{kind = .Value, content = key})
}
offset = key_end + 2
}
if offset < len(tpl._template) {
append(&tpl.fields, Field{kind = .Text, content = string(tpl._template[offset:])})
}
}
destroy :: proc(tpl: ^Template) {
delete(tpl.fields)
delete(tpl._template)
}
@(private)
write_value :: proc(b: ^strings.Builder, data: any) -> Write_Error {
ti := reflect.type_info_base(type_info_of(data.id))
#partial switch info in ti.variant {
case reflect.Type_Info_Integer:
switch d in data {
case int:
strings.write_int(b, d)
case uint:
strings.write_uint(b, d)
}
// TODO: Add num decimals
case reflect.Type_Info_Float:
switch f in data {
case f16:
strings.write_f16(b, f, 'f')
case f32:
strings.write_f32(b, f, 'f')
case f64:
strings.write_f64(b, f, 'f')
}
case reflect.Type_Info_Rune:
strings.write_rune(b, data.(rune))
case reflect.Type_Info_String:
strings.write_string(b, data.(string))
case reflect.Type_Info_Pointer:
raw_ptr, _ := reflect.as_pointer(data)
if raw_ptr != nil {
write_value(b, reflect.deref(data))
}
case:
return .Unsupported_Type
}
return .None
}
@(private)
parse_block :: proc(block: []Field, b: ^strings.Builder, data: any) {
for i := 0; i < len(block); i += 1 {
f := block[i]
key := f.content
#partial switch f.kind {
case .Text:
strings.write_string(b, f.content)
case .Value:
ti := reflect.type_info_base(type_info_of(data.id))
if info, ok := ti.variant.(reflect.Type_Info_Struct); ok {
for name, u in info.names[:info.field_count] {
tpl_name, _ := tpl_name_from_tag_value(
reflect.struct_tag_get(reflect.Struct_Tag(info.tags[u]), "tpl"),
)
if tpl_name == "-" {
continue
}
if tpl_name == key || name == key {
data := uintptr(data.data) + info.offsets[u]
write_value(b, any{rawptr(data), info.types[u].id})
break
}
}
} else {
write_value(b, data)
}
case .Block_Start:
ti := reflect.type_info_base(type_info_of(data.id))
if info, ok := ti.variant.(reflect.Type_Info_Struct); ok {
for name, u in info.names[:info.field_count] {
tpl_name, _ := tpl_name_from_tag_value(
reflect.struct_tag_get(reflect.Struct_Tag(info.tags[u]), "tpl"),
)
if tpl_name == "-" {
continue
}
if tpl_name == key || name == key {
data := uintptr(data.data) + info.offsets[u]
a := any{rawptr(data), info.types[u].id}
block_end := -1
for j in i ..< len(block) {
if block[j].kind == .Block_End && block[j].content == f.content {
block_end = j
break
}
}
if block_end == -1 {
continue
}
#partial switch block_info in info.types[u].variant {
case reflect.Type_Info_Slice:
slice := cast(^mem.Raw_Slice)data
for si in 0 ..< slice.len {
slice_data := uintptr(slice.data) + uintptr(si * block_info.elem_size)
parse_block(block[i + 1:block_end], b, any{rawptr(slice_data), block_info.elem.id})
}
case reflect.Type_Info_Struct:
parse_block(block[i + 1:block_end], b, a)
case reflect.Type_Info_Pointer:
raw_ptr, _ := reflect.as_pointer(a)
if raw_ptr != nil {
parse_block(block[i + 1:block_end], b, reflect.deref(a))
}
}
i = block_end
break
}
}
}
}
}
}
render :: proc(tpl: ^Template, b: ^strings.Builder, data: any) {
parse_block(tpl.fields[:], b, data)
}