Skip to content
Snippets Groups Projects
engine.go 11.89 KiB
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
}