-
Volker Schukai authoredVolker Schukai authored
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
}