resource_chain_test.go (29570B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package hugolib
15
16 import (
17 "fmt"
18 "io"
19 "io/ioutil"
20 "math/rand"
21 "net/http"
22 "net/http/httptest"
23 "os"
24 "path/filepath"
25 "strings"
26 "testing"
27 "time"
28
29 "github.com/gohugoio/hugo/helpers"
30
31 qt "github.com/frankban/quicktest"
32
33 "github.com/gohugoio/hugo/common/loggers"
34 "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
35 )
36
37 func TestResourceChainBasic(t *testing.T) {
38 failIfHandler := func(h http.Handler) http.Handler {
39 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40 if r.URL.Path == "/fail.jpg" {
41 http.Error(w, "{ msg: failed }", 500)
42 return
43 }
44 h.ServeHTTP(w, r)
45
46 })
47 }
48 ts := httptest.NewServer(
49 failIfHandler(http.FileServer(http.Dir("testdata/"))),
50 )
51 t.Cleanup(func() {
52 ts.Close()
53 })
54
55 b := newTestSitesBuilder(t)
56 b.WithTemplatesAdded("index.html", fmt.Sprintf(`
57 {{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | fingerprint "sha512" | minify | fingerprint }}
58 {{ $cssFingerprinted1 := "body { background-color: lightblue; }" | resources.FromString "styles.css" | minify | fingerprint }}
59 {{ $cssFingerprinted2 := "body { background-color: orange; }" | resources.FromString "styles2.css" | minify | fingerprint }}
60
61
62 HELLO: {{ $hello.Name }}|{{ $hello.RelPermalink }}|{{ $hello.Content | safeHTML }}
63
64 {{ $img := resources.Get "images/sunset.jpg" }}
65 {{ $fit := $img.Fit "200x200" }}
66 {{ $fit2 := $fit.Fit "100x200" }}
67 {{ $img = $img | fingerprint }}
68 SUNSET: {{ $img.Name }}|{{ $img.RelPermalink }}|{{ $img.Width }}|{{ len $img.Content }}
69 FIT: {{ $fit.Name }}|{{ $fit.RelPermalink }}|{{ $fit.Width }}
70 CSS integrity Data first: {{ $cssFingerprinted1.Data.Integrity }} {{ $cssFingerprinted1.RelPermalink }}
71 CSS integrity Data last: {{ $cssFingerprinted2.RelPermalink }} {{ $cssFingerprinted2.Data.Integrity }}
72
73 {{ $failedImg := resources.GetRemote "%[1]s/fail.jpg" }}
74 {{ $rimg := resources.GetRemote "%[1]s/sunset.jpg" }}
75 {{ $remotenotfound := resources.GetRemote "%[1]s/notfound.jpg" }}
76 {{ $localnotfound := resources.Get "images/notfound.jpg" }}
77 {{ $gopherprotocol := resources.GetRemote "gopher://example.org" }}
78 {{ $rfit := $rimg.Fit "200x200" }}
79 {{ $rfit2 := $rfit.Fit "100x200" }}
80 {{ $rimg = $rimg | fingerprint }}
81 SUNSET REMOTE: {{ $rimg.Name }}|{{ $rimg.RelPermalink }}|{{ $rimg.Width }}|{{ len $rimg.Content }}
82 FIT REMOTE: {{ $rfit.Name }}|{{ $rfit.RelPermalink }}|{{ $rfit.Width }}
83 REMOTE NOT FOUND: {{ if $remotenotfound }}FAILED{{ else}}OK{{ end }}
84 LOCAL NOT FOUND: {{ if $localnotfound }}FAILED{{ else}}OK{{ end }}
85 PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ . | safeHTML }}{{ end }}
86 PRINT PROTOCOL ERROR2: {{ with $gopherprotocol }}{{ .Err | safeHTML }}{{ end }}
87 PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}Err: {{ .Err | safeHTML }}{{ with .Err }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }}
88 FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg.Err }}|{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}|
89 `, ts.URL))
90
91 fs := b.Fs.Source
92
93 imageDir := filepath.Join("assets", "images")
94 b.Assert(os.MkdirAll(imageDir, 0777), qt.IsNil)
95 src, err := os.Open("testdata/sunset.jpg")
96 b.Assert(err, qt.IsNil)
97 out, err := fs.Create(filepath.Join(imageDir, "sunset.jpg"))
98 b.Assert(err, qt.IsNil)
99 _, err = io.Copy(out, src)
100 b.Assert(err, qt.IsNil)
101 out.Close()
102
103 b.Running()
104
105 for i := 0; i < 2; i++ {
106
107 b.Build(BuildCfg{})
108
109 b.AssertFileContent("public/index.html",
110 fmt.Sprintf(`
111 SUNSET: images/sunset.jpg|/images/sunset.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587
112 FIT: images/sunset.jpg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fit_q75_box.jpg|200
113 CSS integrity Data first: sha256-od9YaHw8nMOL8mUy97Sy8sKwMV3N4hI3aVmZXATxH+8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css
114 CSS integrity Data last: /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03+ZmKY/1t2GCOjEEOXj2x2qow94vCc7o=
115
116 SUNSET REMOTE: sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587
117 FIT REMOTE: sunset_%[1]s.jpg|/sunset_%[1]s_hu59e56ffff1bc1d8d122b1403d34e039f_0_200x200_fit_q75_box.jpg|200
118 REMOTE NOT FOUND: OK
119 LOCAL NOT FOUND: OK
120 PRINT PROTOCOL ERROR DETAILS: Err: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"||
121 FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource: Internal Server Error|Body: { msg: failed }
122 |StatusCode: 500|ContentLength: 16|ContentType: text/plain; charset=utf-8|
123
124
125 `, helpers.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
126
127 b.AssertFileContent("public/styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css", "body{background-color:#add8e6}")
128 b.AssertFileContent("public//styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css", "body{background-color:orange}")
129
130 b.EditFiles("page1.md", `
131 ---
132 title: "Page 1 edit"
133 summary: "Edited summary"
134 ---
135
136 Edited content.
137
138 `)
139
140 b.Assert(b.Fs.WorkingDirWritable.Remove("public"), qt.IsNil)
141 b.H.ResourceSpec.ClearCaches()
142
143 }
144 }
145
146 func TestResourceChainPostProcess(t *testing.T) {
147 t.Parallel()
148
149 rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
150
151 b := newTestSitesBuilder(t)
152 b.WithConfigFile("toml", `[minify]
153 minifyOutput = true
154 [minify.tdewolff]
155 [minify.tdewolff.html]
156 keepQuotes = false
157 keepWhitespace = false`)
158 b.WithContent("page1.md", "---\ntitle: Page1\n---")
159 b.WithContent("page2.md", "---\ntitle: Page2\n---")
160
161 b.WithTemplates(
162 "_default/single.html", `{{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }}
163 HELLO: {{ $hello.RelPermalink }}
164 `,
165 "index.html", `Start.
166 {{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }}
167
168 HELLO: {{ $hello.RelPermalink }}|Integrity: {{ $hello.Data.Integrity }}|MediaType: {{ $hello.MediaType.Type }}
169 HELLO2: Name: {{ $hello.Name }}|Content: {{ $hello.Content }}|Title: {{ $hello.Title }}|ResourceType: {{ $hello.ResourceType }}
170
171 // Issue #8884
172 <a href="hugo.rocks">foo</a>
173 <a href="{{ $hello.RelPermalink }}" integrity="{{ $hello.Data.Integrity}}">Hello</a>
174 `+strings.Repeat("a b", rnd.Intn(10)+1)+`
175
176
177 End.`)
178
179 b.Running()
180 b.Build(BuildCfg{})
181 b.AssertFileContent("public/index.html",
182 `Start.
183 HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html|Integrity: md5-otHLJPJLMip9rVIEFMUj6Q==|MediaType: text/html
184 HELLO2: Name: hello.html|Content: <h1>Hello World!</h1>|Title: hello.html|ResourceType: text
185 <a href=hugo.rocks>foo</a>
186 <a href="/hello.min.a2d1cb24f24b322a7dad520414c523e9.html" integrity="md5-otHLJPJLMip9rVIEFMUj6Q==">Hello</a>
187 End.`)
188
189 b.AssertFileContent("public/page1/index.html", `HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html`)
190 b.AssertFileContent("public/page2/index.html", `HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html`)
191 }
192
193 func BenchmarkResourceChainPostProcess(b *testing.B) {
194 for i := 0; i < b.N; i++ {
195 b.StopTimer()
196 s := newTestSitesBuilder(b)
197 for i := 0; i < 300; i++ {
198 s.WithContent(fmt.Sprintf("page%d.md", i+1), "---\ntitle: Page\n---")
199 }
200 s.WithTemplates("_default/single.html", `Start.
201 Some text.
202
203
204 {{ $hello1 := "<h1> Hello World 2! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }}
205 {{ $hello2 := "<h1> Hello World 2! </h1>" | resources.FromString (printf "%s.html" .Path) | minify | fingerprint "md5" | resources.PostProcess }}
206
207 Some more text.
208
209 HELLO: {{ $hello1.RelPermalink }}|Integrity: {{ $hello1.Data.Integrity }}|MediaType: {{ $hello1.MediaType.Type }}
210
211 Some more text.
212
213 HELLO2: Name: {{ $hello2.Name }}|Content: {{ $hello2.Content }}|Title: {{ $hello2.Title }}|ResourceType: {{ $hello2.ResourceType }}
214
215 Some more text.
216
217 HELLO2_2: Name: {{ $hello2.Name }}|Content: {{ $hello2.Content }}|Title: {{ $hello2.Title }}|ResourceType: {{ $hello2.ResourceType }}
218
219 End.
220 `)
221
222 b.StartTimer()
223 s.Build(BuildCfg{})
224
225 }
226 }
227
228 func TestResourceChains(t *testing.T) {
229 t.Parallel()
230
231 c := qt.New(t)
232
233 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
234 switch r.URL.Path {
235 case "/css/styles1.css":
236 w.Header().Set("Content-Type", "text/css")
237 w.Write([]byte(`h1 {
238 font-style: bold;
239 }`))
240 return
241
242 case "/js/script1.js":
243 w.Write([]byte(`var x; x = 5, document.getElementById("demo").innerHTML = x * 10`))
244 return
245
246 case "/mydata/json1.json":
247 w.Write([]byte(`{
248 "employees": [
249 {
250 "firstName": "John",
251 "lastName": "Doe"
252 },
253 {
254 "firstName": "Anna",
255 "lastName": "Smith"
256 },
257 {
258 "firstName": "Peter",
259 "lastName": "Jones"
260 }
261 ]
262 }`))
263 return
264
265 case "/mydata/xml1.xml":
266 w.Write([]byte(`
267 <hello>
268 <world>Hugo Rocks!</<world>
269 </hello>`))
270 return
271
272 case "/mydata/svg1.svg":
273 w.Header().Set("Content-Disposition", `attachment; filename="image.svg"`)
274 w.Write([]byte(`
275 <svg height="100" width="100">
276 <path d="M1e2 1e2H3e2 2e2z"/>
277 </svg>`))
278 return
279
280 case "/mydata/html1.html":
281 w.Write([]byte(`
282 <html>
283 <a href=#>Cool</a>
284 </html>`))
285 return
286
287 case "/authenticated/":
288 w.Header().Set("Content-Type", "text/plain")
289 if r.Header.Get("Authorization") == "Bearer abcd" {
290 w.Write([]byte(`Welcome`))
291 return
292 }
293 http.Error(w, "Forbidden", http.StatusForbidden)
294 return
295
296 case "/post":
297 w.Header().Set("Content-Type", "text/plain")
298 if r.Method == http.MethodPost {
299 body, err := ioutil.ReadAll(r.Body)
300 if err != nil {
301 http.Error(w, "Internal server error", http.StatusInternalServerError)
302 return
303 }
304 w.Write(body)
305 return
306 }
307 http.Error(w, "Bad request", http.StatusBadRequest)
308 return
309 }
310
311 http.Error(w, "Not found", http.StatusNotFound)
312 return
313 }))
314 t.Cleanup(func() {
315 ts.Close()
316 })
317
318 tests := []struct {
319 name string
320 shouldRun func() bool
321 prepare func(b *sitesBuilder)
322 verify func(b *sitesBuilder)
323 }{
324 {"tocss", func() bool { return scss.Supports() }, func(b *sitesBuilder) {
325 b.WithTemplates("home.html", `
326 {{ $scss := resources.Get "scss/styles2.scss" | toCSS }}
327 {{ $sass := resources.Get "sass/styles3.sass" | toCSS }}
328 {{ $scssCustomTarget := resources.Get "scss/styles2.scss" | toCSS (dict "targetPath" "styles/main.css") }}
329 {{ $scssCustomTargetString := resources.Get "scss/styles2.scss" | toCSS "styles/main.css" }}
330 {{ $scssMin := resources.Get "scss/styles2.scss" | toCSS | minify }}
331 {{ $scssFromTempl := ".{{ .Kind }} { color: blue; }" | resources.FromString "kindofblue.templ" | resources.ExecuteAsTemplate "kindofblue.scss" . | toCSS (dict "targetPath" "styles/templ.css") | minify }}
332 {{ $bundle1 := slice $scssFromTempl $scssMin | resources.Concat "styles/bundle1.css" }}
333 T1: Len Content: {{ len $scss.Content }}|RelPermalink: {{ $scss.RelPermalink }}|Permalink: {{ $scss.Permalink }}|MediaType: {{ $scss.MediaType.Type }}
334 T2: Content: {{ $scssMin.Content }}|RelPermalink: {{ $scssMin.RelPermalink }}
335 T3: Content: {{ len $scssCustomTarget.Content }}|RelPermalink: {{ $scssCustomTarget.RelPermalink }}|MediaType: {{ $scssCustomTarget.MediaType.Type }}
336 T4: Content: {{ len $scssCustomTargetString.Content }}|RelPermalink: {{ $scssCustomTargetString.RelPermalink }}|MediaType: {{ $scssCustomTargetString.MediaType.Type }}
337 T5: Content: {{ $sass.Content }}|T5 RelPermalink: {{ $sass.RelPermalink }}|
338 T6: {{ $bundle1.Permalink }}
339 `)
340 }, func(b *sitesBuilder) {
341 b.AssertFileContent("public/index.html", `T1: Len Content: 24|RelPermalink: /scss/styles2.css|Permalink: http://example.com/scss/styles2.css|MediaType: text/css`)
342 b.AssertFileContent("public/index.html", `T2: Content: body{color:#333}|RelPermalink: /scss/styles2.min.css`)
343 b.AssertFileContent("public/index.html", `T3: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`)
344 b.AssertFileContent("public/index.html", `T4: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`)
345 b.AssertFileContent("public/index.html", `T5: Content: .content-navigation {`)
346 b.AssertFileContent("public/index.html", `T5 RelPermalink: /sass/styles3.css|`)
347 b.AssertFileContent("public/index.html", `T6: http://example.com/styles/bundle1.css`)
348
349 c.Assert(b.CheckExists("public/styles/templ.min.css"), qt.Equals, false)
350 b.AssertFileContent("public/styles/bundle1.css", `.home{color:blue}body{color:#333}`)
351 }},
352
353 {"minify", func() bool { return true }, func(b *sitesBuilder) {
354 b.WithConfigFile("toml", `[minify]
355 [minify.tdewolff]
356 [minify.tdewolff.html]
357 keepWhitespace = false
358 `)
359 b.WithTemplates("home.html", fmt.Sprintf(`
360 Min CSS: {{ ( resources.Get "css/styles1.css" | minify ).Content }}
361 Min CSS Remote: {{ ( resources.GetRemote "%[1]s/css/styles1.css" | minify ).Content }}
362 Min JS: {{ ( resources.Get "js/script1.js" | resources.Minify ).Content | safeJS }}
363 Min JS Remote: {{ ( resources.GetRemote "%[1]s/js/script1.js" | minify ).Content }}
364 Min JSON: {{ ( resources.Get "mydata/json1.json" | resources.Minify ).Content | safeHTML }}
365 Min JSON Remote: {{ ( resources.GetRemote "%[1]s/mydata/json1.json" | resources.Minify ).Content | safeHTML }}
366 Min XML: {{ ( resources.Get "mydata/xml1.xml" | resources.Minify ).Content | safeHTML }}
367 Min XML Remote: {{ ( resources.GetRemote "%[1]s/mydata/xml1.xml" | resources.Minify ).Content | safeHTML }}
368 Min SVG: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }}
369 Min SVG Remote: {{ ( resources.GetRemote "%[1]s/mydata/svg1.svg" | resources.Minify ).Content | safeHTML }}
370 Min SVG again: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }}
371 Min HTML: {{ ( resources.Get "mydata/html1.html" | resources.Minify ).Content | safeHTML }}
372 Min HTML Remote: {{ ( resources.GetRemote "%[1]s/mydata/html1.html" | resources.Minify ).Content | safeHTML }}
373 `, ts.URL))
374 }, func(b *sitesBuilder) {
375 b.AssertFileContent("public/index.html", `Min CSS: h1{font-style:bold}`)
376 b.AssertFileContent("public/index.html", `Min CSS Remote: h1{font-style:bold}`)
377 b.AssertFileContent("public/index.html", `Min JS: var x=5;document.getElementById("demo").innerHTML=x*10`)
378 b.AssertFileContent("public/index.html", `Min JS Remote: var x=5;document.getElementById("demo").innerHTML=x*10`)
379 b.AssertFileContent("public/index.html", `Min JSON: {"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"},{"firstName":"Peter","lastName":"Jones"}]}`)
380 b.AssertFileContent("public/index.html", `Min JSON Remote: {"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"},{"firstName":"Peter","lastName":"Jones"}]}`)
381 b.AssertFileContent("public/index.html", `Min XML: <hello><world>Hugo Rocks!</<world></hello>`)
382 b.AssertFileContent("public/index.html", `Min XML Remote: <hello><world>Hugo Rocks!</<world></hello>`)
383 b.AssertFileContent("public/index.html", `Min SVG: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`)
384 b.AssertFileContent("public/index.html", `Min SVG Remote: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`)
385 b.AssertFileContent("public/index.html", `Min SVG again: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`)
386 b.AssertFileContent("public/index.html", `Min HTML: <html><a href=#>Cool</a></html>`)
387 b.AssertFileContent("public/index.html", `Min HTML Remote: <html><a href=#>Cool</a></html>`)
388 }},
389
390 {"remote", func() bool { return true }, func(b *sitesBuilder) {
391 b.WithTemplates("home.html", fmt.Sprintf(`
392 {{$js := resources.GetRemote "%[1]s/js/script1.js" }}
393 Remote Filename: {{ $js.RelPermalink }}
394 {{$svg := resources.GetRemote "%[1]s/mydata/svg1.svg" }}
395 Remote Content-Disposition: {{ $svg.RelPermalink }}
396 {{$auth := resources.GetRemote "%[1]s/authenticated/" (dict "headers" (dict "Authorization" "Bearer abcd")) }}
397 Remote Authorization: {{ $auth.Content }}
398 {{$post := resources.GetRemote "%[1]s/post" (dict "method" "post" "body" "Request body") }}
399 Remote POST: {{ $post.Content }}
400 `, ts.URL))
401 }, func(b *sitesBuilder) {
402 b.AssertFileContent("public/index.html", `Remote Filename: /script1_`)
403 b.AssertFileContent("public/index.html", `Remote Content-Disposition: /image_`)
404 b.AssertFileContent("public/index.html", `Remote Authorization: Welcome`)
405 b.AssertFileContent("public/index.html", `Remote POST: Request body`)
406 }},
407
408 {"concat", func() bool { return true }, func(b *sitesBuilder) {
409 b.WithTemplates("home.html", `
410 {{ $a := "A" | resources.FromString "a.txt"}}
411 {{ $b := "B" | resources.FromString "b.txt"}}
412 {{ $c := "C" | resources.FromString "c.txt"}}
413 {{ $textResources := .Resources.Match "*.txt" }}
414 {{ $combined := slice $a $b $c | resources.Concat "bundle/concat.txt" }}
415 T1: Content: {{ $combined.Content }}|RelPermalink: {{ $combined.RelPermalink }}|Permalink: {{ $combined.Permalink }}|MediaType: {{ $combined.MediaType.Type }}
416 {{ with $textResources }}
417 {{ $combinedText := . | resources.Concat "bundle/concattxt.txt" }}
418 T2: Content: {{ $combinedText.Content }}|{{ $combinedText.RelPermalink }}
419 {{ end }}
420 {{/* https://github.com/gohugoio/hugo/issues/5269 */}}
421 {{ $css := "body { color: blue; }" | resources.FromString "styles.css" }}
422 {{ $minified := resources.Get "css/styles1.css" | minify }}
423 {{ slice $css $minified | resources.Concat "bundle/mixed.css" }}
424 {{/* https://github.com/gohugoio/hugo/issues/5403 */}}
425 {{ $d := "function D {} // A comment" | resources.FromString "d.js"}}
426 {{ $e := "(function E {})" | resources.FromString "e.js"}}
427 {{ $f := "(function F {})()" | resources.FromString "f.js"}}
428 {{ $jsResources := .Resources.Match "*.js" }}
429 {{ $combinedJs := slice $d $e $f | resources.Concat "bundle/concatjs.js" }}
430 T3: Content: {{ $combinedJs.Content }}|{{ $combinedJs.RelPermalink }}
431 `)
432 }, func(b *sitesBuilder) {
433 b.AssertFileContent("public/index.html", `T1: Content: ABC|RelPermalink: /bundle/concat.txt|Permalink: http://example.com/bundle/concat.txt|MediaType: text/plain`)
434 b.AssertFileContent("public/bundle/concat.txt", "ABC")
435
436 b.AssertFileContent("public/index.html", `T2: Content: t1t|t2t|`)
437 b.AssertFileContent("public/bundle/concattxt.txt", "t1t|t2t|")
438
439 b.AssertFileContent("public/index.html", `T3: Content: function D {} // A comment
440 ;
441 (function E {})
442 ;
443 (function F {})()|`)
444 b.AssertFileContent("public/bundle/concatjs.js", `function D {} // A comment
445 ;
446 (function E {})
447 ;
448 (function F {})()`)
449 }},
450
451 {"concat and fingerprint", func() bool { return true }, func(b *sitesBuilder) {
452 b.WithTemplates("home.html", `
453 {{ $a := "A" | resources.FromString "a.txt"}}
454 {{ $b := "B" | resources.FromString "b.txt"}}
455 {{ $c := "C" | resources.FromString "c.txt"}}
456 {{ $combined := slice $a $b $c | resources.Concat "bundle/concat.txt" }}
457 {{ $fingerprinted := $combined | fingerprint }}
458 Fingerprinted: {{ $fingerprinted.RelPermalink }}
459 `)
460 }, func(b *sitesBuilder) {
461 b.AssertFileContent("public/index.html", "Fingerprinted: /bundle/concat.b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78.txt")
462 b.AssertFileContent("public/bundle/concat.b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78.txt", "ABC")
463 }},
464
465 {"fromstring", func() bool { return true }, func(b *sitesBuilder) {
466 b.WithTemplates("home.html", `
467 {{ $r := "Hugo Rocks!" | resources.FromString "rocks/hugo.txt" }}
468 {{ $r.Content }}|{{ $r.RelPermalink }}|{{ $r.Permalink }}|{{ $r.MediaType.Type }}
469 `)
470 }, func(b *sitesBuilder) {
471 b.AssertFileContent("public/index.html", `Hugo Rocks!|/rocks/hugo.txt|http://example.com/rocks/hugo.txt|text/plain`)
472 b.AssertFileContent("public/rocks/hugo.txt", "Hugo Rocks!")
473 }},
474 {"execute-as-template", func() bool {
475 return true
476 }, func(b *sitesBuilder) {
477 b.WithTemplates("home.html", `
478 {{ $var := "Hugo Page" }}
479 {{ if .IsHome }}
480 {{ $var = "Hugo Home" }}
481 {{ end }}
482 T1: {{ $var }}
483 {{ $result := "{{ .Kind | upper }}" | resources.FromString "mytpl.txt" | resources.ExecuteAsTemplate "result.txt" . }}
484 T2: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }}
485 `)
486 }, func(b *sitesBuilder) {
487 b.AssertFileContent("public/index.html", `T2: HOME|/result.txt|text/plain`, `T1: Hugo Home`)
488 }},
489 {"fingerprint", func() bool { return true }, func(b *sitesBuilder) {
490 b.WithTemplates("home.html", `
491 {{ $r := "ab" | resources.FromString "rocks/hugo.txt" }}
492 {{ $result := $r | fingerprint }}
493 {{ $result512 := $r | fingerprint "sha512" }}
494 {{ $resultMD5 := $r | fingerprint "md5" }}
495 T1: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }}|{{ $result.Data.Integrity }}|
496 T2: {{ $result512.Content }}|{{ $result512.RelPermalink}}|{{$result512.MediaType.Type }}|{{ $result512.Data.Integrity }}|
497 T3: {{ $resultMD5.Content }}|{{ $resultMD5.RelPermalink}}|{{$resultMD5.MediaType.Type }}|{{ $resultMD5.Data.Integrity }}|
498 {{ $r2 := "bc" | resources.FromString "rocks/hugo2.txt" | fingerprint }}
499 {{/* https://github.com/gohugoio/hugo/issues/5296 */}}
500 T4: {{ $r2.Data.Integrity }}|
501
502
503 `)
504 }, func(b *sitesBuilder) {
505 b.AssertFileContent("public/index.html", `T1: ab|/rocks/hugo.fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603.txt|text/plain|sha256-+44g/C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM=|`)
506 b.AssertFileContent("public/index.html", `T2: ab|/rocks/hugo.2d408a0717ec188158278a796c689044361dc6fdde28d6f04973b80896e1823975cdbf12eb63f9e0591328ee235d80e9b5bf1aa6a44f4617ff3caf6400eb172d.txt|text/plain|sha512-LUCKBxfsGIFYJ4p5bGiQRDYdxv3eKNbwSXO4CJbhgjl1zb8S62P54FkTKO4jXYDptb8apqRPRhf/PK9kAOsXLQ==|`)
507 b.AssertFileContent("public/index.html", `T3: ab|/rocks/hugo.187ef4436122d1cc2f40dc2b92f0eba0.txt|text/plain|md5-GH70Q2Ei0cwvQNwrkvDroA==|`)
508 b.AssertFileContent("public/index.html", `T4: sha256-Hgu9bGhroFC46wP/7txk/cnYCUf86CGrvl1tyNJSxaw=|`)
509 }},
510 // https://github.com/gohugoio/hugo/issues/5226
511 {"baseurl-path", func() bool { return true }, func(b *sitesBuilder) {
512 b.WithSimpleConfigFileAndBaseURL("https://example.com/hugo/")
513 b.WithTemplates("home.html", `
514 {{ $r1 := "ab" | resources.FromString "rocks/hugo.txt" }}
515 T1: {{ $r1.Permalink }}|{{ $r1.RelPermalink }}
516 `)
517 }, func(b *sitesBuilder) {
518 b.AssertFileContent("public/index.html", `T1: https://example.com/hugo/rocks/hugo.txt|/hugo/rocks/hugo.txt`)
519 }},
520
521 // https://github.com/gohugoio/hugo/issues/4944
522 {"Prevent resource publish on .Content only", func() bool { return true }, func(b *sitesBuilder) {
523 b.WithTemplates("home.html", `
524 {{ $cssInline := "body { color: green; }" | resources.FromString "inline.css" | minify }}
525 {{ $cssPublish1 := "body { color: blue; }" | resources.FromString "external1.css" | minify }}
526 {{ $cssPublish2 := "body { color: orange; }" | resources.FromString "external2.css" | minify }}
527
528 Inline: {{ $cssInline.Content }}
529 Publish 1: {{ $cssPublish1.Content }} {{ $cssPublish1.RelPermalink }}
530 Publish 2: {{ $cssPublish2.Permalink }}
531 `)
532 }, func(b *sitesBuilder) {
533 b.AssertFileContent("public/index.html",
534 `Inline: body{color:green}`,
535 "Publish 1: body{color:blue} /external1.min.css",
536 "Publish 2: http://example.com/external2.min.css",
537 )
538 b.Assert(b.CheckExists("public/external2.css"), qt.Equals, false)
539 b.Assert(b.CheckExists("public/external1.css"), qt.Equals, false)
540 b.Assert(b.CheckExists("public/external2.min.css"), qt.Equals, true)
541 b.Assert(b.CheckExists("public/external1.min.css"), qt.Equals, true)
542 b.Assert(b.CheckExists("public/inline.min.css"), qt.Equals, false)
543 }},
544
545 {"unmarshal", func() bool { return true }, func(b *sitesBuilder) {
546 b.WithTemplates("home.html", `
547 {{ $toml := "slogan = \"Hugo Rocks!\"" | resources.FromString "slogan.toml" | transform.Unmarshal }}
548 {{ $csv1 := "\"Hugo Rocks\",\"Hugo is Fast!\"" | resources.FromString "slogans.csv" | transform.Unmarshal }}
549 {{ $csv2 := "a;b;c" | transform.Unmarshal (dict "delimiter" ";") }}
550 {{ $xml := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><note><to>You</to><from>Me</from><heading>Reminder</heading><body>Do not forget XML</body></note>" | transform.Unmarshal }}
551
552 Slogan: {{ $toml.slogan }}
553 CSV1: {{ $csv1 }} {{ len (index $csv1 0) }}
554 CSV2: {{ $csv2 }}
555 XML: {{ $xml.body }}
556 `)
557 }, func(b *sitesBuilder) {
558 b.AssertFileContent("public/index.html",
559 `Slogan: Hugo Rocks!`,
560 `[[Hugo Rocks Hugo is Fast!]] 2`,
561 `CSV2: [[a b c]]`,
562 `XML: Do not forget XML`,
563 )
564 }},
565 {"resources.Get", func() bool { return true }, func(b *sitesBuilder) {
566 b.WithTemplates("home.html", `NOT FOUND: {{ if (resources.Get "this-does-not-exist") }}FAILED{{ else }}OK{{ end }}`)
567 }, func(b *sitesBuilder) {
568 b.AssertFileContent("public/index.html", "NOT FOUND: OK")
569 }},
570
571 {"template", func() bool { return true }, func(b *sitesBuilder) {}, func(b *sitesBuilder) {
572 }},
573 }
574
575 for _, test := range tests {
576 test := test
577 t.Run(test.name, func(t *testing.T) {
578 if !test.shouldRun() {
579 t.Skip()
580 }
581 t.Parallel()
582
583 b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger())
584 b.WithContent("_index.md", `
585 ---
586 title: Home
587 ---
588
589 Home.
590
591 `,
592 "page1.md", `
593 ---
594 title: Hello1
595 ---
596
597 Hello1
598 `,
599 "page2.md", `
600 ---
601 title: Hello2
602 ---
603
604 Hello2
605 `,
606 "t1.txt", "t1t|",
607 "t2.txt", "t2t|",
608 )
609
610 b.WithSourceFile(filepath.Join("assets", "css", "styles1.css"), `
611 h1 {
612 font-style: bold;
613 }
614 `)
615
616 b.WithSourceFile(filepath.Join("assets", "js", "script1.js"), `
617 var x;
618 x = 5;
619 document.getElementById("demo").innerHTML = x * 10;
620 `)
621
622 b.WithSourceFile(filepath.Join("assets", "mydata", "json1.json"), `
623 {
624 "employees":[
625 {"firstName":"John", "lastName":"Doe"},
626 {"firstName":"Anna", "lastName":"Smith"},
627 {"firstName":"Peter", "lastName":"Jones"}
628 ]
629 }
630 `)
631
632 b.WithSourceFile(filepath.Join("assets", "mydata", "svg1.svg"), `
633 <svg height="100" width="100">
634 <path d="M 100 100 L 300 100 L 200 100 z"/>
635 </svg>
636 `)
637
638 b.WithSourceFile(filepath.Join("assets", "mydata", "xml1.xml"), `
639 <hello>
640 <world>Hugo Rocks!</<world>
641 </hello>
642 `)
643
644 b.WithSourceFile(filepath.Join("assets", "mydata", "html1.html"), `
645 <html>
646 <a href="#">
647 Cool
648 </a >
649 </html>
650 `)
651
652 b.WithSourceFile(filepath.Join("assets", "scss", "styles2.scss"), `
653 $color: #333;
654
655 body {
656 color: $color;
657 }
658 `)
659
660 b.WithSourceFile(filepath.Join("assets", "sass", "styles3.sass"), `
661 $color: #333;
662
663 .content-navigation
664 border-color: $color
665
666 `)
667
668 test.prepare(b)
669 b.Build(BuildCfg{})
670 test.verify(b)
671 })
672 }
673 }
674
675 func TestMultiSiteResource(t *testing.T) {
676 t.Parallel()
677 c := qt.New(t)
678
679 b := newMultiSiteTestDefaultBuilder(t)
680
681 b.CreateSites().Build(BuildCfg{})
682
683 // This build is multilingual, but not multihost. There should be only one pipes.txt
684 b.AssertFileContent("public/fr/index.html", "French Home Page", "String Resource: /blog/text/pipes.txt")
685 c.Assert(b.CheckExists("public/fr/text/pipes.txt"), qt.Equals, false)
686 c.Assert(b.CheckExists("public/en/text/pipes.txt"), qt.Equals, false)
687 b.AssertFileContent("public/en/index.html", "Default Home Page", "String Resource: /blog/text/pipes.txt")
688 b.AssertFileContent("public/text/pipes.txt", "Hugo Pipes")
689 }
690
691 func TestResourcesMatch(t *testing.T) {
692 t.Parallel()
693
694 b := newTestSitesBuilder(t)
695
696 b.WithContent("page.md", "")
697
698 b.WithSourceFile(
699 "assets/images/img1.png", "png",
700 "assets/images/img2.jpg", "jpg",
701 "assets/jsons/data1.json", "json1 content",
702 "assets/jsons/data2.json", "json2 content",
703 "assets/jsons/data3.xml", "xml content",
704 )
705
706 b.WithTemplates("index.html", `
707 {{ $jsons := (resources.Match "jsons/*.json") }}
708 {{ $json := (resources.GetMatch "jsons/*.json") }}
709 {{ printf "jsonsMatch: %d" (len $jsons) }}
710 {{ printf "imagesByType: %d" (len (resources.ByType "image") ) }}
711 {{ printf "applicationByType: %d" (len (resources.ByType "application") ) }}
712 JSON: {{ $json.RelPermalink }}: {{ $json.Content }}
713 {{ range $jsons }}
714 {{- .RelPermalink }}: {{ .Content }}
715 {{ end }}
716 `)
717
718 b.Build(BuildCfg{})
719
720 b.AssertFileContent("public/index.html",
721 "JSON: /jsons/data1.json: json1 content",
722 "jsonsMatch: 2",
723 "imagesByType: 2",
724 "applicationByType: 3",
725 "/jsons/data1.json: json1 content")
726 }
727
728 func TestResourceMinifyDisabled(t *testing.T) {
729 t.Parallel()
730
731 b := newTestSitesBuilder(t).WithConfigFile("toml", `
732 baseURL = "https://example.org"
733
734 [minify]
735 disableXML=true
736
737
738 `)
739
740 b.WithContent("page.md", "")
741
742 b.WithSourceFile(
743 "assets/xml/data.xml", "<root> <foo> asdfasdf </foo> </root>",
744 )
745
746 b.WithTemplates("index.html", `
747 {{ $xml := resources.Get "xml/data.xml" | minify | fingerprint }}
748 XML: {{ $xml.Content | safeHTML }}|{{ $xml.RelPermalink }}
749 `)
750
751 b.Build(BuildCfg{})
752
753 b.AssertFileContent("public/index.html", `
754 XML: <root> <foo> asdfasdf </foo> </root>|/xml/data.min.3be4fddd19aaebb18c48dd6645215b822df74701957d6d36e59f203f9c30fd9f.xml
755 `)
756 }