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