github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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 111 // and creates a Resource object that can be used for further transformations. 112 func (ns *Namespace) Get(filename interface{}) (resource.Resource, error) { 113 filenamestr, err := cast.ToStringE(filename) 114 if err != nil { 115 return nil, err 116 } 117 118 filenamestr = filepath.Clean(filenamestr) 119 120 return ns.createClient.Get(filenamestr) 121 } 122 123 // GetMatch finds the first Resource matching the given pattern, or nil if none found. 124 // 125 // It looks for files in the assets file system. 126 // 127 // See Match for a more complete explanation about the rules used. 128 func (ns *Namespace) GetMatch(pattern interface{}) (resource.Resource, error) { 129 patternStr, err := cast.ToStringE(pattern) 130 if err != nil { 131 return nil, err 132 } 133 134 return ns.createClient.GetMatch(patternStr) 135 } 136 137 // Match gets all resources matching the given base path prefix, e.g 138 // "*.png" will match all png files. The "*" does not match path delimiters (/), 139 // so if you organize your resources in sub-folders, you need to be explicit about it, e.g.: 140 // "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and 141 // to match all PNG images below the images folder, use "images/**.jpg". 142 // 143 // The matching is case insensitive. 144 // 145 // Match matches by using the files name with path relative to the file system root 146 // with Unix style slashes (/) and no leading slash, e.g. "images/logo.png". 147 // 148 // See https://github.com/gobwas/glob for the full rules set. 149 // 150 // It looks for files in the assets file system. 151 // 152 // See Match for a more complete explanation about the rules used. 153 func (ns *Namespace) Match(pattern interface{}) (resource.Resources, error) { 154 patternStr, err := cast.ToStringE(pattern) 155 if err != nil { 156 return nil, err 157 } 158 159 return ns.createClient.Match(patternStr) 160 } 161 162 // Concat concatenates a slice of Resource objects. These resources must 163 // (currently) be of the same Media Type. 164 func (ns *Namespace) Concat(targetPathIn interface{}, r interface{}) (resource.Resource, error) { 165 targetPath, err := cast.ToStringE(targetPathIn) 166 if err != nil { 167 return nil, err 168 } 169 170 var rr resource.Resources 171 172 switch v := r.(type) { 173 case resource.Resources: 174 rr = v 175 case resource.ResourcesConverter: 176 rr = v.ToResources() 177 default: 178 return nil, fmt.Errorf("slice %T not supported in concat", r) 179 } 180 181 if len(rr) == 0 { 182 return nil, errors.New("must provide one or more Resource objects to concat") 183 } 184 185 return ns.bundlerClient.Concat(targetPath, rr) 186 } 187 188 // FromString creates a Resource from a string published to the relative target path. 189 func (ns *Namespace) FromString(targetPathIn, contentIn interface{}) (resource.Resource, error) { 190 targetPath, err := cast.ToStringE(targetPathIn) 191 if err != nil { 192 return nil, err 193 } 194 content, err := cast.ToStringE(contentIn) 195 if err != nil { 196 return nil, err 197 } 198 199 return ns.createClient.FromString(targetPath, content) 200 } 201 202 // ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with 203 // the given data, and published to the relative target path. 204 func (ns *Namespace) ExecuteAsTemplate(args ...interface{}) (resource.Resource, error) { 205 if len(args) != 3 { 206 return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object") 207 } 208 targetPath, err := cast.ToStringE(args[0]) 209 if err != nil { 210 return nil, err 211 } 212 data := args[1] 213 214 r, ok := args[2].(resources.ResourceTransformer) 215 if !ok { 216 return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2]) 217 } 218 219 return ns.templatesClient.ExecuteAsTemplate(r, targetPath, data) 220 } 221 222 // Fingerprint transforms the given Resource with a MD5 hash of the content in 223 // the RelPermalink and Permalink. 224 func (ns *Namespace) Fingerprint(args ...interface{}) (resource.Resource, error) { 225 if len(args) < 1 || len(args) > 2 { 226 return nil, errors.New("must provide a Resource and (optional) crypto algo") 227 } 228 229 var algo string 230 resIdx := 0 231 232 if len(args) == 2 { 233 resIdx = 1 234 var err error 235 algo, err = cast.ToStringE(args[0]) 236 if err != nil { 237 return nil, err 238 } 239 } 240 241 r, ok := args[resIdx].(resources.ResourceTransformer) 242 if !ok { 243 return nil, fmt.Errorf("%T can not be transformed", args[resIdx]) 244 } 245 246 return ns.integrityClient.Fingerprint(r, algo) 247 } 248 249 // Minify minifies the given Resource using the MediaType to pick the correct 250 // minifier. 251 func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource, error) { 252 return ns.minifyClient.Minify(r) 253 } 254 255 // ToCSS converts the given Resource to CSS. You can optional provide an Options 256 // object or a target path (string) as first argument. 257 func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) { 258 const ( 259 // Transpiler implementation can be controlled from the client by 260 // setting the 'transpiler' option. 261 // Default is currently 'libsass', but that may change. 262 transpilerDart = "dartsass" 263 transpilerLibSass = "libsass" 264 ) 265 266 var ( 267 r resources.ResourceTransformer 268 m map[string]interface{} 269 targetPath string 270 err error 271 ok bool 272 transpiler = transpilerLibSass 273 ) 274 275 r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args) 276 277 if !ok { 278 r, m, err = resourcehelpers.ResolveArgs(args) 279 if err != nil { 280 return nil, err 281 } 282 } 283 284 if m != nil { 285 maps.PrepareParams(m) 286 if t, found := m["transpiler"]; found { 287 switch t { 288 case transpilerDart, transpilerLibSass: 289 transpiler = cast.ToString(t) 290 default: 291 return nil, errors.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart) 292 } 293 } 294 } 295 296 if transpiler == transpilerLibSass { 297 var options scss.Options 298 if targetPath != "" { 299 options.TargetPath = helpers.ToSlashTrimLeading(targetPath) 300 } else if m != nil { 301 options, err = scss.DecodeOptions(m) 302 if err != nil { 303 return nil, err 304 } 305 } 306 307 return ns.scssClientLibSass.ToCSS(r, options) 308 } 309 310 if m == nil { 311 m = make(map[string]interface{}) 312 } 313 if targetPath != "" { 314 m["targetPath"] = targetPath 315 } 316 317 client, err := ns.getscssClientDartSass() 318 if err != nil { 319 return nil, err 320 } 321 322 return client.ToCSS(r, m) 323 324 } 325 326 // PostCSS processes the given Resource with PostCSS 327 func (ns *Namespace) PostCSS(args ...interface{}) (resource.Resource, error) { 328 r, m, err := resourcehelpers.ResolveArgs(args) 329 if err != nil { 330 return nil, err 331 } 332 var options postcss.Options 333 if m != nil { 334 options, err = postcss.DecodeOptions(m) 335 if err != nil { 336 return nil, err 337 } 338 } 339 340 return ns.postcssClient.Process(r, options) 341 } 342 343 func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) { 344 return ns.deps.ResourceSpec.PostProcess(r) 345 } 346 347 // Babel processes the given Resource with Babel. 348 func (ns *Namespace) Babel(args ...interface{}) (resource.Resource, error) { 349 r, m, err := resourcehelpers.ResolveArgs(args) 350 if err != nil { 351 return nil, err 352 } 353 var options babel.Options 354 if m != nil { 355 options, err = babel.DecodeOptions(m) 356 357 if err != nil { 358 return nil, err 359 } 360 } 361 362 return ns.babelClient.Process(r, options) 363 }