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  }