package engine import ( "fmt" "github.com/andybalholm/cascadia" "gitlab.schukai.com/oss/libraries/go/utilities/data.git" "golang.org/x/net/html" "io" "reflect" "strings" ) const attributePrefix = "data-" type Engine struct { attributePrefix string errors []error logNode *html.Node transformer *data.Transformer } func (e *Engine) SetLogNode(node *html.Node) *Engine { e.logNode = node return e } func (e *Engine) SetAttributePrefix(prefix string) *Engine { e.attributePrefix = prefix return e } func (e *Engine) GetAttributePrefix() string { return e.attributePrefix } func (e *Engine) GetLogNode() *html.Node { return e.logNode } func New(dataset map[any]any) *Engine { return &Engine{ attributePrefix: attributePrefix, transformer: data.NewTransformer(&dataset), } } func (e *Engine) HasErrors() bool { return len(e.errors) > 0 } func (e *Engine) GetErrors() []error { return e.errors } func (e *Engine) ProcessNode(node *html.Node) *Engine { if node == nil { return e } if node.Type == html.ElementNode || node.Type == html.DocumentNode { e.ProcessElement(node) } return e } func functionExists(obj interface{}, funcName string) bool { m := reflect.ValueOf(obj).MethodByName(funcName) if m.IsValid() { return true } return false } //func (e *Engine) callFunction(funcName string, node *html.Node, value string) *Engine { // reflect.ValueOf(e).MethodByName(funcName).Call([]reflect.Value{reflect.ValueOf(node), reflect.ValueOf(value)}) // return e //} func (e *Engine) ProcessElement(node *html.Node) *Engine { if node.Type != html.ElementNode && node.Type != html.DocumentNode { e.errors = append(e.errors, &UnsupportedTypeError{Message: "Unsupported element type", NodeType: node.Type}) return e } runChildren := true for _, attr := range node.Attr { if attr.Namespace != "" || len(attr.Key) < len(e.attributePrefix) || attr.Key[:len(e.attributePrefix)] != e.attributePrefix { continue } fktName := attr.Key[len(e.attributePrefix):] fktName = strings.ToLower(fktName) switch fktName { case "repeat": runChildren = false node.Attr = removeAttribute(node.Attr, attr.Key) e.processRepeat(node, attr) case "debug": node.Attr = removeAttribute(node.Attr, attr.Key) e.processDebug(node, attr) case "attributes": node.Attr = removeAttribute(node.Attr, attr.Key) e.processAttributes(node, attr) case "condition": node.Attr = removeAttribute(node.Attr, attr.Key) e.processCondition(node, attr) case "replace": node.Attr = removeAttribute(node.Attr, attr.Key) e.processReplace(node, attr) case "replace-self": node.Attr = removeAttribute(node.Attr, attr.Key) e.processReplaceSelf(node, attr) case "remove": node.Attr = removeAttribute(node.Attr, attr.Key) e.processRemove(node, attr) case "removetag": node.Attr = removeAttribute(node.Attr, attr.Key) e.processRemoveTag(node, attr) default: continue } } if runChildren { e.walkNodes(node) } return e } func (e *Engine) processRepeat(node *html.Node, attr html.Attribute) *Engine { parent := node.Parent if parent == nil { e.errors = append(e.errors, &UnsupportedTypeError{Message: "Node has no parent", NodeType: node.Type}) return e } v := attr.Val if v == "" { return e } p := strings.Index(v, " ") if p == -1 { return e } key := v[:p] instruction := v[p+1:] iterator, err := e.transformer.Transform(instruction) if err != nil { e.errors = append(e.errors, err) return e } var nextSibling *html.Node for child := parent.FirstChild; child != nil; child = child.NextSibling { if child == node { nextSibling = node.NextSibling parent.RemoveChild(node) break } } data := e.transformer.Dataset() if data == nil { data = &map[any]any{} } var errors []error switch reflect.TypeOf(iterator).Kind() { case reflect.Map: listValue := reflect.ValueOf(iterator) for _, key := range listValue.MapKeys() { item := listValue.MapIndex(key).Interface() errors = runNode(node, data, key.String(), item, nextSibling, parent) } case reflect.Slice: listValue := reflect.ValueOf(iterator) for i := 0; i < listValue.Len(); i++ { item := listValue.Index(i).Interface() errors = runNode(node, data, key, item, nextSibling, parent) } default: e.errors = append(e.errors, &UnsupportedTypeError{Message: "Unsupported iterator type", NodeType: node.Type}) } if errors != nil { e.errors = append(e.errors, errors...) } return e } func runNode(node *html.Node, data *map[any]any, key string, item interface{}, nextSibling *html.Node, parent *html.Node) []error { _, ok := (*data)[key] if ok { return []error{&UnsupportedTypeError{Message: "Key already exists", NodeType: node.Type}} } (*data)[key] = item defer delete(*data, key) x := New(*data) template := CloneNode(node) template.Parent = nil template.PrevSibling = nil template.NextSibling = nil x.ProcessNodes(template) if nextSibling != nil { parent.InsertBefore(template, nextSibling) nextSibling = template } else { parent.AppendChild(template) } return x.errors } func cloneNode(n *html.Node, cache map[*html.Node]*html.Node) *html.Node { if n == nil { return nil } if val, ok := cache[n]; ok { return val } val := &html.Node{} cache[n] = val val.Type = n.Type val.Data = n.Data val.DataAtom = n.DataAtom val.Namespace = n.Namespace val.Attr = make([]html.Attribute, len(n.Attr)) copy(val.Attr, n.Attr) for child := n.FirstChild; child != nil; child = child.NextSibling { val.AppendChild(cloneNode(child, cache)) } return val } func (e *Engine) processReplaceSelf(node *html.Node, attr html.Attribute) *Engine { replace := attr.Val if replace == "" { return e } r, err := e.transformer.Transform(replace) if err != nil { e.errors = append(e.errors, err) return e } if node.Parent == nil { e.errors = append(e.errors, &UnsupportedTypeError{Message: "Node has no parent", NodeType: node.Type}) return e } parent := node.Parent parent.InsertBefore(&html.Node{ Type: html.TextNode, Data: r.(string), }, node) parent.RemoveChild(node) return e } func (e *Engine) processReplace(node *html.Node, attr html.Attribute) *Engine { replace := attr.Val if replace == "" { return e } r, err := e.transformer.Transform(replace) if err != nil { e.errors = append(e.errors, err) return e } for child := node.FirstChild; child != nil; child = child.NextSibling { node.RemoveChild(child) } nn := &html.Node{ Type: html.TextNode, } switch r.(type) { case string: nn.Data = r.(string) default: nn.Data = fmt.Sprintf("%v", r) } node.AppendChild(nn) return e } func (e *Engine) processRemove(node *html.Node, attr html.Attribute) *Engine { if node.Parent == nil { e.errors = append(e.errors, &UnsupportedTypeError{Message: "Node has no parent", NodeType: node.Type}) return e } var condition any var err error v := attr.Val if v != "" { condition, err = e.transformer.Transform(v) if err != nil { e.errors = append(e.errors, err) return e } switch condition.(type) { case bool: if !condition.(bool) { return e } } } removeNode(node, e, v) return e } func (e *Engine) processRemoveTag(node *html.Node, attr html.Attribute) *Engine { parent := node.Parent var condition any var err error v := attr.Val if v != "" { condition, err = e.transformer.Transform(v) if err != nil { e.errors = append(e.errors, err) return e } switch condition.(type) { case bool: if !condition.(bool) { return e } } } if parent == nil { prefix := e.attributePrefix node.Attr = append(node.Attr, html.Attribute{ Key: prefix + "condition-hide", Val: v, }) return e } for node.FirstChild != nil { child := node.FirstChild node.RemoveChild(child) parent.InsertBefore(child, node) } parent.RemoveChild(node) return e } func (e *Engine) processDebug(node *html.Node, attr html.Attribute) *Engine { e.logNode = node return e } func (e *Engine) processCondition(node *html.Node, attr html.Attribute) *Engine { condition := attr.Val if condition == "" { return e } r, err := e.transformer.Transform(condition) if err != nil { e.errors = append(e.errors, err) return e } shouldRemove := true switch r.(type) { case bool: if r == true { shouldRemove = false } } if shouldRemove { removeNode(node, e, condition) } return e } func removeNode(node *html.Node, e *Engine, condition string) { if node.Parent != nil { node.Parent.RemoveChild(node) } else { prefix := e.attributePrefix node.Attr = append(node.Attr, html.Attribute{ Key: prefix + "condition-hide", Val: condition, }) } } func isHtmlFragment(html string) bool { if strings.HasPrefix(html, "<!DOCTYPE html>") { return false } if strings.HasPrefix(html, "<html") { return false } return true } func (e *Engine) ProcessHtml(w io.Writer, r io.Reader) *Engine { var err error content, err := io.ReadAll(r) if err != nil { e.errors = append(e.errors, err) return e } stringContent := string(content) isFragment := isHtmlFragment(stringContent) var doc *html.Node selector := "body" if isFragment { c := strings.TrimSpace(stringContent) if strings.HasPrefix(c, "<tr") { selector = "table > tbody" c = "<table>" + c } if strings.HasSuffix(c, "</tr>") { c = c + "</table>" } if strings.HasPrefix(c, "<td") { selector = "table > tbody > tr" c = "<table><tr>" + c } if strings.HasSuffix(c, "</td>") { c = c + "</tr></table>" } stringContent = c } doc, err = html.Parse(strings.NewReader(stringContent)) if err != nil { e.errors = append(e.errors, err) return e } e.ProcessNodes(doc) if isFragment { sel, err := cascadia.Compile(selector) if err != nil { e.errors = append(e.errors, err) return e } body := sel.MatchFirst(doc) stringBuilder := &strings.Builder{} err = html.Render(stringBuilder, body) if err != nil { e.errors = append(e.errors, err) } result := stringBuilder.String() if selector == "body" { result = strings.TrimPrefix(result, "<body>") result = strings.TrimSuffix(result, "</body>") } if selector == "table > tbody" { result = strings.TrimPrefix(result, "<tbody>") result = strings.TrimSuffix(result, "</tbody>") } if selector == "table > tbody > tr" { result = strings.TrimPrefix(result, "<tr>") result = strings.TrimSuffix(result, "</tr>") } w.Write([]byte(result)) return e } err = html.Render(w, doc) if err != nil { e.errors = append(e.errors, err) } return e } func (e *Engine) walkNodes(n *html.Node) *Engine { if n == nil { return e } for c := n.FirstChild; c != nil; { //ns := c.NextSibling e.ProcessNode(c) c = c.NextSibling } return e } func (e *Engine) ProcessNodes(node *html.Node) *Engine { if node == nil { return e } e.ProcessNode(node) e.walkNodes(node) return e } func (e *Engine) processAttributes(node *html.Node, attr html.Attribute) *Engine { value := attr.Val value = strings.TrimSpace(value) values := strings.Split(value, ",") if len(values) == 0 { return e } for _, v := range values { v = strings.TrimSpace(v) if v == "" { continue } pos := strings.Index(v, " ") if pos == -1 { e.errors = append(e.errors, &InvalidAttributeError{ Message: "Invalid attribute", Attribute: v, Node: node, }) continue } key := v[:pos] instruction := v[pos+1:] val, err := e.transformer.Transform(instruction) if err != nil { e.errors = append(e.errors, err) continue } var cVal string if val != nil { cVal = val.(string) } node.Attr = removeAttribute(node.Attr, key) node.Attr = append(node.Attr, html.Attribute{ Key: key, Val: cVal, }) } return e } func removeAttribute(attrs []html.Attribute, key string) []html.Attribute { var result []html.Attribute for _, attr := range attrs { if attr.Key == key { continue } result = append(result, attr) } return result }