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  }