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
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,20 +94,364 @@ 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 {
......@@ -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
for _, attr := range attrs {
if attr.Key == key {
continue
}
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'
result = append(result, attr)
}
} 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 == '.'
}
}
return "Process" + n.String()
return result
}
......@@ -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 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,
},
},
}
e := New(mapData)
stringWriter := &strings.Builder{}
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) {
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>
</div>`))
e.ProcessNodes(doc)
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{}
html.Render(stringWriter, doc)
assert.Equal(t, `<div data-test="test">`, stringWriter.String())
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