Skip to content

Commit df520e3

Browse files
jmooringclaude
authored andcommitted
resources/page: Fix shared reader in Source.ValueAsOpenReadSeekCloser
Closes #14684 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b55d452 commit df520e3

File tree

3 files changed

+64
-1
lines changed

3 files changed

+64
-1
lines changed

hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,3 +893,29 @@ baseURL = "https://example.com"
893893
// _content.gotmpl which was siblings of index.md (leaf bundles) was mistakingly classified as a content resource.
894894
hugolib.Test(t, files)
895895
}
896+
897+
// Issue 14684
898+
func TestPagesFromGoTmplAddResourceFromStringContent(t *testing.T) {
899+
t.Parallel()
900+
901+
files := `
902+
-- hugo.toml --
903+
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
904+
baseURL = "https://example.com"
905+
-- assets/a/pixel.png --
906+
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
907+
-- layouts/single.html --
908+
{{ with .Resources.Get "pixel.png" }}
909+
{{ with .Resize "1x1" }}Resized: {{ .Width }}x{{ .Height }}|{{ end }}
910+
{{ end }}
911+
-- content/_content.gotmpl --
912+
{{ $pixel := resources.Get "a/pixel.png" }}
913+
{{ $content := dict "mediaType" $pixel.MediaType.Type "value" $pixel.Content }}
914+
{{ $.AddPage (dict "path" "p1" "title" "p1") }}
915+
{{ $.AddResource (dict "path" "p1/pixel.png" "content" $content) }}
916+
`
917+
918+
b := hugolib.Test(t, files)
919+
920+
b.AssertFileContent("public/p1/index.html", "Resized: 1x1|")
921+
}

resources/page/pagemeta/page_frontmatter.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,10 @@ func (s Source) ValueAsString() string {
555555
}
556556

557557
func (s Source) ValueAsOpenReadSeekCloser() hugio.OpenReadSeekCloser {
558-
return hugio.NewOpenReadSeekCloser(hugio.NewReadSeekerNoOpCloserFromString(s.ValueAsString()))
558+
content := s.ValueAsString()
559+
return func() (hugio.ReadSeekCloser, error) {
560+
return hugio.NewReadSeekerNoOpCloserFromString(content), nil
561+
}
559562
}
560563

561564
// FrontMatterOnlyValues holds values that can only be set via front matter.

resources/page/pagemeta/page_frontmatter_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package pagemeta_test
1515

1616
import (
17+
"io"
1718
"strings"
1819
"testing"
1920
"time"
@@ -154,6 +155,39 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
154155
c.Assert(d.PageConfigLate.Dates.ExpiryDate.IsZero(), qt.Equals, true)
155156
}
156157

158+
// Issue 14684
159+
// Each call to the opener must return an independent reader. With the old
160+
// implementation, opener() returned the same shared reader seeked to 0, so a
161+
// second open would reset the position of a reader already in use.
162+
func TestSourceValueAsOpenReadSeekCloserIsIndependent(t *testing.T) {
163+
c := qt.New(t)
164+
s := pagemeta.Source{Value: "abcdefgh"}
165+
opener := s.ValueAsOpenReadSeekCloser()
166+
167+
r1, err := opener()
168+
c.Assert(err, qt.IsNil)
169+
defer r1.Close()
170+
171+
// Partially consume r1.
172+
buf := make([]byte, 4)
173+
_, err = io.ReadFull(r1, buf)
174+
c.Assert(err, qt.IsNil)
175+
c.Assert(string(buf), qt.Equals, "abcd")
176+
177+
// Open a second reader and fully consume it.
178+
r2, err := opener()
179+
c.Assert(err, qt.IsNil)
180+
defer r2.Close()
181+
all, err := io.ReadAll(r2)
182+
c.Assert(err, qt.IsNil)
183+
c.Assert(string(all), qt.Equals, "abcdefgh")
184+
185+
// r1's position must be unaffected; it should yield the remaining half.
186+
rest, err := io.ReadAll(r1)
187+
c.Assert(err, qt.IsNil)
188+
c.Assert(string(rest), qt.Equals, "efgh")
189+
}
190+
157191
func TestContentMediaTypeFromMarkup(t *testing.T) {
158192
c := qt.New(t)
159193
logger := loggers.NewDefault()

0 commit comments

Comments
 (0)