Repository: gohugoio/hugo (language: go)

Issue:
ToCSS' errors need some improvements
<img width="822" alt="image" src="https://user-images.githubusercontent.com/394382/168467022-e98c85c2-3ca4-483d-b084-6624faaaf784.png">

Also check

* errors in main (stdin)
* both dart and libsass


Discussion / hints:

Fix patch:
diff --git a/commands/server.go b/commands/server.go
index 036fa9eaaa4..f616810039c 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -482,6 +482,7 @@ func removeErrorPrefixFromLog(content string) string {
 var logReplacer = strings.NewReplacer(
 	"can't", "can’t", // Chroma lexer does'nt do well with "can't"
 	"*hugolib.pageState", "page.Page", // Page is the public interface.
+	"Rebuild failed:", "",
 )
 
 func cleanErrorLog(content string) string {
diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go
index 0f22f545f40..18c21e51b2c 100644
--- a/common/herrors/error_locator.go
+++ b/common/herrors/error_locator.go
@@ -52,6 +52,16 @@ var NopLineMatcher = func(m LineMatcher) int {
 	return 1
 }
 
+// OffsetMatcher is a line matcher that matches by offset.
+var OffsetMatcher = func(m LineMatcher) int {
+	if m.Offset+len(m.Line) >= m.Position.Offset {
+		// We found the line, but return 0 to signal that we want to determine
+		// the column from the error.
+		return 0
+	}
+	return -1
+}
+
 // ErrorContext contains contextual information about an error. This will
 // typically be the lines surrounding some problem in a file.
 type ErrorContext struct {
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
index 31824565473..e6baaf6e37e 100644
--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -19,6 +19,8 @@ import (
 	"io"
 	"path/filepath"
 
+	"github.com/bep/godartsass"
+	"github.com/bep/golibsass/libsass/libsasserrors"
 	"github.com/gohugoio/hugo/common/paths"
 	"github.com/gohugoio/hugo/common/text"
 	"github.com/pelletier/go-toml/v2"
@@ -132,7 +134,22 @@ func (e fileError) Position() text.Position {
 }
 
 func (e *fileError) Error() string {
-	return fmt.Sprintf("%s: %s", e.position, e.cause)
+	return fmt.Sprintf("%s: %s", e.position, e.causeString())
+}
+
+func (e *fileError) causeString() string {
+	if e.cause == nil {
+		return ""
+	}
+	switch v := e.cause.(type) {
+	// Avoid repeating the file info in the error message.
+	case godartsass.SassError:
+		return v.Message
+	case libsasserrors.Error:
+		return v.Message
+	default:
+		return v.Error()
+	}
 }
 
 func (e *fileError) Unwrap() error {
@@ -140,9 +157,17 @@ func (e *fileError) Unwrap() error {
 }
 
 // NewFileError creates a new FileError that wraps err.
+// It will try to extract the filename and line number from err.
+func NewFileError(err error) FileError {
+	// Filetype is used to determine the Chroma lexer to use.
+	fileType, pos := extractFileTypePos(err)
+	return &fileError{cause: err, fileType: fileType, position: pos}
+}
+
+// NewFileErrorFromName creates a new FileError that wraps err.
 // The value for name should identify the file, the best
 // being the full filename to the file on disk.
-func NewFileError(name string, err error) FileError {
+func NewFileErrorFromName(err error, name string) FileError {
 	// Filetype is used to determine the Chroma lexer to use.
 	fileType, pos := extractFileTypePos(err)
 	pos.Filename = name
@@ -155,7 +180,7 @@ func NewFileError(name string, err error) FileError {
 }
 
 // NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
-func NewFileErrorFromPos(pos text.Position, err error) FileError {
+func NewFileErrorFromPos(err error, pos text.Position) FileError {
 	// Filetype is used to determine the Chroma lexer to use.
 	fileType, _ := extractFileTypePos(err)
 	if fileType == "" {
@@ -165,20 +190,69 @@ func NewFileErrorFromPos(pos text.Position, err error) FileError {
 
 }
 
+func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
+	fe := NewFileError(err)
+	pos := fe.Position()
+	if pos.Filename == "" {
+		return fe
+	}
+
+	f, realFilename, err2 := openFile(pos.Filename, fs)
+	if err2 != nil {
+		return fe
+	}
+
+	pos.Filename = realFilename
+	defer f.Close()
+	return fe.UpdateContent(f, linematcher)
+}
+
+func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError {
+	if err == nil {
+		panic("err is nil")
+	}
+	f, realFilename, err2 := openFile(pos.Filename, fs)
+	if err2 != nil {
+		return NewFileErrorFromPos(err, pos)
+	}
+	pos.Filename = realFilename
+	defer f.Close()
+	return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher)
+}
+
 // NewFileErrorFromFile is a convenience method to create a new FileError from a file.
-func NewFileErrorFromFile(err error, filename, realFilename string, fs afero.Fs, linematcher LineMatcherFn) FileError {
+func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError {
 	if err == nil {
 		panic("err is nil")
 	}
-	if linematcher == nil {
-		linematcher = SimpleLineMatcher
+	f, realFilename, err2 := openFile(filename, fs)
+	if err2 != nil {
+		return NewFileErrorFromName(err, realFilename)
 	}
+	defer f.Close()
+	return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
+}
+
+func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
+	realFilename := filename
+
+	// We want the most specific filename possible in the error message.
+	fi, err2 := fs.Stat(filename)
+	if err2 == nil {
+		if s, ok := fi.(interface {
+			Filename() string
+		}); ok {
+			realFilename = s.Filename()
+		}
+
+	}
+
 	f, err2 := fs.Open(filename)
 	if err2 != nil {
-		return NewFileError(realFilename, err)
+		return nil, realFilename, err2
 	}
-	defer f.Close()
-	return NewFileError(realFilename, err).UpdateContent(f, linematcher)
+
+	return f, realFilename, nil
 }
 
 // Cause returns the underlying error or itself if it does not implement Unwrap.
@@ -191,8 +265,15 @@ func Cause(err error) error {
 
 func extractFileTypePos(err error) (string, text.Position) {
 	err = Cause(err)
+
 	var fileType string
 
+	// LibSass, DartSass
+	if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 {
+		_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
+		return fileType, pos
+	}
+
 	// Default to line 1 col 1 if we don't find any better.
 	pos := text.Position{
 		Offset:       -1,
@@ -227,6 +308,10 @@ func extractFileTypePos(err error) (string, text.Position) {
 		}
 	}
 
+	if fileType == "" && pos.Filename != "" {
+		_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
+	}
+
 	return fileType, pos
 }
 
@@ -290,3 +375,20 @@ func exctractLineNumberAndColumnNumber(e error) (int, int) {
 
 	return -1, -1
 }
+
+func extractPosition(e error) (pos text.Position) {
+	switch v := e.(type) {
+	case godartsass.SassError:
+		span := v.Span
+		start := span.Start
+		filename, _ := paths.UrlToFilename(span.Url)
+		pos.Filename = filename
+		pos.Offset = start.Offset
+		pos.ColumnNumber = start.Column
+	case libsasserrors.Error:
+		pos.Filename = v.File
+		pos.LineNumber = v.Line
+		pos.ColumnNumber = v.Column
+	}
+	return
+}
diff --git a/common/paths/url.go b/common/paths/url.go
index 193b0cd0e04..c538d8f2cbe 100644
--- a/common/paths/url.go
+++ b/common/paths/url.go
@@ -17,6 +17,7 @@ import (
 	"fmt"
 	"net/url"
 	"path"
+	"path/filepath"
 	"strings"
 )
 
@@ -152,3 +153,29 @@ func Uglify(in string) string {
 	// /section/name.html -> /section/name.html
 	return path.Clean(in)
 }
+
+// UrlToFilename converts the URL s to a filename.
+// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
+func UrlToFilename(s string) (string, bool) {
+	u, err := url.ParseRequestURI(s)
+
+	if err != nil {
+		return filepath.FromSlash(s), false
+	}
+
+	p := u.Path
+
+	if p == "" {
+		p, _ = url.QueryUnescape(u.Opaque)
+		return filepath.FromSlash(p), true
+	}
+
+	p = filepath.FromSlash(p)
+
+	if u.Host != "" {
+		// C:\data\file.txt
+		p = strings.ToUpper(u.Host) + ":" + p
+	}
+
+	return p, true
+}
diff --git a/config/configLoader.go b/config/configLoader.go
index 008ebbfae1c..d25546cdbc7 100644
--- a/config/configLoader.go
+++ b/config/configLoader.go
@@ -59,7 +59,7 @@ func FromConfigString(config, configType string) (Provider, error) {
 func FromFile(fs afero.Fs, filename string) (Provider, error) {
 	m, err := loadConfigFromFile(fs, filename)
 	if err != nil {
-		return nil, herrors.NewFileErrorFromFile(err, filename, filename, fs, nil)
+		return nil, herrors.NewFileErrorFromFile(err, filename, fs, nil)
 	}
 	return NewFrom(m), nil
 }
diff --git a/docs/content/en/hugo-pipes/postcss.md b/docs/content/en/hugo-pipes/postcss.md
index fddc7e9cf10..154f97f0bad 100755
--- a/docs/content/en/hugo-pipes/postcss.md
+++ b/docs/content/en/hugo-pipes/postcss.md
@@ -44,6 +44,10 @@ URL imports (e.g. `@import url('https://fonts.googleapis.com/css?family=Open+San
 Note that this import routine does not care about the CSS spec, so you can have @import anywhere in the file.
 Hugo will look for imports relative to the module mount and will respect theme overrides.
 
+skipInlineImportsNotFound [bool] {{< new-in "0.99.0" >}}
+
+Before Hugo 0.99.0 when `inlineImports` was enabled and we failed to resolve an import, we logged it as a warning. We now fail the build. If you have regular CSS imports in your CSS that you want to preserve, you can either use imports with URL or media queries (Hugo does not try to resolve those) or set `skipInlineImportsNotFound` to true.
+
 _If no configuration file is used:_
 
 use [string]
diff --git a/go.mod b/go.mod
index 0bd9cf233e2..45fc1ad6122 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/bep/gitmap v1.1.2
 	github.com/bep/goat v0.5.0
 	github.com/bep/godartsass v0.14.0
-	github.com/bep/golibsass v1.0.0
+	github.com/bep/golibsass v1.1.0
 	github.com/bep/gowebp v0.1.0
 	github.com/bep/overlayfs v0.6.0
 	github.com/bep/tmc v0.5.1
diff --git a/go.sum b/go.sum
index d40817e3616..506f3244291 100644
--- a/go.sum
+++ b/go.sum
@@ -175,6 +175,8 @@ github.com/bep/godartsass v0.14.0 h1:pPb6XkpyDEppS+wK0veh7OXDQc4xzOJI9Qcjb743UeQ
 github.com/bep/godartsass v0.14.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8=
 github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw=
 github.com/bep/golibsass v1.0.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
+github.com/bep/golibsass v1.1.0 h1:pjtXr00IJZZaOdfryNa9wARTB3Q0BmxC3/V1KNcgyTw=
+github.com/bep/golibsass v1.1.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
 github.com/bep/gowebp v0.1.0 h1:4/iQpfnxHyXs3x/aTxMMdOpLEQQhFmF6G7EieWPTQyo=
 github.com/bep/gowebp v0.1.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
 github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo=
diff --git a/hugofs/fileinfo.go b/hugofs/fileinfo.go
index d923d632dfa..1d46a74642c 100644
--- a/hugofs/fileinfo.go
+++ b/hugofs/fileinfo.go
@@ -139,6 +139,17 @@ type fileInfoMeta struct {
 	m *FileMeta
 }
 
+type filenameProvider interface {
+	Filename() string
+}
+
+var _ filenameProvider = (*fileInfoMeta)(nil)
+
+// Filename returns the full filename.
+func (fi *fileInfoMeta) Filename() string {
+	return fi.m.Filename
+}
+
 // Name returns the file's name. Note that we follow symlinks,
 // if supported by the file system, and the Name given here will be the
 // name of the symlink, which is what Hugo needs in all situations.
diff --git a/hugolib/config.go b/hugolib/config.go
index 1e7cf6b0651..e63d6da4ec1 100644
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -511,5 +511,5 @@ func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err er
 }
 
 func (l configLoader) wrapFileError(err error, filename string) error {
-	return herrors.NewFileErrorFromFile(err, filename, filename, l.Fs, nil)
+	return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil)
 }
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
index 4026f58d3f9..6be26d60e99 100644
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -968,7 +968,7 @@ func (h *HugoSites) errWithFileContext(err error, f source.File) error {
 	}
 	realFilename := fim.Meta().Filename
 
-	return herrors.NewFileErrorFromFile(err, realFilename, realFilename, h.SourceSpec.Fs.Source, nil)
+	return herrors.NewFileErrorFromFile(err, realFilename, h.SourceSpec.Fs.Source, nil)
 
 }
 
diff --git a/hugolib/page.go b/hugolib/page.go
index e9f9371052a..19d9695326a 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -588,7 +588,7 @@ func (p *pageState) wrapError(err error) error {
 		}
 	}
 
-	return herrors.NewFileErrorFromFile(err, filename, filename, p.s.SourceSpec.Fs.Source, herrors.NopLineMatcher)
+	return herrors.NewFileErrorFromFile(err, filename, p.s.SourceSpec.Fs.Source, herrors.NopLineMatcher)
 
 }
 
@@ -788,7 +788,7 @@ func (p *pageState) outputFormat() (f output.Format) {
 
 func (p *pageState) parseError(err error, input []byte, offset int) error {
 	pos := p.posFromInput(input, offset)
-	return herrors.NewFileError(p.File().Filename(), err).UpdatePosition(pos)
+	return herrors.NewFileErrorFromName(err, p.File().Filename()).UpdatePosition(pos)
 }
 
 func (p *pageState) pathOrTitle() string {
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index f8cda6b8da1..45d5f1ca8dd 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -298,7 +298,7 @@ func renderShortcode(
 			var err error
 			tmpl, err = s.TextTmpl().Parse(templName, templStr)
 			if err != nil {
-				fe := herrors.NewFileError(p.File().Filename(), err)
+				fe := herrors.NewFileErrorFromName(err, p.File().Filename())
 				pos := fe.Position()
 				pos.LineNumber += p.posOffset(sc.pos).LineNumber
 				fe = fe.UpdatePosition(pos)
@@ -391,7 +391,7 @@ func renderShortcode(
 	result, err := renderShortcodeWithPage(s.Tmpl(), tmpl, data)
 
 	if err != nil && sc.isInline {
-		fe := herrors.NewFileError(p.File().Filename(), err)
+		fe := herrors.NewFileErrorFromName(err, p.File().Filename())
 		pos := fe.Position()
 		pos.LineNumber += p.posOffset(sc.pos).LineNumber
 		fe = fe.UpdatePosition(pos)
diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go
index 4c0934badea..d9d334567f9 100644
--- a/langs/i18n/translationProvider.go
+++ b/langs/i18n/translationProvider.go
@@ -138,6 +138,6 @@ func errWithFileContext(inerr error, r source.File) error {
 	}
 	defer f.Close()
 
-	return herrors.NewFileError(realFilename, inerr).UpdateContent(f, nil)
+	return herrors.NewFileErrorFromName(inerr, realFilename).UpdateContent(f, nil)
 
 }
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index 5ebc2a9ef73..d56667ceba8 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -130,7 +130,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
 	ctx.AddIdentity(cr)
 
 	if err != nil {
-		return ast.WalkContinue, herrors.NewFileErrorFromPos(cbctx.createPos(), err)
+		return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
 	}
 
 	return ast.WalkContinue, nil
diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go
index fe0581734ee..7a76b8eea0f 100644
--- a/parser/metadecoders/decoder.go
+++ b/parser/metadecoders/decoder.go
@@ -260,7 +260,7 @@ func (d Decoder) unmarshalORG(data []byte, v any) error {
 }
 
 func toFileError(f Format, data []byte, err error) error {
-	return herrors.NewFileError(fmt.Sprintf("_stream.%s", f), err).UpdateContent(bytes.NewReader(data), nil)
+	return herrors.NewFileErrorFromName(err, fmt.Sprintf("_stream.%s", f)).UpdateContent(bytes.NewReader(data), nil)
 }
 
 // stringifyMapKeys recurses into in and changes all instances of
diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go
index 00012b4e8b7..23e28f6754d 100644
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -165,7 +165,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
 
 			if err == nil {
 				fe := herrors.
-					NewFileError(path, errors.New(errorMessage)).
+					NewFileErrorFromName(errors.New(errorMessage), path).
 					UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}).
 					UpdateContent(f, nil)
 
diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go
index a5c86df6fb0..ed08f7202fe 100644
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/gohugoio/hugo/common/collections"
 	"github.com/gohugoio/hugo/common/hexec"
+	"github.com/gohugoio/hugo/common/text"
 	"github.com/gohugoio/hugo/hugofs"
 
 	"github.com/gohugoio/hugo/common/hugo"
@@ -49,9 +50,10 @@ import (
 
 const importIdentifier = "@import"
 
-var cssSyntaxErrorRe = regexp.MustCompile(`> (\d+) \|`)
-
-var shouldImportRe = regexp.MustCompile(`^@import ["'].*["'];?\s*(/\*.*\*/)?$`)
+var (
+	cssSyntaxErrorRe = regexp.MustCompile(`> (\d+) \|`)
+	shouldImportRe   = regexp.MustCompile(`^@import ["'].*["'];?\s*(/\*.*\*/)?$`)
+)
 
 // New creates a new Client with the given specification.
 func New(rs *resources.Spec) *Client {
@@ -100,6 +102,12 @@ type Options struct {
 	// so you can have @import anywhere in the file.
 	InlineImports bool
 
+	// When InlineImports is enabled, we fail the build if an import cannot be resolved.
+	// You can enable this to allow the build to continue and leave the import statement in place.
+	// Note that the inline importer does not process url location or imports with media queries,
+	// so those will be left as-is even without enabling this option.
+	SkipInlineImportsNotFound bool
+
 	// Options for when not using a config file
 	Use         string // List of postcss plugins to use
 	Parser      string //  Custom postcss parser
@@ -204,6 +212,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
 	imp := newImportResolver(
 		ctx.From,
 		ctx.InPath,
+		t.options,
 		t.rs.Assets.Fs, t.rs.Logger,
 	)
 
@@ -239,6 +248,7 @@ type fileOffset struct {
 type importResolver struct {
 	r      io.Reader
 	inPath string
+	opts   Options
 
 	contentSeen map[string]bool
 	linemap     map[int]fileOffset
@@ -246,12 +256,13 @@ type importResolver struct {
 	logger      loggers.Logger
 }
 
-func newImportResolver(r io.Reader, inPath string, fs afero.Fs, logger loggers.Logger) *importResolver {
+func newImportResolver(r io.Reader, inPath string, opts Options, fs afero.Fs, logger loggers.Logger) *importResolver {
 	return &importResolver{
 		r:      r,
 		inPath: inPath,
 		fs:     fs, logger: logger,
 		linemap: make(map[int]fileOffset), contentSeen: make(map[string]bool),
+		opts: opts,
 	}
 }
 
@@ -282,21 +293,32 @@ func (imp *importResolver) importRecursive(
 	i := 0
 	for offset, line := range lines {
 		i++
-		line = strings.TrimSpace(line)
+		lineTrimmed := strings.TrimSpace(line)
+		column := strings.Index(line, lineTrimmed)
+		line = lineTrimmed
 
 		if !imp.shouldImport(line) {
 			trackLine(i, offset, line)
 		} else {
-			i--
 			path := strings.Trim(strings.TrimPrefix(line, importIdentifier), " \"';")
 			filename := filepath.Join(basePath, path)
 			importContent, hash := imp.contentHash(filename)
+
 			if importContent == nil {
-				trackLine(i, offset, "ERROR")
-				imp.logger.Warnf("postcss: Failed to resolve CSS @import in %q for path %q", inPath, filename)
-				continue
+				if imp.opts.SkipInlineImportsNotFound {
+					trackLine(i, offset, line)
+					continue
+				}
+				pos := text.Position{
+					Filename:     inPath,
+					LineNumber:   offset + 1,
+					ColumnNumber: column + 1,
+				}
+				return 0, "", herrors.NewFileErrorFromFileInPos(fmt.Errorf("failed to resolve CSS @import \"%s\"", filename), pos, imp.fs, nil)
 			}
 
+			i--
+
 			if imp.contentSeen[hash] {
 				i++
 				// Just replace the line with an empty string.
@@ -399,7 +421,7 @@ func (imp *importResolver) toFileError(output string) error {
 	}
 	defer f.Close()
 
-	ferr := herrors.NewFileError(realFilename, inErr)
+	ferr := herrors.NewFileErrorFromName(inErr, realFilename)
 	pos := ferr.Position()
 	pos.LineNumber = file.Offset + 1
 	return ferr.UpdatePosition(pos).UpdateContent(f, nil)
diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go
index fb794f4e8c7..7c3a7ecbab7 100644
--- a/resources/resource_transformers/tocss/dartsass/client.go
+++ b/resources/resource_transformers/tocss/dartsass/client.go
@@ -20,7 +20,9 @@ import (
 	"io"
 	"strings"
 
+	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/helpers"
+	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/hugolib/filesystems"
 	"github.com/gohugoio/hugo/resources"
 	"github.com/gohugoio/hugo/resources/resource"
@@ -33,6 +35,10 @@ import (
 // used as part of the cache key.
 const transformationName = "tocss-dart"
 
+// See https://github.com/sass/dart-sass-embedded/issues/24
+// Note: This prefix must be all lower case.
+const dartSassStdinPrefix = "hugostdin:"
+
 func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) {
 	if !Supports() {
 		return &Client{dartSassNotAvailable: true}, nil
@@ -44,7 +50,7 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error)
 
 	transpiler, err := godartsass.Start(godartsass.Options{
 		LogEventHandler: func(event godartsass.LogEvent) {
-			message := strings.ReplaceAll(event.Message, stdinPrefix, "")
+			message := strings.ReplaceAll(event.Message, dartSassStdinPrefix, "")
 			switch event.Type {
 			case godartsass.LogEventTypeDebug:
 				// Log as Info for now, we may adjust this if it gets too chatty.
@@ -94,7 +100,7 @@ func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result,
 		if err.Error() == "unexpected EOF" {
 			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)
 		}
-		return res, err
+		return res, herrors.NewFileErrorFromFileInErr(err, hugofs.Os, herrors.OffsetMatcher)
 	}
 
 	return res, err
diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go
index be4367b2f76..9d17d3bccf8 100644
--- a/resources/resource_transformers/tocss/dartsass/transform.go
+++ b/resources/resource_transformers/tocss/dartsass/transform.go
@@ -16,13 +16,12 @@ package dartsass
 import (
 	"fmt"
 	"io"
-	"net/url"
 	"path"
 	"path/filepath"
 	"strings"
 
-	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/hexec"
+	"github.com/gohugoio/hugo/common/paths"
 	"github.com/gohugoio/hugo/htesting"
 	"github.com/gohugoio/hugo/media"
 
@@ -38,9 +37,6 @@ import (
 )
 
 const (
-	// See https://github.com/sass/dart-sass-embedded/issues/24
-	// Note: This prefix must be all lower case.
-	stdinPrefix                = "hugostdin:"
 	dartSassEmbeddedBinaryName = "dart-sass-embedded"
 )
 
@@ -76,7 +72,7 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
 	}
 
 	baseDir := path.Dir(ctx.SourcePath)
-	filename := stdinPrefix
+	filename := dartSassStdinPrefix
 
 	if ctx.SourcePath != "" {
 		filename += t.c.sfs.RealFilename(ctx.SourcePath)
@@ -108,26 +104,6 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
 
 	res, err := t.c.toCSS(args, ctx.From)
 	if err != nil {
-		if sassErr, ok := err.(godartsass.SassError); ok {
-			start := sassErr.Span.Start
-			context := strings.TrimSpace(sassErr.Span.Context)
-			filename, _ := urlToFilename(sassErr.Span.Url)
-			if strings.HasPrefix(filename, stdinPrefix) {
-				filename = filename[len(stdinPrefix):]
-			}
-
-			offsetMatcher := func(m herrors.LineMatcher) int {
-				if m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context) {
-					// We found the line, but return 0 to signal that we want to determine
-					// the column from the error.
-					return 0
-				}
-				return -1
-			}
-
-			return herrors.NewFileErrorFromFile(sassErr, filename, filename, hugofs.Os, offsetMatcher)
-
-		}
 		return err
 	}
 
@@ -154,7 +130,7 @@ type importResolver struct {
 }
 
 func (t importResolver) CanonicalizeURL(url string) (string, error) {
-	filePath, isURL := urlToFilename(url)
+	filePath, isURL := paths.UrlToFilename(url)
 	var prevDir string
 	var pathDir string
 	if isURL {
@@ -200,23 +176,7 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
 }
 
 func (t importResolver) Load(url string) (string, error) {
-	filename, _ := urlToFilename(url)
+	filename, _ := paths.UrlToFilename(url)
 	b, err := afero.ReadFile(hugofs.Os, filename)
 	return string(b), err
 }
-
-// TODO(bep) add tests
-func urlToFilename(urls string) (string, bool) {
-	u, err := url.ParseRequestURI(urls)
-	if err != nil {
-		return filepath.FromSlash(urls), false
-	}
-	p := filepath.FromSlash(u.Path)
-
-	if u.Host != "" {
-		// C:\data\file.txt
-		p = strings.ToUpper(u.Host) + ":" + p
-	}
-
-	return p, true
-}
diff --git a/resources/resource_transformers/tocss/scss/client_extended.go b/resources/resource_transformers/tocss/scss/client_extended.go
index c9b347c4cf4..bfee39499d1 100644
--- a/resources/resource_transformers/tocss/scss/client_extended.go
+++ b/resources/resource_transformers/tocss/scss/client_extended.go
@@ -47,6 +47,7 @@ func (c *Client) ToCSS(res resources.ResourceTransformer, opts Options) (resourc
 	}
 
 	return res.Transform(&toCSSTransformation{c: c, options: internalOptions})
+
 }
 
 type toCSSTransformation struct {
diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go
index f9f4786f572..57ac1671155 100644
--- a/resources/resource_transformers/tocss/scss/tocss.go
+++ b/resources/resource_transformers/tocss/scss/tocss.go
@@ -20,10 +20,13 @@ import (
 	"fmt"
 	"io"
 	"path"
+
 	"path/filepath"
 	"strings"
 
 	"github.com/bep/golibsass/libsass"
+	"github.com/bep/golibsass/libsass/libsasserrors"
+	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/media"
@@ -136,7 +139,14 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx
 
 	res, err := t.c.toCSS(options.to, ctx.To, ctx.From)
 	if err != nil {
-		return err
+		if sasserr, ok := err.(libsasserrors.Error); ok {
+			if sasserr.File == "stdin" && ctx.SourcePath != "" {
+				sasserr.File = t.c.sfs.RealFilename(ctx.SourcePath)
+				err = sasserr
+			}
+		}
+		return herrors.NewFileErrorFromFileInErr(err, hugofs.Os, nil)
+
 	}
 
 	if options.from.EnableSourceMap && res.SourceMapContent != "" {
@@ -180,7 +190,7 @@ func (c *Client) toCSS(options libsass.Options, dst io.Writer, src io.Reader) (l
 
 	res, err = transpiler.Execute(in)
 	if err != nil {
-		return res, fmt.Errorf("SCSS processing failed: %w", err)
+		return res, err
 	}
 
 	out := res.CSS
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 25f7957fd4f..c79605cbceb 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -554,7 +554,7 @@ func (t *templateHandler) addFileContext(templ tpl.Template, inerr error) error
 		}
 		defer f.Close()
 
-		fe := herrors.NewFileError(info.realFilename, inErr)
+		fe := herrors.NewFileErrorFromName(inErr, info.realFilename)
 		fe.UpdateContent(f, lineMatcher)
 
 		if !fe.ErrorContext().Position.IsValid() {
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
index 751b4ddbc96..ac8a72df5e0 100644
--- a/tpl/tplimpl/template_errors.go
+++ b/tpl/tplimpl/template_errors.go
@@ -53,7 +53,7 @@ func (t templateInfo) resolveType() templateType {
 
 func (info templateInfo) errWithFileContext(what string, err error) error {
 	err = fmt.Errorf(what+": %w", err)
-	fe := herrors.NewFileError(info.realFilename, err)
+	fe := herrors.NewFileErrorFromName(err, info.realFilename)
 	f, err := info.fs.Open(info.filename)
 	if err != nil {
 		return err
diff --git a/transform/chain.go b/transform/chain.go
index 2d70b93562c..a5f042d9666 100644
--- a/transform/chain.go
+++ b/transform/chain.go
@@ -113,9 +113,9 @@ func (c *Chain) Apply(to io.Writer, from io.Reader) error {
 				filename = tempfile.Name()
 				defer tempfile.Close()
 				_, _ = io.Copy(tempfile, fb.from)
-				return herrors.NewFileErrorFromFile(err, filename, filename, hugofs.Os, nil)
+				return herrors.NewFileErrorFromFile(err, filename, hugofs.Os, nil)
 			}
-			return herrors.NewFileError(filename, err).UpdateContent(fb.from, nil)
+			return herrors.NewFileErrorFromName(err, filename).UpdateContent(fb.from, nil)
 
 		}
 	}


Test patch:
diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go
index 41e244109a4..0b260a2550e 100644
--- a/common/herrors/file_error_test.go
+++ b/common/herrors/file_error_test.go
@@ -30,7 +30,7 @@ func TestNewFileError(t *testing.T) {
 
 	c := qt.New(t)
 
-	fe := NewFileError("foo.html", errors.New("bar"))
+	fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
 	c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
 
 	lines := ""
@@ -70,7 +70,7 @@ func TestNewFileErrorExtractFromMessage(t *testing.T) {
 		{errors.New(`execute of template failed: template: index.html:2:5: executing "index.html" at <partial "foo.html" .>: error calling partial: "/layouts/partials/foo.html:3:6": execute of template failed: template: partials/foo.html:3:6: executing "partials/foo.html" at <.ThisDoesNotExist>: can't evaluate field ThisDoesNotExist in type *hugolib.pageStat`), 0, 2, 5},
 	} {
 
-		got := NewFileError("test.txt", test.in)
+		got := NewFileErrorFromName(test.in, "test.txt")
 
 		errMsg := qt.Commentf("[%d][%T]", i, got)
 
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index c3fb9fffc1c..0ec202f89b4 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -168,8 +168,9 @@ func (s *IntegrationTestBuilder) destinationExists(filename string) bool {
 	return b
 }
 
-func (s *IntegrationTestBuilder) AssertIsFileError(err error) {
+func (s *IntegrationTestBuilder) AssertIsFileError(err error) herrors.FileError {
 	s.Assert(err, qt.ErrorAs, new(herrors.FileError))
+	return herrors.UnwrapFileError(err)
 }
 
 func (s *IntegrationTestBuilder) AssertRenderCountContent(count int) {
diff --git a/resources/resource_transformers/postcss/integration_test.go b/resources/resource_transformers/postcss/integration_test.go
index 4101818be5e..fdebcc52c37 100644
--- a/resources/resource_transformers/postcss/integration_test.go
+++ b/resources/resource_transformers/postcss/integration_test.go
@@ -48,7 +48,7 @@ class-in-b {
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
-@import "components/all.css";
+  @import "components/all.css";
 h1 {
 	@apply text-2xl font-bold;
 }
@@ -140,3 +140,49 @@ func TestTransformPostCSSError(t *testing.T) {
 	c.Assert(err.Error(), qt.Contains, "a.css:4:2")
 
 }
+
+// #9895
+func TestTransformPostCSSImportError(t *testing.T) {
+	if !htesting.IsCI() {
+		t.Skip("Skip long running test when running locally")
+	}
+
+	c := qt.New(t)
+
+	s, err := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:               c,
+			NeedsOsFS:       true,
+			NeedsNpmInstall: true,
+			LogLevel:        jww.LevelInfo,
+			TxtarString:     strings.ReplaceAll(postCSSIntegrationTestFiles, `@import "components/all.css";`, `@import "components/doesnotexist.css";`),
+		}).BuildE()
+
+	s.AssertIsFileError(err)
+	c.Assert(err.Error(), qt.Contains, "styles.css:4:3")
+	c.Assert(err.Error(), qt.Contains, filepath.FromSlash(`failed to resolve CSS @import "css/components/doesnotexist.css"`))
+
+}
+
+func TestTransformPostCSSImporSkipInlineImportsNotFound(t *testing.T) {
+	if !htesting.IsCI() {
+		t.Skip("Skip long running test when running locally")
+	}
+
+	c := qt.New(t)
+
+	files := strings.ReplaceAll(postCSSIntegrationTestFiles, `@import "components/all.css";`, `@import "components/doesnotexist.css";`)
+	files = strings.ReplaceAll(files, `{{ $options := dict "inlineImports" true }}`, `{{ $options := dict "inlineImports" true "skipInlineImportsNotFound" true }}`)
+
+	s := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:               c,
+			NeedsOsFS:       true,
+			NeedsNpmInstall: true,
+			LogLevel:        jww.LevelInfo,
+			TxtarString:     files,
+		}).Build()
+
+	s.AssertFileContent("public/css/styles.css", `@import "components/doesnotexist.css";`)
+
+}
diff --git a/resources/resource_transformers/postcss/postcss_test.go b/resources/resource_transformers/postcss/postcss_test.go
index 4548bca9866..f5d49300ac2 100644
--- a/resources/resource_transformers/postcss/postcss_test.go
+++ b/resources/resource_transformers/postcss/postcss_test.go
@@ -60,6 +60,7 @@ func TestShouldImport(t *testing.T) {
 		{input: `@import 'navigation.css';`, expect: true},
 		{input: `@import url("navigation.css");`, expect: false},
 		{input: `@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i,800,800i&display=swap');`, expect: false},
+		{input: `@import "printstyle.css" print;`, expect: false},
 	} {
 		c.Assert(imp.shouldImport(test.input), qt.Equals, test.expect)
 	}
@@ -88,12 +89,12 @@ A_STYLE2
 @import "b.css";
 LOCAL_STYLE
 @import "c.css";
-@import "e.css";
-@import "missing.css";`)
+@import "e.css";`)
 
 	imp := newImportResolver(
 		mainStyles,
 		"styles.css",
+		Options{},
 		fs, loggers.NewErrorLogger(),
 	)
 
@@ -108,8 +109,7 @@ C_STYLE
 A_STYLE1
 A_STYLE2
 LOCAL_STYLE
-E_STYLE
-@import "missing.css";`)
+E_STYLE`)
 
 	dline := imp.linemap[3]
 	c.Assert(dline, qt.DeepEquals, fileOffset{
@@ -151,6 +151,7 @@ LOCAL_STYLE
 		imp := newImportResolver(
 			strings.NewReader(mainStyles),
 			"styles.css",
+			Options{},
 			fs, logger,
 		)
 
diff --git a/resources/resource_transformers/tocss/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go
index a1ac1d59f13..c127057a5a4 100644
--- a/resources/resource_transformers/tocss/dartsass/integration_test.go
+++ b/resources/resource_transformers/tocss/dartsass/integration_test.go
@@ -14,8 +14,10 @@
 package dartsass_test
 
 import (
+	"strings"
 	"testing"
 
+	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/hugolib"
 	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
 	jww "github.com/spf13/jwalterweatherman"
@@ -196,3 +198,76 @@ T1: {{ $r.Content }}
 	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:1:0: bar`)
 
 }
+
+func TestTransformErrors(t *testing.T) {
+	if !dartsass.Supports() {
+		t.Skip()
+	}
+
+	c := qt.New(t)
+
+	const filesTemplate = `
+-- config.toml --
+-- assets/scss/components/_foo.scss --
+/* comment line 1 */
+$foocolor: #ccc;
+
+foo {
+	color: $foocolor;
+}
+-- assets/scss/main.scss --
+/* comment line 1 */
+/* comment line 2 */
+@import "components/foo";
+/* comment line 4 */
+
+  $maincolor: #eee;
+
+body {
+	color: $maincolor;
+}
+
+-- layouts/index.html --
+{{ $cssOpts := dict "transpiler" "dartsass" }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts  | minify  }}
+T1: {{ $r.Content }}
+
+	`
+
+	c.Run("error in main", func(c *qt.C) {
+		b, err := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           c,
+				TxtarString: strings.Replace(filesTemplate, "$maincolor: #eee;", "$maincolor #eee;", 1),
+				NeedsOsFS:   true,
+			}).BuildE()
+
+		b.Assert(err, qt.IsNotNil)
+		b.Assert(err.Error(), qt.Contains, `main.scss:8:13":`)
+		b.Assert(err.Error(), qt.Contains, `: expected ":".`)
+		fe := b.AssertIsFileError(err)
+		b.Assert(fe.ErrorContext(), qt.IsNotNil)
+		b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"  $maincolor #eee;", "", "body {", "\tcolor: $maincolor;", "}"})
+		b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
+
+	})
+
+	c.Run("error in import", func(c *qt.C) {
+		b, err := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           c,
+				TxtarString: strings.Replace(filesTemplate, "$foocolor: #ccc;", "$foocolor #ccc;", 1),
+				NeedsOsFS:   true,
+			}).BuildE()
+
+		b.Assert(err, qt.IsNotNil)
+		b.Assert(err.Error(), qt.Contains, `_foo.scss:2:10":`)
+		b.Assert(err.Error(), qt.Contains, `: expected ":".`)
+		fe := b.AssertIsFileError(err)
+		b.Assert(fe.ErrorContext(), qt.IsNotNil)
+		b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 1 */", "$foocolor #ccc;", "", "foo {"})
+		b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
+
+	})
+
+}
diff --git a/resources/resource_transformers/tocss/scss/integration_test.go b/resources/resource_transformers/tocss/scss/integration_test.go
index cbc7e192bc2..72c0fd988e9 100644
--- a/resources/resource_transformers/tocss/scss/integration_test.go
+++ b/resources/resource_transformers/tocss/scss/integration_test.go
@@ -14,6 +14,8 @@
 package scss_test
 
 import (
+	"path/filepath"
+	"strings"
 	"testing"
 
 	qt "github.com/frankban/quicktest"
@@ -133,7 +135,7 @@ moo {
 -- config.toml --
 theme = 'mytheme'
 -- layouts/index.html --
-{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) "transpiler" "dartsass" ) }}
+{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }}
 {{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts  | minify  }}
 T1: {{ $r.Content }}
 -- themes/mytheme/assets/scss/components/_boo.scss --
@@ -171,3 +173,75 @@ zoo {
 
 	b.AssertFileContent("public/index.html", `T1: moo{color:#ccc}boo{color:green}zoo{color:pink}`)
 }
+
+func TestTransformErrors(t *testing.T) {
+	if !scss.Supports() {
+		t.Skip()
+	}
+
+	c := qt.New(t)
+
+	const filesTemplate = `
+-- config.toml --
+theme = 'mytheme'
+-- assets/scss/components/_foo.scss --
+/* comment line 1 */
+$foocolor: #ccc;
+
+foo {
+	color: $foocolor;
+}
+-- themes/mytheme/assets/scss/main.scss --
+/* comment line 1 */
+/* comment line 2 */
+@import "components/foo";
+/* comment line 4 */
+
+$maincolor: #eee;
+
+body {
+	color: $maincolor;
+}
+
+-- layouts/index.html --
+{{ $cssOpts := dict }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts  | minify  }}
+T1: {{ $r.Content }}
+
+	`
+
+	c.Run("error in main", func(c *qt.C) {
+		b, err := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           c,
+				TxtarString: strings.Replace(filesTemplate, "$maincolor: #eee;", "$maincolor #eee;", 1),
+				NeedsOsFS:   true,
+			}).BuildE()
+
+		b.Assert(err, qt.IsNotNil)
+		b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`themes/mytheme/assets/scss/main.scss:6:1": expected ':' after $maincolor in assignment statement`))
+		fe := b.AssertIsFileError(err)
+		b.Assert(fe.ErrorContext(), qt.IsNotNil)
+		b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 4 */", "", "$maincolor #eee;", "", "body {"})
+		b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
+
+	})
+
+	c.Run("error in import", func(c *qt.C) {
+		b, err := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           c,
+				TxtarString: strings.Replace(filesTemplate, "$foocolor: #ccc;", "$foocolor #ccc;", 1),
+				NeedsOsFS:   true,
+			}).BuildE()
+
+		b.Assert(err, qt.IsNotNil)
+		b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`assets/scss/components/_foo.scss:2:1": expected ':' after $foocolor in assignment statement`))
+		fe := b.AssertIsFileError(err)
+		b.Assert(fe.ErrorContext(), qt.IsNotNil)
+		b.Assert(fe.ErrorContext().Lines, qt.DeepEquals, []string{"/* comment line 1 */", "$foocolor #ccc;", "", "foo {"})
+		b.Assert(fe.ErrorContext().ChromaLexer, qt.Equals, "scss")
+
+	})
+
+}