From 24ac2b71968b51c6c92b99d329dbd1070912c512 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 13 May 2025 10:04:39 +0200 Subject: [PATCH 01/32] feat: import packages from extension verifier --- .golangci.yml | 2 - go.mod | 28 + go.sum | 140 ++ internal/htmlparser/parser.go | 1408 +++++++++++++++++ internal/htmlparser/parser_test.go | 123 ++ .../htmlparser/testdata/01-basic-element.txt | 3 + internal/htmlparser/testdata/02-sub-nodes.txt | 7 + .../testdata/03-attributes-single.txt | 3 + .../htmlparser/testdata/04-attributes.txt | 6 + .../testdata/05-children-with-comment.txt | 3 + .../testdata/06-multiple-comments.txt | 3 + .../testdata/07-comment-with-nested-tags.txt | 4 + .../08-comment-with-special-characters.txt | 3 + .../testdata/09-elements-with-block.txt | 5 + .../10-multi-line-breaks-get-removed.txt | 10 + ...1-multi-line-between-elements-only-one.txt | 7 + .../12-multi-line-between-only-elements.txt | 15 + .../13-long-attribute-is-on-new-line.txt | 5 + .../testdata/14-html-element-with-content.txt | 7 + .../15-multiple-template-elements.txt | 9 + ...6-multiple-template-elements-with-root.txt | 11 + .../testdata/17-starting-tag-in-html-node.txt | 5 + .../18-template-expression-in-div.txt | 3 + .../19-multiple-template-expressions.txt | 3 + .../20-template-expression-with-text.txt | 3 + ...template-expression-in-nested-elements.txt | 5 + .../22-template-expression-in-router-link.txt | 5 + .../23-multiple-long-template-expressions.txt | 6 + .../24-html-comment-before-element.txt | 9 + internal/htmlparser/testdata/25-if.txt | 7 + internal/htmlparser/testdata/26-if-else.txt | 11 + .../htmlparser/testdata/27-if-elseif-else.txt | 19 + .../htmlparser/testdata/28-if-while-attrs.txt | 7 + .../htmlparser/testdata/29-block-nesting.txt | 16 + ...ibute-long-closing-correctly-formatted.txt | 13 + .../htmlparser/testdata/31-block-parent.txt | 11 + .../testdata/32-multi-attribute-selfclose.txt | 10 + .../testdata/33-comment-over-block.txt | 8 + .../testdata/34-comment-over-block-nested.txt | 17 + .../testdata/35-element-content-format.txt | 5 + .../testdata/36-block-around-if.txt | 41 + .../testdata/37-formatting-element.txt | 25 + ...38-formatting-element-content-oneliner.txt | 7 + internal/llm/gemini.go | 49 + internal/llm/main.go | 27 + internal/llm/openai.go | 141 ++ internal/llm/openai_test.go | 186 +++ internal/llm/openrouter.go | 121 ++ 48 files changed, 2560 insertions(+), 2 deletions(-) create mode 100644 internal/htmlparser/parser.go create mode 100644 internal/htmlparser/parser_test.go create mode 100644 internal/htmlparser/testdata/01-basic-element.txt create mode 100644 internal/htmlparser/testdata/02-sub-nodes.txt create mode 100644 internal/htmlparser/testdata/03-attributes-single.txt create mode 100644 internal/htmlparser/testdata/04-attributes.txt create mode 100644 internal/htmlparser/testdata/05-children-with-comment.txt create mode 100644 internal/htmlparser/testdata/06-multiple-comments.txt create mode 100644 internal/htmlparser/testdata/07-comment-with-nested-tags.txt create mode 100644 internal/htmlparser/testdata/08-comment-with-special-characters.txt create mode 100644 internal/htmlparser/testdata/09-elements-with-block.txt create mode 100644 internal/htmlparser/testdata/10-multi-line-breaks-get-removed.txt create mode 100644 internal/htmlparser/testdata/11-multi-line-between-elements-only-one.txt create mode 100644 internal/htmlparser/testdata/12-multi-line-between-only-elements.txt create mode 100644 internal/htmlparser/testdata/13-long-attribute-is-on-new-line.txt create mode 100644 internal/htmlparser/testdata/14-html-element-with-content.txt create mode 100644 internal/htmlparser/testdata/15-multiple-template-elements.txt create mode 100644 internal/htmlparser/testdata/16-multiple-template-elements-with-root.txt create mode 100644 internal/htmlparser/testdata/17-starting-tag-in-html-node.txt create mode 100644 internal/htmlparser/testdata/18-template-expression-in-div.txt create mode 100644 internal/htmlparser/testdata/19-multiple-template-expressions.txt create mode 100644 internal/htmlparser/testdata/20-template-expression-with-text.txt create mode 100644 internal/htmlparser/testdata/21-template-expression-in-nested-elements.txt create mode 100644 internal/htmlparser/testdata/22-template-expression-in-router-link.txt create mode 100644 internal/htmlparser/testdata/23-multiple-long-template-expressions.txt create mode 100644 internal/htmlparser/testdata/24-html-comment-before-element.txt create mode 100644 internal/htmlparser/testdata/25-if.txt create mode 100644 internal/htmlparser/testdata/26-if-else.txt create mode 100644 internal/htmlparser/testdata/27-if-elseif-else.txt create mode 100644 internal/htmlparser/testdata/28-if-while-attrs.txt create mode 100644 internal/htmlparser/testdata/29-block-nesting.txt create mode 100644 internal/htmlparser/testdata/30-attribute-long-closing-correctly-formatted.txt create mode 100644 internal/htmlparser/testdata/31-block-parent.txt create mode 100644 internal/htmlparser/testdata/32-multi-attribute-selfclose.txt create mode 100644 internal/htmlparser/testdata/33-comment-over-block.txt create mode 100644 internal/htmlparser/testdata/34-comment-over-block-nested.txt create mode 100644 internal/htmlparser/testdata/35-element-content-format.txt create mode 100644 internal/htmlparser/testdata/36-block-around-if.txt create mode 100644 internal/htmlparser/testdata/37-formatting-element.txt create mode 100644 internal/htmlparser/testdata/38-formatting-element-content-oneliner.txt create mode 100644 internal/llm/gemini.go create mode 100644 internal/llm/main.go create mode 100644 internal/llm/openai.go create mode 100644 internal/llm/openai_test.go create mode 100644 internal/llm/openrouter.go diff --git a/.golangci.yml b/.golangci.yml index 201baf6d..49dabc35 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,7 +17,6 @@ linters: - unused - whitespace - asciicheck - - godot - gocyclo - gocritic - errcheck @@ -40,7 +39,6 @@ linters: - ginkgolinter - gocheckcompilerdirectives - goconst - - godot - godox - nilnil exclusions: diff --git a/go.mod b/go.mod index ddafb028..e809d5fc 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,14 @@ require ( ) require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/ai v0.8.0 // indirect + cloud.google.com/go/auth v0.6.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/smithy-go v1.22.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/bubbles v0.21.0 // indirect @@ -48,6 +55,15 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/generative-ai-go v0.20.1 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/jaswdr/faker/v2 v2.5.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -57,6 +73,18 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.186.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/grpc v1.64.1 // indirect ) require ( diff --git a/go.sum b/go.sum index 1bcc36d3..7d7109e2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,21 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= +cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= +cloud.google.com/go/auth v0.6.0 h1:5x+d6b5zdezZ7gmLWD1m/xNjnaQ2YDhmIz/HH3doy1g= +cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -10,6 +24,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -26,6 +42,7 @@ github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= @@ -56,6 +73,8 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -64,24 +83,66 @@ 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanw/esbuild v0.25.4 h1:k1bTSim+usBG27w7BfOCorhgx3tO+6bAfMj5pR+6SKg= github.com/evanw/esbuild v0.25.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/friendsofshopware/go-shopware-admin-api-sdk v0.0.0-20240608075117-3c16ae8b5f02 h1:M8DoMB8zLcxWCbltAWhgofyB+Vc35VnYVMCUS6hLV2E= github.com/friendsofshopware/go-shopware-admin-api-sdk v0.0.0-20240608075117-3c16ae8b5f02/go.mod h1:1Ou4HCVCMqON0XEz/uLlhPlY3sp3JVs3sKymxcYUx5k= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ= +github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= @@ -128,6 +189,7 @@ github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -145,7 +207,12 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= @@ -170,32 +237,105 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo= github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug= +google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/htmlparser/parser.go b/internal/htmlparser/parser.go new file mode 100644 index 00000000..aa04b25d --- /dev/null +++ b/internal/htmlparser/parser.go @@ -0,0 +1,1408 @@ +package htmlparser + +import ( + "fmt" + "strings" + "unicode" +) + +// Attribute represents an HTML attribute with key and value. +type Attribute struct { + Key string + Value string +} + +func (a Attribute) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + if a.Value == "" { + return builder.String() + a.Key + } + return builder.String() + a.Key + "=\"" + a.Value + "\"" +} + +// Node is the interface for nodes in our AST. +type Node interface { + Dump(indent int) string +} + +type NodeList []Node + +// IndentConfig holds configuration for indentation in HTML output. +type IndentConfig struct { + SpaceIndent bool + IndentSize int +} + +// DefaultIndentConfig creates a default indentation config with spaces. +func DefaultIndentConfig() IndentConfig { + return IndentConfig{ + SpaceIndent: true, + IndentSize: 4, + } +} + +// GetIndent returns the indentation string based on configuration. +func (c IndentConfig) GetIndent() string { + if c.SpaceIndent { + return strings.Repeat(" ", c.IndentSize) + } + return "\t" +} + +// The global indentation config that will be used by all nodes. +var indentConfig = DefaultIndentConfig() + +// SetIndentConfig updates the global indentation configuration. +func SetIndentConfig(config IndentConfig) { + indentConfig = config +} + +func (nodeList NodeList) Dump(indent int) string { + var builder strings.Builder + for i, node := range nodeList { + if _, ok := node.(*CommentNode); ok { + builder.WriteString(node.Dump(indent)) + builder.WriteString("\n") + continue + } + if i > 0 { + // Add newline between non-comment nodes if not first + if _, ok := nodeList[i-1].(*CommentNode); !ok { + builder.WriteString("\n") + + // Add extra newline between template elements + if isTemplateElement(node) && i > 0 && isTemplateElement(nodeList[i-1]) { + builder.WriteString("\n") + } + } + } + builder.WriteString(node.Dump(indent)) + } + + // Remove trailing newlines + result := builder.String() + if len(nodeList) > 0 { + result = strings.TrimRight(result, "\n") + // Only add ending newline if the original string had at least one + if strings.HasSuffix(builder.String(), "\n") { + result += "\n" + } + } + + return result +} + +// Helper function to check if a node is a template element. +func isTemplateElement(node Node) bool { + if elem, ok := node.(*ElementNode); ok { + return elem.Tag == "template" + } + return false +} + +// RawNode holds unchanged text. +type RawNode struct { + Text string + Line int // added field +} + +// Dump returns the raw text. +func (r *RawNode) Dump(indent int) string { + return r.Text +} + +// CommentNode represents an HTML comment. +type CommentNode struct { + Text string + Line int +} + +// Dump returns the comment text with HTML comment syntax. +func (c *CommentNode) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("") + + return builder.String() +} + +// TemplateExpressionNode represents a {{...}} template expression. +type TemplateExpressionNode struct { + Expression string + Line int +} + +// Dump returns the template expression with {{ }} delimiters. +func (t *TemplateExpressionNode) Dump(indent int) string { + return "{{" + t.Expression + "}}" +} + +// ElementNode represents an HTML element. +type ElementNode struct { + Tag string + Attributes NodeList + Children NodeList + SelfClosing bool + Line int // added field +} + +// Dump returns the HTML representation of the element and its children. +func (e *ElementNode) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + + // Add initial indentation + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("<" + e.Tag) + + attributesDidNewLine := false + + // Add attributes + if len(e.Attributes) > 0 { + if len(e.Attributes) == 1 { + attributeStr := e.Attributes[0].Dump(indent + 1) + _, isIfNode := e.Attributes[0].(*TwigIfNode) + + if len(attributeStr) > 80 || isIfNode { + builder.WriteString("\n") + builder.WriteString(attributeStr) + builder.WriteString("\n") + attributesDidNewLine = true + } else { + if !isIfNode { + attributeStr = e.Attributes[0].Dump(0) + } + builder.WriteString(" ") + builder.WriteString(attributeStr) + } + } else { + for _, attr := range e.Attributes { + builder.WriteString("\n") + attributesDidNewLine = true + builder.WriteString(attr.Dump(indent + 1)) + } + builder.WriteString("\n") + } + } + + if attributesDidNewLine { + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + } + + // Handle self-closing tags + if e.SelfClosing { + builder.WriteString("/>") + return builder.String() + } + + builder.WriteString(">") + + // Handle children + if len(e.Children) > 0 { + // Special case: if all children are text/comments/template expressions, keep them on same line + allSimpleNodes := true + hasLongTemplateExpression := false + multipleTemplateExpressions := 0 + multipleShortTemplateExpressions := false + + // Count template expressions and check for long ones + for _, child := range e.Children { + if tplExpr, ok := child.(*TemplateExpressionNode); ok { + multipleTemplateExpressions++ + if len(tplExpr.Dump(0)) > 30 { + hasLongTemplateExpression = true + } + } else if _, ok := child.(*RawNode); !ok { + if _, ok := child.(*CommentNode); !ok { + allSimpleNodes = false + break + } + } + } + + // Check if we have multiple short template expressions + if multipleTemplateExpressions > 1 && !hasLongTemplateExpression { + // Check if they're short enough to stay on one line + totalLength := 0 + for _, child := range e.Children { + if tplExpr, ok := child.(*TemplateExpressionNode); ok { + totalLength += len(tplExpr.Dump(indent + 1)) + } + } + // If the combined length is short, keep them on the same line + if totalLength <= 100 { + multipleShortTemplateExpressions = true + } + } + + if allSimpleNodes { + // Format based on content + if hasLongTemplateExpression || (multipleTemplateExpressions > 1 && !multipleShortTemplateExpressions) { + // For template expressions that are long or multiple long ones, add nice formatting + builder.WriteString("\n") + for _, child := range e.Children { + if _, ok := child.(*TemplateExpressionNode); ok { + for j := 0; j < indent+1; j++ { + builder.WriteString(indentStr) + } + builder.WriteString(child.Dump(indent+1) + "\n") + } else if raw, ok := child.(*RawNode); ok { + trimmed := strings.TrimSpace(raw.Text) + if trimmed != "" { + for j := 0; j < indent+1; j++ { + builder.WriteString(indentStr) + } + builder.WriteString(trimmed + "\n") + } + } else { + builder.WriteString(child.Dump(indent + 1)) + } + } + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + } else { + // For simple content, keep on the same line + for _, child := range e.Children { + builder.WriteString(child.Dump(indent)) + } + } + } else { + // For complex nodes, format with proper indentation + var nonEmptyChildren NodeList + for _, child := range e.Children { + if raw, ok := child.(*RawNode); ok { + if strings.TrimSpace(raw.Text) != "" { + nonEmptyChildren = append(nonEmptyChildren, raw) + } + } else { + nonEmptyChildren = append(nonEmptyChildren, child) + } + } + + // Check for template elements and add extra newlines between them + for i, child := range nonEmptyChildren { + builder.WriteString("\n") + + // Add an extra newline between template elements + if i > 0 && isTemplateElement(child) && isTemplateElement(nonEmptyChildren[i-1]) { + builder.WriteString("\n") + } + + if elementChild, ok := child.(*ElementNode); ok { + builder.WriteString(elementChild.Dump(indent + 1)) + } else { + for j := 0; j < indent+1; j++ { + builder.WriteString(indentStr) + } + builder.WriteString(strings.TrimSpace(child.Dump(indent + 1))) + } + } + builder.WriteString("\n") + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + } + } + + builder.WriteString("") + return builder.String() +} + +// TwigBlockNode represents a twig block. +type TwigBlockNode struct { + Name string + Children NodeList + Line int +} + +// Dump returns the twig block with proper formatting. +func (t *TwigBlockNode) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + builder.WriteString("{% block " + t.Name + " %}") + + // Filter out empty nodes and normalize newlines + var nonEmptyChildren NodeList + for _, child := range t.Children { + if raw, ok := child.(*RawNode); ok { + if strings.TrimSpace(raw.Text) != "" { + nonEmptyChildren = append(nonEmptyChildren, raw) + } + } else if twigBlock, ok := child.(*TwigBlockNode); ok { + if strings.TrimSpace(twigBlock.Dump(0)) != "" { + nonEmptyChildren = append(nonEmptyChildren, twigBlock) + } + } else { + nonEmptyChildren = append(nonEmptyChildren, child) + } + } + + if len(nonEmptyChildren) > 0 { + builder.WriteString("\n") + for i, child := range nonEmptyChildren { + if elementChild, ok := child.(*ElementNode); ok { + builder.WriteString(elementChild.Dump(indent + 1)) + } else { + builder.WriteString(child.Dump(indent + 1)) + } + + _, isComment := child.(*CommentNode) + + if i < len(nonEmptyChildren)-1 { + // Add an extra newline between elements + if isComment { + builder.WriteString("\n") + } else { + builder.WriteString("\n\n") + } + } + } + builder.WriteString("\n") + + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("{% endblock %}") + } else { + builder.WriteString("{% endblock %}") + } + + return builder.String() +} + +// TwigIfNode represents a Twig if block +type TwigIfNode struct { + Condition string + Children NodeList + ElseIfConditions []string + ElseIfChildren []NodeList + ElseChildren NodeList + Line int +} + +// Dump returns the twig if block with proper formatting +func (t *TwigIfNode) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("{% if " + t.Condition + " %}") + + // Filter out empty nodes and normalize newlines for if branch + var nonEmptyChildren NodeList + for _, child := range t.Children { + if raw, ok := child.(*RawNode); ok { + if strings.TrimSpace(raw.Text) != "" { + nonEmptyChildren = append(nonEmptyChildren, raw) + } + } else { + nonEmptyChildren = append(nonEmptyChildren, child) + } + } + + if len(nonEmptyChildren) > 0 { + builder.WriteString("\n") + for i, child := range nonEmptyChildren { + if elementChild, ok := child.(*ElementNode); ok { + builder.WriteString(elementChild.Dump(indent + 1)) + } else { + for i := 0; i < indent+1; i++ { + builder.WriteString(indentStr) + } + builder.WriteString(strings.TrimSpace(child.Dump(indent + 1))) + } + if i < len(nonEmptyChildren)-1 { + // Add an extra newline between elements + builder.WriteString("\n") + } + } + builder.WriteString("\n") + } + + // Handle elseif branches if they exist + for i, condition := range t.ElseIfConditions { + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + builder.WriteString("{% elseif " + condition + " %}") + + // Filter out empty nodes and normalize newlines for elseif branch + nonEmptyChildren = NodeList{} + for _, child := range t.ElseIfChildren[i] { + if raw, ok := child.(*RawNode); ok { + if strings.TrimSpace(raw.Text) != "" { + nonEmptyChildren = append(nonEmptyChildren, raw) + } + } else { + nonEmptyChildren = append(nonEmptyChildren, child) + } + } + + if len(nonEmptyChildren) > 0 { + builder.WriteString("\n") + for j, child := range nonEmptyChildren { + if elementChild, ok := child.(*ElementNode); ok { + builder.WriteString(elementChild.Dump(indent + 1)) + } else { + for i := 0; i < indent+1; i++ { + builder.WriteString(indentStr) + } + builder.WriteString(strings.TrimSpace(child.Dump(indent + 1))) + } + if j < len(nonEmptyChildren)-1 { + // Add an extra newline between elements + builder.WriteString("\n") + } + } + builder.WriteString("\n") + } + } + + // Handle else branch if it exists + if len(t.ElseChildren) > 0 { + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + builder.WriteString("{% else %}") + + // Filter out empty nodes and normalize newlines for else branch + var nonEmptyElseChildren NodeList + for _, child := range t.ElseChildren { + if raw, ok := child.(*RawNode); ok { + if strings.TrimSpace(raw.Text) != "" { + nonEmptyElseChildren = append(nonEmptyElseChildren, raw) + } + } else { + nonEmptyElseChildren = append(nonEmptyElseChildren, child) + } + } + + if len(nonEmptyElseChildren) > 0 { + builder.WriteString("\n") + for i, child := range nonEmptyElseChildren { + if elementChild, ok := child.(*ElementNode); ok { + builder.WriteString(elementChild.Dump(indent + 1)) + } else { + for i := 0; i < indent+1; i++ { + builder.WriteString(indentStr) + } + builder.WriteString(strings.TrimSpace(child.Dump(indent + 1))) + } + if i < len(nonEmptyElseChildren)-1 { + // Add an extra newline between elements + builder.WriteString("\n") + } + } + builder.WriteString("\n") + } + } + + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("{% endif %}") + return builder.String() +} + +// ParentNode represents a twig parent() call +type ParentNode struct { + Line int +} + +func (p *ParentNode) Dump(indent int) string { + var builder strings.Builder + indentStr := indentConfig.GetIndent() + for i := 0; i < indent; i++ { + builder.WriteString(indentStr) + } + + builder.WriteString("{% parent() %}") + + return builder.String() +} + +// Parser holds the state for our simple parser. +type Parser struct { + input string + pos int + length int +} + +// NewParser creates a new parser for the given input. +func NewParser(input string) (NodeList, error) { + p := &Parser{input: input, pos: 0, length: len(input)} + + return p.parseNodes("") +} + +// current returns the current byte (or zero if at the end). +func (p *Parser) current() byte { + if p.pos >= p.length { + return 0 + } + return p.input[p.pos] +} + +// peek returns the next n characters (or what remains). +func (p *Parser) peek(n int) string { + if p.pos+n > p.length { + return p.input[p.pos:] + } + return p.input[p.pos : p.pos+n] +} + +// skipWhitespace advances the position over any whitespace. +func (p *Parser) skipWhitespace() { + for p.pos < p.length && + (p.input[p.pos] == ' ' || p.input[p.pos] == '\n' || + p.input[p.pos] == '\r' || p.input[p.pos] == '\t') { + p.pos++ + } +} + +// Helper to get line number at a given position. +func (p *Parser) getLineAt(pos int) int { + return strings.Count(p.input[:pos], "\n") + 1 +} + +// parseComment parses an HTML comment and returns a CommentNode +func (p *Parser) parseComment() (*CommentNode, error) { + if p.peek(4) != "") + if idx == -1 { + return nil, fmt.Errorf("unterminated comment starting at pos %d", startPos) + } + + commentText := strings.TrimSpace(p.input[start : start+idx]) + p.pos += idx + 3 // skip past "-->" + + return &CommentNode{ + Text: commentText, + Line: p.getLineAt(startPos), + }, nil +} + +// parseNodes parses a list of nodes until an optional stop tag (used for element children). +func (p *Parser) parseNodes(stopTag string) (NodeList, error) { + var nodes NodeList + rawStart := p.pos + + for p.pos < p.length { + // Check for endblock if we're parsing twig block children + if stopTag == "" && p.peek(2) == "{%" { + peek := p.input[p.pos:] + if strings.HasPrefix(peek, "{% endblock") { + break + } + } + + if p.peek(2) == "{%" { + if p.pos > rawStart { + text := p.input[rawStart:p.pos] + if strings.TrimSpace(text) != "" { + nodes = append(nodes, &RawNode{ + Text: text, + Line: p.getLineAt(rawStart), + }) + } + } + + // Try parsing twig directives first + directive, err := p.parseTwigDirective() + if err != nil { + return nodes, err + } + if directive != nil { + nodes = append(nodes, directive) + rawStart = p.pos + continue + } + + // If not a directive, try parsing as a block + startPos := p.pos + block, err := p.parseTwigBlock() + if err != nil { + return nodes, err + } + if block != nil { + nodes = append(nodes, block) + rawStart = p.pos + continue + } + + // If not a block, try parsing as an if statement + p.pos = startPos + ifNode, err := p.parseTwigIf() + if err != nil { + return nodes, err + } + if ifNode != nil { + nodes = append(nodes, ifNode) + rawStart = p.pos + continue + } + + // If it wasn't a block or if statement, reset position and continue as raw text + p.pos = startPos + } + + // Parse template expressions {{ ... }} + if p.peek(2) == "{{" { + if p.pos > rawStart { + text := p.input[rawStart:p.pos] + if text != "" { + nodes = append(nodes, &RawNode{ + Text: text, + Line: p.getLineAt(rawStart), + }) + } + } + + expression, err := p.parseTemplateExpression() + if err != nil { + return nodes, err + } + + nodes = append(nodes, expression) + rawStart = p.pos + continue + } + + if p.peek(4) == " +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/06-multiple-comments.txt b/internal/htmlparser/testdata/06-multiple-comments.txt new file mode 100644 index 00000000..0471603d --- /dev/null +++ b/internal/htmlparser/testdata/06-multiple-comments.txt @@ -0,0 +1,3 @@ +
Content
+----- +
Content
\ No newline at end of file diff --git a/internal/htmlparser/testdata/07-comment-with-nested-tags.txt b/internal/htmlparser/testdata/07-comment-with-nested-tags.txt new file mode 100644 index 00000000..c410404e --- /dev/null +++ b/internal/htmlparser/testdata/07-comment-with-nested-tags.txt @@ -0,0 +1,4 @@ +
actual content
+----- + +
actual content
\ No newline at end of file diff --git a/internal/htmlparser/testdata/08-comment-with-special-characters.txt b/internal/htmlparser/testdata/08-comment-with-special-characters.txt new file mode 100644 index 00000000..896ae312 --- /dev/null +++ b/internal/htmlparser/testdata/08-comment-with-special-characters.txt @@ -0,0 +1,3 @@ +
+----- +
\ No newline at end of file diff --git a/internal/htmlparser/testdata/09-elements-with-block.txt b/internal/htmlparser/testdata/09-elements-with-block.txt new file mode 100644 index 00000000..e98d2a44 --- /dev/null +++ b/internal/htmlparser/testdata/09-elements-with-block.txt @@ -0,0 +1,5 @@ +{% block foo %}Click me{% endblock %} +----- +{% block foo %} + Click me +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/10-multi-line-breaks-get-removed.txt b/internal/htmlparser/testdata/10-multi-line-breaks-get-removed.txt new file mode 100644 index 00000000..eeb25b6d --- /dev/null +++ b/internal/htmlparser/testdata/10-multi-line-breaks-get-removed.txt @@ -0,0 +1,10 @@ +{% block test %}Click me + + +Click me{% endblock %} +----- +{% block test %} + Click me + + Click me +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/11-multi-line-between-elements-only-one.txt b/internal/htmlparser/testdata/11-multi-line-between-elements-only-one.txt new file mode 100644 index 00000000..df015397 --- /dev/null +++ b/internal/htmlparser/testdata/11-multi-line-between-elements-only-one.txt @@ -0,0 +1,7 @@ + +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/12-multi-line-between-only-elements.txt b/internal/htmlparser/testdata/12-multi-line-between-only-elements.txt new file mode 100644 index 00000000..df5e86d8 --- /dev/null +++ b/internal/htmlparser/testdata/12-multi-line-between-only-elements.txt @@ -0,0 +1,15 @@ + +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/13-long-attribute-is-on-new-line.txt b/internal/htmlparser/testdata/13-long-attribute-is-on-new-line.txt new file mode 100644 index 00000000..2ae5fd0a --- /dev/null +++ b/internal/htmlparser/testdata/13-long-attribute-is-on-new-line.txt @@ -0,0 +1,5 @@ + +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/14-html-element-with-content.txt b/internal/htmlparser/testdata/14-html-element-with-content.txt new file mode 100644 index 00000000..a7dd9c85 --- /dev/null +++ b/internal/htmlparser/testdata/14-html-element-with-content.txt @@ -0,0 +1,7 @@ + +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/15-multiple-template-elements.txt b/internal/htmlparser/testdata/15-multiple-template-elements.txt new file mode 100644 index 00000000..133c3c42 --- /dev/null +++ b/internal/htmlparser/testdata/15-multiple-template-elements.txt @@ -0,0 +1,9 @@ + +----- + + + \ No newline at end of file diff --git a/internal/htmlparser/testdata/16-multiple-template-elements-with-root.txt b/internal/htmlparser/testdata/16-multiple-template-elements-with-root.txt new file mode 100644 index 00000000..76593986 --- /dev/null +++ b/internal/htmlparser/testdata/16-multiple-template-elements-with-root.txt @@ -0,0 +1,11 @@ + +----- + + + + + \ No newline at end of file diff --git a/internal/htmlparser/testdata/17-starting-tag-in-html-node.txt b/internal/htmlparser/testdata/17-starting-tag-in-html-node.txt new file mode 100644 index 00000000..78402224 --- /dev/null +++ b/internal/htmlparser/testdata/17-starting-tag-in-html-node.txt @@ -0,0 +1,5 @@ +

