github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/resource_transformers/tocss/dartsass/transform.go (about)

     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 dartsass
    15  
    16  import (
    17  	"fmt"
    18  	"io"
    19  	"net/url"
    20  	"path"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/cli/safeexec"
    25  
    26  	"github.com/gohugoio/hugo/common/herrors"
    27  	"github.com/gohugoio/hugo/htesting"
    28  	"github.com/gohugoio/hugo/media"
    29  
    30  	"github.com/gohugoio/hugo/resources"
    31  
    32  	"github.com/gohugoio/hugo/resources/internal"
    33  
    34  	"github.com/spf13/afero"
    35  
    36  	"github.com/gohugoio/hugo/hugofs"
    37  
    38  	"github.com/bep/godartsass"
    39  )
    40  
    41  // See https://github.com/sass/dart-sass-embedded/issues/24
    42  const stdinPlaceholder = "HUGOSTDIN"
    43  
    44  // Supports returns whether dart-sass-embedded is found in $PATH.
    45  func Supports() bool {
    46  	if htesting.SupportsAll() {
    47  		return true
    48  	}
    49  	p, err := safeexec.LookPath("dart-sass-embedded")
    50  	return err == nil && p != ""
    51  }
    52  
    53  type transform struct {
    54  	optsm map[string]interface{}
    55  	c     *Client
    56  }
    57  
    58  func (t *transform) Key() internal.ResourceTransformationKey {
    59  	return internal.NewResourceTransformationKey(transformationName, t.optsm)
    60  }
    61  
    62  func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
    63  	ctx.OutMediaType = media.CSSType
    64  
    65  	opts, err := decodeOptions(t.optsm)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	if opts.TargetPath != "" {
    71  		ctx.OutPath = opts.TargetPath
    72  	} else {
    73  		ctx.ReplaceOutPathExtension(".css")
    74  	}
    75  
    76  	baseDir := path.Dir(ctx.SourcePath)
    77  
    78  	args := godartsass.Args{
    79  		URL:          stdinPlaceholder,
    80  		IncludePaths: t.c.sfs.RealDirs(baseDir),
    81  		ImportResolver: importResolver{
    82  			baseDir: baseDir,
    83  			c:       t.c,
    84  		},
    85  		OutputStyle:     godartsass.ParseOutputStyle(opts.OutputStyle),
    86  		EnableSourceMap: opts.EnableSourceMap,
    87  	}
    88  
    89  	// Append any workDir relative include paths
    90  	for _, ip := range opts.IncludePaths {
    91  		info, err := t.c.workFs.Stat(filepath.Clean(ip))
    92  		if err == nil {
    93  			filename := info.(hugofs.FileMetaInfo).Meta().Filename
    94  			args.IncludePaths = append(args.IncludePaths, filename)
    95  		}
    96  	}
    97  
    98  	if ctx.InMediaType.SubType == media.SASSType.SubType {
    99  		args.SourceSyntax = godartsass.SourceSyntaxSASS
   100  	}
   101  
   102  	res, err := t.c.toCSS(args, ctx.From)
   103  	if err != nil {
   104  		if sassErr, ok := err.(godartsass.SassError); ok {
   105  			start := sassErr.Span.Start
   106  			context := strings.TrimSpace(sassErr.Span.Context)
   107  			filename, _ := urlToFilename(sassErr.Span.Url)
   108  			if filename == stdinPlaceholder {
   109  				if ctx.SourcePath == "" {
   110  					return sassErr
   111  				}
   112  				filename = t.c.sfs.RealFilename(ctx.SourcePath)
   113  			}
   114  
   115  			offsetMatcher := func(m herrors.LineMatcher) bool {
   116  				return m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context)
   117  			}
   118  
   119  			ferr, ok := herrors.WithFileContextForFile(
   120  				herrors.NewFileError("scss", -1, -1, start.Column, sassErr),
   121  				filename,
   122  				filename,
   123  				hugofs.Os,
   124  				offsetMatcher)
   125  
   126  			if !ok {
   127  				return sassErr
   128  			}
   129  
   130  			return ferr
   131  		}
   132  		return err
   133  	}
   134  
   135  	out := res.CSS
   136  
   137  	_, err = io.WriteString(ctx.To, out)
   138  	if err != nil {
   139  		return err
   140  	}
   141  
   142  	if opts.EnableSourceMap && res.SourceMap != "" {
   143  		if err := ctx.PublishSourceMap(res.SourceMap); err != nil {
   144  			return err
   145  		}
   146  		_, err = fmt.Fprintf(ctx.To, "\n\n/*# sourceMappingURL=%s */", path.Base(ctx.OutPath)+".map")
   147  	}
   148  
   149  	return err
   150  }
   151  
   152  type importResolver struct {
   153  	baseDir string
   154  	c       *Client
   155  }
   156  
   157  func (t importResolver) CanonicalizeURL(url string) (string, error) {
   158  	filePath, isURL := urlToFilename(url)
   159  	var prevDir string
   160  	var pathDir string
   161  	if isURL {
   162  		var found bool
   163  		prevDir, found = t.c.sfs.MakePathRelative(filepath.Dir(filePath))
   164  
   165  		if !found {
   166  			// Not a member of this filesystem, let Dart Sass handle it.
   167  			return "", nil
   168  		}
   169  	} else {
   170  		prevDir = t.baseDir
   171  		pathDir = path.Dir(url)
   172  	}
   173  
   174  	basePath := filepath.Join(prevDir, pathDir)
   175  	name := filepath.Base(filePath)
   176  
   177  	// Pick the first match.
   178  	var namePatterns []string
   179  	if strings.Contains(name, ".") {
   180  		namePatterns = []string{"_%s", "%s"}
   181  	} else if strings.HasPrefix(name, "_") {
   182  		namePatterns = []string{"_%s.scss", "_%s.sass"}
   183  	} else {
   184  		namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"}
   185  	}
   186  
   187  	name = strings.TrimPrefix(name, "_")
   188  
   189  	for _, namePattern := range namePatterns {
   190  		filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name))
   191  		fi, err := t.c.sfs.Fs.Stat(filenameToCheck)
   192  		if err == nil {
   193  			if fim, ok := fi.(hugofs.FileMetaInfo); ok {
   194  				return "file://" + filepath.ToSlash(fim.Meta().Filename), nil
   195  			}
   196  		}
   197  	}
   198  
   199  	// Not found, let Dart Dass handle it
   200  	return "", nil
   201  }
   202  
   203  func (t importResolver) Load(url string) (string, error) {
   204  	filename, _ := urlToFilename(url)
   205  	b, err := afero.ReadFile(hugofs.Os, filename)
   206  	return string(b), err
   207  }
   208  
   209  // TODO(bep) add tests
   210  func urlToFilename(urls string) (string, bool) {
   211  	u, err := url.ParseRequestURI(urls)
   212  	if err != nil {
   213  		return filepath.FromSlash(urls), false
   214  	}
   215  	p := filepath.FromSlash(u.Path)
   216  
   217  	if u.Host != "" {
   218  		// C:\data\file.txt
   219  		p = strings.ToUpper(u.Host) + ":" + p
   220  	}
   221  
   222  	return p, true
   223  }