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&#43;8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css
   111  CSS integrity Data last:  /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03&#43;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(&#34;demo&#34;).innerHTML=x*10`)
   391  			b.AssertFileContent("public/index.html", `Min JS Remote: var x=5;document.getElementById(&#34;demo&#34;).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-&#43;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  }