{{ $tc('swag-customized-products.detail.tabGeneral.cardExclusion.emptyTitle', (searchTerm.length <= 0) ? 1 : 0) }}

+----- +

+ {{ $tc('swag-customized-products.detail.tabGeneral.cardExclusion.emptyTitle', (searchTerm.length <= 0) ? 1 : 0) }} +

\ No newline at end of file diff --git a/internal/htmlparser/testdata/18-template-expression-in-div.txt b/internal/htmlparser/testdata/18-template-expression-in-div.txt new file mode 100644 index 00000000..019c9d25 --- /dev/null +++ b/internal/htmlparser/testdata/18-template-expression-in-div.txt @@ -0,0 +1,3 @@ +
{{ someVariable }}
+----- +
{{ someVariable }}
\ No newline at end of file diff --git a/internal/htmlparser/testdata/19-multiple-template-expressions.txt b/internal/htmlparser/testdata/19-multiple-template-expressions.txt new file mode 100644 index 00000000..b68a8a37 --- /dev/null +++ b/internal/htmlparser/testdata/19-multiple-template-expressions.txt @@ -0,0 +1,3 @@ +
{{ firstVar }}{{ secondVar }}
+----- +
{{ firstVar }}{{ secondVar }}
\ No newline at end of file diff --git a/internal/htmlparser/testdata/20-template-expression-with-text.txt b/internal/htmlparser/testdata/20-template-expression-with-text.txt new file mode 100644 index 00000000..94dc93e3 --- /dev/null +++ b/internal/htmlparser/testdata/20-template-expression-with-text.txt @@ -0,0 +1,3 @@ +
Before {{ expression }} After
+----- +
Before {{ expression }} After
\ No newline at end of file diff --git a/internal/htmlparser/testdata/21-template-expression-in-nested-elements.txt b/internal/htmlparser/testdata/21-template-expression-in-nested-elements.txt new file mode 100644 index 00000000..a48a42d8 --- /dev/null +++ b/internal/htmlparser/testdata/21-template-expression-in-nested-elements.txt @@ -0,0 +1,5 @@ +
{{ nestedExpression }}
+----- +
+ {{ nestedExpression }} +
\ No newline at end of file diff --git a/internal/htmlparser/testdata/22-template-expression-in-router-link.txt b/internal/htmlparser/testdata/22-template-expression-in-router-link.txt new file mode 100644 index 00000000..c74a64d6 --- /dev/null +++ b/internal/htmlparser/testdata/22-template-expression-in-router-link.txt @@ -0,0 +1,5 @@ +{{ item.mainPseudovariant.product.translated.name }} +----- + + {{ item.mainPseudovariant.product.translated.name }} + \ No newline at end of file diff --git a/internal/htmlparser/testdata/23-multiple-long-template-expressions.txt b/internal/htmlparser/testdata/23-multiple-long-template-expressions.txt new file mode 100644 index 00000000..28c30137 --- /dev/null +++ b/internal/htmlparser/testdata/23-multiple-long-template-expressions.txt @@ -0,0 +1,6 @@ +
{{ item.mainPseudovariant.product.translated.name }}{{ item.mainPseudovariant.product.translated.description }}
+----- +
+ {{ item.mainPseudovariant.product.translated.name }} + {{ item.mainPseudovariant.product.translated.description }} +
\ No newline at end of file diff --git a/internal/htmlparser/testdata/24-html-comment-before-element.txt b/internal/htmlparser/testdata/24-html-comment-before-element.txt new file mode 100644 index 00000000..e8f51d59 --- /dev/null +++ b/internal/htmlparser/testdata/24-html-comment-before-element.txt @@ -0,0 +1,9 @@ + + +

Content

+
+----- + + +

Content

+
\ No newline at end of file diff --git a/internal/htmlparser/testdata/25-if.txt b/internal/htmlparser/testdata/25-if.txt new file mode 100644 index 00000000..8e0f8453 --- /dev/null +++ b/internal/htmlparser/testdata/25-if.txt @@ -0,0 +1,7 @@ +{% if foo %} + +{% endif %} +----- +{% if foo %} + +{% endif %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/26-if-else.txt b/internal/htmlparser/testdata/26-if-else.txt new file mode 100644 index 00000000..80e6f559 --- /dev/null +++ b/internal/htmlparser/testdata/26-if-else.txt @@ -0,0 +1,11 @@ +{% if foo %} + +{% else %} + +{% endif %} +----- +{% if foo %} + +{% else %} + +{% endif %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/27-if-elseif-else.txt b/internal/htmlparser/testdata/27-if-elseif-else.txt new file mode 100644 index 00000000..12363f51 --- /dev/null +++ b/internal/htmlparser/testdata/27-if-elseif-else.txt @@ -0,0 +1,19 @@ +{% if foo %} + +{% elseif bla %} + +{% elseif yea %} + +{% else %} + +{% endif %} +----- +{% if foo %} + +{% elseif bla %} + +{% elseif yea %} + +{% else %} + +{% endif %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/28-if-while-attrs.txt b/internal/htmlparser/testdata/28-if-while-attrs.txt new file mode 100644 index 00000000..4b66e5b5 --- /dev/null +++ b/internal/htmlparser/testdata/28-if-while-attrs.txt @@ -0,0 +1,7 @@ + +----- + \ No newline at end of file diff --git a/internal/htmlparser/testdata/29-block-nesting.txt b/internal/htmlparser/testdata/29-block-nesting.txt new file mode 100644 index 00000000..47977fb8 --- /dev/null +++ b/internal/htmlparser/testdata/29-block-nesting.txt @@ -0,0 +1,16 @@ +{% block a %} +{% block b %} +{% block c %} +{% block d %} +{% endblock %} +{% endblock %} +{% endblock %} +{% endblock %} +----- +{% block a %} + {% block b %} + {% block c %} + {% block d %}{% endblock %} + {% endblock %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/30-attribute-long-closing-correctly-formatted.txt b/internal/htmlparser/testdata/30-attribute-long-closing-correctly-formatted.txt new file mode 100644 index 00000000..dcabacd3 --- /dev/null +++ b/internal/htmlparser/testdata/30-attribute-long-closing-correctly-formatted.txt @@ -0,0 +1,13 @@ +
+
+

Hello World

+
+
+----- +
+
+

Hello World

+
+
\ No newline at end of file diff --git a/internal/htmlparser/testdata/31-block-parent.txt b/internal/htmlparser/testdata/31-block-parent.txt new file mode 100644 index 00000000..31858afb --- /dev/null +++ b/internal/htmlparser/testdata/31-block-parent.txt @@ -0,0 +1,11 @@ +{% block a %} +{% block b %} +{% parent() %} +{% endblock %} +{% endblock %} +----- +{% block a %} + {% block b %} + {% parent() %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/32-multi-attribute-selfclose.txt b/internal/htmlparser/testdata/32-multi-attribute-selfclose.txt new file mode 100644 index 00000000..6143e9da --- /dev/null +++ b/internal/htmlparser/testdata/32-multi-attribute-selfclose.txt @@ -0,0 +1,10 @@ +
+ +
+----- +
+ +
\ No newline at end of file diff --git a/internal/htmlparser/testdata/33-comment-over-block.txt b/internal/htmlparser/testdata/33-comment-over-block.txt new file mode 100644 index 00000000..ea953698 --- /dev/null +++ b/internal/htmlparser/testdata/33-comment-over-block.txt @@ -0,0 +1,8 @@ + + + + +{% block sw_import_export_tabs_profiles %}{% endblock %} +----- + +{% block sw_import_export_tabs_profiles %}{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/34-comment-over-block-nested.txt b/internal/htmlparser/testdata/34-comment-over-block-nested.txt new file mode 100644 index 00000000..61ea6e08 --- /dev/null +++ b/internal/htmlparser/testdata/34-comment-over-block-nested.txt @@ -0,0 +1,17 @@ + + + + +{% block sw_import_export_tabs_profiles %} + + + + {% block foo %} + {% endblock %} +{% endblock %} +----- + +{% block sw_import_export_tabs_profiles %} + + {% block foo %}{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/35-element-content-format.txt b/internal/htmlparser/testdata/35-element-content-format.txt new file mode 100644 index 00000000..0d24fc6e --- /dev/null +++ b/internal/htmlparser/testdata/35-element-content-format.txt @@ -0,0 +1,5 @@ +{{ $tc('iwvs-import-export.page.colorTab') }} +----- + + {{ $tc('iwvs-import-export.page.colorTab') }} + \ No newline at end of file diff --git a/internal/htmlparser/testdata/36-block-around-if.txt b/internal/htmlparser/testdata/36-block-around-if.txt new file mode 100644 index 00000000..00f0511e --- /dev/null +++ b/internal/htmlparser/testdata/36-block-around-if.txt @@ -0,0 +1,41 @@ + +{% block sw_cms_element_product_slider_config_settings_min_width %} +{% parent %} + + +{% block sw_cms_element_product_slider_config_settings_slides_mobile %} + +{% endblock %} +{% endblock %} +----- + +{% block sw_cms_element_product_slider_config_settings_min_width %} + {% parent() %} + + + {% block sw_cms_element_product_slider_config_settings_slides_mobile %} + + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/37-formatting-element.txt b/internal/htmlparser/testdata/37-formatting-element.txt new file mode 100644 index 00000000..a32151ff --- /dev/null +++ b/internal/htmlparser/testdata/37-formatting-element.txt @@ -0,0 +1,25 @@ +{% block sw_cms_block_product_listing_preview %} +
+
+
+
+
+
+
+ + +
+{% endblock %} +----- +{% block sw_cms_block_product_listing_preview %} +
+
+
+
+
+
+
+ + +
+{% endblock %} \ No newline at end of file diff --git a/internal/htmlparser/testdata/38-formatting-element-content-oneliner.txt b/internal/htmlparser/testdata/38-formatting-element-content-oneliner.txt new file mode 100644 index 00000000..3a48587f --- /dev/null +++ b/internal/htmlparser/testdata/38-formatting-element-content-oneliner.txt @@ -0,0 +1,7 @@ +
+ {{ somewhatLongFooBarBaz }}/{{ somewhatLongerFooBarBaz }} +
+----- +
+ {{ somewhatLongFooBarBaz }}/{{ somewhatLongerFooBarBaz }} +
\ No newline at end of file diff --git a/internal/llm/gemini.go b/internal/llm/gemini.go new file mode 100644 index 00000000..13bb134d --- /dev/null +++ b/internal/llm/gemini.go @@ -0,0 +1,49 @@ +package llm + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/google/generative-ai-go/genai" + "google.golang.org/api/option" + + "github.com/shopware/shopware-cli/logging" +) + +type GeminiClient struct { + client *genai.Client +} + +func newGeminiClient() (*GeminiClient, error) { + apiKey := os.Getenv("GEMINI_API_KEY") + + if apiKey == "" { + return nil, fmt.Errorf("GEMINI_API_KEY is not set") + } + + client, err := genai.NewClient(context.Background(), option.WithAPIKey(apiKey)) + if err != nil { + return nil, err + } + + return &GeminiClient{client: client}, nil +} + +func (c *GeminiClient) Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) { + resp, err := c.client.GenerativeModel(options.Model).GenerateContent(ctx, genai.Text(options.SystemPrompt+"\n\n"+prompt)) + if err != nil { + if strings.Contains(err.Error(), "Resource has been exhausted") { + logging.FromContext(ctx).Warn("Resource exhausted, waiting 15 seconds before retrying") + time.Sleep(15 * time.Second) + + return c.Generate(ctx, prompt, options) + } + + return "", err + } + + return string(resp.Candidates[0].Content.Parts[0].(genai.Text)), nil +} diff --git a/internal/llm/main.go b/internal/llm/main.go new file mode 100644 index 00000000..31a0dfe2 --- /dev/null +++ b/internal/llm/main.go @@ -0,0 +1,27 @@ +package llm + +import "context" + +type LLMOptions struct { + Model string + SystemPrompt string +} + +type LLMClient interface { + Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) +} + +func NewLLMClient(provider string) (LLMClient, error) { + switch provider { + case "ollama": + return newOpenAIClient(), nil + case "openai": + return newOpenAIClient(), nil + case "gemini": + return newGeminiClient() + case "openrouter": + return newOpenRouterClient() + } + + panic("Invalid provider: " + provider) +} diff --git a/internal/llm/openai.go b/internal/llm/openai.go new file mode 100644 index 00000000..63e48a85 --- /dev/null +++ b/internal/llm/openai.go @@ -0,0 +1,141 @@ +package llm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/shopware/shopware-cli/logging" +) + +// Client represents an OpenAI API client. +type Client struct { + host string + apiKey string + client *http.Client +} + +// ChatMessage represents a message in the chat completion request. +type ChatMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// ChatCompletionRequest represents the request body for chat completion. +type ChatCompletionRequest struct { + Model string `json:"model"` + Messages []ChatMessage `json:"messages"` +} + +// ChatCompletionResponse represents the response from the chat completion endpoint. +type ChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message ChatMessage `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +// newOpenAIClient creates a new OpenAI client instance. +func newOpenAIClient() *Client { + host := os.Getenv("OPENAI_API_HOST") + ollamaHost := os.Getenv("OLLAMA_HOST") + if host == "" { + if ollamaHost != "" { + host = ollamaHost + } else { + host = "https://api.openai.com" + } + } + + apiKey := os.Getenv("OPENAI_API_KEY") + + return &Client{ + host: host, + apiKey: apiKey, + client: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +// Generate sends a chat completion request to the OpenAI API +func (c *Client) Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) { + messages := []ChatMessage{ + { + Role: "user", + Content: prompt, + }, + } + + if options.SystemPrompt != "" { + // Insert system message at the beginning + messages = append([]ChatMessage{{ + Role: "system", + Content: options.SystemPrompt, + }}, messages...) + } + + reqBody := ChatCompletionRequest{ + Model: options.Model, + Messages: messages, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal request body: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/v1/chat/completions", c.host), bytes.NewBuffer(jsonBody)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) + } + + resp, err := c.client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + logging.FromContext(ctx).Warn("failed to close response body: %v", err) + } + }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + } + + var response ChatCompletionResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + if len(response.Choices) == 0 { + return "", fmt.Errorf("no completion choices returned") + } + + return response.Choices[0].Message.Content, nil +} diff --git a/internal/llm/openai_test.go b/internal/llm/openai_test.go new file mode 100644 index 00000000..4c860a9a --- /dev/null +++ b/internal/llm/openai_test.go @@ -0,0 +1,186 @@ +package llm + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOpenAIGenerate(t *testing.T) { + // Create a test server to mock the OpenAI API + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check request method and path + assert.Equal(t, http.MethodPost, r.Method, "Request method should be POST") + assert.Equal(t, "/v1/chat/completions", r.URL.Path, "Request path should be /v1/chat/completions") + + // Check for authorization header + authHeader := r.Header.Get("Authorization") + assert.Equal(t, "Bearer test-api-key", authHeader, "Authorization header should be properly set") + + // Decode request body to verify content + var reqBody map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&reqBody) + if !assert.NoError(t, err, "Should decode request body without error") { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Check model + model, ok := reqBody["model"].(string) + assert.True(t, ok, "Model should be a string") + assert.Equal(t, "gpt-3.5-turbo", model, "Model should be gpt-3.5-turbo") + + // Check messages + messages, ok := reqBody["messages"].([]interface{}) + assert.True(t, ok, "Messages should be an array") + + // Different response based on request type + var responseContent string + switch len(messages) { + case 1: + // Regular prompt + responseContent = "This is a test response to a regular prompt" + case 2: + // With system prompt + responseContent = "This is a test response with system context" + default: + assert.Failf(t, "Unexpected messages length", "Got %d messages, expected 1 or 2", len(messages)) + responseContent = "Unexpected configuration" + } + + // Send a successful response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Simplified mock response that matches the ChatCompletionResponse structure + responseJSON := fmt.Sprintf(`{ + "id": "test-id", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "%s" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15 + } + }`, responseContent) + + _, err = w.Write([]byte(responseJSON)) + assert.NoError(t, err, "Should write response without error") + })) + defer server.Close() + + // Set environment variables for the test + t.Setenv("OPENAI_API_HOST", server.URL) + t.Setenv("OPENAI_API_KEY", "test-api-key") + + // Get an OpenAI client instance + client, err := NewLLMClient("openai") + require.NoError(t, err, "Creating OpenAI client should not error") + + // Test cases + tests := []struct { + name string + prompt string + options *LLMOptions + expected string + expectError bool + errorMessage string + }{ + { + name: "basic prompt", + prompt: "Hello, world!", + options: &LLMOptions{Model: "gpt-3.5-turbo"}, + expected: "This is a test response to a regular prompt", + }, + { + name: "with system prompt", + prompt: "Hello, assistant!", + options: &LLMOptions{Model: "gpt-3.5-turbo", SystemPrompt: "You are a helpful assistant"}, + expected: "This is a test response with system context", + }, + } + + // Run test cases + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := client.Generate(t.Context(), tt.prompt, tt.options) + + if tt.expectError { + assert.Error(t, err, "Should return an error") + if tt.errorMessage != "" { + assert.Contains(t, err.Error(), tt.errorMessage, "Error should contain expected message") + } + return + } + + assert.NoError(t, err, "Should not return an error") + assert.Equal(t, tt.expected, result, "Should return expected result") + }) + } +} + +func TestOpenAIGenerateErrors(t *testing.T) { + // Test case 1: Server error + t.Run("server error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(`{"error": {"message": "Server error", "type": "server_error"}}`)) + assert.NoError(t, err, "Should write response without error") + })) + defer server.Close() + + // Set environment variables for the test + t.Setenv("OPENAI_API_HOST", server.URL) + t.Setenv("OPENAI_API_KEY", "test-key") + + client, err := NewLLMClient("openai") + assert.NoError(t, err, "Creating OpenAI client should not error") + + _, err = client.Generate(t.Context(), "Test prompt", &LLMOptions{ + Model: "gpt-3.5-turbo", + }) + + assert.Error(t, err, "Should return an error when server returns error") + assert.Contains(t, err.Error(), "unexpected status code", "Error should mention unexpected status code") + }) + + // Test case 2: Empty choices + t.Run("empty choices", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"id": "test-id", "object": "chat.completion", "choices": [], "usage": {"prompt_tokens": 10, "completion_tokens": 0, "total_tokens": 10}}`)) + assert.NoError(t, err, "Should write response without error") + })) + defer server.Close() + + // Set environment variables for the test + t.Setenv("OPENAI_API_HOST", server.URL) + t.Setenv("OPENAI_API_KEY", "test-key") + + client, err := NewLLMClient("openai") + require.NoError(t, err, "Creating OpenAI client should not error") + + _, err = client.Generate(t.Context(), "Test prompt", &LLMOptions{ + Model: "gpt-3.5-turbo", + }) + + assert.Error(t, err, "Should return an error when response has empty choices") + assert.Contains(t, err.Error(), "no completion choices", "Error should mention no completion choices") + }) +} diff --git a/internal/llm/openrouter.go b/internal/llm/openrouter.go new file mode 100644 index 00000000..0463f0a0 --- /dev/null +++ b/internal/llm/openrouter.go @@ -0,0 +1,121 @@ +package llm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/shopware/shopware-cli/logging" +) + +// OpenRouterClient represents an OpenRouter API client. +type OpenRouterClient struct { + client *http.Client + apiKey string +} + +// OpenRouterRequest represents the request body for text generation. +type OpenRouterRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// OpenRouterResponse represents the response from the OpenRouter API. +type OpenRouterResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` +} + +// newOpenRouterClient creates a new OpenRouter client instance. +func newOpenRouterClient() (*OpenRouterClient, error) { + apiKey := os.Getenv("OPENROUTER_API_KEY") + if apiKey == "" { + return nil, fmt.Errorf("OPENROUTER_API_KEY is not set") + } + + return &OpenRouterClient{ + apiKey: apiKey, + client: &http.Client{ + Timeout: 120 * time.Second, + }, + }, nil +} + +// Generate sends a generation request to the OpenRouter API. +func (c *OpenRouterClient) Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) { + messages := []Message{ + { + Role: "system", + Content: options.SystemPrompt, + }, + { + Role: "user", + Content: prompt, + }, + } + + reqBody := OpenRouterRequest{ + Model: options.Model, + Messages: messages, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal request body: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(jsonBody)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + req.Header.Set("HTTP-Referer", "https://github.com/shopwareLabs/extension-verifier") + req.Header.Set("X-Title", "Shopware Extension Verifier") + + resp, err := c.client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send request: %w", err) + } + + defer func() { + if err := resp.Body.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to close response body: %v\n", err) + } + }() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var response OpenRouterResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + if len(response.Choices) == 0 { + logging.FromContext(ctx).Error("no response choices returned", "body", string(body)) + return "", fmt.Errorf("no response choices returned") + } + + return response.Choices[0].Message.Content, nil +} From 5bbb3edd047ad26344e6353819a0da9e0b54cb39 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 13 May 2025 10:08:01 +0200 Subject: [PATCH 02/32] refactor: extract HTML comment start constant and add linter directives --- internal/htmlparser/parser.go | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/internal/htmlparser/parser.go b/internal/htmlparser/parser.go index aa04b25d..e9a78b53 100644 --- a/internal/htmlparser/parser.go +++ b/internal/htmlparser/parser.go @@ -6,6 +6,8 @@ import ( "unicode" ) +const htmlCommentStart = "