455 lines
13 KiB
Go
455 lines
13 KiB
Go
package processor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/antchfx/xmlquery"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// XMLProcessor implements the Processor interface using XPath
|
|
type XMLProcessor struct {
|
|
Logger Logger
|
|
}
|
|
|
|
// NewXMLProcessor creates a new XMLProcessor
|
|
func NewXMLProcessor(logger Logger) *XMLProcessor {
|
|
return &XMLProcessor{
|
|
Logger: logger,
|
|
}
|
|
}
|
|
|
|
// Process implements the Processor interface for XMLProcessor
|
|
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) {
|
|
// Use pattern as XPath expression
|
|
xpathExpr := pattern
|
|
|
|
// Read file content
|
|
fullPath := filepath.Join(".", filename)
|
|
content, err := os.ReadFile(fullPath)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
}
|
|
|
|
fileContent := string(content)
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content))
|
|
}
|
|
|
|
// Process the content
|
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, xpathExpr, luaExpr, originalExpr)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
// If we made modifications, save the file
|
|
if modCount > 0 {
|
|
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
|
}
|
|
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Made %d XML node modifications to %s and saved (%d bytes)",
|
|
modCount, fullPath, len(modifiedContent))
|
|
}
|
|
}
|
|
|
|
return modCount, matchCount, nil
|
|
}
|
|
|
|
// ToLua implements the Processor interface for XMLProcessor
|
|
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
|
|
// Currently not used directly as this is handled in Process
|
|
return nil
|
|
}
|
|
|
|
// FromLua implements the Processor interface for XMLProcessor
|
|
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
|
// Currently not used directly as this is handled in Process
|
|
return nil, nil
|
|
}
|
|
|
|
// XMLNodeToString converts an XML node to a string representation
|
|
func (p *XMLProcessor) XMLNodeToString(node *xmlquery.Node) string {
|
|
// Use a simple string representation for now
|
|
var sb strings.Builder
|
|
|
|
// Start tag with attributes
|
|
if node.Type == xmlquery.ElementNode {
|
|
sb.WriteString("<")
|
|
sb.WriteString(node.Data)
|
|
|
|
// Add attributes
|
|
for _, attr := range node.Attr {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(attr.Name.Local)
|
|
sb.WriteString("=\"")
|
|
sb.WriteString(attr.Value)
|
|
sb.WriteString("\"")
|
|
}
|
|
|
|
// If self-closing
|
|
if node.FirstChild == nil {
|
|
sb.WriteString("/>")
|
|
return sb.String()
|
|
}
|
|
|
|
sb.WriteString(">")
|
|
} else if node.Type == xmlquery.TextNode {
|
|
// Just write the text content
|
|
sb.WriteString(node.Data)
|
|
return sb.String()
|
|
} else if node.Type == xmlquery.CommentNode {
|
|
// Write comment
|
|
sb.WriteString("<!--")
|
|
sb.WriteString(node.Data)
|
|
sb.WriteString("-->")
|
|
return sb.String()
|
|
}
|
|
|
|
// Add children
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
sb.WriteString(p.XMLNodeToString(child))
|
|
}
|
|
|
|
// End tag for elements
|
|
if node.Type == xmlquery.ElementNode {
|
|
sb.WriteString("</")
|
|
sb.WriteString(node.Data)
|
|
sb.WriteString(">")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// NodeToLuaTable creates a Lua table from an XML node
|
|
func (p *XMLProcessor) NodeToLuaTable(L *lua.LState, node *xmlquery.Node) lua.LValue {
|
|
nodeTable := L.NewTable()
|
|
|
|
// Add node name
|
|
L.SetField(nodeTable, "name", lua.LString(node.Data))
|
|
|
|
// Add node type
|
|
switch node.Type {
|
|
case xmlquery.ElementNode:
|
|
L.SetField(nodeTable, "type", lua.LString("element"))
|
|
case xmlquery.TextNode:
|
|
L.SetField(nodeTable, "type", lua.LString("text"))
|
|
case xmlquery.AttributeNode:
|
|
L.SetField(nodeTable, "type", lua.LString("attribute"))
|
|
case xmlquery.CommentNode:
|
|
L.SetField(nodeTable, "type", lua.LString("comment"))
|
|
default:
|
|
L.SetField(nodeTable, "type", lua.LString("other"))
|
|
}
|
|
|
|
// Add node text content if it's a text node
|
|
if node.Type == xmlquery.TextNode {
|
|
L.SetField(nodeTable, "content", lua.LString(node.Data))
|
|
}
|
|
|
|
// Add attributes if it's an element node
|
|
if node.Type == xmlquery.ElementNode && len(node.Attr) > 0 {
|
|
attrsTable := L.NewTable()
|
|
for _, attr := range node.Attr {
|
|
L.SetField(attrsTable, attr.Name.Local, lua.LString(attr.Value))
|
|
}
|
|
L.SetField(nodeTable, "attributes", attrsTable)
|
|
}
|
|
|
|
// Add children if any
|
|
if node.FirstChild != nil {
|
|
childrenTable := L.NewTable()
|
|
i := 1
|
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
// Skip empty text nodes (whitespace)
|
|
if child.Type == xmlquery.TextNode && strings.TrimSpace(child.Data) == "" {
|
|
continue
|
|
}
|
|
|
|
childTable := p.NodeToLuaTable(L, child)
|
|
childrenTable.RawSetInt(i, childTable)
|
|
i++
|
|
}
|
|
L.SetField(nodeTable, "children", childrenTable)
|
|
}
|
|
|
|
return nodeTable
|
|
}
|
|
|
|
// GetModifiedNode retrieves a modified node from Lua
|
|
func (p *XMLProcessor) GetModifiedNode(L *lua.LState, originalNode *xmlquery.Node) (*xmlquery.Node, bool) {
|
|
// Check if we have a node global with changes
|
|
nodeTable := L.GetGlobal("node")
|
|
if nodeTable == lua.LNil || nodeTable.Type() != lua.LTTable {
|
|
return originalNode, false
|
|
}
|
|
|
|
// Clone the node since we don't want to modify the original
|
|
clonedNode := *originalNode
|
|
|
|
// For text nodes, check if content was changed
|
|
if originalNode.Type == xmlquery.TextNode {
|
|
contentField := L.GetField(nodeTable.(*lua.LTable), "content")
|
|
if contentField != lua.LNil {
|
|
if strContent, ok := contentField.(lua.LString); ok {
|
|
if string(strContent) != originalNode.Data {
|
|
clonedNode.Data = string(strContent)
|
|
return &clonedNode, true
|
|
}
|
|
}
|
|
}
|
|
return originalNode, false
|
|
}
|
|
|
|
// For element nodes, attributes might have been changed
|
|
if originalNode.Type == xmlquery.ElementNode {
|
|
attrsField := L.GetField(nodeTable.(*lua.LTable), "attributes")
|
|
if attrsField != lua.LNil && attrsField.Type() == lua.LTTable {
|
|
attrsTable := attrsField.(*lua.LTable)
|
|
|
|
// Check if any attributes changed
|
|
changed := false
|
|
for _, attr := range originalNode.Attr {
|
|
newValue := L.GetField(attrsTable, attr.Name.Local)
|
|
if newValue != lua.LNil {
|
|
if strValue, ok := newValue.(lua.LString); ok {
|
|
if string(strValue) != attr.Value {
|
|
// Create a new attribute with the changed value
|
|
for i, a := range clonedNode.Attr {
|
|
if a.Name.Local == attr.Name.Local {
|
|
clonedNode.Attr[i].Value = string(strValue)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if changed {
|
|
return &clonedNode, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// No changes detected
|
|
return originalNode, false
|
|
}
|
|
|
|
// SetupXMLHelpers adds XML-specific helper functions to Lua
|
|
func (p *XMLProcessor) SetupXMLHelpers(L *lua.LState) {
|
|
// Helper function to create a new XML node
|
|
L.SetGlobal("new_node", L.NewFunction(func(L *lua.LState) int {
|
|
nodeName := L.CheckString(1)
|
|
nodeTable := L.NewTable()
|
|
L.SetField(nodeTable, "name", lua.LString(nodeName))
|
|
L.SetField(nodeTable, "type", lua.LString("element"))
|
|
L.SetField(nodeTable, "attributes", L.NewTable())
|
|
L.SetField(nodeTable, "children", L.NewTable())
|
|
L.Push(nodeTable)
|
|
return 1
|
|
}))
|
|
|
|
// Helper function to set an attribute
|
|
L.SetGlobal("set_attr", L.NewFunction(func(L *lua.LState) int {
|
|
nodeTable := L.CheckTable(1)
|
|
attrName := L.CheckString(2)
|
|
attrValue := L.CheckString(3)
|
|
|
|
attrsTable := L.GetField(nodeTable, "attributes")
|
|
if attrsTable == lua.LNil {
|
|
attrsTable = L.NewTable()
|
|
L.SetField(nodeTable, "attributes", attrsTable)
|
|
}
|
|
|
|
L.SetField(attrsTable.(*lua.LTable), attrName, lua.LString(attrValue))
|
|
return 0
|
|
}))
|
|
|
|
// Helper function to add a child node
|
|
L.SetGlobal("add_child", L.NewFunction(func(L *lua.LState) int {
|
|
parentTable := L.CheckTable(1)
|
|
childTable := L.CheckTable(2)
|
|
|
|
childrenTable := L.GetField(parentTable, "children")
|
|
if childrenTable == lua.LNil {
|
|
childrenTable = L.NewTable()
|
|
L.SetField(parentTable, "children", childrenTable)
|
|
}
|
|
|
|
childrenTbl := childrenTable.(*lua.LTable)
|
|
childrenTbl.RawSetInt(childrenTbl.Len()+1, childTable)
|
|
return 0
|
|
}))
|
|
}
|
|
|
|
// ProcessContent implements the Processor interface for XMLProcessor
|
|
// It processes XML content directly without file I/O
|
|
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) {
|
|
// Parse the XML document
|
|
doc, err := xmlquery.Parse(strings.NewReader(content))
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("error parsing XML: %v", err)
|
|
}
|
|
|
|
// Find nodes matching XPath expression
|
|
nodes, err := xmlquery.QueryAll(doc, pattern)
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("invalid XPath expression: %v", err)
|
|
}
|
|
|
|
// Log what we found
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("XML mode selected with XPath expression: %s (found %d matching nodes)",
|
|
pattern, len(nodes))
|
|
}
|
|
|
|
if len(nodes) == 0 {
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("No XML nodes matched XPath expression: %s", pattern)
|
|
}
|
|
return content, 0, 0, nil
|
|
}
|
|
|
|
// Initialize Lua state
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
|
|
// Setup Lua helper functions
|
|
if err := InitLuaHelpers(L); err != nil {
|
|
return "", 0, 0, err
|
|
}
|
|
|
|
// Register XML-specific helper functions
|
|
p.SetupXMLHelpers(L)
|
|
|
|
// Track modifications
|
|
matchCount := len(nodes)
|
|
modificationCount := 0
|
|
modifiedContent := content
|
|
modifications := []ModificationRecord{}
|
|
|
|
// Process each matching node
|
|
for i, node := range nodes {
|
|
// Get the original text representation of this node
|
|
originalNodeText := p.XMLNodeToString(node)
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Found node #%d: %s", i+1, LimitString(originalNodeText, 100))
|
|
}
|
|
|
|
// For text nodes, we'll handle them directly
|
|
if node.Type == xmlquery.TextNode && node.Parent != nil {
|
|
// If this is a text node, we'll use its value directly
|
|
// Get the node's text content
|
|
textContent := node.Data
|
|
|
|
// Set up Lua environment
|
|
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric
|
|
L.SetGlobal("s1", lua.LString(textContent))
|
|
|
|
// Try to convert to number if possible
|
|
if floatVal, err := strconv.ParseFloat(textContent, 64); err == nil {
|
|
L.SetGlobal("v1", lua.LNumber(floatVal))
|
|
}
|
|
|
|
// Execute user's Lua script
|
|
if err := L.DoString(luaExpr); err != nil {
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
|
|
}
|
|
continue // Skip this node on error
|
|
}
|
|
|
|
// Check for modifications
|
|
modVal := L.GetGlobal("v1")
|
|
if v, ok := modVal.(lua.LNumber); ok {
|
|
// If we have a numeric result, convert it to string
|
|
newValue := strconv.FormatFloat(float64(v), 'f', -1, 64)
|
|
if newValue != textContent {
|
|
// Replace the node content in the document
|
|
parentStr := p.XMLNodeToString(node.Parent)
|
|
newParentStr := strings.Replace(parentStr, textContent, newValue, 1)
|
|
modifiedContent = strings.Replace(modifiedContent, parentStr, newParentStr, 1)
|
|
modificationCount++
|
|
|
|
// Record the modification
|
|
modifications = append(modifications, ModificationRecord{
|
|
File: "",
|
|
OldValue: textContent,
|
|
NewValue: newValue,
|
|
Operation: originalExpr,
|
|
Context: fmt.Sprintf("(XPath: %s)", pattern),
|
|
})
|
|
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Modified text node #%d: '%s' -> '%s'",
|
|
i+1, LimitString(textContent, 30), LimitString(newValue, 30))
|
|
}
|
|
}
|
|
}
|
|
continue // Move to next node
|
|
}
|
|
|
|
// Convert the node to a Lua table
|
|
nodeTable := p.NodeToLuaTable(L, node)
|
|
|
|
// Set the node in Lua global variable for user script
|
|
L.SetGlobal("node", nodeTable)
|
|
|
|
// Execute user's Lua script
|
|
if err := L.DoString(luaExpr); err != nil {
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err)
|
|
}
|
|
continue // Skip this node on error
|
|
}
|
|
|
|
// Get modified node from Lua
|
|
modifiedNode, changed := p.GetModifiedNode(L, node)
|
|
if !changed {
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Node #%d was not modified by script", i+1)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Render the modified node back to XML
|
|
modifiedNodeText := p.XMLNodeToString(modifiedNode)
|
|
|
|
// Replace just this node in the document
|
|
if originalNodeText != modifiedNodeText {
|
|
modifiedContent = strings.Replace(
|
|
modifiedContent,
|
|
originalNodeText,
|
|
modifiedNodeText,
|
|
1)
|
|
modificationCount++
|
|
|
|
// Record the modification for reporting
|
|
modifications = append(modifications, ModificationRecord{
|
|
File: "",
|
|
OldValue: LimitString(originalNodeText, 30),
|
|
NewValue: LimitString(modifiedNodeText, 30),
|
|
Operation: originalExpr,
|
|
Context: fmt.Sprintf("(XPath: %s)", pattern),
|
|
})
|
|
|
|
if p.Logger != nil {
|
|
p.Logger.Printf("Modified node #%d", i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if p.Logger != nil && modificationCount > 0 {
|
|
p.Logger.Printf("Made %d XML node modifications", modificationCount)
|
|
}
|
|
|
|
return modifiedContent, modificationCount, matchCount, nil
|
|
}
|