hugo/tpl/css/css.go
Bjørn Erik Pedersen e1317dd322 Add css.TailwindCSS
Closes #12618
Closes #12620
2024-06-25 15:48:02 +02:00

199 lines
5 KiB
Go

package css
import (
"context"
"errors"
"fmt"
"sync"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types/css"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/resource_transformers/babel"
"github.com/gohugoio/hugo/resources/resource_transformers/cssjs"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
"github.com/gohugoio/hugo/tpl/internal"
"github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
"github.com/spf13/cast"
)
const name = "css"
// Namespace provides template functions for the "css" namespace.
type Namespace struct {
d *deps.Deps
scssClientLibSass *scss.Client
postcssClient *cssjs.PostCSSClient
tailwindcssClient *cssjs.TailwindCSSClient
babelClient *babel.Client
// The Dart Client requires a os/exec process, so only
// create it if we really need it.
// This is mostly to avoid creating one per site build test.
scssClientDartSassInit sync.Once
scssClientDartSass *dartsass.Client
}
// Quoted returns a string that needs to be quoted in CSS.
func (ns *Namespace) Quoted(v any) css.QuotedString {
s := cast.ToString(v)
return css.QuotedString(s)
}
// Unquoted returns a string that does not need to be quoted in CSS.
func (ns *Namespace) Unquoted(v any) css.UnquotedString {
s := cast.ToString(v)
return css.UnquotedString(s)
}
// PostCSS processes the given Resource with PostCSS.
func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
r, m, err := resourcehelpers.ResolveArgs(args)
if err != nil {
return nil, err
}
return ns.postcssClient.Process(r, m)
}
// TailwindCSS processes the given Resource with tailwindcss.
func (ns *Namespace) TailwindCSS(args ...any) (resource.Resource, error) {
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
r, m, err := resourcehelpers.ResolveArgs(args)
if err != nil {
return nil, err
}
return ns.tailwindcssClient.Process(r, m)
}
// Sass processes the given Resource with SASS.
func (ns *Namespace) Sass(args ...any) (resource.Resource, error) {
if len(args) > 2 {
return nil, errors.New("must not provide more arguments than resource object and options")
}
const (
// Transpiler implementation can be controlled from the client by
// setting the 'transpiler' option.
// Default is currently 'libsass', but that may change.
transpilerDart = "dartsass"
transpilerLibSass = "libsass"
)
var (
r resources.ResourceTransformer
m map[string]any
targetPath string
err error
ok bool
transpiler = transpilerLibSass
)
r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
if !ok {
r, m, err = resourcehelpers.ResolveArgs(args)
if err != nil {
return nil, err
}
}
if m != nil {
if t, _, found := maps.LookupEqualFold(m, "transpiler"); found {
switch t {
case transpilerDart, transpilerLibSass:
transpiler = cast.ToString(t)
default:
return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, transpilerLibSass, transpilerDart)
}
}
}
if transpiler == transpilerLibSass {
var options scss.Options
if targetPath != "" {
options.TargetPath = paths.ToSlashTrimLeading(targetPath)
} else if m != nil {
options, err = scss.DecodeOptions(m)
if err != nil {
return nil, err
}
}
return ns.scssClientLibSass.ToCSS(r, options)
}
if m == nil {
m = make(map[string]any)
}
if targetPath != "" {
m["targetPath"] = targetPath
}
client, err := ns.getscssClientDartSass()
if err != nil {
return nil, err
}
return client.ToCSS(r, m)
}
func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
scssClient, err := scss.New(d.BaseFs.Assets, d.ResourceSpec)
if err != nil {
panic(err)
}
ctx := &Namespace{
d: d,
scssClientLibSass: scssClient,
postcssClient: cssjs.NewPostCSSClient(d.ResourceSpec),
tailwindcssClient: cssjs.NewTailwindCSSClient(d.ResourceSpec),
babelClient: babel.New(d.ResourceSpec),
}
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
}
ns.AddMethodMapping(ctx.Sass,
[]string{"toCSS"},
[][2]string{},
)
ns.AddMethodMapping(ctx.PostCSS,
[]string{"postCSS"},
[][2]string{},
)
return ns
}
internal.AddTemplateFuncsNamespace(f)
}
func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
var err error
ns.scssClientDartSassInit.Do(func() {
ns.scssClientDartSass, err = dartsass.New(ns.d.BaseFs.Assets, ns.d.ResourceSpec)
if err != nil {
return
}
ns.d.BuildClosers.Add(ns.scssClientDartSass)
})
return ns.scssClientDartSass, err
}