Skip to content
Snippets Groups Projects
Verified Commit d8a8ed9b authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: basic feature

parent 5b9aa060
No related branches found
No related tags found
No related merge requests found
...@@ -3,6 +3,7 @@ package engine ...@@ -3,6 +3,7 @@ package engine
import ( import (
"fmt" "fmt"
"github.com/andybalholm/cascadia" "github.com/andybalholm/cascadia"
"gitlab.schukai.com/oss/libraries/go/utilities/data.git"
"golang.org/x/net/html" "golang.org/x/net/html"
"io" "io"
"reflect" "reflect"
...@@ -14,8 +15,8 @@ const attributePrefix = "data-" ...@@ -14,8 +15,8 @@ const attributePrefix = "data-"
type Engine struct { type Engine struct {
attributePrefix string attributePrefix string
errors []error errors []error
data *map[string]any
logNode *html.Node logNode *html.Node
transformer *data.Transformer
} }
func (e *Engine) SetLogNode(node *html.Node) *Engine { func (e *Engine) SetLogNode(node *html.Node) *Engine {
...@@ -36,10 +37,11 @@ func (e *Engine) GetLogNode() *html.Node { ...@@ -36,10 +37,11 @@ func (e *Engine) GetLogNode() *html.Node {
return e.logNode return e.logNode
} }
func New(data *map[string]any) *Engine { func New(dataset map[any]any) *Engine {
return &Engine{ return &Engine{
attributePrefix: attributePrefix, attributePrefix: attributePrefix,
data: data, transformer: data.NewTransformer(&dataset),
} }
} }
...@@ -56,7 +58,7 @@ func (e *Engine) ProcessNode(node *html.Node) *Engine { ...@@ -56,7 +58,7 @@ func (e *Engine) ProcessNode(node *html.Node) *Engine {
return e return e
} }
if node.Type == html.ElementNode { if node.Type == html.ElementNode || node.Type == html.DocumentNode {
e.ProcessElement(node) e.ProcessElement(node)
} }
return e return e
...@@ -71,17 +73,19 @@ func functionExists(obj interface{}, funcName string) bool { ...@@ -71,17 +73,19 @@ func functionExists(obj interface{}, funcName string) bool {
return false return false
} }
func (e *Engine) callFunction(funcName string, node *html.Node, value string) *Engine { //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)}) // reflect.ValueOf(e).MethodByName(funcName).Call([]reflect.Value{reflect.ValueOf(node), reflect.ValueOf(value)})
return e // return e
} //}
func (e *Engine) ProcessElement(node *html.Node) *Engine { 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}) e.errors = append(e.errors, &UnsupportedTypeError{Message: "Unsupported element type", NodeType: node.Type})
return e return e
} }
runChildren := true
for _, attr := range node.Attr { for _, attr := range node.Attr {
if attr.Namespace != "" || if attr.Namespace != "" ||
len(attr.Key) < len(e.attributePrefix) || len(attr.Key) < len(e.attributePrefix) ||
...@@ -90,22 +94,366 @@ func (e *Engine) ProcessElement(node *html.Node) *Engine { ...@@ -90,22 +94,366 @@ func (e *Engine) ProcessElement(node *html.Node) *Engine {
} }
fktName := attr.Key[len(e.attributePrefix):] fktName := attr.Key[len(e.attributePrefix):]
fktName = buildFunctionName(fktName, true) fktName = strings.ToLower(fktName)
fmt.Println(fktName) switch fktName {
if !functionExists(e, fktName) { case "repeat":
// e.errors = append(e.errors, &UnsupportedFunctionError{Message: "Unsupported function", FunctionName: fktName}) 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 continue
} }
e.callFunction(fktName, node, attr.Val) }
if runChildren {
e.walkNodes(node)
} }
return e 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 { func isHtmlFragment(html string) bool {
if strings.HasPrefix(html, "<!DOCTYPE html>") { if strings.HasPrefix(html, "<!DOCTYPE html>") {
...@@ -165,7 +513,7 @@ func (e *Engine) ProcessHtml(w io.Writer, r io.Reader) *Engine { ...@@ -165,7 +513,7 @@ func (e *Engine) ProcessHtml(w io.Writer, r io.Reader) *Engine {
return e return e
} }
e.ProcessNode(doc) e.ProcessNodes(doc)
if isFragment { if isFragment {
...@@ -218,9 +566,10 @@ func (e *Engine) walkNodes(n *html.Node) *Engine { ...@@ -218,9 +566,10 @@ func (e *Engine) walkNodes(n *html.Node) *Engine {
return e return e
} }
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; {
//ns := c.NextSibling
e.ProcessNode(c) e.ProcessNode(c)
e.walkNodes(c) c = c.NextSibling
} }
return e return e
...@@ -237,7 +586,9 @@ func (e *Engine) ProcessNodes(node *html.Node) *Engine { ...@@ -237,7 +586,9 @@ func (e *Engine) ProcessNodes(node *html.Node) *Engine {
return e 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) value = strings.TrimSpace(value)
values := strings.Split(value, ",") values := strings.Split(value, ",")
...@@ -264,7 +615,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine { ...@@ -264,7 +615,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine {
key := v[:pos] key := v[:pos]
instruction := v[pos+1:] 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{ node.Attr = append(node.Attr, html.Attribute{
Key: key, Key: key,
Val: cVal, Val: cVal,
...@@ -275,50 +636,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine { ...@@ -275,50 +636,17 @@ func (e *Engine) ProcessAttributes(node *html.Node, value string) *Engine {
return e return e
} }
func (e *Engine) getValue(instruction string) string { func removeAttribute(attrs []html.Attribute, key string) []html.Attribute {
return ""
}
var uppercaseAcronym = map[string]string{ var result []html.Attribute
"ID": "id",
}
// Converts a string to CamelCase for _, attr := range attrs {
func buildFunctionName(s string, initCase bool) string { if attr.Key == key {
s = strings.TrimSpace(s) continue
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 == '.'
} }
result = append(result, attr)
} }
return "Process" + n.String()
return result
} }
...@@ -18,13 +18,13 @@ func TestProcessNode(t *testing.T) { ...@@ -18,13 +18,13 @@ func TestProcessNode(t *testing.T) {
html string html string
hasErrors bool hasErrors bool
expected string expected string
data map[string]any data map[any]any
}{ }{
{ {
html: `<div data-test="test"></div>`, html: `<div data-test="test"></div>`,
hasErrors: false, hasErrors: false,
expected: `<div data-test="test"></div>`, expected: `<div data-test="test"></div>`,
data: map[string]any{ data: map[any]any{
"test": "test", "test": "test",
}, },
}, },
...@@ -32,14 +32,14 @@ func TestProcessNode(t *testing.T) { ...@@ -32,14 +32,14 @@ func TestProcessNode(t *testing.T) {
html: `<div data-test="test"></div>`, html: `<div data-test="test"></div>`,
hasErrors: false, hasErrors: false,
expected: `<div data-test="test"></div>`, expected: `<div data-test="test"></div>`,
data: map[string]any{ data: map[any]any{
"test": "test", "test": "test",
}, },
}, },
} }
for _, td := range testData { for _, td := range testData {
e := New(&td.data) e := New(td.data)
doc, _ := html.Parse(strings.NewReader(td.html)) doc, _ := html.Parse(strings.NewReader(td.html))
e.ProcessNode(doc) e.ProcessNode(doc)
assert.Equal(t, td.hasErrors, e.HasErrors()) assert.Equal(t, td.hasErrors, e.HasErrors())
...@@ -54,42 +54,42 @@ func TestProcessHtml(t *testing.T) { ...@@ -54,42 +54,42 @@ func TestProcessHtml(t *testing.T) {
html string html string
hasErrors bool hasErrors bool
expected string expected string
data map[string]any data map[any]any
}{ }{
{ {
html: `<div data-test="test"></div>`, html: `<div data-test="test"></div>`,
hasErrors: false, hasErrors: false,
expected: `<div data-test="test"></div>`, expected: `<div data-test="test"></div>`,
data: map[string]any{}, data: map[any]any{},
}, },
{ {
html: `<div data-test="test"></div>`, html: `<div data-test="test"></div>`,
hasErrors: false, hasErrors: false,
expected: `<div data-test="test"></div>`, expected: `<div data-test="test"></div>`,
data: map[string]any{}, data: map[any]any{},
}, },
{ {
html: `<li>test1</li><li>test2</li>`, html: `<li>test1</li><li>test2</li>`,
hasErrors: false, hasErrors: false,
expected: `<li>test1</li><li>test2</li>`, 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>`, html: `<tr><td>test1</td><td>test2</td></tr><tr><td>test1</td><td>test2</td></tr>`,
hasErrors: false, hasErrors: false,
expected: `<tr><td>test1</td><td>test2</td></tr><tr><td>test1</td><td>test2</td></tr>`, 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>`, html: `<td>test1</td><td>test2</td>`,
hasErrors: false, hasErrors: false,
expected: `<td>test1</td><td>test2</td>`, expected: `<td>test1</td><td>test2</td>`,
data: map[string]any{}, data: map[any]any{},
}, },
} }
for _, td := range testData { for _, td := range testData {
e := New(&td.data) e := New(td.data)
stringWriter := &strings.Builder{} stringWriter := &strings.Builder{}
e.ProcessHtml(stringWriter, strings.NewReader(td.html)) e.ProcessHtml(stringWriter, strings.NewReader(td.html))
...@@ -133,21 +133,138 @@ func TestGetAttributePrefix(t *testing.T) { ...@@ -133,21 +133,138 @@ func TestGetAttributePrefix(t *testing.T) {
assert.Equal(t, "test", e.GetAttributePrefix()) assert.Equal(t, "test", e.GetAttributePrefix())
} }
func TestProcessNodes(t *testing.T) { func TestProcessNodesSingleForDebug(t *testing.T) {
e := New(nil)
doc, _ := html.Parse(strings.NewReader(`<div data-test="test"> html := `<div><div data-repeat="city path:cities"><p data-replace="path:city.name"></p></div></div>`
<ul data-attributes="class static:hello"> expected := `<div><div><p>London</p></div><div><p>Paris</p></div></div>`
<li>test1</li>
<li>test2</li> mapData := map[any]any{
<li><a href="test"></a></li> "cities": []map[any]any{
</ul> map[any]any{
"name": "London",
"population": 1000000,
},
map[any]any{
"name": "Paris",
"population": 2000000,
},
},
}
</div>`)) e := New(mapData)
e.ProcessNodes(doc)
stringWriter := &strings.Builder{} stringWriter := &strings.Builder{}
html.Render(stringWriter, doc) e.ProcessHtml(stringWriter, strings.NewReader(html))
assert.Equal(t, `<div data-test="test">`, stringWriter.String()) 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>`))
}
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment