Skip to content

Commit d9154cb

Browse files
authored
refactor: remove goquery dependency (#1070)
1 parent ed810ff commit d9154cb

File tree

12 files changed

+415
-136
lines changed

12 files changed

+415
-136
lines changed

.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.835
1+
0.3.836

cmd/templ/generatecmd/proxy/proxy.go

+35-24
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"compress/gzip"
66
"fmt"
7-
"html"
87
"io"
98
stdlog "log"
109
"log/slog"
@@ -17,9 +16,10 @@ import (
1716
"strings"
1817
"time"
1918

20-
"github.com/PuerkitoBio/goquery"
2119
"github.com/a-h/templ/cmd/templ/generatecmd/sse"
20+
"github.com/a-h/templ/internal/htmlfind"
2221
"github.com/andybalholm/brotli"
22+
"golang.org/x/net/html"
2323

2424
_ "embed"
2525
)
@@ -35,28 +35,37 @@ type Handler struct {
3535
sse *sse.Handler
3636
}
3737

38-
func getScriptTag(nonce string) string {
38+
func reloadScript(nonce string) *html.Node {
39+
script := &html.Node{
40+
Type: html.ElementNode,
41+
Data: "script",
42+
Attr: []html.Attribute{
43+
{Key: "src", Val: "/_templ/reload/script.js"},
44+
},
45+
}
3946
if nonce != "" {
40-
var sb strings.Builder
41-
sb.WriteString(`<script src="/_templ/reload/script.js" nonce="`)
42-
sb.WriteString(html.EscapeString(nonce))
43-
sb.WriteString(`"></script>`)
44-
return sb.String()
47+
script.Attr = append(script.Attr, html.Attribute{Key: "nonce", Val: nonce})
4548
}
46-
return `<script src="/_templ/reload/script.js"></script>`
49+
return script
4750
}
4851

49-
func insertScriptTagIntoBody(nonce, body string) (updated string) {
50-
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
52+
var ErrBodyNotFound = fmt.Errorf("body not found")
53+
54+
func insertScriptTagIntoBody(nonce, body string) (updated string, err error) {
55+
n, err := html.Parse(strings.NewReader(body))
5156
if err != nil {
52-
return strings.Replace(body, "</body>", getScriptTag(nonce)+"</body>", -1)
57+
return body, err
5358
}
54-
doc.Find("body").AppendHtml(getScriptTag(nonce))
55-
r, err := doc.Html()
56-
if err != nil {
57-
return strings.Replace(body, "</body>", getScriptTag(nonce)+"</body>", -1)
59+
bodyNodes := htmlfind.All(n, htmlfind.Element("body"))
60+
if len(bodyNodes) == 0 {
61+
return body, ErrBodyNotFound
62+
}
63+
bodyNodes[0].AppendChild(reloadScript(nonce))
64+
buf := new(bytes.Buffer)
65+
if err = html.Render(buf, n); err != nil {
66+
return body, err
5867
}
59-
return r
68+
return buf.String(), nil
6069
}
6170

6271
type passthroughWriteCloser struct {
@@ -121,13 +130,15 @@ func (h *Handler) modifyResponse(r *http.Response) error {
121130

122131
// Update it.
123132
csp := r.Header.Get("Content-Security-Policy")
124-
updated := insertScriptTagIntoBody(parseNonce(csp), string(body))
125-
if log.Enabled(r.Request.Context(), slog.LevelDebug) {
126-
if len(updated) == len(body) {
127-
log.Debug("Reload script not inserted")
128-
} else {
129-
log.Debug("Reload script inserted")
130-
}
133+
updated, err := insertScriptTagIntoBody(parseNonce(csp), string(body))
134+
if err != nil {
135+
log.Warn("Unable to insert reload script", slog.Any("error", err))
136+
updated = string(body)
137+
}
138+
if len(updated) == len(body) {
139+
log.Debug("Reload script not inserted")
140+
} else {
141+
log.Debug("Reload script inserted")
131142
}
132143

133144
// Encode the response.

cmd/templ/generatecmd/proxy/proxy_test.go

+37-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/andybalholm/brotli"
2121
"github.com/google/go-cmp/cmp"
22+
"golang.org/x/net/html"
2223
)
2324

2425
func TestRoundTripper(t *testing.T) {
@@ -49,6 +50,16 @@ func TestRoundTripper(t *testing.T) {
4950
})
5051
}
5152

53+
func getScriptTag(t *testing.T, nonce string) string {
54+
script := reloadScript(nonce)
55+
var buf bytes.Buffer
56+
err := html.Render(&buf, script)
57+
if err != nil {
58+
t.Fatalf("unexpected error rendering script tag: %v", err)
59+
}
60+
return buf.String()
61+
}
62+
5263
func TestProxy(t *testing.T) {
5364
t.Run("plain: non-html content is not modified", func(t *testing.T) {
5465
// Arrange
@@ -136,16 +147,18 @@ func TestProxy(t *testing.T) {
136147
r.Header.Set("Content-Type", "text/html, charset=utf-8")
137148
r.Header.Set("Content-Length", "26")
138149

139-
expectedString := insertScriptTagIntoBody("", `<html><body></body></html>`)
140-
if !strings.Contains(expectedString, getScriptTag("")) {
150+
expectedString, err := insertScriptTagIntoBody("", `<html><body></body></html>`)
151+
if err != nil {
152+
t.Fatalf("unexpected error inserting script: %v", err)
153+
}
154+
if !strings.Contains(expectedString, getScriptTag(t, "")) {
141155
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
142156
}
143157

144158
// Act
145159
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
146160
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
147-
err := h.modifyResponse(r)
148-
if err != nil {
161+
if err = h.modifyResponse(r); err != nil {
149162
t.Fatalf("unexpected error: %v", err)
150163
}
151164

@@ -178,16 +191,18 @@ func TestProxy(t *testing.T) {
178191
const nonce = "this-is-the-nonce"
179192
r.Header.Set("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
180193

181-
expectedString := insertScriptTagIntoBody(nonce, `<html><body></body></html>`)
182-
if !strings.Contains(expectedString, getScriptTag(nonce)) {
194+
expectedString, err := insertScriptTagIntoBody(nonce, `<html><body></body></html>`)
195+
if err != nil {
196+
t.Fatalf("unexpected error inserting script: %v", err)
197+
}
198+
if !strings.Contains(expectedString, getScriptTag(t, nonce)) {
183199
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
184200
}
185201

186202
// Act
187203
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
188204
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
189-
err := h.modifyResponse(r)
190-
if err != nil {
205+
if err = h.modifyResponse(r); err != nil {
191206
t.Fatalf("unexpected error: %v", err)
192207
}
193208

@@ -218,8 +233,11 @@ func TestProxy(t *testing.T) {
218233
r.Header.Set("Content-Type", "text/html, charset=utf-8")
219234
r.Header.Set("Content-Length", "26")
220235

221-
expectedString := insertScriptTagIntoBody("", `<html><body><script>console.log("<body></body>")</script></body></html>`)
222-
if !strings.Contains(expectedString, getScriptTag("")) {
236+
expectedString, err := insertScriptTagIntoBody("", `<html><body><script>console.log("<body></body>")</script></body></html>`)
237+
if err != nil {
238+
t.Fatalf("unexpected error inserting script: %v", err)
239+
}
240+
if !strings.Contains(expectedString, getScriptTag(t, "")) {
223241
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
224242
}
225243
if !strings.Contains(expectedString, `console.log("<body></body>")`) {
@@ -229,8 +247,7 @@ func TestProxy(t *testing.T) {
229247
// Act
230248
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
231249
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
232-
err := h.modifyResponse(r)
233-
if err != nil {
250+
if err = h.modifyResponse(r); err != nil {
234251
t.Fatalf("unexpected error: %v", err)
235252
}
236253

@@ -295,7 +312,10 @@ func TestProxy(t *testing.T) {
295312
}
296313
gzw.Close()
297314

298-
expectedString := insertScriptTagIntoBody("", body)
315+
expectedString, err := insertScriptTagIntoBody("", body)
316+
if err != nil {
317+
t.Fatalf("unexpected error inserting script: %v", err)
318+
}
299319

300320
var expectedBytes bytes.Buffer
301321
gzw = gzip.NewWriter(&expectedBytes)
@@ -356,7 +376,10 @@ func TestProxy(t *testing.T) {
356376
}
357377
brw.Close()
358378

359-
expectedString := insertScriptTagIntoBody("", body)
379+
expectedString, err := insertScriptTagIntoBody("", body)
380+
if err != nil {
381+
t.Fatalf("unexpected error inserting script: %v", err)
382+
}
360383

361384
var expectedBytes bytes.Buffer
362385
brw = brotli.NewWriter(&expectedBytes)

cmd/templ/generatecmd/testwatch/generate_test.go

+35-9
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import (
1818
"testing"
1919
"time"
2020

21-
"github.com/PuerkitoBio/goquery"
2221
"github.com/a-h/templ/cmd/templ/generatecmd"
2322
"github.com/a-h/templ/cmd/templ/generatecmd/modcheck"
23+
"github.com/a-h/templ/internal/htmlfind"
24+
"golang.org/x/net/html"
2425
)
2526

2627
//go:embed testdata/*
@@ -76,12 +77,13 @@ func getPort() (port int, err error) {
7677
return
7778
}
7879

79-
func getHTML(url string) (doc *goquery.Document, err error) {
80+
func getHTML(url string) (n *html.Node, err error) {
8081
resp, err := http.Get(url)
8182
if err != nil {
8283
return nil, fmt.Errorf("failed to get %q: %w", url, err)
8384
}
84-
return goquery.NewDocumentFromReader(resp.Body)
85+
defer resp.Body.Close()
86+
return html.Parse(resp.Body)
8587
}
8688

8789
func TestCanAccessDirect(t *testing.T) {
@@ -99,7 +101,11 @@ func TestCanAccessDirect(t *testing.T) {
99101
if err != nil {
100102
t.Fatalf("failed to read HTML: %v", err)
101103
}
102-
countText := doc.Find(`div[data-testid="count"]`).Text()
104+
countElements := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "count")))
105+
if len(countElements) != 1 {
106+
t.Fatalf("expected 1 count element, got %d", len(countElements))
107+
}
108+
countText := countElements[0].FirstChild.Data
103109
actualCount, err := strconv.Atoi(countText)
104110
if err != nil {
105111
t.Fatalf("got count %q instead of integer", countText)
@@ -124,7 +130,11 @@ func TestCanAccessViaProxy(t *testing.T) {
124130
if err != nil {
125131
t.Fatalf("failed to read HTML: %v", err)
126132
}
127-
countText := doc.Find(`div[data-testid="count"]`).Text()
133+
countElements := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "count")))
134+
if len(countElements) != 1 {
135+
t.Fatalf("expected 1 count element, got %d", len(countElements))
136+
}
137+
countText := countElements[0].FirstChild.Data
128138
actualCount, err := strconv.Atoi(countText)
129139
if err != nil {
130140
t.Fatalf("got count %q instead of integer", countText)
@@ -195,7 +205,11 @@ func TestFileModificationsResultInSSEWithGzip(t *testing.T) {
195205
if err != nil {
196206
t.Fatalf("failed to read HTML: %v", err)
197207
}
198-
if text := doc.Find(`div[data-testid="modification"]`).Text(); text != "Original" {
208+
modified := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "modification")))
209+
if len(modified) != 1 {
210+
t.Fatalf("expected 1 modification element, got %d", len(modified))
211+
}
212+
if text := modified[0].FirstChild.Data; text != "Original" {
199213
t.Errorf("expected %q, got %q", "Original", text)
200214
}
201215

@@ -236,7 +250,11 @@ loop:
236250
if err != nil {
237251
t.Fatalf("failed to read HTML: %v", err)
238252
}
239-
if text := doc.Find(`div[data-testid="modification"]`).Text(); text != "Updated" {
253+
modified = htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "modification")))
254+
if len(modified) != 1 {
255+
t.Fatalf("expected 1 modification element, got %d", len(modified))
256+
}
257+
if text := modified[0].FirstChild.Data; text != "Updated" {
240258
t.Errorf("expected %q, got %q", "Updated", text)
241259
}
242260
}
@@ -263,7 +281,11 @@ func TestFileModificationsResultInSSE(t *testing.T) {
263281
if err != nil {
264282
t.Fatalf("failed to read HTML: %v", err)
265283
}
266-
if text := doc.Find(`div[data-testid="modification"]`).Text(); text != "Original" {
284+
modified := htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "modification")))
285+
if len(modified) != 1 {
286+
t.Fatalf("expected 1 modification element, got %d", len(modified))
287+
}
288+
if text := modified[0].FirstChild.Data; text != "Original" {
267289
t.Errorf("expected %q, got %q", "Original", text)
268290
}
269291

@@ -304,7 +326,11 @@ loop:
304326
if err != nil {
305327
t.Fatalf("failed to read HTML: %v", err)
306328
}
307-
if text := doc.Find(`div[data-testid="modification"]`).Text(); text != "Updated" {
329+
modified = htmlfind.All(doc, htmlfind.Element("div", htmlfind.Attr("data-testid", "modification")))
330+
if len(modified) != 1 {
331+
t.Fatalf("expected 1 modification element, got %d", len(modified))
332+
}
333+
if text := modified[0].FirstChild.Data; text != "Updated" {
308334
t.Errorf("expected %q, got %q", "Updated", text)
309335
}
310336
}

examples/blog/go.mod

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/a-h/templ/examples/blog
2+
3+
go 1.23.3
4+
5+
require (
6+
github.com/PuerkitoBio/goquery v1.10.1
7+
github.com/a-h/templ v0.3.833
8+
)
9+
10+
require (
11+
github.com/andybalholm/cascadia v1.3.3 // indirect
12+
golang.org/x/net v0.33.0 // indirect
13+
)
14+
15+
replace github.com/a-h/templ => ../../

0 commit comments

Comments
 (0)