github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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 return value, 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 if len(args) > 2 { 157 return nil, errors.New("must not provide more arguments than URL and options") 158 } 159 160 urlstr, err := cast.ToStringE(args[0]) 161 if err != nil { 162 return nil, err 163 } 164 165 var options map[string]any 166 167 if len(args) > 1 { 168 options, err = maps.ToStringMapE(args[1]) 169 if err != nil { 170 return nil, err 171 } 172 } 173 174 return ns.createClient.FromRemote(urlstr, options) 175 176 } 177 178 r, err := get(args...) 179 if err != nil { 180 switch v := err.(type) { 181 case *create.HTTPError: 182 return resources.NewErrorResource(resource.NewResourceError(v, v.Data)) 183 default: 184 return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any))) 185 } 186 187 } 188 return r 189 190 } 191 192 // GetMatch finds the first Resource matching the given pattern, or nil if none found. 193 // 194 // It looks for files in the assets file system. 195 // 196 // See Match for a more complete explanation about the rules used. 197 func (ns *Namespace) GetMatch(pattern any) resource.Resource { 198 patternStr, err := cast.ToStringE(pattern) 199 if err != nil { 200 panic(err) 201 } 202 203 r, err := ns.createClient.GetMatch(patternStr) 204 if err != nil { 205 panic(err) 206 } 207 208 return r 209 } 210 211 // ByType returns resources of a given resource type (e.g. "image"). 212 func (ns *Namespace) ByType(typ any) resource.Resources { 213 return ns.createClient.ByType(cast.ToString(typ)) 214 } 215 216 // Match gets all resources matching the given base path prefix, e.g 217 // "*.png" will match all png files. The "*" does not match path delimiters (/), 218 // so if you organize your resources in sub-folders, you need to be explicit about it, e.g.: 219 // "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and 220 // to match all PNG images below the images folder, use "images/**.jpg". 221 // 222 // The matching is case insensitive. 223 // 224 // Match matches by using the files name with path relative to the file system root 225 // with Unix style slashes (/) and no leading slash, e.g. "images/logo.png". 226 // 227 // See https://github.com/gobwas/glob for the full rules set. 228 // 229 // It looks for files in the assets file system. 230 // 231 // See Match for a more complete explanation about the rules used. 232 func (ns *Namespace) Match(pattern any) resource.Resources { 233 patternStr, err := cast.ToStringE(pattern) 234 if err != nil { 235 panic(err) 236 } 237 238 r, err := ns.createClient.Match(patternStr) 239 if err != nil { 240 panic(err) 241 } 242 243 return r 244 } 245 246 // Concat concatenates a slice of Resource objects. These resources must 247 // (currently) be of the same Media Type. 248 func (ns *Namespace) Concat(targetPathIn any, r any) (resource.Resource, error) { 249 targetPath, err := cast.ToStringE(targetPathIn) 250 if err != nil { 251 return nil, err 252 } 253 254 var rr resource.Resources 255 256 switch v := r.(type) { 257 case resource.Resources: 258 rr = v 259 case resource.ResourcesConverter: 260 rr = v.ToResources() 261 default: 262 return nil, fmt.Errorf("slice %T not supported in concat", r) 263 } 264 265 if len(rr) == 0 { 266 return nil, errors.New("must provide one or more Resource objects to concat") 267 } 268 269 return ns.bundlerClient.Concat(targetPath, rr) 270 } 271 272 // FromString creates a Resource from a string published to the relative target path. 273 func (ns *Namespace) FromString(targetPathIn, contentIn any) (resource.Resource, error) { 274 targetPath, err := cast.ToStringE(targetPathIn) 275 if err != nil { 276 return nil, err 277 } 278 content, err := cast.ToStringE(contentIn) 279 if err != nil { 280 return nil, err 281 } 282 283 return ns.createClient.FromString(targetPath, content) 284 } 285 286 // ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with 287 // the given data, and published to the relative target path. 288 func (ns *Namespace) ExecuteAsTemplate(ctx context.Context, args ...any) (resource.Resource, error) { 289 if len(args) != 3 { 290 return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object") 291 } 292 targetPath, err := cast.ToStringE(args[0]) 293 if err != nil { 294 return nil, err 295 } 296 data := args[1] 297 298 r, ok := args[2].(resources.ResourceTransformer) 299 if !ok { 300 return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2]) 301 } 302 303 return ns.templatesClient.ExecuteAsTemplate(ctx, r, targetPath, data) 304 } 305 306 // Fingerprint transforms the given Resource with a MD5 hash of the content in 307 // the RelPermalink and Permalink. 308 func (ns *Namespace) Fingerprint(args ...any) (resource.Resource, error) { 309 if len(args) < 1 { 310 return nil, errors.New("must provide a Resource object") 311 } 312 313 if len(args) > 2 { 314 return nil, errors.New("must not provide more arguments than Resource and hash algorithm") 315 } 316 317 var algo string 318 resIdx := 0 319 320 if len(args) == 2 { 321 resIdx = 1 322 var err error 323 algo, err = cast.ToStringE(args[0]) 324 if err != nil { 325 return nil, err 326 } 327 } 328 329 r, ok := args[resIdx].(resources.ResourceTransformer) 330 if !ok { 331 return nil, fmt.Errorf("%T can not be transformed", args[resIdx]) 332 } 333 334 return ns.integrityClient.Fingerprint(r, algo) 335 } 336 337 // Minify minifies the given Resource using the MediaType to pick the correct 338 // minifier. 339 func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource, error) { 340 return ns.minifyClient.Minify(r) 341 } 342 343 // ToCSS converts the given Resource to CSS. You can optional provide an Options object 344 // as second argument. As an option, you can e.g. specify e.g. the target path (string) 345 // for the converted CSS resource. 346 func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) { 347 348 if len(args) > 2 { 349 return nil, errors.New("must not provide more arguments than resource object and options") 350 } 351 352 const ( 353 // Transpiler implementation can be controlled from the client by 354 // setting the 'transpiler' option. 355 // Default is currently 'libsass', but that may change. 356 transpilerDart = "dartsass" 357 transpilerLibSass = "libsass" 358 ) 359 360 var ( 361 r resources.ResourceTransformer 362 m map[string]any 363 targetPath string 364 err error 365 ok bool 366 transpiler = transpilerLibSass 367 ) 368 369 r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args) 370 371 if !ok { 372 r, m, err = resourcehelpers.ResolveArgs(args) 373 if err != nil { 374 return nil, err 375 } 376 } 377 378 if m != nil { 379 if t, found := maps.LookupEqualFold(m, "transpiler"); found { 380 switch t { 381 case transpilerDart, transpilerLibSass: 382 transpiler = cast.ToString(t) 383 default: 384 return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart) 385 } 386 } 387 } 388 389 if transpiler == transpilerLibSass { 390 var options scss.Options 391 if targetPath != "" { 392 options.TargetPath = helpers.ToSlashTrimLeading(targetPath) 393 } else if m != nil { 394 options, err = scss.DecodeOptions(m) 395 if err != nil { 396 return nil, err 397 } 398 } 399 400 return ns.scssClientLibSass.ToCSS(r, options) 401 } 402 403 if m == nil { 404 m = make(map[string]any) 405 } 406 if targetPath != "" { 407 m["targetPath"] = targetPath 408 } 409 410 client, err := ns.getscssClientDartSass() 411 if err != nil { 412 return nil, err 413 } 414 415 return client.ToCSS(r, m) 416 417 } 418 419 // PostCSS processes the given Resource with PostCSS 420 func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) { 421 422 if len(args) > 2 { 423 return nil, errors.New("must not provide more arguments than resource object and options") 424 } 425 426 r, m, err := resourcehelpers.ResolveArgs(args) 427 if err != nil { 428 return nil, err 429 } 430 431 return ns.postcssClient.Process(r, m) 432 } 433 434 // PostProcess processes r after the build. 435 func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) { 436 return ns.deps.ResourceSpec.PostProcess(r) 437 } 438 439 // Babel processes the given Resource with Babel. 440 func (ns *Namespace) Babel(args ...any) (resource.Resource, error) { 441 442 if len(args) > 2 { 443 return nil, errors.New("must not provide more arguments than resource object and options") 444 } 445 446 r, m, err := resourcehelpers.ResolveArgs(args) 447 if err != nil { 448 return nil, err 449 } 450 var options babel.Options 451 if m != nil { 452 options, err = babel.DecodeOptions(m) 453 454 if err != nil { 455 return nil, err 456 } 457 } 458 459 return ns.babelClient.Process(r, options) 460 }