package mustache import "base:runtime" import "core:fmt" import "core:reflect" import "core:strconv" import "core:strings" // base_value unwraps union variants and strips Named/Distinct layers, // returning the "peeled" any value and its base type info. base_value :: proc(a: any) -> (val: any, info: ^runtime.Type_Info) { if a == nil { return } val = a info = nil ti := type_info_of(val.id) if ti == nil { return } base := runtime.type_info_base(ti) if _, ok := base.variant.(runtime.Type_Info_Union); ok { variant := reflect.get_union_variant(val) if variant == nil { return {}, nil } else { return base_value(variant) } } info = base return } // lookup_in resolves a single key in a container (struct or map). // Returns found=false if the key doesn't exist or the container is not a struct/map. lookup_in :: proc(container: any, key: string) -> (result: any, found: bool) { if container == nil { return } val, info := base_value(container) if info == nil { return } #partial switch v in info.variant { case runtime.Type_Info_Struct: result = reflect.struct_field_value_by_name(val, key, allow_using = true) found = result != nil case runtime.Type_Info_Map: mi := v rm_ptr := (^runtime.Raw_Map)(val.data) if rm_ptr.len == 0 { return } k := key seed := runtime.map_seed(rm_ptr^) h := mi.map_info.key_hasher(&k, seed) value_ptr := runtime.__dynamic_map_get(rm_ptr, mi.map_info, h, &k) if value_ptr != nil { if _, ok := mi.value.variant.(runtime.Type_Info_Any); ok { result = (^any)(value_ptr)^ } else { result = any{value_ptr, mi.value.id} } found = true } } return } // resolve_name resolves a (possibly dotted) name from the context stack. // The first segment walks the stack top-to-bottom; remaining segments // resolve against the prior result only. resolve_name :: proc(name: string, ctx: []any) -> any { if name == "." { if len(ctx) > 0 { return ctx[len(ctx) - 1] } else { return nil } } parts: [16]string part_count := 0 start := 0 for i in 0 ..< len(name) { if name[i] == '.' { if part_count < len(parts) { parts[part_count] = name[start:i] part_count += 1 } start = i + 1 } } if part_count < len(parts) { parts[part_count] = name[start:] part_count += 1 } if part_count == 0 { return nil } dot_parts := parts[:part_count] result: any = nil found := false for i := len(ctx) - 1; i >= 0; i -= 1 { result, found = lookup_in(ctx[i], dot_parts[0]) if found { break } } if !found { return nil } if len(dot_parts) == 1 { return result } for i := 1; i < len(dot_parts); i += 1 { result, found = lookup_in(result, dot_parts[i]) if !found { return nil } } return result } collect_map_keys :: proc(container: any, allocator := context.temp_allocator) -> []string { val, info := base_value(container) if info == nil do return nil if _, ok := info.variant.(runtime.Type_Info_Map); !ok { return nil } out := make([dynamic]string, 0, 4, allocator) it := 0 for { key, _ := reflect.iterate_map(val, &it) or_break key_str := key.(string) or_continue append(&out, key_str) } return out[:] } // is_truthy checks mustache truthiness. is_truthy :: proc(a: any) -> bool { if a == nil { return false } val, info := base_value(a) if info == nil { return false } if reflect.is_nil(val) { return false } #partial switch _ in info.variant { case runtime.Type_Info_Slice, runtime.Type_Info_Dynamic_Array, runtime.Type_Info_Map: return reflect.length(val) > 0 case: return true } } call_interp_lambda :: proc(val: any) -> (result: string, ok: bool) { switch v in val { case proc() -> string: return v(), true case proc() -> int: return fmt.tprintf("%d", v()), true case proc() -> bool: return "true" if v() else "false", true case: return "", false } } call_section_lambda :: proc(val: any, text: string) -> (result: string, ok: bool) { switch v in val { case proc(_: string) -> string: return v(text), true case proc(_: string) -> int: return fmt.tprintf("%d", v(text)), true case proc(_: string) -> bool: return "true" if v(text) else "false", true case: return "", false } } // list_info returns element type info, count, and data pointer for a list value. // Returns elem_info=nil if the value is not a list. list_info :: proc(a: any) -> (elem_info: ^runtime.Type_Info, count: int, data: rawptr) { val, info := base_value(a) if info == nil { return } #partial switch v in info.variant { case runtime.Type_Info_Slice: raw := (^runtime.Raw_Slice)(val.data)^ return v.elem, raw.len, raw.data case runtime.Type_Info_Dynamic_Array: raw := (^runtime.Raw_Dynamic_Array)(val.data)^ return v.elem, raw.len, raw.data case runtime.Type_Info_Array: return v.elem, v.count, val.data case: return nil, 0, nil } } // extract_list_element returns the n-th element of a list (described by // elem_info/data from list_info) as a ready-to-use any. // // Unwraps [dynamic]any element types so downstream lookup_in sees the inner // value's real type, not a double-wrapped any-of-any. extract_list_element :: proc(elem_info: ^runtime.Type_Info, data: rawptr, n: int) -> any { elem_ptr := rawptr(uintptr(data) + uintptr(n) * uintptr(elem_info.size)) if _, is_any := elem_info.variant.(runtime.Type_Info_Any); is_any { return (^any)(elem_ptr)^ } return any{elem_ptr, elem_info.id} } // any_to_string converts a scalar value to a string using the temp allocator. any_to_string :: proc(a: any) -> string { if a == nil { return "" } val, _ := base_value(a) switch v in val { case string: return v case bool: return "true" if v else "false" case i64: return fmt.tprintf("%d", v) case f64: // return fmt.tprintf("%.3f", v) return format_f64(v) case int: return fmt.tprintf("%d", v) case: return "" } } // format_f64 produces the shortest string that round-trips to the same f64. // Works around Odin's strconv not implementing shortest representation. format_f64 :: proc(v: f64) -> string { buf: [64]byte for prec in 1 ..= 17 { s := strconv.write_float(buf[:], v, 'g', prec, 64) if len(s) > 0 && s[0] == '+' { s = s[1:] } parsed, ok := strconv.parse_f64(s) if ok && parsed == v { return strings.clone(s, context.temp_allocator) } } s := strconv.write_float(buf[:], v, 'g', -1, 64) if len(s) > 0 && s[0] == '+' { s = s[1:] } return strings.clone(s, context.temp_allocator) } // write_value stringifies a value and writes it to the builder, // optionally HTML-escaped. write_value :: proc(b: ^strings.Builder, a: any, escape: bool) { s := any_to_string(a) if len(s) == 0 { return } if !escape { strings.write_string(b, s) return } start := 0 for i in 0 ..< len(s) { switch s[i] { case '&', '<', '>', '"': if i > start { strings.write_string(b, s[start:i]) } switch s[i] { case '&': strings.write_string(b, "&") case '<': strings.write_string(b, "<") case '>': strings.write_string(b, ">") case '"': strings.write_string(b, """) } start = i + 1 } } if start < len(s) { strings.write_string(b, s[start:]) } }