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