Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.

Commit b6a686e

Browse files
fix: return raw source from RenderJS/RenderCSS to prevent HTML-escaping
RenderJS() and RenderCSS() were executing templates through html/template's Execute(), which HTML-escapes characters like < to &lt; in the output. This broke main.js with 8 instances of &lt; in for-loops and comparisons, causing JS syntax errors. Use t.Tree.Root.String() to return the raw template source instead, since these files contain no Go template directives. Adds regression tests for both functions. Another manifestation of the text/template → html/template migration bug from 8f1470b. Fixes #10 Co-authored-by: Jonathan Popham <jonathanpopham@users.noreply.github.com>
1 parent 35fb113 commit b6a686e

2 files changed

Lines changed: 64 additions & 10 deletions

File tree

internal/pssg/render/render.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -245,29 +245,25 @@ func (e *Engine) render(name string, data interface{}) (string, error) {
245245
}
246246

247247
// RenderCSS reads and returns the CSS template content.
248+
// Returns raw source instead of executing through html/template,
249+
// which would HTML-escape characters like < to &lt; in CSS.
248250
func (e *Engine) RenderCSS() (string, error) {
249251
t := e.tmpl.Lookup("_styles.css")
250252
if t == nil {
251253
return "", nil
252254
}
253-
var buf bytes.Buffer
254-
if err := t.Execute(&buf, nil); err != nil {
255-
return "", err
256-
}
257-
return buf.String(), nil
255+
return t.Tree.Root.String(), nil
258256
}
259257

260258
// RenderJS reads and returns the JS template content.
259+
// Returns raw source instead of executing through html/template,
260+
// which would HTML-escape characters like < to &lt; in JS code.
261261
func (e *Engine) RenderJS() (string, error) {
262262
t := e.tmpl.Lookup("_main.js")
263263
if t == nil {
264264
return "", nil
265265
}
266-
var buf bytes.Buffer
267-
if err := t.Execute(&buf, nil); err != nil {
268-
return "", err
269-
}
270-
return buf.String(), nil
266+
return t.Tree.Root.String(), nil
271267
}
272268

273269
// GenerateCookModePrompt builds a cook-with-AI prompt for a recipe.

internal/pssg/render/render_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"html/template"
77
"regexp"
8+
"strings"
89
"testing"
910
)
1011

@@ -157,6 +158,63 @@ func TestTemplateHTMLWouldDoubleEncode(t *testing.T) {
157158
t.Log("Warning: template.HTML in script tag parsed as object — behavior may have changed in this Go version")
158159
}
159160

161+
// TestRenderJSNotHTMLEscaped verifies that RenderJS() returns raw JS source
162+
// without HTML-escaping. This is a regression test for the html/template
163+
// migration bug where Execute() would escape < to &lt; in JS code.
164+
func TestRenderJSNotHTMLEscaped(t *testing.T) {
165+
jsSource := `for (var i = 0; i < items.length; i++) { if (i < max) { process(i); } }`
166+
167+
tmpl := template.New("").Funcs(BuildFuncMap())
168+
_, err := tmpl.New("_main.js").Parse(jsSource)
169+
if err != nil {
170+
t.Fatalf("failed to parse JS template: %v", err)
171+
}
172+
173+
engine := &Engine{tmpl: tmpl}
174+
result, err := engine.RenderJS()
175+
if err != nil {
176+
t.Fatalf("RenderJS() error: %v", err)
177+
}
178+
179+
if strings.Contains(result, "&lt;") {
180+
t.Fatalf("RenderJS() HTML-escaped '<' to '&lt;': %s", result)
181+
}
182+
if strings.Contains(result, "&gt;") {
183+
t.Fatalf("RenderJS() HTML-escaped '>' to '&gt;': %s", result)
184+
}
185+
if strings.Contains(result, "&amp;") {
186+
t.Fatalf("RenderJS() HTML-escaped '&' to '&amp;': %s", result)
187+
}
188+
if !strings.Contains(result, "i < items.length") {
189+
t.Fatalf("RenderJS() did not preserve '<' in JS code: %s", result)
190+
}
191+
}
192+
193+
// TestRenderCSSNotHTMLEscaped verifies that RenderCSS() returns raw CSS source
194+
// without HTML-escaping.
195+
func TestRenderCSSNotHTMLEscaped(t *testing.T) {
196+
cssSource := `.container > .child { color: red; } /* a > b */`
197+
198+
tmpl := template.New("").Funcs(BuildFuncMap())
199+
_, err := tmpl.New("_styles.css").Parse(cssSource)
200+
if err != nil {
201+
t.Fatalf("failed to parse CSS template: %v", err)
202+
}
203+
204+
engine := &Engine{tmpl: tmpl}
205+
result, err := engine.RenderCSS()
206+
if err != nil {
207+
t.Fatalf("RenderCSS() error: %v", err)
208+
}
209+
210+
if strings.Contains(result, "&gt;") {
211+
t.Fatalf("RenderCSS() HTML-escaped '>' to '&gt;': %s", result)
212+
}
213+
if !strings.Contains(result, "> .child") {
214+
t.Fatalf("RenderCSS() did not preserve '>' in CSS code: %s", result)
215+
}
216+
}
217+
160218
// TestLengthWithVariousTypes verifies the reflect-based length function
161219
// works with all slice types including taxonomy.Entry slices.
162220
func TestLengthWithVariousTypes(t *testing.T) {

0 commit comments

Comments
 (0)