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