From d8a8ed9b093be57eee98adcc25eb13f60efee4fa Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sun, 18 Dec 2022 15:54:47 +0100
Subject: [PATCH] feat: basic feature

---
 engine/engine.go      | 452 ++++++++++++++++++++++++++++++++++++------
 engine/engine_test.go | 163 ++++++++++++---
 2 files changed, 530 insertions(+), 85 deletions(-)

diff --git a/engine/engine.go b/engine/engine.go
index c4a643a..8da38d5 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -3,6 +3,7 @@ package engine
 import (
 	"fmt"
 	"github.com/andybalholm/cascadia"
+	"gitlab.schukai.com/oss/libraries/go/utilities/data.git"
 	"golang.org/x/net/html"
 	"io"
 	"reflect"
@@ -14,8 +15,8 @@ const attributePrefix = "data-"
 type Engine struct {
 	attributePrefix string
 	errors          []error
-	data            *map[string]any
 	logNode         *html.Node
+	transformer     *data.Transformer
 }
 
 func (e *Engine) SetLogNode(node *html.Node) *Engine {
@@ -36,10 +37,11 @@ func (e *Engine) GetLogNode() *html.Node {
 	return e.logNode
 }
 
-func New(data *map[string]any) *Engine {
+func New(dataset map[any]any) *Engine {
+
 	return &Engine{
 		attributePrefix: attributePrefix,
-		data:            data,
+		transformer:     data.NewTransformer(&dataset),
 	}
 }
 
@@ -56,7 +58,7 @@ func (e *Engine) ProcessNode(node *html.Node) *Engine {
 		return e
 	}
 
-	if node.Type == html.ElementNode {
+	if node.Type == html.ElementNode || node.Type == html.DocumentNode {
 		e.ProcessElement(node)
 	}
 	return e
@@ -71,17 +73,19 @@ func functionExists(obj interface{}, funcName string) bool {
 	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) 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 {
+	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) ||
@@ -90,22 +94,366 @@ func (e *Engine) ProcessElement(node *html.Node) *Engine {
 		}
 
 		fktName := attr.Key[len(e.attributePrefix):]
-		fktName = buildFunctionName(fktName, true)
-
-		fmt.Println(fktName)
-		if !functionExists(e, fktName) {
-			//			e.errors = append(e.errors, &UnsupportedFunctionError{Message: "Unsupported function", FunctionName: fktName})
+		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
 		}
 
-		e.callFunction(fktName, node, attr.Val)
+	}
 
+	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, make(map[*html.Node]*html.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>") {
@@ -165,7 +513,7 @@ func (e *Engine) ProcessHtml(w io.Writer, r io.Reader) *Engine {
 		return e
 	}
 
-	e.ProcessNode(doc)
+	e.ProcessNodes(doc)
 
 	if isFragment {
 
@@ -218,9 +566,10 @@ func (e *Engine) walkNodes(n *html.Node) *Engine {
 		return e
 	}
 
-	for c := n.FirstChild; c != nil; c = c.NextSibling {
+	for c := n.FirstChild; c != nil; {
+		//ns := c.NextSibling
 		e.ProcessNode(c)
-		e.walkNodes(c)
+		c = c.NextSibling
 	}
 
 	return e
@@ -237,7 +586,9 @@ func (e *Engine) ProcessNodes(node *html.Node) *Engine {
 	return e
 }
 
-func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine {
+func (e *Engine) processAttributes(node *html.Node, attr html.Attribute) *Engine {
+
+	value := attr.Val
 
 	value = strings.TrimSpace(value)
 	values := strings.Split(value, ",")
@@ -264,7 +615,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine {
 		key := v[:pos]
 		instruction := v[pos+1:]
 
-		cVal := e.getValue(instruction)
+		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 = append(node.Attr, html.Attribute{
 			Key: key,
 			Val: cVal,
@@ -275,50 +636,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine {
 	return e
 }
 
-func (e *Engine) getValue(instruction string) string {
-	return ""
-}
+func removeAttribute(attrs []html.Attribute, key string) []html.Attribute {
 
-var uppercaseAcronym = map[string]string{
-	"ID": "id",
-}
+	var result []html.Attribute
 
-// Converts a string to CamelCase
-func buildFunctionName(s string, initCase bool) string {
-	s = strings.TrimSpace(s)
-	if s == "" {
-		return s
-	}
-	if a, ok := uppercaseAcronym[s]; ok {
-		s = a
-	}
-
-	n := strings.Builder{}
-	n.Grow(len(s))
-	capNext := initCase
-	for i, v := range []byte(s) {
-		vIsCap := v >= 'A' && v <= 'Z'
-		vIsLow := v >= 'a' && v <= 'z'
-		if capNext {
-			if vIsLow {
-				v += 'A'
-				v -= 'a'
-			}
-		} else if i == 0 {
-			if vIsCap {
-				v += 'a'
-				v -= 'A'
-			}
-		}
-		if vIsCap || vIsLow {
-			n.WriteByte(v)
-			capNext = false
-		} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
-			n.WriteByte(v)
-			capNext = true
-		} else {
-			capNext = v == '_' || v == ' ' || v == '-' || v == '.'
+	for _, attr := range attrs {
+		if attr.Key == key {
+			continue
 		}
+
+		result = append(result, attr)
 	}
-	return "Process" + n.String()
+
+	return result
 }
diff --git a/engine/engine_test.go b/engine/engine_test.go
index f8335ce..222fe6c 100644
--- a/engine/engine_test.go
+++ b/engine/engine_test.go
@@ -18,13 +18,13 @@ func TestProcessNode(t *testing.T) {
 		html      string
 		hasErrors bool
 		expected  string
-		data      map[string]any
+		data      map[any]any
 	}{
 		{
 			html:      `<div data-test="test"></div>`,
 			hasErrors: false,
 			expected:  `<div data-test="test"></div>`,
-			data: map[string]any{
+			data: map[any]any{
 				"test": "test",
 			},
 		},
@@ -32,14 +32,14 @@ func TestProcessNode(t *testing.T) {
 			html:      `<div data-test="test"></div>`,
 			hasErrors: false,
 			expected:  `<div data-test="test"></div>`,
-			data: map[string]any{
+			data: map[any]any{
 				"test": "test",
 			},
 		},
 	}
 
 	for _, td := range testData {
-		e := New(&td.data)
+		e := New(td.data)
 		doc, _ := html.Parse(strings.NewReader(td.html))
 		e.ProcessNode(doc)
 		assert.Equal(t, td.hasErrors, e.HasErrors())
@@ -54,42 +54,42 @@ func TestProcessHtml(t *testing.T) {
 		html      string
 		hasErrors bool
 		expected  string
-		data      map[string]any
+		data      map[any]any
 	}{
 		{
 			html:      `<div data-test="test"></div>`,
 			hasErrors: false,
 			expected:  `<div data-test="test"></div>`,
-			data:      map[string]any{},
+			data:      map[any]any{},
 		},
 		{
 			html:      `<div data-test="test"></div>`,
 			hasErrors: false,
 			expected:  `<div data-test="test"></div>`,
-			data:      map[string]any{},
+			data:      map[any]any{},
 		},
 		{
 			html:      `<li>test1</li><li>test2</li>`,
 			hasErrors: false,
 			expected:  `<li>test1</li><li>test2</li>`,
-			data:      map[string]any{},
+			data:      map[any]any{},
 		},
 		{
 			html:      `<tr><td>test1</td><td>test2</td></tr><tr><td>test1</td><td>test2</td></tr>`,
 			hasErrors: false,
 			expected:  `<tr><td>test1</td><td>test2</td></tr><tr><td>test1</td><td>test2</td></tr>`,
-			data:      map[string]any{},
+			data:      map[any]any{},
 		},
 		{
 			html:      `<td>test1</td><td>test2</td>`,
 			hasErrors: false,
 			expected:  `<td>test1</td><td>test2</td>`,
-			data:      map[string]any{},
+			data:      map[any]any{},
 		},
 	}
 
 	for _, td := range testData {
-		e := New(&td.data)
+		e := New(td.data)
 
 		stringWriter := &strings.Builder{}
 		e.ProcessHtml(stringWriter, strings.NewReader(td.html))
@@ -133,21 +133,138 @@ func TestGetAttributePrefix(t *testing.T) {
 	assert.Equal(t, "test", e.GetAttributePrefix())
 }
 
-func TestProcessNodes(t *testing.T) {
-	e := New(nil)
-	doc, _ := html.Parse(strings.NewReader(`<div data-test="test">
-<ul data-attributes="class static:hello">
-<li>test1</li>
-<li>test2</li>
-<li><a href="test"></a></li>
-</ul>
+func TestProcessNodesSingleForDebug(t *testing.T) {
+
+	html := `<div><div data-repeat="city path:cities"><p data-replace="path:city.name"></p></div></div>`
+	expected := `<div><div><p>London</p></div><div><p>Paris</p></div></div>`
+
+	mapData := map[any]any{
+		"cities": []map[any]any{
+			map[any]any{
+				"name":       "London",
+				"population": 1000000,
+			},
+			map[any]any{
+				"name":       "Paris",
+				"population": 2000000,
+			},
+		},
+	}
 
-</div>`))
-	e.ProcessNodes(doc)
+	e := New(mapData)
 
 	stringWriter := &strings.Builder{}
-	html.Render(stringWriter, doc)
-	assert.Equal(t, `<div data-test="test">`, stringWriter.String())
+	e.ProcessHtml(stringWriter, strings.NewReader(html))
+	processedDoc := stringWriter.String()
+	assert.Equal(t, expected, processedDoc)
+
+	if e.HasErrors() {
+		for _, err := range e.GetErrors() {
+			t.Log(err)
+		}
+		t.Fail()
+	}
+
+}
+
+func TestProcessNodes(t *testing.T) {
+
+	testData := []struct {
+		html      string
+		hasErrors bool
+		expected  string
+	}{
+		{
+			html:      `<div><div data-repeat="city path:cities"><p data-replace="path:city.name"></p></div></div>`,
+			hasErrors: false,
+			expected:  `<div><div><p>London</p></div><div><p>Paris</p></div></div>`,
+		},
+		{
+			html:      `<div><div data-replace="path:test"></div></div>`,
+			hasErrors: false,
+			expected:  `<div><div>test</div></div>`,
+		},
+		{
+			html:      `<div><div data-replace-self="path:test"></div></div>`,
+			hasErrors: false,
+			expected:  `<div>test</div>`,
+		},
+		{
+			html:      `<div><div data-attributes="class path:test | toupper"></div></div>`,
+			hasErrors: false,
+			expected:  `<div><div class="TEST"></div></div>`,
+		},
+		{
+			html:      `<div><div data-attributes="class path:test | toupper, style path:cities.0.name | lowercase"></div></div>`,
+			hasErrors: false,
+			expected:  `<div><div class="TEST" style="london"></div></div>`,
+		},
+		{
+			html:      `<div><div data-repeat="sub path:list"><p data-replace-self="index:sub"></p></div></div>`,
+			hasErrors: false,
+			expected:  `<div><div>test1</div><div>test2</div><div>test3</div></div>`,
+		},
+	}
+
+	for _, td := range testData {
+
+		t.Run("test", func(t *testing.T) {
+
+			mapData := map[any]any{
+				"test": "test",
+				"cities": []map[any]any{
+					map[any]any{
+						"name":       "London",
+						"population": 1000000,
+					},
+					map[any]any{
+						"name":       "Paris",
+						"population": 2000000,
+					},
+				},
+				"list": []string{"test1", "test2", "test3"},
+			}
+
+			e := New(mapData)
+
+			stringWriter := &strings.Builder{}
+			e.ProcessHtml(stringWriter, strings.NewReader(td.html))
+			processedDoc := stringWriter.String()
+			assert.Equal(t, td.expected, processedDoc)
+
+			if e.HasErrors() {
+				for _, err := range e.GetErrors() {
+					t.Log(err)
+				}
+				t.Fail()
+			}
+
+		})
+	}
+
+}
+
+func BenchmarkProcessNodes(b *testing.B) {
+
+	mapData := map[any]any{
+		"test": "test",
+		"cities": []map[any]any{
+			map[any]any{
+				"name":       "London",
+				"population": 1000000,
+			},
+			map[any]any{
+				"name":       "Paris",
+				"population": 2000000,
+			},
+		},
+	}
+
+	for i := 0; i < b.N; i++ {
+		e := New(mapData)
+		stringWriter := &strings.Builder{}
+		e.ProcessHtml(stringWriter, strings.NewReader(`<div><div data-repeat="city path:cities"><p data-replace="path:city.name"></p></div></div>`))
+	}
 
 }
 
-- 
GitLab