client.go (4495B)
1 // Copyright 2020 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 godartsass integrates with the Dass Sass Embedded protocol to transpile
15 // SCSS/SASS.
16 package dartsass
17
18 import (
19 "fmt"
20 "io"
21 "strings"
22
23 "github.com/gohugoio/hugo/common/herrors"
24 "github.com/gohugoio/hugo/helpers"
25 "github.com/gohugoio/hugo/hugofs"
26 "github.com/gohugoio/hugo/hugolib/filesystems"
27 "github.com/gohugoio/hugo/resources"
28 "github.com/gohugoio/hugo/resources/resource"
29 "github.com/spf13/afero"
30
31 "github.com/bep/godartsass"
32 "github.com/mitchellh/mapstructure"
33 )
34
35 // used as part of the cache key.
36 const transformationName = "tocss-dart"
37
38 // See https://github.com/sass/dart-sass-embedded/issues/24
39 // Note: This prefix must be all lower case.
40 const dartSassStdinPrefix = "hugostdin:"
41
42 func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) {
43 if !Supports() {
44 return &Client{dartSassNotAvailable: true}, nil
45 }
46
47 if err := rs.ExecHelper.Sec().CheckAllowedExec(dartSassEmbeddedBinaryName); err != nil {
48 return nil, err
49 }
50
51 transpiler, err := godartsass.Start(godartsass.Options{
52 LogEventHandler: func(event godartsass.LogEvent) {
53 message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
54 switch event.Type {
55 case godartsass.LogEventTypeDebug:
56 // Log as Info for now, we may adjust this if it gets too chatty.
57 rs.Logger.Infof("Dart Sass: %s", message)
58 default:
59 // The rest are either deprecations or @warn statements.
60 rs.Logger.Warnf("Dart Sass: %s", message)
61 }
62 },
63 })
64 if err != nil {
65 return nil, err
66 }
67 return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs, transpiler: transpiler}, nil
68 }
69
70 type Client struct {
71 dartSassNotAvailable bool
72 rs *resources.Spec
73 sfs *filesystems.SourceFilesystem
74 workFs afero.Fs
75 transpiler *godartsass.Transpiler
76 }
77
78 func (c *Client) ToCSS(res resources.ResourceTransformer, args map[string]any) (resource.Resource, error) {
79 if c.dartSassNotAvailable {
80 return res.Transform(resources.NewFeatureNotAvailableTransformer(transformationName, args))
81 }
82 return res.Transform(&transform{c: c, optsm: args})
83 }
84
85 func (c *Client) Close() error {
86 if c.transpiler == nil {
87 return nil
88 }
89 return c.transpiler.Close()
90 }
91
92 func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result, error) {
93 var res godartsass.Result
94
95 in := helpers.ReaderToString(src)
96 args.Source = in
97
98 res, err := c.transpiler.Execute(args)
99 if err != nil {
100 if err.Error() == "unexpected EOF" {
101 return res, fmt.Errorf("got unexpected EOF when executing %q. The user running hugo must have read and execute permissions on this program. With execute permissions only, this error is thrown.", dartSassEmbeddedBinaryName)
102 }
103 return res, herrors.NewFileErrorFromFileInErr(err, hugofs.Os, herrors.OffsetMatcher)
104 }
105
106 return res, err
107 }
108
109 type Options struct {
110
111 // Hugo, will by default, just replace the extension of the source
112 // to .css, e.g. "scss/main.scss" becomes "scss/main.css". You can
113 // control this by setting this, e.g. "styles/main.css" will create
114 // a Resource with that as a base for RelPermalink etc.
115 TargetPath string
116
117 // Hugo automatically adds the entry directories (where the main.scss lives)
118 // for project and themes to the list of include paths sent to LibSASS.
119 // Any paths set in this setting will be appended. Note that these will be
120 // treated as relative to the working dir, i.e. no include paths outside the
121 // project/themes.
122 IncludePaths []string
123
124 // Default is nested.
125 // One of nested, expanded, compact, compressed.
126 OutputStyle string
127
128 // When enabled, Hugo will generate a source map.
129 EnableSourceMap bool
130 }
131
132 func decodeOptions(m map[string]any) (opts Options, err error) {
133 if m == nil {
134 return
135 }
136 err = mapstructure.WeakDecode(m, &opts)
137
138 if opts.TargetPath != "" {
139 opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
140 }
141
142 return
143 }