Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
f64a71185e | |||
1f66af6d5b | |||
2b8d86ca87 | |||
08d5d707d0 | |||
d904f8ec13 | |||
074296c478 | |||
438ad0391c | |||
c168553022 | |||
0422e6c143 | |||
583b2169dc | |||
56ac0c7101 | |||
1ff139ef15 | |||
1d39b5287f | |||
4f70eaa329 | |||
a796e8cac5 |
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -9,13 +9,8 @@
|
|||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}",
|
"program": "${fileDirname}",
|
||||||
"args": [
|
"args": []
|
||||||
"-mode=json",
|
|
||||||
"$..name",
|
|
||||||
"v='pero'",
|
|
||||||
"test.json"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
116
README.md
116
README.md
@@ -1,116 +0,0 @@
|
|||||||
# Big Chef
|
|
||||||
|
|
||||||
A Go-based tool for modifying XML, JSON, and text documents using XPath/JSONPath/Regex expressions and Lua transformations.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Multi-Format Processing**:
|
|
||||||
- XML (XPath)
|
|
||||||
- JSON (JSONPath)
|
|
||||||
- Text (Regex)
|
|
||||||
- **Node Value Modification**: Update text values in XML elements, JSON properties or text matches
|
|
||||||
- **Attribute Manipulation**: Modify XML attributes, JSON object keys or regex capture groups
|
|
||||||
- **Conditional Logic**: Apply transformations based on document content
|
|
||||||
- **Complex Operations**:
|
|
||||||
- Mathematical calculations
|
|
||||||
- String manipulations
|
|
||||||
- Date conversions
|
|
||||||
- Structural changes
|
|
||||||
- Whole ass Lua environment
|
|
||||||
- **Error Handling**: Comprehensive error detection for:
|
|
||||||
- Invalid XML/JSON
|
|
||||||
- Malformed XPath/JSONPath
|
|
||||||
- Lua syntax errors
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### 1. Basic field modification
|
|
||||||
```xml
|
|
||||||
<!-- Input -->
|
|
||||||
<price>44.95</price>
|
|
||||||
|
|
||||||
<!-- Command -->
|
|
||||||
chef -xml "//price" "v=v*2" input.xml
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<price>89.9</price>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Supports glob patterns
|
|
||||||
```xml
|
|
||||||
chef -xml "//price" "v=v*2" data/**.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Attribute Update
|
|
||||||
```xml
|
|
||||||
<!-- Input -->
|
|
||||||
<item price="10.50"/>
|
|
||||||
|
|
||||||
<!-- Command -->
|
|
||||||
chef -xml "//item/@price" "v=v*2" input.xml
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<item price="21"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. JSONPath Transformation
|
|
||||||
```json
|
|
||||||
// Input
|
|
||||||
{
|
|
||||||
"products": [
|
|
||||||
{"name": "Widget", "price": 19.99},
|
|
||||||
{"name": "Gadget", "price": 29.99}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command
|
|
||||||
chef -json "$.products[*].price" "v=v*0.75" input.json
|
|
||||||
|
|
||||||
// Output
|
|
||||||
{
|
|
||||||
"products": [
|
|
||||||
{"name": "Widget", "price": 14.99},
|
|
||||||
{"name": "Gadget", "price": 22.49}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Regex Text Replacement
|
|
||||||
Regex works slightly differently, up to 12 match groups are provided as v1..v12 and s1..s12 for numbers and strings respectively.
|
|
||||||
A special shorthand "!num" is also provided that simply expands to `(\d*\.?\d+)`.
|
|
||||||
```xml
|
|
||||||
<!-- Input -->
|
|
||||||
<description>Price: $15.00 Special Offer</description>
|
|
||||||
|
|
||||||
<!-- Command -->
|
|
||||||
chef "Price: $!num Special Offer" "v1 = v1 * 0.92" input.xml
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<description>Price: $13.80 Special Offer</description>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Conditional Transformation
|
|
||||||
```xml
|
|
||||||
<!-- Input -->
|
|
||||||
<item stock="5" price="10.00"/>
|
|
||||||
|
|
||||||
<!-- Command -->
|
|
||||||
chef -xml "//item" "if tonumber(v.stock) > 0 then v.price = v.price * 0.8 end" input.xml
|
|
||||||
|
|
||||||
<!-- Output -->
|
|
||||||
<item stock="5" price="8.00"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go build -o chef main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Process XML file
|
|
||||||
./chef -xml "//price" "v=v*1.2" input.xml
|
|
||||||
|
|
||||||
# Process JSON file
|
|
||||||
./chef -json "$.prices[*]" "v=v*0.9" input.json
|
|
||||||
```
|
|
31
go.mod
31
go.mod
@@ -3,37 +3,18 @@ module modify
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1
|
|
||||||
github.com/antchfx/xmlquery v1.4.4
|
github.com/antchfx/xmlquery v1.4.4
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||||
github.com/yuin/gopher-lua v1.1.1
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
|
||||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
|
||||||
github.com/cloudflare/circl v1.6.0 // indirect
|
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
|
||||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
|
||||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
|
||||||
golang.org/x/crypto v0.35.0 // indirect
|
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||||
|
github.com/PaesslerAG/jsonpath v0.1.1 // indirect
|
||||||
github.com/antchfx/xpath v1.3.3 // indirect
|
github.com/antchfx/xpath v1.3.3 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.14.0
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/sergi/go-diff v1.3.1 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
89
go.sum
89
go.sum
@@ -1,99 +1,38 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
|
||||||
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
|
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
|
||||||
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
|
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
|
||||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
|
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
|
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
|
||||||
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
|
||||||
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
|
||||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||||
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
|
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
|
||||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
|
||||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
|
||||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
|
||||||
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
|
||||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
|
||||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
|
||||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
|
||||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
|
||||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
|
||||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -101,16 +40,14 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -119,13 +56,9 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -133,8 +66,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -144,20 +75,16 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -167,11 +94,5 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
183
main.go
183
main.go
@@ -5,13 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
|
|
||||||
"modify/processor"
|
"modify/processor"
|
||||||
)
|
)
|
||||||
@@ -23,16 +19,20 @@ type GlobalStats struct {
|
|||||||
FailedFiles int
|
FailedFiles int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeRegex FileMode = "regex"
|
||||||
|
ModeXML FileMode = "xml"
|
||||||
|
ModeJSON FileMode = "json"
|
||||||
|
)
|
||||||
|
|
||||||
var stats GlobalStats
|
var stats GlobalStats
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
|
|
||||||
var (
|
var (
|
||||||
jsonFlag = flag.Bool("json", false, "Process JSON files")
|
fileModeFlag = flag.String("mode", "regex", "Processing mode: regex, xml, json")
|
||||||
xmlFlag = flag.Bool("xml", false, "Process XML files")
|
verboseFlag = flag.Bool("verbose", false, "Enable verbose output")
|
||||||
gitFlag = flag.Bool("git", false, "Use git to manage files")
|
|
||||||
resetFlag = flag.Bool("reset", false, "Reset files to their original state")
|
|
||||||
repo *git.Repository
|
|
||||||
worktree *git.Worktree
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -43,43 +43,28 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// TODO: Implement some sort of git integration
|
|
||||||
// Maybe use go-git
|
|
||||||
// Specify a -git flag
|
|
||||||
// If we are operating with git then:
|
|
||||||
// Inmitialize a repo if one doesn't exist (try to open right?)
|
|
||||||
// For each file matched by glob first figure out if it's already tracked
|
|
||||||
// If not tracked then track it and commit (either it alone or maybe multiple together somehow)
|
|
||||||
// Then reset the file (to undo previous modifications)
|
|
||||||
// THEN change the file
|
|
||||||
// In addition add a -undo flag that will ONLY reset the files without changing them
|
|
||||||
// Only for the ones matched by glob
|
|
||||||
// ^ important because binary files would fuck us up
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
||||||
fmt.Fprintf(os.Stderr, " -json\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " Process JSON files\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " -xml\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " Process XML files\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " -git\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " Use git to manage files\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " -reset\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " Reset files to their original state\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " -mode string\n")
|
fmt.Fprintf(os.Stderr, " -mode string\n")
|
||||||
fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n")
|
fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -xpath string\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " XPath expression (for XML mode)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -jsonpath string\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " JSONPath expression (for JSON mode)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -verbose\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Enable verbose output\n")
|
||||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||||
fmt.Fprintf(os.Stderr, " Regex mode (default):\n")
|
fmt.Fprintf(os.Stderr, " Regex mode (default):\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s \"<value>(\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s \"<value>(\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " XML mode:\n")
|
fmt.Fprintf(os.Stderr, " XML mode:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s -xml \"//value\" \"*1.5\" data.xml\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -mode=xml -xpath=\"//value\" \"*1.5\" data.xml\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
||||||
fmt.Fprintf(os.Stderr, " %s -json \"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -mode=json -jsonpath=\"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n")
|
fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n")
|
||||||
fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n")
|
fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n")
|
||||||
fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n")
|
fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n")
|
||||||
fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n")
|
fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n")
|
||||||
fmt.Fprintf(os.Stderr, " For XML and JSON, the captured values are exposed as 'v', which can be of any type we capture (string, number, table).\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
|
fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
|
||||||
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
|
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
|
||||||
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
|
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
|
||||||
@@ -87,12 +72,9 @@ func main() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if *resetFlag {
|
|
||||||
*gitFlag = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
log.Printf("At least %d arguments are required", 3)
|
fmt.Fprintf(os.Stderr, "%s mode requires %d arguments minimum\n", *fileModeFlag, 3)
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -101,9 +83,15 @@ func main() {
|
|||||||
var pattern, luaExpr string
|
var pattern, luaExpr string
|
||||||
var filePatterns []string
|
var filePatterns []string
|
||||||
|
|
||||||
|
if *fileModeFlag == "regex" {
|
||||||
pattern = args[0]
|
pattern = args[0]
|
||||||
luaExpr = args[1]
|
luaExpr = args[1]
|
||||||
filePatterns = args[2:]
|
filePatterns = args[2:]
|
||||||
|
} else {
|
||||||
|
// For XML/JSON modes, pattern comes from flags
|
||||||
|
luaExpr = args[0]
|
||||||
|
filePatterns = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the Lua expression
|
// Prepare the Lua expression
|
||||||
originalLuaExpr := luaExpr
|
originalLuaExpr := luaExpr
|
||||||
@@ -112,14 +100,6 @@ func main() {
|
|||||||
logger.Printf("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr)
|
logger.Printf("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *gitFlag {
|
|
||||||
err := setupGit()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand file patterns with glob support
|
// Expand file patterns with glob support
|
||||||
files, err := expandFilePatterns(filePatterns)
|
files, err := expandFilePatterns(filePatterns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -132,33 +112,23 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if *gitFlag {
|
|
||||||
err := cleanupGitFiles(files)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if *resetFlag {
|
|
||||||
log.Printf("Files reset to their original state, nothing more to do")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the processor based on mode
|
// Create the processor based on mode
|
||||||
var proc processor.Processor
|
var proc processor.Processor
|
||||||
switch {
|
switch *fileModeFlag {
|
||||||
case *xmlFlag:
|
case "regex":
|
||||||
proc = &processor.XMLProcessor{}
|
|
||||||
logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
|
|
||||||
pattern, luaExpr, len(files))
|
|
||||||
case *jsonFlag:
|
|
||||||
proc = &processor.JSONProcessor{}
|
|
||||||
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
|
|
||||||
pattern, luaExpr, len(files))
|
|
||||||
default:
|
|
||||||
proc = &processor.RegexProcessor{}
|
proc = &processor.RegexProcessor{}
|
||||||
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
|
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
|
||||||
pattern, luaExpr, len(files))
|
pattern, luaExpr, len(files))
|
||||||
|
// case "xml":
|
||||||
|
// proc = &processor.XMLProcessor{}
|
||||||
|
// pattern = *xpathFlag
|
||||||
|
// logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
|
||||||
|
// pattern, luaExpr, len(files))
|
||||||
|
// case "json":
|
||||||
|
// proc = &processor.JSONProcessor{}
|
||||||
|
// pattern = *jsonpathFlag
|
||||||
|
// logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
|
||||||
|
// pattern, luaExpr, len(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -169,8 +139,7 @@ func main() {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
logger.Printf("Processing file: %s", file)
|
logger.Printf("Processing file: %s", file)
|
||||||
|
|
||||||
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
|
modCount, matchCount, err := proc.Process(file, pattern, luaExpr)
|
||||||
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
||||||
stats.FailedFiles++
|
stats.FailedFiles++
|
||||||
@@ -193,35 +162,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupGit() error {
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get current working directory: %w", err)
|
|
||||||
}
|
|
||||||
logger.Printf("Current working directory obtained: %s", cwd)
|
|
||||||
|
|
||||||
logger.Printf("Attempting to open git repository at %s", cwd)
|
|
||||||
repo, err = git.PlainOpen(cwd)
|
|
||||||
if err != nil {
|
|
||||||
logger.Printf("No existing git repository found at %s, attempting to initialize a new git repository.", cwd)
|
|
||||||
repo, err = git.PlainInit(cwd, false)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err)
|
|
||||||
}
|
|
||||||
logger.Printf("Successfully initialized a new git repository at %s", cwd)
|
|
||||||
} else {
|
|
||||||
logger.Printf("Successfully opened existing git repository at %s", cwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Printf("Attempting to obtain worktree for repository at %s", cwd)
|
|
||||||
worktree, err = repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err)
|
|
||||||
}
|
|
||||||
logger.Printf("Successfully obtained worktree for repository at %s", cwd)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandFilePatterns(patterns []string) ([]string, error) {
|
func expandFilePatterns(patterns []string) ([]string, error) {
|
||||||
var files []string
|
var files []string
|
||||||
filesMap := make(map[string]bool)
|
filesMap := make(map[string]bool)
|
||||||
@@ -240,50 +180,3 @@ func expandFilePatterns(patterns []string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupGitFiles(files []string) error {
|
|
||||||
for _, file := range files {
|
|
||||||
logger.Printf("Checking file: %s", file)
|
|
||||||
status, err := worktree.Status()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err)
|
|
||||||
return fmt.Errorf("error getting worktree status: %w", err)
|
|
||||||
}
|
|
||||||
if status.IsUntracked(file) {
|
|
||||||
logger.Printf("Detected untracked file: %s. Attempting to add it to the git index.", file)
|
|
||||||
_, err = worktree.Add(file)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err)
|
|
||||||
return fmt.Errorf("error adding file to git: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := filepath.Base(file)
|
|
||||||
logger.Printf("File %s added successfully. Now committing it with message: 'Track %s'", filename, filename)
|
|
||||||
_, err = worktree.Commit("Track "+filename, &git.CommitOptions{
|
|
||||||
Author: &object.Signature{
|
|
||||||
Name: "Big Chef",
|
|
||||||
Email: "bigchef@bigchef.com",
|
|
||||||
When: time.Now(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err)
|
|
||||||
return fmt.Errorf("error committing file: %w", err)
|
|
||||||
}
|
|
||||||
logger.Printf("Successfully committed file: %s with message: 'Track %s'", filename, filename)
|
|
||||||
} else {
|
|
||||||
logger.Printf("File %s is already tracked. Restoring it to the working tree.", file)
|
|
||||||
err := worktree.Restore(&git.RestoreOptions{
|
|
||||||
Files: []string{file},
|
|
||||||
Staged: true,
|
|
||||||
Worktree: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err)
|
|
||||||
return fmt.Errorf("error restoring file: %w", err)
|
|
||||||
}
|
|
||||||
logger.Printf("File %s restored successfully.", file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@@ -3,8 +3,10 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"modify/processor/jsonpath"
|
"modify/processor/jsonpath"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
@@ -12,6 +14,39 @@ import (
|
|||||||
// JSONProcessor implements the Processor interface for JSON documents
|
// JSONProcessor implements the Processor interface for JSON documents
|
||||||
type JSONProcessor struct{}
|
type JSONProcessor struct{}
|
||||||
|
|
||||||
|
// Process implements the Processor interface for JSONProcessor
|
||||||
|
func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
|
// Read file content
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(cwd, filename)
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent := string(content)
|
||||||
|
|
||||||
|
// Process the content
|
||||||
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modCount, matchCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessContent implements the Processor interface for JSONProcessor
|
// ProcessContent implements the Processor interface for JSONProcessor
|
||||||
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||||
// Parse JSON document
|
// Parse JSON document
|
||||||
@@ -32,125 +67,99 @@ func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr s
|
|||||||
return content, 0, 0, nil
|
return content, 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
modCount := 0
|
|
||||||
for _, node := range nodes {
|
|
||||||
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
|
|
||||||
|
|
||||||
// Initialize Lua
|
// Initialize Lua
|
||||||
L, err := NewLuaState()
|
L, err := NewLuaState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
|
return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||||
}
|
}
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
log.Println("Lua state initialized successfully.")
|
|
||||||
|
|
||||||
err = p.ToLua(L, node.Value)
|
err = p.ToLua(L, nodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
|
return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("Converted node value to Lua: %v", node.Value)
|
|
||||||
|
|
||||||
originalScript := luaExpr
|
|
||||||
fullScript := BuildLuaScript(luaExpr)
|
|
||||||
log.Printf("Original script: %q, Full script: %q", originalScript, fullScript)
|
|
||||||
|
|
||||||
// Execute Lua script
|
// Execute Lua script
|
||||||
log.Printf("Executing Lua script: %q", fullScript)
|
if err := L.DoString(luaExpr); err != nil {
|
||||||
if err := L.DoString(fullScript); err != nil {
|
return content, len(nodes), 0, fmt.Errorf("error executing Lua %s: %v", luaExpr, err)
|
||||||
return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
|
|
||||||
}
|
}
|
||||||
log.Println("Lua script executed successfully.")
|
|
||||||
|
|
||||||
// Get modified value
|
// Get modified value
|
||||||
result, err := p.FromLua(L)
|
result, err := p.FromLua(L)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
|
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("Retrieved modified value from Lua: %v", result)
|
|
||||||
|
|
||||||
modified := false
|
|
||||||
modified = L.GetGlobal("modified").String() == "true"
|
|
||||||
if !modified {
|
|
||||||
log.Printf("No changes made to node at path: %s", node.Path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the modification to the JSON data
|
// Apply the modification to the JSON data
|
||||||
err = p.updateJSONValue(jsonData, node.Path, result)
|
err = p.updateJSONValue(jsonData, pattern, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
|
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result)
|
|
||||||
modCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the modified JSON back to a string with same formatting
|
// Convert the modified JSON back to a string with same formatting
|
||||||
var jsonBytes []byte
|
var jsonBytes []byte
|
||||||
|
if indent, err := detectJsonIndentation(content); err == nil && indent != "" {
|
||||||
|
// Use detected indentation for output formatting
|
||||||
|
jsonBytes, err = json.MarshalIndent(jsonData, "", indent)
|
||||||
|
} else {
|
||||||
|
// Fall back to standard 2-space indent
|
||||||
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
||||||
if err != nil {
|
|
||||||
return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
|
|
||||||
}
|
}
|
||||||
return string(jsonBytes), modCount, matchCount, nil
|
|
||||||
|
// We changed all the nodes trust me bro
|
||||||
|
return string(jsonBytes), len(nodes), len(nodes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detectJsonIndentation tries to determine the indentation used in the original JSON
|
||||||
|
func detectJsonIndentation(content string) (string, error) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
return "", fmt.Errorf("not enough lines to detect indentation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the first indented line
|
||||||
|
for i := 1; i < len(lines); i++ {
|
||||||
|
line := lines[i]
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate leading whitespace
|
||||||
|
indent := line[:len(line)-len(trimmed)]
|
||||||
|
if len(indent) > 0 {
|
||||||
|
return indent, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no indentation detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// / Selects from the root node
|
||||||
|
// // Selects nodes in the document from the current node that match the selection no matter where they are
|
||||||
|
// . Selects the current node
|
||||||
|
// @ Selects attributes
|
||||||
|
|
||||||
|
// /bookstore/* Selects all the child element nodes of the bookstore element
|
||||||
|
// //* Selects all elements in the document
|
||||||
|
|
||||||
|
// /bookstore/book[1] Selects the first book element that is the child of the bookstore element.
|
||||||
|
// /bookstore/book[last()] Selects the last book element that is the child of the bookstore element
|
||||||
|
// /bookstore/book[last()-1] Selects the last but one book element that is the child of the bookstore element
|
||||||
|
// /bookstore/book[position()<3] Selects the first two book elements that are children of the bookstore element
|
||||||
|
// //title[@lang] Selects all the title elements that have an attribute named lang
|
||||||
|
// //title[@lang='en'] Selects all the title elements that have a "lang" attribute with a value of "en"
|
||||||
|
// /bookstore/book[price>35.00] Selects all the book elements of the bookstore element that have a price element with a value greater than 35.00
|
||||||
|
// /bookstore/book[price>35.00]/title Selects all the title elements of the book elements of the bookstore element that have a price element with a value greater than 35.00
|
||||||
|
|
||||||
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
||||||
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
||||||
// Special handling for root node
|
|
||||||
if path == "$" {
|
|
||||||
// For the root node, we'll copy the value to the jsonData reference
|
|
||||||
// This is a special case since we can't directly replace the interface{} variable
|
|
||||||
|
|
||||||
// We need to handle different types of root elements
|
|
||||||
switch rootValue := newValue.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
// For objects, we need to copy over all keys
|
|
||||||
rootMap, ok := jsonData.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
// If the original wasn't a map, completely replace it with the new map
|
|
||||||
// This is handled by the jsonpath.Set function
|
|
||||||
return jsonpath.Set(jsonData, path, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the original map
|
|
||||||
for k := range rootMap {
|
|
||||||
delete(rootMap, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy all keys from the new map
|
|
||||||
for k, v := range rootValue {
|
|
||||||
rootMap[k] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
// For arrays, we need to handle similarly
|
|
||||||
rootArray, ok := jsonData.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
// If the original wasn't an array, use jsonpath.Set
|
|
||||||
return jsonpath.Set(jsonData, path, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear and recreate the array
|
|
||||||
*&rootArray = rootValue
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
// For other types, use jsonpath.Set
|
|
||||||
return jsonpath.Set(jsonData, path, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For non-root paths, use the regular Set method
|
|
||||||
err := jsonpath.Set(jsonData, path, newValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLua converts JSON values to Lua variables
|
// ToLua converts JSON values to Lua variables
|
||||||
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||||
table, err := ToLua(L, data)
|
table, err := ToLuaTable(L, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -161,5 +170,5 @@ func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
|||||||
// FromLua retrieves values from Lua
|
// FromLua retrieves values from Lua
|
||||||
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||||
luaValue := L.GetGlobal("v")
|
luaValue := L.GetGlobal("v")
|
||||||
return FromLua(L, luaValue)
|
return FromLuaTable(L, luaValue.(*lua.LTable))
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -138,6 +138,10 @@ func Set(data interface{}, path string, value interface{}) error {
|
|||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(steps) <= 1 {
|
||||||
|
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
|
||||||
|
}
|
||||||
|
|
||||||
success := false
|
success := false
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,6 +157,10 @@ func SetAll(data interface{}, path string, value interface{}) error {
|
|||||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(steps) <= 1 {
|
||||||
|
return fmt.Errorf("cannot set root node; the provided path %q is invalid", path)
|
||||||
|
}
|
||||||
|
|
||||||
success := false
|
success := false
|
||||||
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,20 +178,17 @@ func setWithPath(node interface{}, steps []JSONStep, success *bool, value interf
|
|||||||
// Skip root step
|
// Skip root step
|
||||||
actualSteps := steps
|
actualSteps := steps
|
||||||
if len(steps) > 0 && steps[0].Type == RootStep {
|
if len(steps) > 0 && steps[0].Type == RootStep {
|
||||||
|
if len(steps) == 1 {
|
||||||
|
return fmt.Errorf("cannot set root node; the provided path %q is invalid", currentPath)
|
||||||
|
}
|
||||||
actualSteps = steps[1:]
|
actualSteps = steps[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have no steps left, we're setting the root value
|
// Process the first step
|
||||||
if len(actualSteps) == 0 {
|
if len(actualSteps) == 0 {
|
||||||
// For the root node, we need to handle it differently depending on what's passed in
|
return fmt.Errorf("cannot set root node; no steps provided for path %q", currentPath)
|
||||||
// since we can't directly replace the interface{} variable
|
|
||||||
|
|
||||||
// We'll signal success and let the JSONProcessor handle updating the root
|
|
||||||
*success = true
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the first step
|
|
||||||
step := actualSteps[0]
|
step := actualSteps[0]
|
||||||
remainingSteps := actualSteps[1:]
|
remainingSteps := actualSteps[1:]
|
||||||
isLastStep := len(remainingSteps) == 0
|
isLastStep := len(remainingSteps) == 0
|
||||||
|
@@ -355,14 +355,14 @@ func TestSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("setting on root should not fail (anymore)", func(t *testing.T) {
|
t.Run("setting on root should fail", func(t *testing.T) {
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"name": "John",
|
"name": "John",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Set(data, "$", "Jane")
|
err := Set(data, "$", "Jane")
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Errorf("Set() returned error: %v", err)
|
t.Errorf("Set() returned no error, expected error for setting on root")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,20 +2,16 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"reflect"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Processor defines the interface for all file processors
|
// Processor defines the interface for all file processors
|
||||||
type Processor interface {
|
type Processor interface {
|
||||||
// Process handles processing a file with the given pattern and Lua expression
|
// Process handles processing a file with the given pattern and Lua expression
|
||||||
// Now implemented as a base function in processor.go
|
Process(filename string, pattern string, luaExpr string) (int, int, error)
|
||||||
// Process(filename string, pattern string, luaExpr string) (int, int, error)
|
|
||||||
|
|
||||||
// ProcessContent handles processing a string content directly with the given pattern and Lua expression
|
// ProcessContent handles processing a string content directly with the given pattern and Lua expression
|
||||||
// Returns the modified content, modification count, match count, and any error
|
// Returns the modified content, modification count, match count, and any error
|
||||||
@@ -56,139 +52,65 @@ func NewLuaState() (*lua.LState, error) {
|
|||||||
return L, nil
|
return L, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
|
// ToLuaTable converts a struct or map to a Lua table recursively
|
||||||
// Read file content
|
func ToLuaTable(L *lua.LState, data interface{}) (*lua.LTable, error) {
|
||||||
cwd, err := os.Getwd()
|
luaTable := L.NewTable()
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := filepath.Join(cwd, filename)
|
|
||||||
content, err := os.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent := string(content)
|
|
||||||
|
|
||||||
// Process the content
|
|
||||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modCount, matchCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToLua converts a struct or map to a Lua table recursively
|
|
||||||
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
case *xmlquery.Node:
|
|
||||||
luaTable := L.NewTable()
|
|
||||||
luaTable.RawSetString("text", lua.LString(v.Data))
|
|
||||||
// Should be a map, simple key value pairs
|
|
||||||
attr, err := ToLua(L, v.Attr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
luaTable.RawSetString("attr", attr)
|
|
||||||
return luaTable, nil
|
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
luaTable := L.NewTable()
|
|
||||||
for key, value := range v {
|
for key, value := range v {
|
||||||
luaValue, err := ToLua(L, value)
|
luaValue, err := ToLuaTable(L, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
luaTable.RawSetString(key, luaValue)
|
luaTable.RawSetString(key, luaValue)
|
||||||
}
|
}
|
||||||
return luaTable, nil
|
case struct{}:
|
||||||
case []interface{}:
|
val := reflect.ValueOf(v)
|
||||||
luaTable := L.NewTable()
|
for i := 0; i < val.NumField(); i++ {
|
||||||
for i, value := range v {
|
field := val.Type().Field(i)
|
||||||
luaValue, err := ToLua(L, value)
|
luaValue, err := ToLuaTable(L, val.Field(i).Interface())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
luaTable.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
|
luaTable.RawSetString(field.Name, luaValue)
|
||||||
}
|
}
|
||||||
return luaTable, nil
|
|
||||||
case string:
|
case string:
|
||||||
return lua.LString(v), nil
|
luaTable.RawSetString("v", lua.LString(v))
|
||||||
case bool:
|
case bool:
|
||||||
return lua.LBool(v), nil
|
luaTable.RawSetString("v", lua.LBool(v))
|
||||||
case float64:
|
case float64:
|
||||||
return lua.LNumber(v), nil
|
luaTable.RawSetString("v", lua.LNumber(v))
|
||||||
case nil:
|
|
||||||
return lua.LNil, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported data type: %T", data)
|
return nil, fmt.Errorf("unsupported data type: %T", data)
|
||||||
}
|
}
|
||||||
|
return luaTable, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromLua converts a Lua table to a struct or map recursively
|
// FromLuaTable converts a Lua table to a struct or map recursively
|
||||||
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
func FromLuaTable(L *lua.LState, luaTable *lua.LTable) (map[string]interface{}, error) {
|
||||||
switch v := luaValue.(type) {
|
|
||||||
// Well shit...
|
|
||||||
// Tables in lua are both maps and arrays
|
|
||||||
// As arrays they are ordered and as maps, obviously, not
|
|
||||||
// So when we parse them to a go map we fuck up the order for arrays
|
|
||||||
// We have to find a better way....
|
|
||||||
case *lua.LTable:
|
|
||||||
isArray, err := IsLuaTableArray(L, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isArray {
|
|
||||||
result := make([]interface{}, 0)
|
|
||||||
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
||||||
converted, _ := FromLua(L, value)
|
|
||||||
result = append(result, converted)
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
} else {
|
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
||||||
converted, _ := FromLua(L, value)
|
|
||||||
result[key.String()] = converted
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
case lua.LString:
|
|
||||||
return string(v), nil
|
|
||||||
case lua.LBool:
|
|
||||||
return bool(v), nil
|
|
||||||
case lua.LNumber:
|
|
||||||
return float64(v), nil
|
|
||||||
default:
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
|
luaTable.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||||
L.SetGlobal("table_to_check", v)
|
switch v := value.(type) {
|
||||||
|
case *lua.LTable:
|
||||||
// Use our predefined helper function from InitLuaHelpers
|
nestedMap, err := FromLuaTable(L, v)
|
||||||
err := L.DoString(`is_array = isArray(table_to_check)`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error determining if table is array: %w", err)
|
return
|
||||||
}
|
}
|
||||||
|
result[key.String()] = nestedMap
|
||||||
|
case lua.LString:
|
||||||
|
result[key.String()] = string(v)
|
||||||
|
case lua.LBool:
|
||||||
|
result[key.String()] = bool(v)
|
||||||
|
case lua.LNumber:
|
||||||
|
result[key.String()] = float64(v)
|
||||||
|
default:
|
||||||
|
result[key.String()] = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Check the result of our Lua function
|
return result, nil
|
||||||
isArray := L.GetGlobal("is_array")
|
|
||||||
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
|
|
||||||
if !lua.LVIsFalse(isArray) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitLuaHelpers initializes common Lua helper functions
|
// InitLuaHelpers initializes common Lua helper functions
|
||||||
@@ -197,10 +119,7 @@ func InitLuaHelpers(L *lua.LState) error {
|
|||||||
-- Custom Lua helpers for math operations
|
-- Custom Lua helpers for math operations
|
||||||
function min(a, b) return math.min(a, b) end
|
function min(a, b) return math.min(a, b) end
|
||||||
function max(a, b) return math.max(a, b) end
|
function max(a, b) return math.max(a, b) end
|
||||||
function round(x, n)
|
function round(x) return math.floor(x + 0.5) end
|
||||||
if n == nil then n = 0 end
|
|
||||||
return math.floor(x * 10^n + 0.5) / 10^n
|
|
||||||
end
|
|
||||||
function floor(x) return math.floor(x) end
|
function floor(x) return math.floor(x) end
|
||||||
function ceil(x) return math.ceil(x) end
|
function ceil(x) return math.ceil(x) end
|
||||||
function upper(s) return string.upper(s) end
|
function upper(s) return string.upper(s) end
|
||||||
@@ -220,28 +139,10 @@ end
|
|||||||
function is_number(str)
|
function is_number(str)
|
||||||
return tonumber(str) ~= nil
|
return tonumber(str) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function isArray(t)
|
|
||||||
if type(t) ~= "table" then return false end
|
|
||||||
local max = 0
|
|
||||||
local count = 0
|
|
||||||
for k, _ in pairs(t) do
|
|
||||||
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
max = math.max(max, k)
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
return max == count
|
|
||||||
end
|
|
||||||
|
|
||||||
modified = false
|
|
||||||
`
|
`
|
||||||
if err := L.DoString(helperScript); err != nil {
|
if err := L.DoString(helperScript); err != nil {
|
||||||
return fmt.Errorf("error loading helper functions: %v", err)
|
return fmt.Errorf("error loading helper functions: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
L.SetGlobal("print", L.NewFunction(printToGo))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +157,8 @@ func LimitString(s string, maxLen int) string {
|
|||||||
return s[:maxLen-3] + "..."
|
return s[:maxLen-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrependLuaAssignment(luaExpr string) string {
|
// BuildLuaScript prepares a Lua expression from shorthand notation
|
||||||
|
func BuildLuaScript(luaExpr string) string {
|
||||||
// Auto-prepend v1 for expressions starting with operators
|
// Auto-prepend v1 for expressions starting with operators
|
||||||
if strings.HasPrefix(luaExpr, "*") ||
|
if strings.HasPrefix(luaExpr, "*") ||
|
||||||
strings.HasPrefix(luaExpr, "/") ||
|
strings.HasPrefix(luaExpr, "/") ||
|
||||||
@@ -274,44 +176,10 @@ func PrependLuaAssignment(luaExpr string) string {
|
|||||||
if !strings.Contains(luaExpr, "=") {
|
if !strings.Contains(luaExpr, "=") {
|
||||||
luaExpr = "v1 = " + luaExpr
|
luaExpr = "v1 = " + luaExpr
|
||||||
}
|
}
|
||||||
|
|
||||||
return luaExpr
|
return luaExpr
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildLuaScript prepares a Lua expression from shorthand notation
|
|
||||||
func BuildLuaScript(luaExpr string) string {
|
|
||||||
luaExpr = PrependLuaAssignment(luaExpr)
|
|
||||||
|
|
||||||
// This allows the user to specify whether or not they modified a value
|
|
||||||
// If they do nothing we assume they did modify (no return at all)
|
|
||||||
// If they return before our return then they themselves specify what they did
|
|
||||||
// If nothing is returned lua assumes nil
|
|
||||||
// So we can say our value was modified if the return value is either nil or true
|
|
||||||
// If the return value is false then the user wants to keep the original
|
|
||||||
fullScript := fmt.Sprintf(`
|
|
||||||
function run()
|
|
||||||
%s
|
|
||||||
end
|
|
||||||
local res = run()
|
|
||||||
modified = res == nil or res
|
|
||||||
`, luaExpr)
|
|
||||||
|
|
||||||
return fullScript
|
|
||||||
}
|
|
||||||
|
|
||||||
func printToGo(L *lua.LState) int {
|
|
||||||
// Get the number of arguments passed to the Lua print function
|
|
||||||
n := L.GetTop()
|
|
||||||
// Create a slice to hold the arguments
|
|
||||||
args := make([]interface{}, n)
|
|
||||||
for i := 1; i <= n; i++ {
|
|
||||||
args[i-1] = L.Get(i) // Get the argument from Lua stack
|
|
||||||
}
|
|
||||||
// Print the arguments to Go's stdout
|
|
||||||
log.Print("Lua: ")
|
|
||||||
log.Println(args...)
|
|
||||||
return 0 // No return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max returns the maximum of two integers
|
// Max returns the maximum of two integers
|
||||||
func Max(a, b int) int {
|
func Max(a, b int) int {
|
||||||
if a > b {
|
if a > b {
|
||||||
|
@@ -2,7 +2,8 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +14,34 @@ import (
|
|||||||
// RegexProcessor implements the Processor interface using regex patterns
|
// RegexProcessor implements the Processor interface using regex patterns
|
||||||
type RegexProcessor struct{}
|
type RegexProcessor struct{}
|
||||||
|
|
||||||
|
// Process implements the Processor interface for RegexProcessor
|
||||||
|
func (p *RegexProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Process the content
|
||||||
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modCount, matchCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
||||||
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
|
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||||
captures, ok := data.([]string)
|
captures, ok := data.([]string)
|
||||||
@@ -66,60 +95,27 @@ func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
|||||||
return modifications, nil
|
return modifications, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type NamedCapture struct {
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
Range [2]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessContent applies regex replacement with Lua processing
|
// ProcessContent applies regex replacement with Lua processing
|
||||||
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||||
// Handle special pattern modifications
|
// Handle special pattern modifications
|
||||||
if !strings.HasPrefix(pattern, "(?s)") {
|
if !strings.HasPrefix(pattern, "(?s)") {
|
||||||
pattern = "(?s)" + pattern
|
pattern = "(?s)" + pattern
|
||||||
log.Printf("Pattern modified to include (?s): %s", pattern)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The order of these replaces is important
|
|
||||||
// This one handles !num-s inside of named capture groups
|
|
||||||
// If it were not here our !num in a named capture group would
|
|
||||||
// Expand to another capture group in the capture group
|
|
||||||
// We really only want one (our named) capture group
|
|
||||||
namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)
|
|
||||||
pattern = namedGroupNum.ReplaceAllStringFunc(pattern, func(match string) string {
|
|
||||||
parts := namedGroupNum.FindStringSubmatch(match)
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
replacement := `\d*\.?\d+`
|
|
||||||
return parts[1] + replacement
|
|
||||||
})
|
|
||||||
pattern = strings.ReplaceAll(pattern, "!num", `"?(\d*\.?\d+)"?`)
|
|
||||||
pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
|
|
||||||
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
|
|
||||||
// !rep(pattern, count) repeats the pattern n times
|
|
||||||
// Inserting !any between each repetition
|
|
||||||
pattern = repPattern.ReplaceAllStringFunc(pattern, func(match string) string {
|
|
||||||
parts := repPattern.FindStringSubmatch(match)
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
repeatedPattern := parts[1]
|
|
||||||
count := parts[2]
|
|
||||||
repetitions, _ := strconv.Atoi(count)
|
|
||||||
return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
|
|
||||||
})
|
|
||||||
|
|
||||||
compiledPattern, err := regexp.Compile(pattern)
|
compiledPattern, err := regexp.Compile(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error compiling pattern: %v", err)
|
|
||||||
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
|
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("Compiled pattern successfully: %s", pattern)
|
|
||||||
|
|
||||||
previous := luaExpr
|
previous := luaExpr
|
||||||
luaExpr = BuildLuaScript(luaExpr)
|
luaExpr = BuildLuaScript(luaExpr)
|
||||||
log.Printf("Changing Lua expression from: %s to: %s", previous, luaExpr)
|
fmt.Printf("Changing Lua expression from: %s to: %s\n", previous, luaExpr)
|
||||||
|
|
||||||
|
L, err := NewLuaState()
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||||
|
}
|
||||||
|
defer L.Close()
|
||||||
|
|
||||||
// Initialize Lua environment
|
// Initialize Lua environment
|
||||||
modificationCount := 0
|
modificationCount := 0
|
||||||
@@ -127,27 +123,12 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
// Process all regex matches
|
// Process all regex matches
|
||||||
result := content
|
result := content
|
||||||
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
||||||
log.Printf("Found %d matches in the content", len(indices))
|
|
||||||
|
|
||||||
// We walk backwards because we're replacing something with something else that might be longer
|
// We walk backwards because we're replacing something with something else that might be longer
|
||||||
// And in the case it is longer than the original all indicces past that change will be fucked up
|
// And in the case it is longer than the original all indicces past that change will be fucked up
|
||||||
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
||||||
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
||||||
for i := len(indices) - 1; i >= 0; i-- {
|
for i := len(indices) - 1; i >= 0; i-- {
|
||||||
L, err := NewLuaState()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error creating Lua state: %v", err)
|
|
||||||
return "", 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
|
||||||
}
|
|
||||||
// Hmm... Maybe we don't want to defer this..
|
|
||||||
// Maybe we want to close them every iteration
|
|
||||||
// We'll leave it as is for now
|
|
||||||
defer L.Close()
|
|
||||||
log.Printf("Lua state created successfully")
|
|
||||||
|
|
||||||
matchIndices := indices[i]
|
matchIndices := indices[i]
|
||||||
log.Printf("Processing match indices: %v", matchIndices)
|
|
||||||
|
|
||||||
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
||||||
// Plus it's a little cleaner to surgically replace our matches
|
// Plus it's a little cleaner to surgically replace our matches
|
||||||
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
|
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
|
||||||
@@ -156,104 +137,50 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
// As if concatenating in the middle of the array
|
// As if concatenating in the middle of the array
|
||||||
// Plus it supports lookarounds
|
// Plus it supports lookarounds
|
||||||
match := content[matchIndices[0]:matchIndices[1]]
|
match := content[matchIndices[0]:matchIndices[1]]
|
||||||
log.Printf("Matched content: %s", match)
|
|
||||||
|
|
||||||
groups := matchIndices[2:]
|
groups := matchIndices[2:]
|
||||||
if len(groups) <= 0 {
|
if len(groups) <= 0 {
|
||||||
log.Println("No capture groups for lua to chew on")
|
fmt.Println("No capture groups for lua to chew on")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(groups)%2 == 1 {
|
if len(groups)%2 == 1 {
|
||||||
log.Println("Odd number of indices of groups, what the fuck?")
|
fmt.Println("Odd number of indices of groups, what the fuck?")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, index := range groups {
|
|
||||||
if index == -1 {
|
|
||||||
// return "", 0, 0, fmt.Errorf("negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. Please check the regex pattern and input content.", matchIndices)
|
|
||||||
log.Printf("Negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. This is not an error but it's possibly not what you want.", matchIndices)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
captures := make([]string, 0, len(groups)/2)
|
captures := make([]string, 0, len(groups)/2)
|
||||||
for j := 0; j < len(groups); j += 2 {
|
for j := 0; j < len(groups); j += 2 {
|
||||||
if groups[j] == -1 || groups[j+1] == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
captures = append(captures, content[groups[j]:groups[j+1]])
|
captures = append(captures, content[groups[j]:groups[j+1]])
|
||||||
}
|
}
|
||||||
log.Printf("Captured groups: %v", captures)
|
|
||||||
|
|
||||||
// We have to use array to preserve order
|
|
||||||
// Very important for the reconstruction step
|
|
||||||
// Because we must overwrite the values in reverse order
|
|
||||||
// See comments a few dozen lines above for more details
|
|
||||||
namedCaptures := make([]NamedCapture, 0, len(groups)/2)
|
|
||||||
groupNames := compiledPattern.SubexpNames()[1:]
|
|
||||||
for i, name := range groupNames {
|
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if groups[i*2] == -1 || groups[i*2+1] == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
namedCaptures = append(namedCaptures, NamedCapture{
|
|
||||||
Name: name,
|
|
||||||
Value: captures[i],
|
|
||||||
Range: [2]int{groups[i*2], groups[i*2+1]},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Named captures: %v", namedCaptures)
|
|
||||||
|
|
||||||
if err := p.ToLua(L, captures); err != nil {
|
if err := p.ToLua(L, captures); err != nil {
|
||||||
log.Printf("Error setting Lua variables: %v", err)
|
fmt.Println("Error setting Lua variables:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Println("Lua variables set successfully")
|
|
||||||
|
|
||||||
for _, capture := range namedCaptures {
|
|
||||||
if capture.Name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if val, err := strconv.ParseFloat(capture.Value, 64); err == nil {
|
|
||||||
L.SetGlobal(capture.Name, lua.LNumber(val))
|
|
||||||
} else {
|
|
||||||
L.SetGlobal(capture.Name, lua.LString(capture.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := L.DoString(luaExpr); err != nil {
|
if err := L.DoString(luaExpr); err != nil {
|
||||||
log.Printf("Error executing Lua code %s for group %s: %v", luaExpr, captures, err)
|
fmt.Printf("Error executing Lua code %s for group %s: %v", luaExpr, captures, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Println("Lua code executed successfully")
|
|
||||||
|
|
||||||
// Get modifications from Lua
|
// Get modifications from Lua
|
||||||
modResult, err := p.FromLua(L)
|
modResult, err := p.FromLua(L)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting modifications: %v", err)
|
fmt.Println("Error getting modifications:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply modifications to the matched text
|
// Apply modifications to the matched text
|
||||||
modsMap, ok := modResult.(map[int]string)
|
modsMap, ok := modResult.(map[int]string)
|
||||||
if !ok || len(modsMap) == 0 {
|
if !ok || len(modsMap) == 0 {
|
||||||
log.Println("No modifications to apply")
|
fmt.Println("No modifications to apply")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
replacement := ""
|
|
||||||
replacementVar := L.GetGlobal("replacement")
|
|
||||||
if replacementVar.Type() != lua.LTNil {
|
|
||||||
replacement = replacementVar.String()
|
|
||||||
}
|
|
||||||
if replacement == "" {
|
|
||||||
// Apply the modifications to the original match
|
// Apply the modifications to the original match
|
||||||
replacement = match
|
replacement := match
|
||||||
for i := len(modsMap) - 1; i >= 0; i-- {
|
for i := len(modsMap) - 1; i >= 0; i-- {
|
||||||
newVal := modsMap[i]
|
newVal := modsMap[i]
|
||||||
log.Printf("Applying modification: %s", newVal)
|
|
||||||
// Indices of the group are relative to content
|
// Indices of the group are relative to content
|
||||||
// To relate them to match we have to subtract the match start index
|
// To relate them to match we have to subtract the match start index
|
||||||
groupStart := groups[i*2] - matchIndices[0]
|
groupStart := groups[i*2] - matchIndices[0]
|
||||||
@@ -261,22 +188,9 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
|||||||
replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := len(namedCaptures) - 1; i >= 0; i-- {
|
|
||||||
capture := namedCaptures[i]
|
|
||||||
if capture.Name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
groupStart := capture.Range[0] - matchIndices[0]
|
|
||||||
groupEnd := capture.Range[1] - matchIndices[0]
|
|
||||||
luaValue := L.GetGlobal(capture.Name).String()
|
|
||||||
replacement = replacement[:groupStart] + luaValue + replacement[groupEnd:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modificationCount++
|
modificationCount++
|
||||||
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
|
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
|
||||||
log.Printf("Modification count updated: %d", modificationCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Process completed with %d modifications", modificationCount)
|
|
||||||
return result, modificationCount, len(indices), nil
|
return result, modificationCount, len(indices), nil
|
||||||
}
|
}
|
||||||
|
@@ -35,11 +35,10 @@ func TestBuildLuaScript(t *testing.T) {
|
|||||||
{"v1 * 2", "v1 = v1 * 2"},
|
{"v1 * 2", "v1 = v1 * 2"},
|
||||||
{"v1 * v2", "v1 = v1 * v2"},
|
{"v1 * v2", "v1 = v1 * v2"},
|
||||||
{"v1 / v2", "v1 = v1 / v2"},
|
{"v1 / v2", "v1 = v1 / v2"},
|
||||||
{"12", "v1 = 12"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
result := PrependLuaAssignment(c.input)
|
result := BuildLuaScript(c.input)
|
||||||
if result != c.expected {
|
if result != c.expected {
|
||||||
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
|
t.Errorf("BuildLuaScript(%q): expected %q, got %q", c.input, c.expected, result)
|
||||||
}
|
}
|
||||||
@@ -547,793 +546,3 @@ func TestEdgeCases(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNamedCaptureGroups(t *testing.T) {
|
|
||||||
content := `<config>
|
|
||||||
<item>
|
|
||||||
<value>100</value>
|
|
||||||
</item>
|
|
||||||
</config>`
|
|
||||||
|
|
||||||
expected := `<config>
|
|
||||||
<item>
|
|
||||||
<value>200</value>
|
|
||||||
</item>
|
|
||||||
</config>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(?<amount>\d+)</value>`, "amount = amount * 2")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureGroupsNum(t *testing.T) {
|
|
||||||
content := `<config>
|
|
||||||
<item>
|
|
||||||
<value>100</value>
|
|
||||||
</item>
|
|
||||||
</config>`
|
|
||||||
|
|
||||||
expected := `<config>
|
|
||||||
<item>
|
|
||||||
<value>200</value>
|
|
||||||
</item>
|
|
||||||
</config>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(content, `(?s)<value>(?<amount>!num)</value>`, "amount = amount * 2")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleNamedCaptureGroups(t *testing.T) {
|
|
||||||
content := `<product>
|
|
||||||
<name>Widget</name>
|
|
||||||
<price>15.99</price>
|
|
||||||
<quantity>10</quantity>
|
|
||||||
</product>`
|
|
||||||
|
|
||||||
expected := `<product>
|
|
||||||
<name>WIDGET</name>
|
|
||||||
<price>23.99</price>
|
|
||||||
<quantity>15</quantity>
|
|
||||||
</product>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<name>(?<prodName>[^<]+)</name>.*?<price>(?<prodPrice>\d+\.\d+)</price>.*?<quantity>(?<prodQty>\d+)</quantity>`,
|
|
||||||
`prodName = string.upper(prodName)
|
|
||||||
prodPrice = round(prodPrice + 8, 2)
|
|
||||||
prodQty = prodQty + 5`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMixedIndexedAndNamedCaptures(t *testing.T) {
|
|
||||||
content := `<entry>
|
|
||||||
<id>12345</id>
|
|
||||||
<data>value</data>
|
|
||||||
</entry>`
|
|
||||||
|
|
||||||
expected := `<entry>
|
|
||||||
<id>24690</id>
|
|
||||||
<data>VALUE</data>
|
|
||||||
</entry>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<id>(\d+)</id>.*?<data>(?<dataField>[^<]+)</data>`,
|
|
||||||
`v1 = v1 * 2
|
|
||||||
dataField = string.upper(dataField)`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestComplexNestedNamedCaptures(t *testing.T) {
|
|
||||||
content := `<person>
|
|
||||||
<details>
|
|
||||||
<name>John Smith</name>
|
|
||||||
<age>32</age>
|
|
||||||
</details>
|
|
||||||
<contact>
|
|
||||||
<email>john@example.com</email>
|
|
||||||
</contact>
|
|
||||||
</person>`
|
|
||||||
|
|
||||||
expected := `<person>
|
|
||||||
<details>
|
|
||||||
<name>JOHN SMITH (32)</name>
|
|
||||||
<age>32</age>
|
|
||||||
</details>
|
|
||||||
<contact>
|
|
||||||
<email>john@example.com</email>
|
|
||||||
</contact>
|
|
||||||
</person>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<details>.*?<name>(?<fullName>[^<]+)</name>.*?<age>(?<age>\d+)</age>`,
|
|
||||||
`fullName = string.upper(fullName) .. " (" .. age .. ")"`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureWithVariableReadback(t *testing.T) {
|
|
||||||
content := `<stats>
|
|
||||||
<health>100</health>
|
|
||||||
<mana>200</mana>
|
|
||||||
</stats>`
|
|
||||||
|
|
||||||
expected := `<stats>
|
|
||||||
<health>150</health>
|
|
||||||
<mana>300</mana>
|
|
||||||
</stats>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<health>(?<hp>\d+)</health>.*?<mana>(?<mp>\d+)</mana>`,
|
|
||||||
`hp = hp * 1.5
|
|
||||||
mp = mp * 1.5`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureWithSpecialCharsInName(t *testing.T) {
|
|
||||||
content := `<data value="42" min="10" max="100" />`
|
|
||||||
|
|
||||||
expected := `<data value="84" min="10" max="100" />`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<data value="(?<val_1>\d+)"`,
|
|
||||||
`val_1 = val_1 * 2`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyNamedCapture(t *testing.T) {
|
|
||||||
content := `<tag attr="" />`
|
|
||||||
|
|
||||||
expected := `<tag attr="default" />`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`attr="(?<value>.*?)"`,
|
|
||||||
`value = value == "" and "default" or value`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleNamedCapturesInSameLine(t *testing.T) {
|
|
||||||
content := `<rect x="10" y="20" width="100" height="50" />`
|
|
||||||
|
|
||||||
expected := `<rect x="20" y="40" width="200" height="100" />`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`x="(?<x>\d+)" y="(?<y>\d+)" width="(?<w>\d+)" height="(?<h>\d+)"`,
|
|
||||||
`x = x * 2
|
|
||||||
y = y * 2
|
|
||||||
w = w * 2
|
|
||||||
h = h * 2`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConditionalNamedCapture(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<item status="active" count="5" />
|
|
||||||
<item status="inactive" count="10" />
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<item status="active" count="10" />
|
|
||||||
<item status="inactive" count="10" />
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<item status="(?<status>[^"]+)" count="(?<count>\d+)"`,
|
|
||||||
`count = status == "active" and count * 2 or count`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 2 {
|
|
||||||
t.Errorf("Expected 2 matches, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 2 {
|
|
||||||
t.Errorf("Expected 2 modifications, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLuaFunctionsOnNamedCaptures(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<user name="john doe" role="user" />
|
|
||||||
<user name="jane smith" role="admin" />
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<user name="John Doe" role="user" />
|
|
||||||
<user name="JANE SMITH" role="admin" />
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<user name="(?<name>[^"]+)" role="(?<role>[^"]+)"`,
|
|
||||||
`-- Capitalize first letters for regular users
|
|
||||||
if role == "user" then
|
|
||||||
name = name:gsub("(%w)(%w*)", function(first, rest) return first:upper()..rest end):gsub(" (%w)(%w*)", " %1%2")
|
|
||||||
else
|
|
||||||
-- Uppercase for admins
|
|
||||||
name = string.upper(name)
|
|
||||||
end`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 2 {
|
|
||||||
t.Errorf("Expected 2 matches, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 2 {
|
|
||||||
t.Errorf("Expected 2 modifications, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For simpler tests, we can use this. More complex string modifications
|
|
||||||
// might need additional transformations before comparison
|
|
||||||
normalizedResult := normalizeWhitespace(result)
|
|
||||||
normalizedExpected := normalizeWhitespace(expected)
|
|
||||||
if normalizedResult != normalizedExpected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureWithMath(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<item price="19.99" quantity="3" />
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<item price="19.99" quantity="3" total="59.97" />
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<item price="(?<price>\d+\.\d+)" quantity="(?<qty>\d+)"!any$`,
|
|
||||||
`-- Calculate and add total
|
|
||||||
replacement = string.format('<item price="%s" quantity="%s" total="%.2f" />',
|
|
||||||
price, qty, price * qty)`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = normalizeWhitespace(result)
|
|
||||||
expected = normalizeWhitespace(expected)
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureWithGlobals(t *testing.T) {
|
|
||||||
content := `<temp unit="C">25</temp>`
|
|
||||||
|
|
||||||
expected := `<temp unit="F">77</temp>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<temp unit="(?<unit>[CF]?)">(?<value>\d+)</temp>`,
|
|
||||||
`if unit == "C" then
|
|
||||||
value = value * 9/5 + 32
|
|
||||||
unit = "F"
|
|
||||||
elseif unit == "F" then
|
|
||||||
value = (value - 32) * 5/9
|
|
||||||
unit = "C"
|
|
||||||
end`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMixedDynamicAndNamedCaptures(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<color rgb="255,0,0" name="red" />
|
|
||||||
<color rgb="0,255,0" name="green" />
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<color rgb="255,0,0" name="RED" hex="#FF0000" />
|
|
||||||
<color rgb="0,255,0" name="GREEN" hex="#00FF00" />
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<color rgb="(?<r>\d+),(?<g>\d+),(?<b>\d+)" name="(?<colorName>[^"]+)" />`,
|
|
||||||
`-- Uppercase the name
|
|
||||||
colorName = string.upper(colorName)
|
|
||||||
|
|
||||||
-- Create hex color
|
|
||||||
local hex = string.format("#%02X%02X%02X", tonumber(r), tonumber(g), tonumber(b))
|
|
||||||
|
|
||||||
-- Replace the entire match
|
|
||||||
replacement = string.format('<color rgb="%s,%s,%s" name="%s" hex="%s" />',
|
|
||||||
r, g, b, colorName, hex)`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 2 {
|
|
||||||
t.Errorf("Expected 2 matches, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 2 {
|
|
||||||
t.Errorf("Expected 2 modifications, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = normalizeWhitespace(result)
|
|
||||||
expected = normalizeWhitespace(expected)
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCapturesWithMultipleReferences(t *testing.T) {
|
|
||||||
content := `<text>Hello world</text>`
|
|
||||||
|
|
||||||
expected := `<text format="uppercase" length="11">HELLO WORLD</text>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<text>(?<content>[^<]+)</text>`,
|
|
||||||
`local uppercaseContent = string.upper(content)
|
|
||||||
local contentLength = string.len(content)
|
|
||||||
replacement = string.format('<text format="uppercase" length="%d">%s</text>',
|
|
||||||
contentLength, uppercaseContent)`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureWithJsonData(t *testing.T) {
|
|
||||||
content := `<data>{"name":"John","age":30}</data>`
|
|
||||||
|
|
||||||
expected := `<data>{"name":"JOHN","age":30}</data>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<data>(?<json>\{.*?\})</data>`,
|
|
||||||
`-- Parse JSON (simplified, assumes valid JSON)
|
|
||||||
local name = json:match('"name":"([^"]+)"')
|
|
||||||
local upperName = string.upper(name)
|
|
||||||
json = json:gsub('"name":"([^"]+)"', '"name":"' .. upperName .. '"')`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamedCaptureInXML(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<product>
|
|
||||||
<sku>ABC-123</sku>
|
|
||||||
<price currency="USD">19.99</price>
|
|
||||||
<stock>25</stock>
|
|
||||||
</product>
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<product>
|
|
||||||
<sku>ABC-123</sku>
|
|
||||||
<price currency="USD">23.99</price>
|
|
||||||
<stock>20</stock>
|
|
||||||
</product>
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<price currency="(?<currency>[^"]+)">(?<price>\d+\.\d+)</price>.*?<stock>(?<stock>\d+)</stock>`,
|
|
||||||
`-- Add 20% to price if USD
|
|
||||||
if currency == "USD" then
|
|
||||||
price = round(price * 1.20, 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Reduce stock by 5
|
|
||||||
stock = stock - 5`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestComprehensiveNamedCaptures(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<products>
|
|
||||||
<product sku="AB-123" status="in-stock">
|
|
||||||
<name>Widget A</name>
|
|
||||||
<price currency="USD">19.99</price>
|
|
||||||
<quantity>15</quantity>
|
|
||||||
</product>
|
|
||||||
<product sku="CD-456" status="out-of-stock">
|
|
||||||
<name>Widget B</name>
|
|
||||||
<price currency="EUR">29.99</price>
|
|
||||||
<quantity>0</quantity>
|
|
||||||
</product>
|
|
||||||
<product sku="EF-789" status="in-stock">
|
|
||||||
<name>Widget C</name>
|
|
||||||
<price currency="GBP">39.99</price>
|
|
||||||
<quantity>5</quantity>
|
|
||||||
</product>
|
|
||||||
</products>
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<products>
|
|
||||||
<product sku="AB-123" status="in-stock" discounted="true">
|
|
||||||
<name>WIDGET A</name>
|
|
||||||
<price currency="USD">15.99</price>
|
|
||||||
<quantity>15</quantity>
|
|
||||||
</product>
|
|
||||||
<product sku="CD-456" status="out-of-stock" discounted="false">
|
|
||||||
<name>Widget B</name>
|
|
||||||
<price currency="EUR">29.99</price>
|
|
||||||
<quantity>0</quantity>
|
|
||||||
</product>
|
|
||||||
<product sku="EF-789" status="in-stock" discounted="true">
|
|
||||||
<name>WIDGET C</name>
|
|
||||||
<price currency="GBP">39.99</price>
|
|
||||||
<quantity>5</quantity>
|
|
||||||
</product>
|
|
||||||
</products>
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`(?s)<product sku="(?<sku>[^"]+)" status="(?<status>[^"]+)"[^>]*>\s*<name>(?<product_name>[^<]+)</name>\s*<price currency="(?<currency>[^"]+)">(?<price>\d+\.\d+)</price>\s*<quantity>(?<qty>\d+)</quantity>`,
|
|
||||||
`-- Only process in-stock items
|
|
||||||
if status == "in-stock" then
|
|
||||||
-- Transform name to uppercase
|
|
||||||
product_name = string.upper(product_name)
|
|
||||||
|
|
||||||
-- Apply discount based on currency
|
|
||||||
local discounted = true
|
|
||||||
if currency == "USD" then
|
|
||||||
price = round(price * 0.8, 2) -- 20% discount for USD
|
|
||||||
elseif currency == "GBP" then
|
|
||||||
price = round(price * 0.8, 2) -- 20% discount for GBP
|
|
||||||
price = price + 8 -- Add shipping cost for GBP
|
|
||||||
else
|
|
||||||
discounted = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add discounted attribute
|
|
||||||
replacement = string.format('<product sku="%s" status="%s" discounted="%s">\n\t\t\t<name>%s</name>\n\t\t\t<price currency="%s">%.2f</price>\n\t\t\t<quantity>%s</quantity>',
|
|
||||||
sku, status, tostring(discounted), product_name, currency, price, qty)
|
|
||||||
else
|
|
||||||
-- Add discounted attribute for out-of-stock items (always false)
|
|
||||||
replacement = string.format('<product sku="%s" status="%s" discounted="false">\n\t\t\t<name>%s</name>\n\t\t\t<price currency="%s">%s</price>\n\t\t\t<quantity>%s</quantity>',
|
|
||||||
sku, status, product_name, currency, price, qty)
|
|
||||||
end`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 3 {
|
|
||||||
t.Errorf("Expected 3 matches, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 3 {
|
|
||||||
t.Errorf("Expected 3 modifications, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize whitespace for comparison
|
|
||||||
normalizedResult := normalizeWhitespace(result)
|
|
||||||
normalizedExpected := normalizeWhitespace(expected)
|
|
||||||
|
|
||||||
if normalizedResult != normalizedExpected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVariousNamedCaptureFormats(t *testing.T) {
|
|
||||||
content := `
|
|
||||||
<data>
|
|
||||||
<entry id="1" value="100" />
|
|
||||||
<entry id="2" value="200" status="active" />
|
|
||||||
<entry id="3" value="300" status="inactive" />
|
|
||||||
</data>
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
<data>
|
|
||||||
<entry id="ID-1" value="200" />
|
|
||||||
<entry id="ID-2" value="400" status="ACTIVE" />
|
|
||||||
<entry id="ID-3" value="300" status="inactive" />
|
|
||||||
</data>
|
|
||||||
`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`<entry id="(?<id_num>\d+)" value="(?<val>\d+)"(?: status="(?<status>[^"]*)")? />`,
|
|
||||||
`-- Prefix the ID with "ID-"
|
|
||||||
id_num = "ID-" .. id_num
|
|
||||||
print(id_num)
|
|
||||||
print(val)
|
|
||||||
print(status)
|
|
||||||
|
|
||||||
-- Double the value except for inactive status
|
|
||||||
if not status or status ~= "inactive" then
|
|
||||||
val = val * 2
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Convert status to uppercase if present and active
|
|
||||||
if status and status == "active" then
|
|
||||||
status = string.upper(status)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Build the replacement based on whether status exists
|
|
||||||
if status then
|
|
||||||
replacement = string.format('<entry id="%s" value="%s" status="%s" />', id_num, val, status)
|
|
||||||
else
|
|
||||||
replacement = string.format('<entry id="%s" value="%s" />', id_num, val)
|
|
||||||
end`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 3 {
|
|
||||||
t.Errorf("Expected 3 matches, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 3 {
|
|
||||||
t.Errorf("Expected 3 modifications, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedResult := normalizeWhitespace(result)
|
|
||||||
normalizedExpected := normalizeWhitespace(expected)
|
|
||||||
|
|
||||||
if normalizedResult != normalizedExpected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSimpleNamedCapture(t *testing.T) {
|
|
||||||
content := `<product name="Widget" price="19.99"/>`
|
|
||||||
|
|
||||||
expected := `<product name="WIDGET" price="19.99"/>`
|
|
||||||
|
|
||||||
p := &RegexProcessor{}
|
|
||||||
result, mods, matches, err := p.ProcessContent(
|
|
||||||
content,
|
|
||||||
`name="(?<product_name>[^"]+)"`,
|
|
||||||
`product_name = string.upper(product_name)`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error processing content: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches != 1 {
|
|
||||||
t.Errorf("Expected 1 match, got %d", matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mods != 1 {
|
|
||||||
t.Errorf("Expected 1 modification, got %d", mods)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
467
processor/xml.go
467
processor/xml.go
@@ -2,8 +2,8 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"os"
|
||||||
"modify/processor/xpath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
"github.com/antchfx/xmlquery"
|
||||||
@@ -13,18 +13,44 @@ import (
|
|||||||
// XMLProcessor implements the Processor interface for XML documents
|
// XMLProcessor implements the Processor interface for XML documents
|
||||||
type XMLProcessor struct{}
|
type XMLProcessor struct{}
|
||||||
|
|
||||||
|
// Process implements the Processor interface for XMLProcessor
|
||||||
|
func (p *XMLProcessor) Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Process the content
|
||||||
|
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modCount, matchCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessContent implements the Processor interface for XMLProcessor
|
// ProcessContent implements the Processor interface for XMLProcessor
|
||||||
func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr string) (string, int, int, error) {
|
func (p *XMLProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||||
// Parse XML document
|
// Parse XML document
|
||||||
// We can't really use encoding/xml here because it requires a pre defined struct
|
|
||||||
// And we HAVE TO parse dynamic unknown XML
|
|
||||||
doc, err := xmlquery.Parse(strings.NewReader(content))
|
doc, err := xmlquery.Parse(strings.NewReader(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
|
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find nodes matching the XPath pattern
|
// Find nodes matching the XPath pattern
|
||||||
nodes, err := xpath.Get(doc, path)
|
nodes, err := xmlquery.QueryAll(doc, pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
|
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
|
||||||
}
|
}
|
||||||
@@ -34,45 +60,104 @@ func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr strin
|
|||||||
return content, 0, 0, nil
|
return content, 0, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Lua
|
||||||
|
L := lua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
|
||||||
|
// Load math library
|
||||||
|
L.Push(L.GetGlobal("require"))
|
||||||
|
L.Push(lua.LString("math"))
|
||||||
|
if err := L.PCall(1, 1, nil); err != nil {
|
||||||
|
return content, 0, 0, fmt.Errorf("error loading Lua math library: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load helper functions
|
||||||
|
if err := InitLuaHelpers(L); err != nil {
|
||||||
|
return content, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// Apply modifications to each node
|
// Apply modifications to each node
|
||||||
modCount := 0
|
modCount := 0
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
L, err := NewLuaState()
|
// Reset Lua state for each node
|
||||||
if err != nil {
|
L.SetGlobal("v1", lua.LNil)
|
||||||
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
L.SetGlobal("s1", lua.LNil)
|
||||||
}
|
|
||||||
defer L.Close()
|
|
||||||
|
|
||||||
err = p.ToLua(L, node)
|
// Get the node value
|
||||||
|
var originalValue string
|
||||||
|
if node.Type == xmlquery.AttributeNode {
|
||||||
|
originalValue = node.InnerText()
|
||||||
|
} else if node.Type == xmlquery.TextNode {
|
||||||
|
originalValue = node.Data
|
||||||
|
} else {
|
||||||
|
originalValue = node.InnerText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Lua variables
|
||||||
|
err = p.ToLua(L, originalValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = L.DoString(BuildLuaScript(luaExpr))
|
// Execute Lua script
|
||||||
if err != nil {
|
if err := L.DoString(luaExpr); err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get modified value
|
||||||
result, err := p.FromLua(L)
|
result, err := p.FromLua(L)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("%#v", result)
|
|
||||||
|
|
||||||
modified := false
|
newValue, ok := result.(string)
|
||||||
modified = L.GetGlobal("modified").String() == "true"
|
if !ok {
|
||||||
if !modified {
|
return content, modCount, matchCount, fmt.Errorf("expected string result from Lua, got %T", result)
|
||||||
log.Printf("No changes made to node at path: %s", node.Data)
|
}
|
||||||
|
|
||||||
|
// Skip if no change
|
||||||
|
if newValue == originalValue {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply modification based on the result
|
// Apply modification
|
||||||
if updatedValue, ok := result.(string); ok {
|
if node.Type == xmlquery.AttributeNode {
|
||||||
// If the result is a simple string, update the node value directly
|
// For attribute nodes, update the attribute value
|
||||||
xpath.Set(doc, path, updatedValue)
|
node.Parent.Attr = append([]xmlquery.Attr{}, node.Parent.Attr...)
|
||||||
} else if nodeData, ok := result.(map[string]interface{}); ok {
|
for i, attr := range node.Parent.Attr {
|
||||||
// If the result is a map, apply more complex updates
|
if attr.Name.Local == node.Data {
|
||||||
updateNodeFromMap(node, nodeData)
|
node.Parent.Attr[i].Value = newValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if node.Type == xmlquery.TextNode {
|
||||||
|
// For text nodes, update the text content
|
||||||
|
node.Data = newValue
|
||||||
|
} else {
|
||||||
|
// For element nodes, replace inner text
|
||||||
|
// Simple approach: set the InnerText directly if there are no child elements
|
||||||
|
if node.FirstChild == nil || (node.FirstChild != nil && node.FirstChild.Type == xmlquery.TextNode && node.FirstChild.NextSibling == nil) {
|
||||||
|
if node.FirstChild != nil {
|
||||||
|
node.FirstChild.Data = newValue
|
||||||
|
} else {
|
||||||
|
// Create a new text node and add it as the first child
|
||||||
|
textNode := &xmlquery.Node{
|
||||||
|
Type: xmlquery.TextNode,
|
||||||
|
Data: newValue,
|
||||||
|
}
|
||||||
|
node.FirstChild = textNode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Complex case: node has mixed content or child elements
|
||||||
|
// Replace just the text content while preserving child elements
|
||||||
|
// This is a simplified approach - more complex XML may need more robust handling
|
||||||
|
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||||
|
if child.Type == xmlquery.TextNode {
|
||||||
|
child.Data = newValue
|
||||||
|
break // Update only the first text node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modCount++
|
modCount++
|
||||||
@@ -84,329 +169,49 @@ func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr strin
|
|||||||
declaration := doc.FirstChild.OutputXML(true)
|
declaration := doc.FirstChild.OutputXML(true)
|
||||||
// Remove the firstChild (declaration) before serializing the rest of the document
|
// Remove the firstChild (declaration) before serializing the rest of the document
|
||||||
doc.FirstChild = doc.FirstChild.NextSibling
|
doc.FirstChild = doc.FirstChild.NextSibling
|
||||||
return ConvertToNamedEntities(declaration + doc.OutputXML(true)), modCount, matchCount, nil
|
return declaration + doc.OutputXML(true), modCount, matchCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert numeric entities to named entities for better readability
|
return doc.OutputXML(true), modCount, matchCount, nil
|
||||||
return ConvertToNamedEntities(doc.OutputXML(true)), modCount, matchCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
|
|
||||||
table, err := p.ToLuaTable(L, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
L.SetGlobal("v", table)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLua converts XML node values to Lua variables
|
// ToLua converts XML node values to Lua variables
|
||||||
func (p *XMLProcessor) ToLuaTable(L *lua.LState, data interface{}) (lua.LValue, error) {
|
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||||
// Check if data is an xmlquery.Node
|
value, ok := data.(string)
|
||||||
node, ok := data.(*xmlquery.Node)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("expected xmlquery.Node, got %T", data)
|
return fmt.Errorf("expected string value, got %T", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a simple table with essential data
|
// Set as string variable
|
||||||
table := L.NewTable()
|
L.SetGlobal("s1", lua.LString(value))
|
||||||
|
|
||||||
// For element nodes, just provide basic info
|
// Try to convert to number if possible
|
||||||
L.SetField(table, "type", lua.LString(nodeTypeToString(node.Type)))
|
L.SetGlobal("v1", lua.LNumber(0)) // Default to 0
|
||||||
L.SetField(table, "name", lua.LString(node.Data))
|
if err := L.DoString(fmt.Sprintf("v1 = tonumber(%q) or 0", value)); err != nil {
|
||||||
L.SetField(table, "value", lua.LString(node.InnerText()))
|
return fmt.Errorf("error converting value to number: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Add children if any
|
return nil
|
||||||
children := L.NewTable()
|
|
||||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
||||||
childTable, err := p.ToLuaTable(L, child)
|
|
||||||
if err == nil {
|
|
||||||
children.Append(childTable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
L.SetField(table, "children", children)
|
|
||||||
|
|
||||||
attrs := L.NewTable()
|
|
||||||
if len(node.Attr) > 0 {
|
|
||||||
for _, attr := range node.Attr {
|
|
||||||
L.SetField(attrs, attr.Name.Local, lua.LString(attr.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
L.SetField(table, "attr", attrs)
|
|
||||||
|
|
||||||
return table, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromLua gets modified values from Lua
|
// FromLua gets modified values from Lua
|
||||||
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||||
luaValue := L.GetGlobal("v")
|
// Check if string variable was modified
|
||||||
|
s1 := L.GetGlobal("s1")
|
||||||
// Handle string values directly
|
if s1 != lua.LNil {
|
||||||
if luaValue.Type() == lua.LTString {
|
if s1Str, ok := s1.(lua.LString); ok {
|
||||||
return luaValue.String(), nil
|
return string(s1Str), nil
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tables (for attributes and more complex updates)
|
|
||||||
if luaValue.Type() == lua.LTTable {
|
|
||||||
return luaTableToMap(L, luaValue.(*lua.LTable)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return luaValue.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple helper to convert a Lua table to a Go map
|
|
||||||
func luaTableToMap(L *lua.LState, table *lua.LTable) map[string]interface{} {
|
|
||||||
result := make(map[string]interface{})
|
|
||||||
|
|
||||||
table.ForEach(func(k, v lua.LValue) {
|
|
||||||
if k.Type() == lua.LTString {
|
|
||||||
key := k.String()
|
|
||||||
|
|
||||||
if v.Type() == lua.LTTable {
|
|
||||||
result[key] = luaTableToMap(L, v.(*lua.LTable))
|
|
||||||
} else {
|
|
||||||
result[key] = v.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple helper to convert node type to string
|
|
||||||
func nodeTypeToString(nodeType xmlquery.NodeType) string {
|
|
||||||
switch nodeType {
|
|
||||||
case xmlquery.ElementNode:
|
|
||||||
return "element"
|
|
||||||
case xmlquery.TextNode:
|
|
||||||
return "text"
|
|
||||||
case xmlquery.AttributeNode:
|
|
||||||
return "attribute"
|
|
||||||
default:
|
|
||||||
return "other"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to update an XML node from a map
|
// Check if numeric variable was modified
|
||||||
func updateNodeFromMap(node *xmlquery.Node, data map[string]interface{}) {
|
v1 := L.GetGlobal("v1")
|
||||||
// Update node value if present
|
if v1 != lua.LNil {
|
||||||
if value, ok := data["value"]; ok {
|
if v1Num, ok := v1.(lua.LNumber); ok {
|
||||||
if strValue, ok := value.(string); ok {
|
return fmt.Sprintf("%v", v1Num), nil
|
||||||
// For element nodes, replace text content
|
|
||||||
if node.Type == xmlquery.ElementNode {
|
|
||||||
// Find the first text child if it exists
|
|
||||||
var textNode *xmlquery.Node
|
|
||||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
||||||
if child.Type == xmlquery.TextNode {
|
|
||||||
textNode = child
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if textNode != nil {
|
// Default return empty string
|
||||||
// Update existing text node
|
return "", nil
|
||||||
textNode.Data = strValue
|
|
||||||
} else {
|
|
||||||
// Create new text node
|
|
||||||
newText := &xmlquery.Node{
|
|
||||||
Type: xmlquery.TextNode,
|
|
||||||
Data: strValue,
|
|
||||||
Parent: node,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert at beginning of children
|
|
||||||
if node.FirstChild != nil {
|
|
||||||
newText.NextSibling = node.FirstChild
|
|
||||||
node.FirstChild.PrevSibling = newText
|
|
||||||
node.FirstChild = newText
|
|
||||||
} else {
|
|
||||||
node.FirstChild = newText
|
|
||||||
node.LastChild = newText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node.Type == xmlquery.TextNode {
|
|
||||||
// Directly update text node
|
|
||||||
node.Data = strValue
|
|
||||||
} else if node.Type == xmlquery.AttributeNode {
|
|
||||||
// Update attribute value
|
|
||||||
if node.Parent != nil {
|
|
||||||
for i, attr := range node.Parent.Attr {
|
|
||||||
if attr.Name.Local == node.Data {
|
|
||||||
node.Parent.Attr[i].Value = strValue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update attributes if present
|
|
||||||
if attrs, ok := data["attr"].(map[string]interface{}); ok && node.Type == xmlquery.ElementNode {
|
|
||||||
for name, value := range attrs {
|
|
||||||
if strValue, ok := value.(string); ok {
|
|
||||||
// Look for existing attribute
|
|
||||||
found := false
|
|
||||||
for i, attr := range node.Attr {
|
|
||||||
if attr.Name.Local == name {
|
|
||||||
node.Attr[i].Value = strValue
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new attribute if not found
|
|
||||||
if !found {
|
|
||||||
node.Attr = append(node.Attr, xmlquery.Attr{
|
|
||||||
Name: struct {
|
|
||||||
Space, Local string
|
|
||||||
}{Local: name},
|
|
||||||
Value: strValue,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get a string representation of node type
|
|
||||||
func nodeTypeName(nodeType xmlquery.NodeType) string {
|
|
||||||
switch nodeType {
|
|
||||||
case xmlquery.ElementNode:
|
|
||||||
return "element"
|
|
||||||
case xmlquery.TextNode:
|
|
||||||
return "text"
|
|
||||||
case xmlquery.AttributeNode:
|
|
||||||
return "attribute"
|
|
||||||
case xmlquery.CommentNode:
|
|
||||||
return "comment"
|
|
||||||
case xmlquery.DeclarationNode:
|
|
||||||
return "declaration"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToNamedEntities replaces numeric XML entities with their named counterparts
|
|
||||||
func ConvertToNamedEntities(xml string) string {
|
|
||||||
// Basic XML entities
|
|
||||||
replacements := map[string]string{
|
|
||||||
// Basic XML entities
|
|
||||||
""": """, // double quote
|
|
||||||
"'": "'", // single quote
|
|
||||||
"<": "<", // less than
|
|
||||||
">": ">", // greater than
|
|
||||||
"&": "&", // ampersand
|
|
||||||
|
|
||||||
// Common symbols
|
|
||||||
" ": " ", // non-breaking space
|
|
||||||
"©": "©", // copyright
|
|
||||||
"®": "®", // registered trademark
|
|
||||||
"€": "€", // euro
|
|
||||||
"£": "£", // pound
|
|
||||||
"¥": "¥", // yen
|
|
||||||
"¢": "¢", // cent
|
|
||||||
"§": "§", // section
|
|
||||||
"™": "™", // trademark
|
|
||||||
"♠": "♠", // spade
|
|
||||||
"♣": "♣", // club
|
|
||||||
"♥": "♥", // heart
|
|
||||||
"♦": "♦", // diamond
|
|
||||||
|
|
||||||
// Special characters
|
|
||||||
"¡": "¡", // inverted exclamation
|
|
||||||
"¿": "¿", // inverted question
|
|
||||||
"«": "«", // left angle quotes
|
|
||||||
"»": "»", // right angle quotes
|
|
||||||
"·": "·", // middle dot
|
|
||||||
"•": "•", // bullet
|
|
||||||
"…": "…", // horizontal ellipsis
|
|
||||||
"′": "′", // prime
|
|
||||||
"″": "″", // double prime
|
|
||||||
"‾": "‾", // overline
|
|
||||||
"⁄": "⁄", // fraction slash
|
|
||||||
|
|
||||||
// Math symbols
|
|
||||||
"±": "±", // plus-minus
|
|
||||||
"×": "×", // multiplication
|
|
||||||
"÷": "÷", // division
|
|
||||||
"∞": "∞", // infinity
|
|
||||||
"≈": "≈", // almost equal
|
|
||||||
"≠": "≠", // not equal
|
|
||||||
"≤": "≤", // less than or equal
|
|
||||||
"≥": "≥", // greater than or equal
|
|
||||||
"∑": "∑", // summation
|
|
||||||
"√": "√", // square root
|
|
||||||
"∫": "∫", // integral
|
|
||||||
|
|
||||||
// Accented characters
|
|
||||||
"À": "À", // A grave
|
|
||||||
"Á": "Á", // A acute
|
|
||||||
"Â": "Â", // A circumflex
|
|
||||||
"Ã": "Ã", // A tilde
|
|
||||||
"Ä": "Ä", // A umlaut
|
|
||||||
"Å": "Å", // A ring
|
|
||||||
"Æ": "Æ", // AE ligature
|
|
||||||
"Ç": "Ç", // C cedilla
|
|
||||||
"È": "È", // E grave
|
|
||||||
"É": "É", // E acute
|
|
||||||
"Ê": "Ê", // E circumflex
|
|
||||||
"Ë": "Ë", // E umlaut
|
|
||||||
"Ì": "Ì", // I grave
|
|
||||||
"Í": "Í", // I acute
|
|
||||||
"Î": "Î", // I circumflex
|
|
||||||
"Ï": "Ï", // I umlaut
|
|
||||||
"Ð": "Ð", // Eth
|
|
||||||
"Ñ": "Ñ", // N tilde
|
|
||||||
"Ò": "Ò", // O grave
|
|
||||||
"Ó": "Ó", // O acute
|
|
||||||
"Ô": "Ô", // O circumflex
|
|
||||||
"Õ": "Õ", // O tilde
|
|
||||||
"Ö": "Ö", // O umlaut
|
|
||||||
"Ø": "Ø", // O slash
|
|
||||||
"Ù": "Ù", // U grave
|
|
||||||
"Ú": "Ú", // U acute
|
|
||||||
"Û": "Û", // U circumflex
|
|
||||||
"Ü": "Ü", // U umlaut
|
|
||||||
"Ý": "Ý", // Y acute
|
|
||||||
"Þ": "Þ", // Thorn
|
|
||||||
"ß": "ß", // Sharp s
|
|
||||||
"à": "à", // a grave
|
|
||||||
"á": "á", // a acute
|
|
||||||
"â": "â", // a circumflex
|
|
||||||
"ã": "ã", // a tilde
|
|
||||||
"ä": "ä", // a umlaut
|
|
||||||
"å": "å", // a ring
|
|
||||||
"æ": "æ", // ae ligature
|
|
||||||
"ç": "ç", // c cedilla
|
|
||||||
"è": "è", // e grave
|
|
||||||
"é": "é", // e acute
|
|
||||||
"ê": "ê", // e circumflex
|
|
||||||
"ë": "ë", // e umlaut
|
|
||||||
"ì": "ì", // i grave
|
|
||||||
"í": "í", // i acute
|
|
||||||
"î": "î", // i circumflex
|
|
||||||
"ï": "ï", // i umlaut
|
|
||||||
"ð": "ð", // eth
|
|
||||||
"ñ": "ñ", // n tilde
|
|
||||||
"ò": "ò", // o grave
|
|
||||||
"ó": "ó", // o acute
|
|
||||||
"ô": "ô", // o circumflex
|
|
||||||
"õ": "õ", // o tilde
|
|
||||||
"ö": "ö", // o umlaut
|
|
||||||
"ø": "ø", // o slash
|
|
||||||
"ù": "ù", // u grave
|
|
||||||
"ú": "ú", // u acute
|
|
||||||
"û": "û", // u circumflex
|
|
||||||
"ü": "ü", // u umlaut
|
|
||||||
"ý": "ý", // y acute
|
|
||||||
"þ": "þ", // thorn
|
|
||||||
"ÿ": "ÿ", // y umlaut
|
|
||||||
}
|
|
||||||
|
|
||||||
result := xml
|
|
||||||
for numeric, named := range replacements {
|
|
||||||
result = strings.ReplaceAll(result, numeric, named)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
@@ -5,21 +5,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper function to normalize whitespace for comparison
|
// Helper function to normalize whitespace for comparison
|
||||||
func normalizeXMLWhitespace(s string) string {
|
func normalizeXMLWhitespace(s string) string {
|
||||||
// Replace all whitespace sequences with a single space
|
// Replace all whitespace sequences with a single space
|
||||||
re := regexp.MustCompile(`\s+`)
|
re := regexp.MustCompile(`\s+`)
|
||||||
s = re.ReplaceAllString(strings.TrimSpace(s), " ")
|
return re.ReplaceAllString(strings.TrimSpace(s), " ")
|
||||||
|
|
||||||
// Normalize XML entities for comparison
|
|
||||||
s = ConvertToNamedEntities(s)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestXMLProcessor_Process_NodeValues(t *testing.T) {
|
func TestXMLProcessor_Process_NodeValues(t *testing.T) {
|
||||||
@@ -47,7 +39,7 @@ func TestXMLProcessor_Process_NodeValues(t *testing.T) {
|
|||||||
<catalog>
|
<catalog>
|
||||||
<book id="bk101">
|
<book id="bk101">
|
||||||
<author>Gambardella, Matthew</author>
|
<author>Gambardella, Matthew</author>
|
||||||
<title>XML Developer's Guide</title>
|
<title>XML Developer's Guide</title>
|
||||||
<genre>Computer</genre>
|
<genre>Computer</genre>
|
||||||
<price>89.9</price>
|
<price>89.9</price>
|
||||||
<publish_date>2000-10-01</publish_date>
|
<publish_date>2000-10-01</publish_date>
|
||||||
@@ -64,7 +56,7 @@ func TestXMLProcessor_Process_NodeValues(t *testing.T) {
|
|||||||
</catalog>`
|
</catalog>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v.value = v.value * 2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v = v * 2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -101,7 +93,7 @@ func TestXMLProcessor_Process_Attributes(t *testing.T) {
|
|||||||
</items>`
|
</items>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//item/@price", "v.value = v.value * 2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//item/@price", "v = v * 2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -138,7 +130,7 @@ func TestXMLProcessor_Process_ElementText(t *testing.T) {
|
|||||||
</names>`
|
</names>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//n/text()", "v.value = string.upper(v.value)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//n/text()", "v = string.upper(v)")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -179,7 +171,7 @@ func TestXMLProcessor_Process_ElementAddition(t *testing.T) {
|
|||||||
</config>`
|
</config>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//settings/*", "v.value = v.value * 2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//settings/*", "v = v * 2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -244,7 +236,7 @@ func TestXMLProcessor_Process_ComplexXML(t *testing.T) {
|
|||||||
</store>`
|
</store>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v.value = round(v.value * 1.2, 3)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//price", "v = v * 1.2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -279,21 +271,19 @@ func TestXMLProcessor_ConditionalModification(t *testing.T) {
|
|||||||
|
|
||||||
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<inventory>
|
<inventory>
|
||||||
<item id="1" stock="5" price="8.00"></item>
|
<item id="1" stock="5" price="8.00"/>
|
||||||
<item id="2" stock="15" price="16.00"></item>
|
<item id="2" stock="15" price="16.00"/>
|
||||||
<item id="3" stock="0" price="15.00"></item>
|
<item id="3" stock="0" price="15.00"/>
|
||||||
</inventory>`
|
</inventory>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
// Apply 20% discount but only for items with stock > 0
|
// Apply 20% discount but only for items with stock > 0
|
||||||
luaExpr := `
|
luaExpr := `
|
||||||
-- In the table-based approach, attributes are accessible directly
|
-- In the table-based approach, attributes are accessible directly
|
||||||
if v.attr.stock and tonumber(v.attr.stock) > 0 then
|
if v.stock and tonumber(v.stock) > 0 then
|
||||||
v.attr.price = tonumber(v.attr.price) * 0.8
|
v.price = tonumber(v.price) * 0.8
|
||||||
-- Format to 2 decimal places
|
-- Format to 2 decimal places
|
||||||
v.attr.price = string.format("%.2f", v.attr.price)
|
v.price = string.format("%.2f", v.price)
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//item", luaExpr)
|
result, modCount, matchCount, err := p.ProcessContent(content, "//item", luaExpr)
|
||||||
@@ -337,7 +327,7 @@ func TestXMLProcessor_Process_SpecialCharacters(t *testing.T) {
|
|||||||
</data>`
|
</data>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//entry", "v.value = string.upper(v.value)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//entry", "v = string.upper(v)")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -372,14 +362,15 @@ func TestXMLProcessor_Process_ChainedOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Apply multiple operations to the price: add tax, apply discount, round
|
// Apply multiple operations to the price: add tax, apply discount, round
|
||||||
luaExpr := `
|
luaExpr := `
|
||||||
local price = v.value
|
-- When v is a numeric string, we can perform math operations directly
|
||||||
|
local price = v
|
||||||
-- Add 15% tax
|
-- Add 15% tax
|
||||||
price = price * 1.15
|
price = price * 1.15
|
||||||
-- Apply 10% discount
|
-- Apply 10% discount
|
||||||
price = price * 0.9
|
price = price * 0.9
|
||||||
-- Round to 2 decimal places
|
-- Round to 2 decimal places
|
||||||
price = round(price, 2)
|
price = math.floor(price * 100 + 0.5) / 100
|
||||||
v.value = price
|
v = price
|
||||||
`
|
`
|
||||||
|
|
||||||
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -431,7 +422,7 @@ func TestXMLProcessor_Process_MathFunctions(t *testing.T) {
|
|||||||
</measurements>`
|
</measurements>`
|
||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//measurement", "v.value = round(v.value)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//measurement", "v = round(v)")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -477,9 +468,9 @@ func TestXMLProcessor_Process_StringOperations(t *testing.T) {
|
|||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//email", `
|
result, modCount, matchCount, err := p.ProcessContent(content, "//email", `
|
||||||
-- With the table approach, v contains the text content directly
|
-- With the table approach, v contains the text content directly
|
||||||
v.value = string.gsub(v.value, "@.+", "@anon.com")
|
v = string.gsub(v, "@.+", "@anon.com")
|
||||||
local username = string.match(v.value, "(.+)@")
|
local username = string.match(v, "(.+)@")
|
||||||
v.value = string.gsub(username, "%.", "") .. "@anon.com"
|
v = string.gsub(username, "%.", "") .. "@anon.com"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -488,7 +479,7 @@ func TestXMLProcessor_Process_StringOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Test phone number masking
|
// Test phone number masking
|
||||||
result, modCount2, matchCount2, err := p.ProcessContent(result, "//phone", `
|
result, modCount2, matchCount2, err := p.ProcessContent(result, "//phone", `
|
||||||
v.value = string.gsub(v.value, "%d%d%d%-%d%d%d%-%d%d%d%d", function(match)
|
v = string.gsub(v, "%d%d%d%-%d%d%d%-%d%d%d%d", function(match)
|
||||||
return string.sub(match, 1, 3) .. "-XXX-XXXX"
|
return string.sub(match, 1, 3) .. "-XXX-XXXX"
|
||||||
end)
|
end)
|
||||||
`)
|
`)
|
||||||
@@ -545,14 +536,14 @@ func TestXMLProcessor_Process_DateManipulation(t *testing.T) {
|
|||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//date", `
|
result, modCount, matchCount, err := p.ProcessContent(content, "//date", `
|
||||||
local year, month, day = string.match(v.value, "(%d%d%d%d)-(%d%d)-(%d%d)")
|
local year, month, day = string.match(v, "(%d%d%d%d)-(%d%d)-(%d%d)")
|
||||||
-- Postpone events by 1 month
|
-- Postpone events by 1 month
|
||||||
month = tonumber(month) + 1
|
month = tonumber(month) + 1
|
||||||
if month > 12 then
|
if month > 12 then
|
||||||
month = 1
|
month = 1
|
||||||
year = tonumber(year) + 1
|
year = tonumber(year) + 1
|
||||||
end
|
end
|
||||||
v.value = string.format("%04d-%02d-%s", tonumber(year), month, day)
|
v = string.format("%04d-%02d-%s", tonumber(year), month, day)
|
||||||
`)
|
`)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -618,6 +609,36 @@ func TestXMLProcessor_Process_Error_InvalidLua(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXMLProcessor_Process_NoChanges(t *testing.T) {
|
||||||
|
content := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<root>
|
||||||
|
<element>123</element>
|
||||||
|
</root>`
|
||||||
|
|
||||||
|
p := &XMLProcessor{}
|
||||||
|
result, modCount, matchCount, err := p.ProcessContent(content, "//element", "v1 = v1")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error processing content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchCount != 1 {
|
||||||
|
t.Errorf("Expected 1 match, got %d", matchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modCount != 0 {
|
||||||
|
t.Errorf("Expected 0 modifications, got %d", modCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize whitespace for comparison
|
||||||
|
normalizedResult := normalizeXMLWhitespace(result)
|
||||||
|
normalizedContent := normalizeXMLWhitespace(content)
|
||||||
|
|
||||||
|
if normalizedResult != normalizedContent {
|
||||||
|
t.Errorf("Expected content to be unchanged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestXMLProcessor_Process_ComplexXPathSelectors(t *testing.T) {
|
func TestXMLProcessor_Process_ComplexXPathSelectors(t *testing.T) {
|
||||||
content := `<?xml version="1.0" encoding="UTF-8"?>
|
content := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<library>
|
<library>
|
||||||
@@ -663,7 +684,7 @@ func TestXMLProcessor_Process_ComplexXPathSelectors(t *testing.T) {
|
|||||||
|
|
||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
// Target only fiction books and apply 20% discount to price
|
// Target only fiction books and apply 20% discount to price
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//book[@category='fiction']/price", "v.value = round(v.value * 0.8, 2)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//book[@category='fiction']/price", "v = v * 0.8")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -746,13 +767,13 @@ func TestXMLProcessor_Process_NestedStructureModification(t *testing.T) {
|
|||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
|
|
||||||
// Boost hero stats by 20%
|
// Boost hero stats by 20%
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//character[@id='hero']/stats/*", "v.value = round(v.value * 1.2)")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//character[@id='hero']/stats/*", "v = math.floor(v * 1.2)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing stats content: %v", err)
|
t.Fatalf("Error processing stats content: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also upgrade hero equipment
|
// Also upgrade hero equipment
|
||||||
result, modCount2, matchCount2, err := p.ProcessContent(result, "//character[@id='hero']/equipment/*/@damage|//character[@id='hero']/equipment/*/@defense", "v.value = v.value + 2")
|
result, modCount2, matchCount2, err := p.ProcessContent(result, "//character[@id='hero']/equipment/*/@damage|//character[@id='hero']/equipment/*/@defense", "v = v + 2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing equipment content: %v", err)
|
t.Fatalf("Error processing equipment content: %v", err)
|
||||||
}
|
}
|
||||||
@@ -814,8 +835,8 @@ func TestXMLProcessor_Process_ElementReplacement(t *testing.T) {
|
|||||||
|
|
||||||
luaExpr := `
|
luaExpr := `
|
||||||
-- With a proper table approach, this becomes much simpler
|
-- With a proper table approach, this becomes much simpler
|
||||||
local price = tonumber(v.attr.price)
|
local price = tonumber(v.price)
|
||||||
local quantity = tonumber(v.attr.quantity)
|
local quantity = tonumber(v.quantity)
|
||||||
|
|
||||||
-- Add a new total element
|
-- Add a new total element
|
||||||
v.total = string.format("%.2f", price * quantity)
|
v.total = string.format("%.2f", price * quantity)
|
||||||
@@ -881,11 +902,11 @@ func TestXMLProcessor_Process_AttributeAddition(t *testing.T) {
|
|||||||
-- We can access the "inStock" element directly
|
-- We can access the "inStock" element directly
|
||||||
if v.inStock == "true" then
|
if v.inStock == "true" then
|
||||||
-- Add a new attribute directly
|
-- Add a new attribute directly
|
||||||
v.attr = v.attr or {}
|
v._attr = v._attr or {}
|
||||||
v.attr.status = "available"
|
v._attr.status = "available"
|
||||||
else
|
else
|
||||||
v.attr = v.attr or {}
|
v._attr = v._attr or {}
|
||||||
v.attr.status = "out-of-stock"
|
v._attr.status = "out-of-stock"
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -1010,9 +1031,9 @@ func TestXMLProcessor_Process_ElementReordering(t *testing.T) {
|
|||||||
luaExpr := `
|
luaExpr := `
|
||||||
-- With table approach, we can reorder elements by redefining the table
|
-- With table approach, we can reorder elements by redefining the table
|
||||||
-- Store the values
|
-- Store the values
|
||||||
local artist = v.attr.artist
|
local artist = v.artist
|
||||||
local title = v.attr.title
|
local title = v.title
|
||||||
local year = v.attr.year
|
local year = v.year
|
||||||
|
|
||||||
-- Clear the table
|
-- Clear the table
|
||||||
for k in pairs(v) do
|
for k in pairs(v) do
|
||||||
@@ -1020,9 +1041,9 @@ func TestXMLProcessor_Process_ElementReordering(t *testing.T) {
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Add elements in the desired order
|
-- Add elements in the desired order
|
||||||
v.attr.title = title
|
v.title = title
|
||||||
v.attr.artist = artist
|
v.artist = artist
|
||||||
v.attr.year = year
|
v.year = year
|
||||||
`
|
`
|
||||||
|
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//song", luaExpr)
|
result, modCount, matchCount, err := p.ProcessContent(content, "//song", luaExpr)
|
||||||
@@ -1173,13 +1194,13 @@ func TestXMLProcessor_Process_DynamicXPath(t *testing.T) {
|
|||||||
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<settings>
|
<settings>
|
||||||
<setting name="timeout" value="60"></setting>
|
<setting name="timeout" value="60" />
|
||||||
<setting name="retries" value="3"></setting>
|
<setting name="retries" value="3" />
|
||||||
<setting name="backoff" value="exponential"></setting>
|
<setting name="backoff" value="exponential" />
|
||||||
</settings>
|
</settings>
|
||||||
<advanced>
|
<advanced>
|
||||||
<setting name="logging" value="debug"></setting>
|
<setting name="logging" value="debug" />
|
||||||
<setting name="timeout" value="120"></setting>
|
<setting name="timeout" value="120" />
|
||||||
</advanced>
|
</advanced>
|
||||||
</configuration>`
|
</configuration>`
|
||||||
|
|
||||||
@@ -1187,7 +1208,7 @@ func TestXMLProcessor_Process_DynamicXPath(t *testing.T) {
|
|||||||
p := &XMLProcessor{}
|
p := &XMLProcessor{}
|
||||||
|
|
||||||
// Double all timeout values in the configuration
|
// Double all timeout values in the configuration
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "//setting[@name='timeout']/@value", "v.value = v.value * 2")
|
result, modCount, matchCount, err := p.ProcessContent(content, "//setting[@name='timeout']/@value", "v = v * 2")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error processing content: %v", err)
|
t.Fatalf("Error processing content: %v", err)
|
||||||
@@ -1242,34 +1263,34 @@ func TestXMLProcessor_Process_TableBasedStructureCreation(t *testing.T) {
|
|||||||
local summary = ""
|
local summary = ""
|
||||||
|
|
||||||
-- Process each child option
|
-- Process each child option
|
||||||
local settings = v.children[1]
|
if v.settings and v.settings.option then
|
||||||
local options = settings.children
|
local options = v.settings.option
|
||||||
-- if settings and options then
|
-- If there's just one option, wrap it in a table
|
||||||
-- if options.attr then
|
if options._attr then
|
||||||
-- options = {options}
|
options = {options}
|
||||||
-- end
|
end
|
||||||
--
|
|
||||||
-- for i, opt in ipairs(options) do
|
for i, opt in ipairs(options) do
|
||||||
-- count = count + 1
|
count = count + 1
|
||||||
-- if opt.attr.name == "debug" then
|
if opt._attr.name == "debug" then
|
||||||
-- summary = summary .. "Debug: " .. (opt.attr.value == "true" and "ON" or "OFF")
|
summary = summary .. "Debug: " .. (opt._attr.value == "true" and "ON" or "OFF")
|
||||||
-- elseif opt.attr.name == "log_level" then
|
elseif opt._attr.name == "log_level" then
|
||||||
-- summary = summary .. "Logging: " .. opt.attr.value
|
summary = summary .. "Logging: " .. opt._attr.value
|
||||||
-- end
|
end
|
||||||
--
|
|
||||||
-- if i < #options then
|
if i < #options then
|
||||||
-- summary = summary .. ", "
|
summary = summary .. ", "
|
||||||
-- end
|
end
|
||||||
-- end
|
end
|
||||||
-- end
|
end
|
||||||
|
|
||||||
-- Create a new calculated section
|
-- Create a new calculated section
|
||||||
-- v.children[2] = {
|
v.calculated = {
|
||||||
-- stats = {
|
stats = {
|
||||||
-- count = tostring(count),
|
count = tostring(count),
|
||||||
-- summary = summary
|
summary = summary
|
||||||
-- }
|
}
|
||||||
-- }
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
result, modCount, matchCount, err := p.ProcessContent(content, "/data", luaExpr)
|
result, modCount, matchCount, err := p.ProcessContent(content, "/data", luaExpr)
|
||||||
@@ -1509,267 +1530,3 @@ func TestXMLProcessor_Process_DeepPathNavigation(t *testing.T) {
|
|||||||
|
|
||||||
// Add more test cases for specific XML manipulation scenarios below
|
// Add more test cases for specific XML manipulation scenarios below
|
||||||
// These tests would cover additional functionality as the implementation progresses
|
// These tests would cover additional functionality as the implementation progresses
|
||||||
|
|
||||||
func TestXMLToLua(t *testing.T) {
|
|
||||||
// Sample XML to test with
|
|
||||||
xmlStr := `
|
|
||||||
<root id="1">
|
|
||||||
<person name="John" age="30">
|
|
||||||
<address type="home">
|
|
||||||
<street>123 Main St</street>
|
|
||||||
<city>Anytown</city>
|
|
||||||
<zip>12345</zip>
|
|
||||||
</address>
|
|
||||||
<contact type="email">john@example.com</contact>
|
|
||||||
</person>
|
|
||||||
<person name="Jane" age="28">
|
|
||||||
<address type="work">
|
|
||||||
<street>456 Business Ave</street>
|
|
||||||
<city>Worktown</city>
|
|
||||||
<zip>54321</zip>
|
|
||||||
</address>
|
|
||||||
<contact type="phone">555-1234</contact>
|
|
||||||
</person>
|
|
||||||
</root>
|
|
||||||
`
|
|
||||||
|
|
||||||
// Parse the XML
|
|
||||||
doc, err := xmlquery.Parse(strings.NewReader(xmlStr))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse XML: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new Lua state
|
|
||||||
L := lua.NewState()
|
|
||||||
defer L.Close()
|
|
||||||
|
|
||||||
// Create an XML processor
|
|
||||||
processor := &XMLProcessor{}
|
|
||||||
|
|
||||||
// Test converting the root element to Lua
|
|
||||||
t.Run("RootElement", func(t *testing.T) {
|
|
||||||
// Find the root element
|
|
||||||
root := doc.SelectElement("root")
|
|
||||||
if root == nil {
|
|
||||||
t.Fatal("Failed to find root element")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to Lua
|
|
||||||
err := processor.ToLua(L, root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to convert to Lua: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
luaTable := L.GetGlobal("v")
|
|
||||||
if luaTable.Type() != lua.LTTable {
|
|
||||||
t.Fatalf("Expected table, got %s", luaTable.Type().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check element type
|
|
||||||
typeVal := L.GetField(luaTable, "type")
|
|
||||||
if typeVal.String() != "element" {
|
|
||||||
t.Errorf("Expected type 'element', got '%s'", typeVal.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check name
|
|
||||||
nameVal := L.GetField(luaTable, "name")
|
|
||||||
if nameVal.String() != "root" {
|
|
||||||
t.Errorf("Expected name 'root', got '%s'", nameVal.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check attributes
|
|
||||||
attrsTable := L.GetField(luaTable, "attributes")
|
|
||||||
if attrsTable.Type() != lua.LTTable {
|
|
||||||
t.Fatalf("Expected attributes table, got %s", attrsTable.Type().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
idVal := L.GetField(attrsTable, "id")
|
|
||||||
if idVal.String() != "1" {
|
|
||||||
t.Errorf("Expected id '1', got '%s'", idVal.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we have children
|
|
||||||
childrenTable := L.GetField(luaTable, "children")
|
|
||||||
if childrenTable.Type() != lua.LTTable {
|
|
||||||
t.Fatalf("Expected children table, got %s", childrenTable.Type().String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test converting a nested element to Lua
|
|
||||||
t.Run("NestedElement", func(t *testing.T) {
|
|
||||||
// Find a nested element
|
|
||||||
street := doc.SelectElement("//street")
|
|
||||||
if street == nil {
|
|
||||||
t.Fatal("Failed to find street element")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to Lua
|
|
||||||
err := processor.ToLua(L, street)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to convert to Lua: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
luaTable := L.GetGlobal("v")
|
|
||||||
if luaTable.Type() != lua.LTTable {
|
|
||||||
t.Fatalf("Expected table, got %s", luaTable.Type().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check element type
|
|
||||||
typeVal := L.GetField(luaTable, "type")
|
|
||||||
if typeVal.String() != "element" {
|
|
||||||
t.Errorf("Expected type 'element', got '%s'", typeVal.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check name
|
|
||||||
nameVal := L.GetField(luaTable, "name")
|
|
||||||
if nameVal.String() != "street" {
|
|
||||||
t.Errorf("Expected name 'street', got '%s'", nameVal.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check value
|
|
||||||
valueVal := L.GetField(luaTable, "value")
|
|
||||||
if valueVal.String() != "123 Main St" {
|
|
||||||
t.Errorf("Expected value '123 Main St', got '%s'", valueVal.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test FromLua with a simple string update
|
|
||||||
t.Run("FromLuaString", func(t *testing.T) {
|
|
||||||
// Set up a Lua state with a string value
|
|
||||||
L := lua.NewState()
|
|
||||||
defer L.Close()
|
|
||||||
L.SetGlobal("v", lua.LString("New Value"))
|
|
||||||
|
|
||||||
// Convert from Lua
|
|
||||||
result, err := processor.FromLua(L)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to convert from Lua: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
strResult, ok := result.(string)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected string result, got %T", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strResult != "New Value" {
|
|
||||||
t.Errorf("Expected 'New Value', got '%s'", strResult)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test FromLua with a complex table update
|
|
||||||
t.Run("FromLuaTable", func(t *testing.T) {
|
|
||||||
// Set up a Lua state with a table value
|
|
||||||
L := lua.NewState()
|
|
||||||
defer L.Close()
|
|
||||||
|
|
||||||
table := L.NewTable()
|
|
||||||
L.SetField(table, "value", lua.LString("Updated Text"))
|
|
||||||
|
|
||||||
attrTable := L.NewTable()
|
|
||||||
L.SetField(attrTable, "id", lua.LString("new-id"))
|
|
||||||
L.SetField(attrTable, "class", lua.LString("highlight"))
|
|
||||||
|
|
||||||
L.SetField(table, "attributes", attrTable)
|
|
||||||
L.SetGlobal("v", table)
|
|
||||||
|
|
||||||
// Convert from Lua
|
|
||||||
result, err := processor.FromLua(L)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to convert from Lua: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
mapResult, ok := result.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected map result, got %T", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check value
|
|
||||||
if value, ok := mapResult["value"]; !ok || value != "Updated Text" {
|
|
||||||
t.Errorf("Expected value 'Updated Text', got '%v'", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check attributes
|
|
||||||
attrs, ok := mapResult["attributes"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected attributes map, got %T", mapResult["attributes"])
|
|
||||||
}
|
|
||||||
|
|
||||||
if id, ok := attrs["id"]; !ok || id != "new-id" {
|
|
||||||
t.Errorf("Expected id 'new-id', got '%v'", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if class, ok := attrs["class"]; !ok || class != "highlight" {
|
|
||||||
t.Errorf("Expected class 'highlight', got '%v'", class)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test updateNodeFromMap with a simple value update
|
|
||||||
t.Run("UpdateNodeValue", func(t *testing.T) {
|
|
||||||
// Create a simple element to update
|
|
||||||
xmlStr := `<test>Original Text</test>`
|
|
||||||
doc, _ := xmlquery.Parse(strings.NewReader(xmlStr))
|
|
||||||
node := doc.SelectElement("test")
|
|
||||||
|
|
||||||
// Create update data
|
|
||||||
updateData := map[string]interface{}{
|
|
||||||
"value": "Updated Text",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the node
|
|
||||||
updateNodeFromMap(node, updateData)
|
|
||||||
|
|
||||||
// Verify the update
|
|
||||||
if node.InnerText() != "Updated Text" {
|
|
||||||
t.Errorf("Expected value 'Updated Text', got '%s'", node.InnerText())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test updateNodeFromMap with attribute updates
|
|
||||||
t.Run("UpdateNodeAttributes", func(t *testing.T) {
|
|
||||||
// Create an element with attributes
|
|
||||||
xmlStr := `<test id="old">Text</test>`
|
|
||||||
doc, _ := xmlquery.Parse(strings.NewReader(xmlStr))
|
|
||||||
node := doc.SelectElement("test")
|
|
||||||
|
|
||||||
// Create update data
|
|
||||||
updateData := map[string]interface{}{
|
|
||||||
"attributes": map[string]interface{}{
|
|
||||||
"id": "new",
|
|
||||||
"class": "added",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the node
|
|
||||||
updateNodeFromMap(node, updateData)
|
|
||||||
|
|
||||||
// Verify the id attribute was updated
|
|
||||||
idFound := false
|
|
||||||
classFound := false
|
|
||||||
for _, attr := range node.Attr {
|
|
||||||
if attr.Name.Local == "id" {
|
|
||||||
idFound = true
|
|
||||||
if attr.Value != "new" {
|
|
||||||
t.Errorf("Expected id 'new', got '%s'", attr.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "class" {
|
|
||||||
classFound = true
|
|
||||||
if attr.Value != "added" {
|
|
||||||
t.Errorf("Expected class 'added', got '%s'", attr.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !idFound {
|
|
||||||
t.Error("Expected to find 'id' attribute but didn't")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !classFound {
|
|
||||||
t.Error("Expected to find 'class' attribute but didn't")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
|
||||||
// The parsing functionality tests have been removed since we're now
|
|
||||||
// delegating XPath parsing to the xmlquery library.
|
|
||||||
package xpath
|
|
@@ -1,4 +0,0 @@
|
|||||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
|
||||||
// The parsing functionality tests have been removed since we're now
|
|
||||||
// delegating XPath parsing to the xmlquery library.
|
|
||||||
package xpath
|
|
@@ -1,133 +0,0 @@
|
|||||||
package xpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get retrieves nodes from XML data using an XPath expression
|
|
||||||
func Get(node *xmlquery.Node, path string) ([]*xmlquery.Node, error) {
|
|
||||||
if node == nil {
|
|
||||||
return nil, errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute xpath query directly
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set updates a single node in the XML data using an XPath expression
|
|
||||||
func Set(node *xmlquery.Node, path string, value interface{}) error {
|
|
||||||
if node == nil {
|
|
||||||
return errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the node to update
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return fmt.Errorf("no nodes found for path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the first matching node
|
|
||||||
updateNodeValue(nodes[0], value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAll updates all nodes that match the XPath expression
|
|
||||||
func SetAll(node *xmlquery.Node, path string, value interface{}) error {
|
|
||||||
if node == nil {
|
|
||||||
return errors.New("nil node provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all nodes to update
|
|
||||||
nodes, err := xmlquery.QueryAll(node, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return fmt.Errorf("no nodes found for path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all matching nodes
|
|
||||||
for _, matchNode := range nodes {
|
|
||||||
updateNodeValue(matchNode, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to update a node's value
|
|
||||||
func updateNodeValue(node *xmlquery.Node, value interface{}) {
|
|
||||||
strValue := fmt.Sprintf("%v", value)
|
|
||||||
|
|
||||||
// Handle different node types
|
|
||||||
switch node.Type {
|
|
||||||
case xmlquery.AttributeNode:
|
|
||||||
// For attribute nodes, update the attribute value
|
|
||||||
parent := node.Parent
|
|
||||||
if parent != nil {
|
|
||||||
for i, attr := range parent.Attr {
|
|
||||||
if attr.Name.Local == node.Data {
|
|
||||||
parent.Attr[i].Value = strValue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case xmlquery.TextNode:
|
|
||||||
// For text nodes, update the text content
|
|
||||||
node.Data = strValue
|
|
||||||
case xmlquery.ElementNode:
|
|
||||||
// For element nodes, clear existing text children and add a new text node
|
|
||||||
// First, remove all existing text children
|
|
||||||
var nonTextChildren []*xmlquery.Node
|
|
||||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
|
||||||
if child.Type != xmlquery.TextNode {
|
|
||||||
nonTextChildren = append(nonTextChildren, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all children
|
|
||||||
node.FirstChild = nil
|
|
||||||
node.LastChild = nil
|
|
||||||
|
|
||||||
// Add a new text node
|
|
||||||
textNode := &xmlquery.Node{
|
|
||||||
Type: xmlquery.TextNode,
|
|
||||||
Data: strValue,
|
|
||||||
Parent: node,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the text node as the first child
|
|
||||||
node.FirstChild = textNode
|
|
||||||
node.LastChild = textNode
|
|
||||||
|
|
||||||
// Add back non-text children
|
|
||||||
for _, child := range nonTextChildren {
|
|
||||||
child.Parent = node
|
|
||||||
|
|
||||||
// If this is the first child being added back
|
|
||||||
if node.FirstChild == textNode && node.LastChild == textNode {
|
|
||||||
node.FirstChild.NextSibling = child
|
|
||||||
child.PrevSibling = node.FirstChild
|
|
||||||
node.LastChild = child
|
|
||||||
} else {
|
|
||||||
// Add to the end of the chain
|
|
||||||
node.LastChild.NextSibling = child
|
|
||||||
child.PrevSibling = node.LastChild
|
|
||||||
node.LastChild = child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,474 +0,0 @@
|
|||||||
package xpath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/antchfx/xmlquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse test XML data once at the beginning for use in multiple tests
|
|
||||||
func parseTestXML(t *testing.T, xmlData string) *xmlquery.Node {
|
|
||||||
doc, err := xmlquery.Parse(strings.NewReader(xmlData))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse test XML: %v", err)
|
|
||||||
}
|
|
||||||
return doc
|
|
||||||
}
|
|
||||||
|
|
||||||
// XML test data as a string for our tests
|
|
||||||
var testXML = `
|
|
||||||
<store>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Fellowship of the Ring</title>
|
|
||||||
<author>J.R.R. Tolkien</author>
|
|
||||||
<year>1954</year>
|
|
||||||
<price>22.99</price>
|
|
||||||
</book>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Two Towers</title>
|
|
||||||
<author>J.R.R. Tolkien</author>
|
|
||||||
<year>1954</year>
|
|
||||||
<price>23.45</price>
|
|
||||||
</book>
|
|
||||||
<book category="technical">
|
|
||||||
<title lang="en">Learning XML</title>
|
|
||||||
<author>Erik T. Ray</author>
|
|
||||||
<year>2003</year>
|
|
||||||
<price>39.95</price>
|
|
||||||
</book>
|
|
||||||
<bicycle>
|
|
||||||
<color>red</color>
|
|
||||||
<price>199.95</price>
|
|
||||||
</bicycle>
|
|
||||||
</store>
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestEvaluator(t *testing.T) {
|
|
||||||
// Parse the test XML data once for all test cases
|
|
||||||
doc := parseTestXML(t, testXML)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
error bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple_element_access",
|
|
||||||
path: "/store/bicycle/color",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recursive_element_access",
|
|
||||||
path: "//price",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard_element_access",
|
|
||||||
path: "/store/book/*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_exists_predicate",
|
|
||||||
path: "//title[@lang]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_equals_predicate",
|
|
||||||
path: "//title[@lang='en']",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "value_comparison_predicate",
|
|
||||||
path: "/store/book[price>35.00]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_predicate",
|
|
||||||
path: "/store/book[last()]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "last_minus_predicate",
|
|
||||||
path: "/store/book[last()-1]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "position_predicate",
|
|
||||||
path: "/store/book[position()<3]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid_index",
|
|
||||||
path: "/store/book[10]/title",
|
|
||||||
error: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent_element",
|
|
||||||
path: "/store/nonexistent",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := Get(doc, tt.path)
|
|
||||||
|
|
||||||
// Handle expected errors
|
|
||||||
if tt.error {
|
|
||||||
if err == nil && len(result) == 0 {
|
|
||||||
// If we expected an error but got empty results instead, that's okay
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// If we got an error as expected, that's okay
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
// If we didn't expect an error but got one, that's a test failure
|
|
||||||
t.Errorf("Get(%q) returned unexpected error: %v", tt.path, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special cases where we don't care about exact matches
|
|
||||||
switch tt.name {
|
|
||||||
case "wildcard_element_access":
|
|
||||||
// Just check that we got some elements
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected multiple elements for wildcard, got none")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case "attribute_exists_predicate", "attribute_equals_predicate":
|
|
||||||
// Just check that we got some titles
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected titles with lang attribute, got none")
|
|
||||||
}
|
|
||||||
// Ensure all are title elements
|
|
||||||
for _, node := range result {
|
|
||||||
if node.Data != "title" {
|
|
||||||
t.Errorf("Expected title elements, got: %s", node.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case "nonexistent_element":
|
|
||||||
// Just check that we got empty results
|
|
||||||
if len(result) != 0 {
|
|
||||||
t.Errorf("Expected empty results for nonexistent element, got %d items", len(result))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other cases, just verify we got results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected results for path %s, got none", tt.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEdgeCases(t *testing.T) {
|
|
||||||
t.Run("nil_node", func(t *testing.T) {
|
|
||||||
result, err := Get(nil, "/store/book")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error for nil node")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) > 0 {
|
|
||||||
t.Errorf("Expected empty result, got %v", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid_xml", func(t *testing.T) {
|
|
||||||
invalidXML, err := xmlquery.Parse(strings.NewReader("<invalid>xml"))
|
|
||||||
if err != nil {
|
|
||||||
// If parsing fails, that's expected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = Get(invalidXML, "/store")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid XML structure")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// For these tests with the simple XML, we expect just one result
|
|
||||||
simpleXML := `<root><book><title lang="en">Test</title></book></root>`
|
|
||||||
doc := parseTestXML(t, simpleXML)
|
|
||||||
|
|
||||||
t.Run("current_node", func(t *testing.T) {
|
|
||||||
result, err := Get(doc, "/root/book/.")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) > 1 {
|
|
||||||
t.Errorf("Expected at most 1 result, got %d", len(result))
|
|
||||||
}
|
|
||||||
if len(result) > 0 {
|
|
||||||
// Verify it's the book node
|
|
||||||
if result[0].Data != "book" {
|
|
||||||
t.Errorf("Expected book node, got %v", result[0].Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attributes", func(t *testing.T) {
|
|
||||||
result, err := Get(doc, "/root/book/title/@lang")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 || result[0].InnerText() != "en" {
|
|
||||||
t.Errorf("Expected 'en', got %v", result[0].InnerText())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetWithPaths(t *testing.T) {
|
|
||||||
// Use a simplified, well-formed XML document
|
|
||||||
simpleXML := `<store>
|
|
||||||
<book category="fiction">
|
|
||||||
<title lang="en">The Book Title</title>
|
|
||||||
<author>Author Name</author>
|
|
||||||
<price>19.99</price>
|
|
||||||
</book>
|
|
||||||
<bicycle>
|
|
||||||
<color>red</color>
|
|
||||||
<price>199.95</price>
|
|
||||||
</bicycle>
|
|
||||||
</store>`
|
|
||||||
|
|
||||||
// Parse the XML for testing
|
|
||||||
doc := parseTestXML(t, simpleXML)
|
|
||||||
|
|
||||||
// Debug: Print the test XML
|
|
||||||
t.Logf("Test XML:\n%s", simpleXML)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
expectedValue string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple_element_access",
|
|
||||||
path: "/store/bicycle/color",
|
|
||||||
expectedValue: "red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attribute_access",
|
|
||||||
path: "/store/book/title/@lang",
|
|
||||||
expectedValue: "en",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recursive_with_attribute",
|
|
||||||
path: "//title[@lang='en']",
|
|
||||||
expectedValue: "The Book Title",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Debug: Print the path we're looking for
|
|
||||||
t.Logf("Looking for path: %s", tt.path)
|
|
||||||
|
|
||||||
result, err := Get(doc, tt.path)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get(%q) returned error: %v", tt.path, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug: Print the results
|
|
||||||
t.Logf("Got %d results", len(result))
|
|
||||||
for i, r := range result {
|
|
||||||
t.Logf("Result %d: Node=%s, Value=%v", i, r.Data, r.InnerText())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we got results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Get(%q) returned no results", tt.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attribute access test, do more specific checks
|
|
||||||
if tt.name == "attribute_access" {
|
|
||||||
// Check the first result's value matches expected
|
|
||||||
if result[0].InnerText() != tt.expectedValue {
|
|
||||||
t.Errorf("Attribute value: got %v, expected %s", result[0].InnerText(), tt.expectedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For simple element access, check the text content
|
|
||||||
if tt.name == "simple_element_access" {
|
|
||||||
if text := result[0].InnerText(); text != tt.expectedValue {
|
|
||||||
t.Errorf("Element text: got %s, expected %s", text, tt.expectedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For recursive with attribute test, check title elements with lang="en"
|
|
||||||
if tt.name == "recursive_with_attribute" {
|
|
||||||
for _, node := range result {
|
|
||||||
// Check the node is a title
|
|
||||||
if node.Data != "title" {
|
|
||||||
t.Errorf("Expected title element, got %s", node.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := node.InnerText(); text != tt.expectedValue {
|
|
||||||
t.Errorf("Text content: got %s, expected %s", text, tt.expectedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check attributes - find the lang attribute
|
|
||||||
hasLang := false
|
|
||||||
for _, attr := range node.Attr {
|
|
||||||
if attr.Name.Local == "lang" && attr.Value == "en" {
|
|
||||||
hasLang = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasLang {
|
|
||||||
t.Errorf("Expected lang=\"en\" attribute, but it was not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
|
||||||
t.Run("simple element", func(t *testing.T) {
|
|
||||||
xmlData := `<root><name>John</name></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/name", "Jane")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
result, err := Get(doc, "/root/name")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 {
|
|
||||||
t.Errorf("Expected 1 result, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := result[0].InnerText(); text != "Jane" {
|
|
||||||
t.Errorf("Expected text 'Jane', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attribute", func(t *testing.T) {
|
|
||||||
xmlData := `<root><element id="123"></element></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/element/@id", "456")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change
|
|
||||||
result, err := Get(doc, "/root/element/@id")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 1 {
|
|
||||||
t.Errorf("Expected 1 result, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attributes, check the inner text
|
|
||||||
if text := result[0].InnerText(); text != "456" {
|
|
||||||
t.Errorf("Expected attribute value '456', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("indexed element", func(t *testing.T) {
|
|
||||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := Set(doc, "/root/items/item[1]", "changed")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Set() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the change using XPath that specifically targets the first item
|
|
||||||
result, err := Get(doc, "/root/items/item[1]")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have results
|
|
||||||
if len(result) == 0 {
|
|
||||||
t.Errorf("Expected at least one result for /root/items/item[1]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check text content
|
|
||||||
if text := result[0].InnerText(); text != "changed" {
|
|
||||||
t.Errorf("Expected text 'changed', got '%s'", text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetAll(t *testing.T) {
|
|
||||||
t.Run("multiple elements", func(t *testing.T) {
|
|
||||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := SetAll(doc, "//item", "changed")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all items are changed
|
|
||||||
result, err := Get(doc, "//item")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("Expected 2 results, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each node
|
|
||||||
for i, node := range result {
|
|
||||||
if text := node.InnerText(); text != "changed" {
|
|
||||||
t.Errorf("Item %d: expected text 'changed', got '%s'", i, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("attributes", func(t *testing.T) {
|
|
||||||
xmlData := `<root><item id="1"/><item id="2"/></root>`
|
|
||||||
doc := parseTestXML(t, xmlData)
|
|
||||||
|
|
||||||
err := SetAll(doc, "//item/@id", "new")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("SetAll() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all attributes are changed
|
|
||||||
result, err := Get(doc, "//item/@id")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get() returned error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("Expected 2 results, got %d", len(result))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For attributes, check inner text
|
|
||||||
for i, node := range result {
|
|
||||||
if text := node.InnerText(); text != "new" {
|
|
||||||
t.Errorf("Attribute %d: expected value 'new', got '%s'", i, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
49
release.sh
49
release.sh
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "Figuring out the tag..."
|
|
||||||
TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
# Get the latest tag
|
|
||||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
|
||||||
# Increment the patch version
|
|
||||||
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
|
|
||||||
VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
|
|
||||||
TAG="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
|
|
||||||
# Create a new tag
|
|
||||||
git tag $TAG
|
|
||||||
git push origin $TAG
|
|
||||||
fi
|
|
||||||
echo "Tag: $TAG"
|
|
||||||
|
|
||||||
echo "Building the thing..."
|
|
||||||
go build -o chef.exe .
|
|
||||||
go install .
|
|
||||||
|
|
||||||
echo "Creating a release..."
|
|
||||||
TOKEN="$GITEA_API_KEY"
|
|
||||||
GITEA="https://git.site.quack-lab.dev"
|
|
||||||
REPO="dave/BigChef"
|
|
||||||
# Create a release
|
|
||||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"tag_name": "'"$TAG"'",
|
|
||||||
"name": "'"$TAG"'",
|
|
||||||
"draft": false,
|
|
||||||
"prerelease": false
|
|
||||||
}' \
|
|
||||||
$GITEA/api/v1/repos/$REPO/releases)
|
|
||||||
|
|
||||||
# Extract the release ID
|
|
||||||
echo $RELEASE_RESPONSE
|
|
||||||
RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
|
|
||||||
echo "Release ID: $RELEASE_ID"
|
|
||||||
|
|
||||||
echo "Uploading the things..."
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@chef.exe" \
|
|
||||||
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=chef.exe"
|
|
||||||
rm chef.exe
|
|
Reference in New Issue
Block a user