resources.go (12087B)
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 resources provides template functions for working with resources.
15 package resources
16
17 import (
18 "fmt"
19 "sync"
20
21 "github.com/gohugoio/hugo/common/herrors"
22
23 "errors"
24
25 "github.com/gohugoio/hugo/common/maps"
26
27 "github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
28
29 "github.com/gohugoio/hugo/helpers"
30 "github.com/gohugoio/hugo/resources/postpub"
31
32 "github.com/gohugoio/hugo/deps"
33 "github.com/gohugoio/hugo/resources"
34 "github.com/gohugoio/hugo/resources/resource"
35
36 "github.com/gohugoio/hugo/resources/resource_factories/bundler"
37 "github.com/gohugoio/hugo/resources/resource_factories/create"
38 "github.com/gohugoio/hugo/resources/resource_transformers/babel"
39 "github.com/gohugoio/hugo/resources/resource_transformers/integrity"
40 "github.com/gohugoio/hugo/resources/resource_transformers/minifier"
41 "github.com/gohugoio/hugo/resources/resource_transformers/postcss"
42 "github.com/gohugoio/hugo/resources/resource_transformers/templates"
43 "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
44 "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
45
46 "github.com/spf13/cast"
47 )
48
49 // New returns a new instance of the resources-namespaced template functions.
50 func New(deps *deps.Deps) (*Namespace, error) {
51 if deps.ResourceSpec == nil {
52 return &Namespace{}, nil
53 }
54
55 scssClient, err := scss.New(deps.BaseFs.Assets, deps.ResourceSpec)
56 if err != nil {
57 return nil, err
58 }
59
60 minifyClient, err := minifier.New(deps.ResourceSpec)
61 if err != nil {
62 return nil, err
63 }
64
65 return &Namespace{
66 deps: deps,
67 scssClientLibSass: scssClient,
68 createClient: create.New(deps.ResourceSpec),
69 bundlerClient: bundler.New(deps.ResourceSpec),
70 integrityClient: integrity.New(deps.ResourceSpec),
71 minifyClient: minifyClient,
72 postcssClient: postcss.New(deps.ResourceSpec),
73 templatesClient: templates.New(deps.ResourceSpec, deps),
74 babelClient: babel.New(deps.ResourceSpec),
75 }, nil
76 }
77
78 var _ resource.ResourceFinder = (*Namespace)(nil)
79
80 // Namespace provides template functions for the "resources" namespace.
81 type Namespace struct {
82 deps *deps.Deps
83
84 createClient *create.Client
85 bundlerClient *bundler.Client
86 scssClientLibSass *scss.Client
87 integrityClient *integrity.Client
88 minifyClient *minifier.Client
89 postcssClient *postcss.Client
90 babelClient *babel.Client
91 templatesClient *templates.Client
92
93 // The Dart Client requires a os/exec process, so only
94 // create it if we really need it.
95 // This is mostly to avoid creating one per site build test.
96 scssClientDartSassInit sync.Once
97 scssClientDartSass *dartsass.Client
98 }
99
100 func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
101 var err error
102 ns.scssClientDartSassInit.Do(func() {
103 ns.scssClientDartSass, err = dartsass.New(ns.deps.BaseFs.Assets, ns.deps.ResourceSpec)
104 if err != nil {
105 return
106 }
107 ns.deps.BuildClosers.Add(ns.scssClientDartSass)
108
109 })
110
111 return ns.scssClientDartSass, err
112 }
113
114 // Copy copies r to the new targetPath in s.
115 func (ns *Namespace) Copy(s any, r resource.Resource) (resource.Resource, error) {
116 targetPath, err := cast.ToStringE(s)
117 if err != nil {
118 panic(err)
119 }
120 return ns.createClient.Copy(r, targetPath)
121 }
122
123 // Get locates the filename given in Hugo's assets filesystem
124 // and creates a Resource object that can be used for further transformations.
125 func (ns *Namespace) Get(filename any) resource.Resource {
126 filenamestr, err := cast.ToStringE(filename)
127 if err != nil {
128 panic(err)
129 }
130 r, err := ns.createClient.Get(filenamestr)
131 if err != nil {
132 panic(err)
133 }
134
135 return r
136 }
137
138 // GetRemote gets the URL (via HTTP(s)) in the first argument in args and creates Resource object that can be used for
139 // further transformations.
140 //
141 // A second argument may be provided with an option map.
142 //
143 // Note: This method does not return any error as a second argument,
144 // for any error situations the error can be checked in .Err.
145 func (ns *Namespace) GetRemote(args ...any) resource.Resource {
146 get := func(args ...any) (resource.Resource, error) {
147 if len(args) < 1 {
148 return nil, errors.New("must provide an URL")
149 }
150
151 urlstr, err := cast.ToStringE(args[0])
152 if err != nil {
153 return nil, err
154 }
155
156 var options map[string]any
157
158 if len(args) > 1 {
159 options, err = maps.ToStringMapE(args[1])
160 if err != nil {
161 return nil, err
162 }
163 }
164
165 return ns.createClient.FromRemote(urlstr, options)
166
167 }
168
169 r, err := get(args...)
170 if err != nil {
171 switch v := err.(type) {
172 case *create.HTTPError:
173 return resources.NewErrorResource(resource.NewResourceError(v, v.Data))
174 default:
175 return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any)))
176 }
177
178 }
179 return r
180
181 }
182
183 // GetMatch finds the first Resource matching the given pattern, or nil if none found.
184 //
185 // It looks for files in the assets file system.
186 //
187 // See Match for a more complete explanation about the rules used.
188 func (ns *Namespace) GetMatch(pattern any) resource.Resource {
189 patternStr, err := cast.ToStringE(pattern)
190 if err != nil {
191 panic(err)
192 }
193
194 r, err := ns.createClient.GetMatch(patternStr)
195 if err != nil {
196 panic(err)
197 }
198
199 return r
200 }
201
202 // ByType returns resources of a given resource type (e.g. "image").
203 func (ns *Namespace) ByType(typ any) resource.Resources {
204 return ns.createClient.ByType(cast.ToString(typ))
205 }
206
207 // Match gets all resources matching the given base path prefix, e.g
208 // "*.png" will match all png files. The "*" does not match path delimiters (/),
209 // so if you organize your resources in sub-folders, you need to be explicit about it, e.g.:
210 // "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and
211 // to match all PNG images below the images folder, use "images/**.jpg".
212 //
213 // The matching is case insensitive.
214 //
215 // Match matches by using the files name with path relative to the file system root
216 // with Unix style slashes (/) and no leading slash, e.g. "images/logo.png".
217 //
218 // See https://github.com/gobwas/glob for the full rules set.
219 //
220 // It looks for files in the assets file system.
221 //
222 // See Match for a more complete explanation about the rules used.
223 func (ns *Namespace) Match(pattern any) resource.Resources {
224 defer herrors.Recover()
225 patternStr, err := cast.ToStringE(pattern)
226 if err != nil {
227 panic(err)
228 }
229
230 r, err := ns.createClient.Match(patternStr)
231 if err != nil {
232 panic(err)
233 }
234
235 return r
236 }
237
238 // Concat concatenates a slice of Resource objects. These resources must
239 // (currently) be of the same Media Type.
240 func (ns *Namespace) Concat(targetPathIn any, r any) (resource.Resource, error) {
241 targetPath, err := cast.ToStringE(targetPathIn)
242 if err != nil {
243 return nil, err
244 }
245
246 var rr resource.Resources
247
248 switch v := r.(type) {
249 case resource.Resources:
250 rr = v
251 case resource.ResourcesConverter:
252 rr = v.ToResources()
253 default:
254 return nil, fmt.Errorf("slice %T not supported in concat", r)
255 }
256
257 if len(rr) == 0 {
258 return nil, errors.New("must provide one or more Resource objects to concat")
259 }
260
261 return ns.bundlerClient.Concat(targetPath, rr)
262 }
263
264 // FromString creates a Resource from a string published to the relative target path.
265 func (ns *Namespace) FromString(targetPathIn, contentIn any) (resource.Resource, error) {
266 targetPath, err := cast.ToStringE(targetPathIn)
267 if err != nil {
268 return nil, err
269 }
270 content, err := cast.ToStringE(contentIn)
271 if err != nil {
272 return nil, err
273 }
274
275 return ns.createClient.FromString(targetPath, content)
276 }
277
278 // ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with
279 // the given data, and published to the relative target path.
280 func (ns *Namespace) ExecuteAsTemplate(args ...any) (resource.Resource, error) {
281 if len(args) != 3 {
282 return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object")
283 }
284 targetPath, err := cast.ToStringE(args[0])
285 if err != nil {
286 return nil, err
287 }
288 data := args[1]
289
290 r, ok := args[2].(resources.ResourceTransformer)
291 if !ok {
292 return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2])
293 }
294
295 return ns.templatesClient.ExecuteAsTemplate(r, targetPath, data)
296 }
297
298 // Fingerprint transforms the given Resource with a MD5 hash of the content in
299 // the RelPermalink and Permalink.
300 func (ns *Namespace) Fingerprint(args ...any) (resource.Resource, error) {
301 if len(args) < 1 || len(args) > 2 {
302 return nil, errors.New("must provide a Resource and (optional) crypto algo")
303 }
304
305 var algo string
306 resIdx := 0
307
308 if len(args) == 2 {
309 resIdx = 1
310 var err error
311 algo, err = cast.ToStringE(args[0])
312 if err != nil {
313 return nil, err
314 }
315 }
316
317 r, ok := args[resIdx].(resources.ResourceTransformer)
318 if !ok {
319 return nil, fmt.Errorf("%T can not be transformed", args[resIdx])
320 }
321
322 return ns.integrityClient.Fingerprint(r, algo)
323 }
324
325 // Minify minifies the given Resource using the MediaType to pick the correct
326 // minifier.
327 func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource, error) {
328 return ns.minifyClient.Minify(r)
329 }
330
331 // ToCSS converts the given Resource to CSS. You can optional provide an Options
332 // object or a target path (string) as first argument.
333 func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
334 const (
335 // Transpiler implementation can be controlled from the client by
336 // setting the 'transpiler' option.
337 // Default is currently 'libsass', but that may change.
338 transpilerDart = "dartsass"
339 transpilerLibSass = "libsass"
340 )
341
342 var (
343 r resources.ResourceTransformer
344 m map[string]any
345 targetPath string
346 err error
347 ok bool
348 transpiler = transpilerLibSass
349 )
350
351 r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
352
353 if !ok {
354 r, m, err = resourcehelpers.ResolveArgs(args)
355 if err != nil {
356 return nil, err
357 }
358 }
359
360 if m != nil {
361 maps.PrepareParams(m)
362 if t, found := m["transpiler"]; found {
363 switch t {
364 case transpilerDart, transpilerLibSass:
365 transpiler = cast.ToString(t)
366 default:
367 return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart)
368 }
369 }
370 }
371
372 if transpiler == transpilerLibSass {
373 var options scss.Options
374 if targetPath != "" {
375 options.TargetPath = helpers.ToSlashTrimLeading(targetPath)
376 } else if m != nil {
377 options, err = scss.DecodeOptions(m)
378 if err != nil {
379 return nil, err
380 }
381 }
382
383 return ns.scssClientLibSass.ToCSS(r, options)
384 }
385
386 if m == nil {
387 m = make(map[string]any)
388 }
389 if targetPath != "" {
390 m["targetPath"] = targetPath
391 }
392
393 client, err := ns.getscssClientDartSass()
394 if err != nil {
395 return nil, err
396 }
397
398 return client.ToCSS(r, m)
399
400 }
401
402 // PostCSS processes the given Resource with PostCSS
403 func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
404 r, m, err := resourcehelpers.ResolveArgs(args)
405 if err != nil {
406 return nil, err
407 }
408
409 return ns.postcssClient.Process(r, m)
410 }
411
412 func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) {
413 return ns.deps.ResourceSpec.PostProcess(r)
414 }
415
416 // Babel processes the given Resource with Babel.
417 func (ns *Namespace) Babel(args ...any) (resource.Resource, error) {
418 r, m, err := resourcehelpers.ResolveArgs(args)
419 if err != nil {
420 return nil, err
421 }
422 var options babel.Options
423 if m != nil {
424 options, err = babel.DecodeOptions(m)
425
426 if err != nil {
427 return nil, err
428 }
429 }
430
431 return ns.babelClient.Process(r, options)
432 }