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