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
|
format(s, ...) - Formats string using Lua string.format
|
||||||
trim(s) - Removes leading/trailing whitespace
|
trim(s) - Removes leading/trailing whitespace
|
||||||
strsplit(inputstr, sep) - Splits string by separator (default: 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)
|
fromCSV(csv, options) - Parses CSV text into rows of fields
|
||||||
toCSV(rows, delimiter) - Converts table of rows to CSV text format (delimiter defaults to ",")
|
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)
|
num(str) - Converts string to number (returns 0 if invalid)
|
||||||
str(num) - Converts number to string
|
str(num) - Converts number to string
|
||||||
is_number(str) - Returns true if string is numeric
|
is_number(str) - Returns true if string is numeric
|
||||||
@@ -467,6 +469,31 @@ TABLE FUNCTIONS:
|
|||||||
dump(table, depth) - Prints table structure recursively
|
dump(table, depth) - Prints table structure recursively
|
||||||
isArray(t) - Returns true if table is a sequential array
|
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:
|
HTTP FUNCTIONS:
|
||||||
fetch(url, options) - Makes HTTP request, returns response table
|
fetch(url, options) - Makes HTTP request, returns response table
|
||||||
options: {method="GET", headers={}, body=""}
|
options: {method="GET", headers={}, body=""}
|
||||||
@@ -480,12 +507,31 @@ UTILITY FUNCTIONS:
|
|||||||
print(...) - Prints arguments to Go logger
|
print(...) - Prints arguments to Go logger
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
|
-- Math
|
||||||
round(3.14159, 2) -> 3.14
|
round(3.14159, 2) -> 3.14
|
||||||
|
min(5, 3) -> 3
|
||||||
|
|
||||||
|
-- String
|
||||||
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
strsplit("a,b,c", ",") -> {"a", "b", "c"}
|
||||||
upper("hello") -> "HELLO"
|
upper("hello") -> "HELLO"
|
||||||
min(5, 3) -> 3
|
|
||||||
num("123") -> 123
|
num("123") -> 123
|
||||||
is_number("abc") -> false
|
|
||||||
fetch("https://api.example.com/data")
|
-- XML (where root is XML element with _tag, _attr, _children fields)
|
||||||
re("(\\w+)@(\\w+)", "user@domain.com") -> {"user@domain.com", "user", "domain.com"}`
|
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"
|
"strings"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
var xmlLogger = logger.Default.WithPrefix("processor/xml")
|
var xmlLogger = logger.Default.WithPrefix("processor/xml")
|
||||||
@@ -445,3 +446,149 @@ func formatNumeric(f float64) string {
|
|||||||
}
|
}
|
||||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
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