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 }