package template import "core:bytes" import "core:mem" import "core:os" import "core:reflect" import "core:strings" import "core:unicode/utf8" @(private) Field_Kind :: enum { Text, Value, // {{name}}, HTML escaped Raw_Value, // {{&name}}, written as is, only for content that is trusted to be HTML 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])}) } switch { case len(key) > 0 && key[0] == '#': append(&tpl.fields, Field{kind = .Block_Start, content = key[1:]}) case len(key) > 0 && key[0] == '/': append(&tpl.fields, Field{kind = .Block_End, content = key[1:]}) case len(key) > 0 && key[0] == '&': append(&tpl.fields, Field{kind = .Raw_Value, content = key[1:]}) case: 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_escaped :: proc(b: ^strings.Builder, s: string) { // All of them are ASCII, so going over bytes leaves any UTF-8 as it was start := 0 for i in 0 ..< len(s) { escaped: string switch s[i] { case '&': escaped = "&" case '<': escaped = "<" case '>': escaped = ">" case '"': escaped = """ case '\'': escaped = "'" case: continue } strings.write_string(b, s[start:i]) strings.write_string(b, escaped) start = i + 1 } strings.write_string(b, s[start:]) } @(private) write_value :: proc(b: ^strings.Builder, data: any, escape: bool) -> 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: if escape { buf, n := utf8.encode_rune(data.(rune)) write_escaped(b, string(buf[:n])) } else { strings.write_rune(b, data.(rune)) } case reflect.Type_Info_String: if escape { write_escaped(b, data.(string)) } else { 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), escape) } 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, .Raw_Value: escape := f.kind == .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 key != "" && (tpl_name == key || name == key) { data := uintptr(data.data) + info.offsets[u] write_value(b, any{rawptr(data), info.types[u].id}, escape) break } } } else { write_value(b, data, escape) } 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 key != "" && (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) }