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) }