Integrate the xml processing with the rest of the project
This commit is contained in:
@@ -457,8 +457,10 @@ STRING FUNCTIONS:
|
||||
format(s, ...) - Formats string using Lua string.format
|
||||
trim(s) - Removes leading/trailing whitespace
|
||||
strsplit(inputstr, sep) - Splits string by separator (default: whitespace)
|
||||
fromCSV(csv, delimiter, hasHeaders) - Parses CSV text into rows of fields (delimiter defaults to ",", hasHeaders defaults to false)
|
||||
toCSV(rows, delimiter) - Converts table of rows to CSV text format (delimiter defaults to ",")
|
||||
fromCSV(csv, options) - Parses CSV text into rows of fields
|
||||
options: {delimiter=",", hasheader=false, hascomments=false}
|
||||
toCSV(rows, options) - Converts table of rows to CSV text format
|
||||
options: {delimiter=",", hasheader=false}
|
||||
num(str) - Converts string to number (returns 0 if invalid)
|
||||
str(num) - Converts number to string
|
||||
is_number(str) - Returns true if string is numeric
|
||||
@@ -467,6 +469,31 @@ TABLE FUNCTIONS:
|
||||
dump(table, depth) - Prints table structure recursively
|
||||
isArray(t) - Returns true if table is a sequential array
|
||||
|
||||
XML HELPER FUNCTIONS:
|
||||
findElements(root, tagName) - Find all elements with specific tag name
|
||||
visitElements(root, callback) - Visit all elements recursively
|
||||
callback(element, depth, path)
|
||||
filterElements(root, predicate) - Find elements matching condition
|
||||
predicate(element) returns true/false
|
||||
getNumAttr(element, attrName) - Get numeric attribute value
|
||||
setNumAttr(element, attrName, value) - Set numeric attribute value
|
||||
modifyNumAttr(element, attrName, func)- Modify numeric attribute with function
|
||||
func(currentValue) returns newValue
|
||||
hasAttr(element, attrName) - Check if attribute exists
|
||||
getAttr(element, attrName) - Get attribute value as string
|
||||
setAttr(element, attrName, value) - Set attribute value
|
||||
getText(element) - Get element text content
|
||||
setText(element, text) - Set element text content
|
||||
|
||||
JSON HELPER FUNCTIONS:
|
||||
visitJSON(data, callback) - Visit all values in JSON structure
|
||||
callback(value, key, parent)
|
||||
findInJSON(data, predicate) - Find values matching condition
|
||||
predicate(value, key, parent) returns true/false
|
||||
modifyJSONNumbers(data, predicate, modifier) - Modify numeric values
|
||||
predicate(value, key, parent) returns true/false
|
||||
modifier(currentValue) returns newValue
|
||||
|
||||
HTTP FUNCTIONS:
|
||||
fetch(url, options) - Makes HTTP request, returns response table
|
||||
options: {method="GET", headers={}, body=""}
|
||||
@@ -480,12 +507,31 @@ UTILITY FUNCTIONS:
|
||||
print(...) - Prints arguments to Go logger
|
||||
|
||||
EXAMPLES:
|
||||
-- Math
|
||||
round(3.14159, 2) -> 3.14
|
||||
min(5, 3) -> 3
|
||||
|
||||
-- String
|
||||
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
||||
upper("hello") -> "HELLO"
|
||||
min(5, 3) -> 3
|
||||
num("123") -> 123
|
||||
is_number("abc") -> false
|
||||
fetch("https://api.example.com/data")
|
||||
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
|
||||
|
||||
-- XML (where root is XML element with _tag, _attr, _children fields)
|
||||
local items = findElements(root, "Item")
|
||||
for _, item in ipairs(items) do
|
||||
modifyNumAttr(item, "Weight", function(w) return w * 2 end)
|
||||
end
|
||||
|
||||
-- JSON (where data is parsed JSON object)
|
||||
visitJSON(data, function(value, key, parent)
|
||||
if type(value) == "number" and key == "price" then
|
||||
parent[key] = value * 1.5
|
||||
end
|
||||
end)
|
||||
|
||||
-- HTTP
|
||||
local response = fetch("https://api.example.com/data")
|
||||
if response.ok then
|
||||
print(response.body)
|
||||
end`
|
||||
}
|
||||
|
||||
147
processor/xml.go
147
processor/xml.go
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var xmlLogger = logger.Default.WithPrefix("processor/xml")
|
||||
@@ -445,3 +446,149 @@ func formatNumeric(f float64) string {
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
|
||||
// ProcessXML applies Lua processing to XML content with surgical editing
|
||||
func ProcessXML(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
|
||||
processXMLLogger := xmlLogger.WithPrefix("ProcessXML").WithField("commandName", command.Name).WithField("file", filename)
|
||||
processXMLLogger.Debug("Starting XML processing for file")
|
||||
|
||||
// Parse XML with position tracking
|
||||
originalElem, err := parseXMLWithPositions(content)
|
||||
if err != nil {
|
||||
processXMLLogger.Error("Failed to parse XML: %v", err)
|
||||
return nil, fmt.Errorf("failed to parse XML: %v", err)
|
||||
}
|
||||
processXMLLogger.Debug("Successfully parsed XML content")
|
||||
|
||||
// Create Lua state
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
processXMLLogger.Error("Error creating Lua state: %v", err)
|
||||
return nil, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
defer L.Close()
|
||||
|
||||
// Set filename global
|
||||
L.SetGlobal("file", lua.LString(filename))
|
||||
|
||||
// Create modifiable copy
|
||||
modifiedElem := deepCopyXMLElement(originalElem)
|
||||
|
||||
// Convert to Lua table and set as global
|
||||
luaTable := xmlElementToLuaTable(L, modifiedElem)
|
||||
L.SetGlobal("root", luaTable)
|
||||
processXMLLogger.Debug("Set XML data as Lua global 'root'")
|
||||
|
||||
// Build and execute Lua script
|
||||
luaExpr := BuildJSONLuaScript(command.Lua) // Reuse JSON script builder
|
||||
processXMLLogger.Debug("Built Lua script from expression: %q", command.Lua)
|
||||
|
||||
if err := L.DoString(luaExpr); err != nil {
|
||||
processXMLLogger.Error("Lua script execution failed: %v\nScript: %s", err, luaExpr)
|
||||
return nil, fmt.Errorf("lua script execution failed: %v", err)
|
||||
}
|
||||
processXMLLogger.Debug("Lua script executed successfully")
|
||||
|
||||
// Check if modification flag is set
|
||||
modifiedVal := L.GetGlobal("modified")
|
||||
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
|
||||
processXMLLogger.Debug("Skipping - no modifications indicated by Lua script")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get the modified data back from Lua
|
||||
modifiedTable := L.GetGlobal("root")
|
||||
if modifiedTable.Type() != lua.LTTable {
|
||||
processXMLLogger.Error("Expected 'root' to be a table after Lua processing")
|
||||
return nil, fmt.Errorf("expected 'root' to be a table after Lua processing")
|
||||
}
|
||||
|
||||
// Apply Lua modifications back to XMLElement
|
||||
luaTableToXMLElement(L, modifiedTable.(*lua.LTable), modifiedElem)
|
||||
|
||||
// Find changes between original and modified
|
||||
changes := findXMLChanges(originalElem, modifiedElem, "")
|
||||
processXMLLogger.Debug("Found %d changes", len(changes))
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Generate surgical replace commands
|
||||
commands := applyXMLChanges(changes)
|
||||
processXMLLogger.Debug("Generated %d replace commands", len(commands))
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// xmlElementToLuaTable converts an XMLElement to a Lua table
|
||||
func xmlElementToLuaTable(L *lua.LState, elem *XMLElement) *lua.LTable {
|
||||
table := L.CreateTable(0, 4)
|
||||
table.RawSetString("_tag", lua.LString(elem.Tag))
|
||||
|
||||
if len(elem.Attributes) > 0 {
|
||||
attrs := L.CreateTable(0, len(elem.Attributes))
|
||||
for name, attr := range elem.Attributes {
|
||||
attrs.RawSetString(name, lua.LString(attr.Value))
|
||||
}
|
||||
table.RawSetString("_attr", attrs)
|
||||
}
|
||||
|
||||
if elem.Text != "" {
|
||||
table.RawSetString("_text", lua.LString(elem.Text))
|
||||
}
|
||||
|
||||
if len(elem.Children) > 0 {
|
||||
children := L.CreateTable(len(elem.Children), 0)
|
||||
for i, child := range elem.Children {
|
||||
children.RawSetInt(i+1, xmlElementToLuaTable(L, child))
|
||||
}
|
||||
table.RawSetString("_children", children)
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
// luaTableToXMLElement applies Lua table modifications back to XMLElement
|
||||
func luaTableToXMLElement(L *lua.LState, table *lua.LTable, elem *XMLElement) {
|
||||
// Update text
|
||||
if textVal := table.RawGetString("_text"); textVal.Type() == lua.LTString {
|
||||
elem.Text = string(textVal.(lua.LString))
|
||||
}
|
||||
|
||||
// Update attributes
|
||||
if attrVal := table.RawGetString("_attr"); attrVal.Type() == lua.LTTable {
|
||||
attrTable := attrVal.(*lua.LTable)
|
||||
// Clear and rebuild attributes
|
||||
elem.Attributes = make(map[string]XMLAttribute)
|
||||
attrTable.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
if key.Type() == lua.LTString && value.Type() == lua.LTString {
|
||||
attrName := string(key.(lua.LString))
|
||||
attrValue := string(value.(lua.LString))
|
||||
elem.Attributes[attrName] = XMLAttribute{Value: attrValue}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update children
|
||||
if childrenVal := table.RawGetString("_children"); childrenVal.Type() == lua.LTTable {
|
||||
childrenTable := childrenVal.(*lua.LTable)
|
||||
newChildren := []*XMLElement{}
|
||||
|
||||
// Iterate over array indices
|
||||
for i := 1; ; i++ {
|
||||
childVal := childrenTable.RawGetInt(i)
|
||||
if childVal.Type() == lua.LTNil {
|
||||
break
|
||||
}
|
||||
if childVal.Type() == lua.LTTable {
|
||||
if i-1 < len(elem.Children) {
|
||||
// Update existing child
|
||||
luaTableToXMLElement(L, childVal.(*lua.LTable), elem.Children[i-1])
|
||||
newChildren = append(newChildren, elem.Children[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
elem.Children = newChildren
|
||||
}
|
||||
}
|
||||
|
||||
165
processor/xml_real_test.go
Normal file
165
processor/xml_real_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cook/utils"
|
||||
)
|
||||
|
||||
func TestRealAfflictionsXML(t *testing.T) {
|
||||
// Read the real Afflictions.xml file
|
||||
content, err := os.ReadFile("../testfiles/Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read Afflictions.xml: %v", err)
|
||||
}
|
||||
|
||||
original := string(content)
|
||||
|
||||
// Test 1: Double all maxstrength values using helper functions
|
||||
command := utils.ModifyCommand{
|
||||
Name: "double_maxstrength",
|
||||
Lua: `
|
||||
-- Double all maxstrength attributes in Affliction elements
|
||||
local afflictions = findElements(root, "Affliction")
|
||||
for _, affliction in ipairs(afflictions) do
|
||||
modifyNumAttr(affliction, "maxstrength", function(val) return val * 2 end)
|
||||
end
|
||||
modified = true
|
||||
`,
|
||||
}
|
||||
|
||||
commands, err := ProcessXML(original, command, "Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessXML failed: %v", err)
|
||||
}
|
||||
|
||||
if len(commands) == 0 {
|
||||
t.Fatal("Expected modifications but got none")
|
||||
}
|
||||
|
||||
t.Logf("Generated %d surgical modifications", len(commands))
|
||||
|
||||
// Apply modifications
|
||||
result, count := utils.ExecuteModifications(commands, original)
|
||||
|
||||
t.Logf("Applied %d modifications", count)
|
||||
|
||||
// Verify specific changes
|
||||
if !strings.Contains(result, `maxstrength="20"`) {
|
||||
t.Errorf("Expected to find maxstrength=\"20\" (doubled from 10)")
|
||||
}
|
||||
if !strings.Contains(result, `maxstrength="480"`) {
|
||||
t.Errorf("Expected to find maxstrength=\"480\" (doubled from 240)")
|
||||
}
|
||||
if !strings.Contains(result, `maxstrength="12"`) {
|
||||
t.Errorf("Expected to find maxstrength=\"12\" (doubled from 6)")
|
||||
}
|
||||
|
||||
// Verify formatting preserved (XML declaration should be there)
|
||||
if !strings.Contains(result, `<?xml`) {
|
||||
t.Errorf("XML declaration not preserved")
|
||||
}
|
||||
|
||||
// Count lines to ensure structure preserved
|
||||
origLines := len(strings.Split(original, "\n"))
|
||||
resultLines := len(strings.Split(result, "\n"))
|
||||
if origLines != resultLines {
|
||||
t.Errorf("Line count changed: original %d, result %d", origLines, resultLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealAfflictionsAttributes(t *testing.T) {
|
||||
// Read the real file
|
||||
content, err := os.ReadFile("../testfiles/Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read Afflictions.xml: %v", err)
|
||||
}
|
||||
|
||||
original := string(content)
|
||||
|
||||
// Test 2: Modify resistance values using helper functions
|
||||
command := utils.ModifyCommand{
|
||||
Name: "increase_resistance",
|
||||
Lua: `
|
||||
-- Increase all minresistance and maxresistance by 50%
|
||||
local effects = findElements(root, "Effect")
|
||||
for _, effect in ipairs(effects) do
|
||||
modifyNumAttr(effect, "minresistance", function(val) return val * 1.5 end)
|
||||
modifyNumAttr(effect, "maxresistance", function(val) return val * 1.5 end)
|
||||
end
|
||||
modified = true
|
||||
`,
|
||||
}
|
||||
|
||||
commands, err := ProcessXML(original, command, "Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessXML failed: %v", err)
|
||||
}
|
||||
|
||||
if len(commands) == 0 {
|
||||
t.Fatal("Expected modifications but got none")
|
||||
}
|
||||
|
||||
t.Logf("Generated %d surgical modifications", len(commands))
|
||||
|
||||
// Apply modifications
|
||||
_, count := utils.ExecuteModifications(commands, original)
|
||||
|
||||
t.Logf("Applied %d modifications", count)
|
||||
|
||||
// Verify we made resistance modifications
|
||||
if count < 10 {
|
||||
t.Errorf("Expected at least 10 resistance modifications, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealAfflictionsNestedModifications(t *testing.T) {
|
||||
// Read the real file
|
||||
content, err := os.ReadFile("../testfiles/Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read Afflictions.xml: %v", err)
|
||||
}
|
||||
|
||||
original := string(content)
|
||||
|
||||
// Test 3: Modify nested Effect attributes using helper functions
|
||||
command := utils.ModifyCommand{
|
||||
Name: "modify_effects",
|
||||
Lua: `
|
||||
-- Double all amount values in ReduceAffliction elements
|
||||
local reduces = findElements(root, "ReduceAffliction")
|
||||
for _, reduce in ipairs(reduces) do
|
||||
modifyNumAttr(reduce, "amount", function(val) return val * 2 end)
|
||||
end
|
||||
modified = true
|
||||
`,
|
||||
}
|
||||
|
||||
commands, err := ProcessXML(original, command, "Afflictions.xml")
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessXML failed: %v", err)
|
||||
}
|
||||
|
||||
if len(commands) == 0 {
|
||||
t.Fatal("Expected modifications but got none")
|
||||
}
|
||||
|
||||
t.Logf("Generated %d surgical modifications for nested elements", len(commands))
|
||||
|
||||
// Apply modifications
|
||||
result, count := utils.ExecuteModifications(commands, original)
|
||||
|
||||
t.Logf("Applied %d modifications", count)
|
||||
|
||||
// Verify nested changes (0.001 * 2 = 0.002)
|
||||
if !strings.Contains(result, `amount="0.002"`) {
|
||||
t.Errorf("Expected to find amount=\"0.002\" (0.001 * 2)")
|
||||
}
|
||||
|
||||
// Verify we modified the nested elements
|
||||
if count < 8 {
|
||||
t.Errorf("Expected at least 8 amount modifications, got %d", count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user