// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package vdom import ( "context" "encoding/json" "fmt" "log" "reflect" "strconv" "strings" "unicode" "github.com/wavetermdev/waveterm/pkg/util/utilfn" ) // ReactNode types = nil | string | Elem // generic hook structure type Hook struct { Init bool // is initialized Idx int // index in the hook array Fn func() func() // for useEffect UnmountFn func() // for useEffect Val any // for useState, useMemo, useRef Deps []any } type Component[P any] func(props P) *VDomElem type styleAttrWrapper struct { StyleAttr string Val any } type classAttrWrapper struct { ClassName string Cond bool } type styleAttrMapWrapper struct { StyleAttrMap map[string]any } func (e *VDomElem) Key() string { keyVal, ok := e.Props[KeyPropKey] if !ok { return "" } keyStr, ok := keyVal.(string) if ok { return keyStr } return "" } func (e *VDomElem) WithKey(key string) *VDomElem { if e == nil { return nil } if e.Props == nil { e.Props = make(map[string]any) } e.Props[KeyPropKey] = key return e } func TextElem(text string) VDomElem { return VDomElem{Tag: TextTag, Text: text} } func mergeProps(props *map[string]any, newProps map[string]any) { if *props == nil { *props = make(map[string]any) } for k, v := range newProps { if v == nil { delete(*props, k) continue } (*props)[k] = v } } func mergeStyleAttr(props *map[string]any, styleAttr styleAttrWrapper) { if *props == nil { *props = make(map[string]any) } if (*props)["style"] == nil { (*props)["style"] = make(map[string]any) } styleMap, ok := (*props)["style"].(map[string]any) if !ok { return } styleMap[styleAttr.StyleAttr] = styleAttr.Val } func mergeClassAttr(props *map[string]any, classAttr classAttrWrapper) { if *props == nil { *props = make(map[string]any) } if classAttr.Cond { if (*props)["className"] == nil { (*props)["className"] = classAttr.ClassName return } classVal, ok := (*props)["className"].(string) if !ok { return } // check if class already exists (must split, contains won't work) splitArr := strings.Split(classVal, " ") for _, class := range splitArr { if class == classAttr.ClassName { return } } (*props)["className"] = classVal + " " + classAttr.ClassName } else { classVal, ok := (*props)["className"].(string) if !ok { return } splitArr := strings.Split(classVal, " ") for i, class := range splitArr { if class == classAttr.ClassName { splitArr = append(splitArr[:i], splitArr[i+1:]...) break } } if len(splitArr) == 0 { delete(*props, "className") } else { (*props)["className"] = strings.Join(splitArr, " ") } } } func Classes(classes ...any) string { var parts []string for _, class := range classes { switch c := class.(type) { case nil: continue case string: if c != "" { parts = append(parts, c) } } // Ignore any other types } return strings.Join(parts, " ") } func H(tag string, props map[string]any, children ...any) *VDomElem { rtn := &VDomElem{Tag: tag, Props: props} if len(children) > 0 { for _, part := range children { elems := partToElems(part) rtn.Children = append(rtn.Children, elems...) } } return rtn } func E(tag string, parts ...any) *VDomElem { rtn := &VDomElem{Tag: tag} for _, part := range parts { if part == nil { continue } props, ok := part.(map[string]any) if ok { mergeProps(&rtn.Props, props) continue } if styleAttr, ok := part.(styleAttrWrapper); ok { mergeStyleAttr(&rtn.Props, styleAttr) continue } if styleAttrMap, ok := part.(styleAttrMapWrapper); ok { for k, v := range styleAttrMap.StyleAttrMap { mergeStyleAttr(&rtn.Props, styleAttrWrapper{StyleAttr: k, Val: v}) } continue } if classAttr, ok := part.(classAttrWrapper); ok { mergeClassAttr(&rtn.Props, classAttr) continue } elems := partToElems(part) rtn.Children = append(rtn.Children, elems...) } return rtn } func Class(name string) classAttrWrapper { return classAttrWrapper{ClassName: name, Cond: true} } func ClassIf(cond bool, name string) classAttrWrapper { return classAttrWrapper{ClassName: name, Cond: cond} } func ClassIfElse(cond bool, name string, elseName string) classAttrWrapper { if cond { return classAttrWrapper{ClassName: name, Cond: true} } return classAttrWrapper{ClassName: elseName, Cond: true} } func If(cond bool, part any) any { if cond { return part } return nil } func IfElse(cond bool, part any, elsePart any) any { if cond { return part } return elsePart } func ForEach[T any](items []T, fn func(T) any) []any { var elems []any for _, item := range items { fnResult := fn(item) elems = append(elems, fnResult) } return elems } func ForEachIdx[T any](items []T, fn func(T, int) any) []any { var elems []any for idx, item := range items { fnResult := fn(item, idx) elems = append(elems, fnResult) } return elems } func Filter[T any](items []T, fn func(T) bool) []T { var elems []T for _, item := range items { if fn(item) { elems = append(elems, item) } } return elems } func FilterIdx[T any](items []T, fn func(T, int) bool) []T { var elems []T for idx, item := range items { if fn(item, idx) { elems = append(elems, item) } } return elems } func Props(props any) map[string]any { m, err := utilfn.StructToMap(props) if err != nil { return nil } return m } func PStyle(styleAttr string, propVal any) any { return styleAttrWrapper{StyleAttr: styleAttr, Val: propVal} } func Fragment(parts ...any) any { return parts } func P(propName string, propVal any) any { if propVal == nil { return map[string]any{propName: nil} } if propName == "style" { strVal, ok := propVal.(string) if ok { styleMap, err := styleAttrStrToStyleMap(strVal, nil) if err == nil { return styleAttrMapWrapper{StyleAttrMap: styleMap} } log.Printf("Error parsing style attribute: %v\n", err) return nil } } return map[string]any{propName: propVal} } func getHookFromCtx(ctx context.Context) (*VDomContextVal, *Hook) { vc := getRenderContext(ctx) if vc == nil { panic("UseState must be called within a component (no context)") } if vc.Comp == nil { panic("UseState must be called within a component (vc.Comp is nil)") } for len(vc.Comp.Hooks) <= vc.HookIdx { vc.Comp.Hooks = append(vc.Comp.Hooks, &Hook{Idx: len(vc.Comp.Hooks)}) } hookVal := vc.Comp.Hooks[vc.HookIdx] vc.HookIdx++ return vc, hookVal } func UseState[T any](ctx context.Context, initialVal T) (T, func(T)) { vc, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true hookVal.Val = initialVal } var rtnVal T rtnVal, ok := hookVal.Val.(T) if !ok { panic("UseState hook value is not a state (possible out of order or conditional hooks)") } setVal := func(newVal T) { hookVal.Val = newVal vc.Root.AddRenderWork(vc.Comp.WaveId) } return rtnVal, setVal } func UseStateWithFn[T any](ctx context.Context, initialVal T) (T, func(T), func(func(T) T)) { vc, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true hookVal.Val = initialVal } var rtnVal T rtnVal, ok := hookVal.Val.(T) if !ok { panic("UseState hook value is not a state (possible out of order or conditional hooks)") } setVal := func(newVal T) { hookVal.Val = newVal vc.Root.AddRenderWork(vc.Comp.WaveId) } setFuncVal := func(updateFunc func(T) T) { hookVal.Val = updateFunc(hookVal.Val.(T)) vc.Root.AddRenderWork(vc.Comp.WaveId) } return rtnVal, setVal, setFuncVal } func UseAtom[T any](ctx context.Context, atomName string) (T, func(T)) { vc, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true closedWaveId := vc.Comp.WaveId hookVal.UnmountFn = func() { atom := vc.Root.GetAtom(atomName) delete(atom.UsedBy, closedWaveId) } } atom := vc.Root.GetAtom(atomName) atom.UsedBy[vc.Comp.WaveId] = true atomVal, ok := atom.Val.(T) if !ok { panic(fmt.Sprintf("UseAtom %q value type mismatch (expected %T, got %T)", atomName, atomVal, atom.Val)) } setVal := func(newVal T) { atom.Val = newVal for waveId := range atom.UsedBy { vc.Root.AddRenderWork(waveId) } } return atomVal, setVal } func UseVDomRef(ctx context.Context) *VDomRef { vc, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true refId := vc.Comp.WaveId + ":" + strconv.Itoa(hookVal.Idx) hookVal.Val = &VDomRef{Type: ObjectType_Ref, RefId: refId} } refVal, ok := hookVal.Val.(*VDomRef) if !ok { panic("UseRef hook value is not a ref (possible out of order or conditional hooks)") } return refVal } func UseRef[T any](ctx context.Context, val T) *VDomSimpleRef[T] { _, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true hookVal.Val = &VDomSimpleRef[T]{Current: val} } refVal, ok := hookVal.Val.(*VDomSimpleRef[T]) if !ok { panic("UseRef hook value is not a ref (possible out of order or conditional hooks)") } return refVal } func UseId(ctx context.Context) string { vc := getRenderContext(ctx) if vc == nil { panic("UseId must be called within a component (no context)") } return vc.Comp.WaveId } func UseRenderTs(ctx context.Context) int64 { vc := getRenderContext(ctx) if vc == nil { panic("UseRenderTs must be called within a component (no context)") } return vc.Root.RenderTs } func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) { if ref == nil || !ref.HasCurrent { return } vc := getRenderContext(ctx) if vc == nil { panic("QueueRefOp must be called within a component (no context)") } if op.RefId == "" { op.RefId = ref.RefId } vc.Root.QueueRefOp(op) } func depsEqual(deps1 []any, deps2 []any) bool { if len(deps1) != len(deps2) { return false } for i := range deps1 { if deps1[i] != deps2[i] { return false } } return true } func UseEffect(ctx context.Context, fn func() func(), deps []any) { // note UseEffect never actually runs anything, it just queues the effect to run later vc, hookVal := getHookFromCtx(ctx) if !hookVal.Init { hookVal.Init = true hookVal.Fn = fn hookVal.Deps = deps vc.Root.AddEffectWork(vc.Comp.WaveId, hookVal.Idx) return } if depsEqual(hookVal.Deps, deps) { return } hookVal.Fn = fn hookVal.Deps = deps vc.Root.AddEffectWork(vc.Comp.WaveId, hookVal.Idx) } func numToString[T any](value T) (string, bool) { switch v := any(value).(type) { case int: return strconv.FormatInt(int64(v), 10), true case int8: return strconv.FormatInt(int64(v), 10), true case int16: return strconv.FormatInt(int64(v), 10), true case int32: return strconv.FormatInt(int64(v), 10), true case int64: return strconv.FormatInt(v, 10), true case uint: return strconv.FormatUint(uint64(v), 10), true case uint8: return strconv.FormatUint(uint64(v), 10), true case uint16: return strconv.FormatUint(uint64(v), 10), true case uint32: return strconv.FormatUint(uint64(v), 10), true case uint64: return strconv.FormatUint(v, 10), true case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32), true case float64: return strconv.FormatFloat(v, 'f', -1, 64), true default: return "", false } } func partToElems(part any) []VDomElem { if part == nil { return nil } switch part := part.(type) { case string: return []VDomElem{TextElem(part)} case *VDomElem: if part == nil { return nil } return []VDomElem{*part} case VDomElem: return []VDomElem{part} case []VDomElem: return part case []*VDomElem: var rtn []VDomElem for _, e := range part { if e == nil { continue } rtn = append(rtn, *e) } return rtn } sval, ok := numToString(part) if ok { return []VDomElem{TextElem(sval)} } partVal := reflect.ValueOf(part) if partVal.Kind() == reflect.Slice { var rtn []VDomElem for i := 0; i < partVal.Len(); i++ { subPart := partVal.Index(i).Interface() rtn = append(rtn, partToElems(subPart)...) } return rtn } stringer, ok := part.(fmt.Stringer) if ok { return []VDomElem{TextElem(stringer.String())} } jsonStr, jsonErr := json.Marshal(part) if jsonErr == nil { return []VDomElem{TextElem(string(jsonStr))} } typeText := "invalid:" + reflect.TypeOf(part).String() return []VDomElem{TextElem(typeText)} } func isWaveTag(tag string) bool { return strings.HasPrefix(tag, "wave:") || strings.HasPrefix(tag, "w:") } func isBaseTag(tag string) bool { if len(tag) == 0 { return false } return tag[0] == '#' || unicode.IsLower(rune(tag[0])) || isWaveTag(tag) }