diff options
27 files changed, 600 insertions, 204 deletions
diff --git a/commands/hugo.go b/commands/hugo.go index ada1e1cef..8dfd4b4bd 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -736,10 +736,7 @@ func (c *commandeer) buildSites(noBuildLock bool) (err error) { func (c *commandeer) handleBuildErr(err error, msg string) { c.buildErr = err - - c.logger.Errorln(msg + ":\n") - c.logger.Errorln(helpers.FirstUpper(err.Error())) - + c.logger.Errorln(msg + ": " + cleanErrorLog(err.Error())) } func (c *commandeer) rebuildSites(events []fsnotify.Event) error { diff --git a/commands/server.go b/commands/server.go index 27b12cb32..036fa9eaa 100644 --- a/commands/server.go +++ b/commands/server.go @@ -469,14 +469,26 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string return mu, listener, u.String(), endpoint, nil } -var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) +var ( + logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) + logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`) + logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`) +) func removeErrorPrefixFromLog(content string) string { return logErrorRe.ReplaceAllLiteralString(content, "") } + +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. +) + func cleanErrorLog(content string) string { - content = strings.ReplaceAll(content, "Rebuild failed:\n", "") - content = strings.ReplaceAll(content, "\n", "") + content = strings.ReplaceAll(content, "\n", " ") + content = logReplacer.Replace(content) + content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "") + content = logDuplicateTemplateParseRe.ReplaceAllString(content, "") seen := make(map[string]bool) parts := strings.Split(content, ": ") keep := make([]string, 0, len(parts)) @@ -515,7 +527,7 @@ func (c *commandeer) serve(s *serverCmd) error { c: c, s: s, errorTemplate: func(ctx any) (io.Reader, error) { - templ, found := c.hugo().Tmpl().Lookup("server/error.html") + templ, found := c.hugo().Tmpl().Lookup("_server/error.html") if !found { panic("template server/error.html not found") } diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index 5d2c10c04..0f22f545f 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -34,11 +34,22 @@ type LineMatcher struct { } // LineMatcherFn is used to match a line with an error. -type LineMatcherFn func(m LineMatcher) bool +// It returns the column number or 0 if the line was found, but column could not be determinde. Returns -1 if no line match. +type LineMatcherFn func(m LineMatcher) int // SimpleLineMatcher simply matches by line number. -var SimpleLineMatcher = func(m LineMatcher) bool { - return m.Position.LineNumber == m.LineNumber +var SimpleLineMatcher = func(m LineMatcher) int { + if m.Position.LineNumber == m.LineNumber { + // We found the line, but don't know the column. + return 0 + } + return -1 +} + +// NopLineMatcher is a matcher that always returns 1. +// This will effectively give line 1, column 1. +var NopLineMatcher = func(m LineMatcher) int { + return 1 } // ErrorContext contains contextual information about an error. This will @@ -52,6 +63,10 @@ type ErrorContext struct { // The position of the error in the Lines above. 0 based. LinesPos int + // The position of the content in the file. Note that this may be different from the error's position set + // in FileError. + Position text.Position + // The lexer to use for syntax highlighting. // https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages ChromaLexer string @@ -78,31 +93,24 @@ func chromaLexerFromFilename(filename string) string { return chromaLexerFromType(ext) } -func locateErrorInString(src string, matcher LineMatcherFn) (*ErrorContext, text.Position) { +func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext { return locateError(strings.NewReader(src), &fileError{}, matcher) } -func locateError(r io.Reader, le FileError, matches LineMatcherFn) (*ErrorContext, text.Position) { +func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext { if le == nil { panic("must provide an error") } - errCtx := &ErrorContext{LinesPos: -1} - pos := text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1} + ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}} b, err := ioutil.ReadAll(r) if err != nil { - return errCtx, pos + return ectx } - lepos := le.Position() - lines := strings.Split(string(b), "\n") - if lepos.ColumnNumber >= 0 { - pos.ColumnNumber = lepos.ColumnNumber - } - lineNo := 0 posBytes := 0 @@ -115,34 +123,36 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) (*ErrorContex Offset: posBytes, Line: line, } - if errCtx.LinesPos == -1 && matches(m) { - pos.LineNumber = lineNo + v := matches(m) + if ectx.LinesPos == -1 && v != -1 { + ectx.Position.LineNumber = lineNo + ectx.Position.ColumnNumber = v break } posBytes += len(line) } - if pos.LineNumber != -1 { - low := pos.LineNumber - 3 + if ectx.Position.LineNumber > 0 { + low := ectx.Position.LineNumber - 3 if low < 0 { low = 0 } - if pos.LineNumber > 2 { - errCtx.LinesPos = 2 + if ectx.Position.LineNumber > 2 { + ectx.LinesPos = 2 } else { - errCtx.LinesPos = pos.LineNumber - 1 + ectx.LinesPos = ectx.Position.LineNumber - 1 } - high := pos.LineNumber + 2 + high := ectx.Position.LineNumber + 2 if high > len(lines) { high = len(lines) } - errCtx.Lines = lines[low:high] + ectx.Lines = lines[low:high] } - return errCtx, pos + return ectx } diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go index 10b016fa8..6135657d8 100644 --- a/common/herrors/error_locator_test.go +++ b/common/herrors/error_locator_test.go @@ -24,8 +24,11 @@ import ( func TestErrorLocator(t *testing.T) { c := qt.New(t) - lineMatcher := func(m LineMatcher) bool { - return strings.Contains(m.Line, "THEONE") + lineMatcher := func(m LineMatcher) int { + if strings.Contains(m.Line, "THEONE") { + return 1 + } + return -1 } lines := `LINE 1 @@ -38,23 +41,25 @@ LINE 7 LINE 8 ` - location, pos := locateErrorInString(lines, lineMatcher) + location := locateErrorInString(lines, lineMatcher) + pos := location.Position c.Assert(location.Lines, qt.DeepEquals, []string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}) c.Assert(pos.LineNumber, qt.Equals, 5) c.Assert(location.LinesPos, qt.Equals, 2) locate := func(s string, m LineMatcherFn) *ErrorContext { - ctx, _ := locateErrorInString(s, m) + ctx := locateErrorInString(s, m) return ctx } c.Assert(locate(`This is THEONE`, lineMatcher).Lines, qt.DeepEquals, []string{"This is THEONE"}) - location, pos = locateErrorInString(`L1 + location = locateErrorInString(`L1 This is THEONE L2 `, lineMatcher) + pos = location.Position c.Assert(pos.LineNumber, qt.Equals, 2) c.Assert(location.LinesPos, qt.Equals, 1) c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This is THEONE", "L2", ""}) @@ -78,16 +83,20 @@ This THEONE c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "L2", "This THEONE", ""}) c.Assert(location.LinesPos, qt.Equals, 2) - location, pos = locateErrorInString("NO MATCH", lineMatcher) - c.Assert(pos.LineNumber, qt.Equals, -1) + location = locateErrorInString("NO MATCH", lineMatcher) + pos = location.Position + c.Assert(pos.LineNumber, qt.Equals, 0) c.Assert(location.LinesPos, qt.Equals, -1) c.Assert(len(location.Lines), qt.Equals, 0) - lineMatcher = func(m LineMatcher) bool { - return m.LineNumber == 6 + lineMatcher = func(m LineMatcher) int { + if m.LineNumber == 6 { + return 1 + } + return -1 } - location, pos = locateErrorInString(`A + location = locateErrorInString(`A B C D @@ -97,35 +106,46 @@ G H I J`, lineMatcher) + pos = location.Position c.Assert(location.Lines, qt.DeepEquals, []string{"D", "E", "F", "G", "H"}) c.Assert(pos.LineNumber, qt.Equals, 6) c.Assert(location.LinesPos, qt.Equals, 2) // Test match EOF - lineMatcher = func(m LineMatcher) bool { - return m.LineNumber == 4 + lineMatcher = func(m LineMatcher) int { + if m.LineNumber == 4 { + return 1 + } + return -1 } - location, pos = locateErrorInString(`A + location = locateErrorInString(`A B C `, lineMatcher) + pos = location.Position + c.Assert(location.Lines, qt.DeepEquals, []string{"B", "C", ""}) c.Assert(pos.LineNumber, qt.Equals, 4) c.Assert(location.LinesPos, qt.Equals, 2) - offsetMatcher := func(m LineMatcher) bool { - return m.Offset == 1 + offsetMatcher := func(m LineMatcher) int { + if m.Offset == 1 { + return 1 + } + return -1 } - location, pos = locateErrorInString(`A + location = locateErrorInString(`A B C D E`, offsetMatcher) + pos = location.Position + c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"}) c.Assert(pos.LineNumber, qt.Equals, 2) c.Assert(location.LinesPos, qt.Equals, 1) diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go index abd36cfbc..5cdb0e53b 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -73,37 +73,40 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE } var ( - contentPos text.Position - posle = fe.position - errorContext *ErrorContext + posle = fe.position + ectx *ErrorContext ) if posle.LineNumber <= 1 && posle.Offset > 0 { // Try to locate the line number from the content if offset is set. - errorContext, contentPos = locateError(r, fe, func(m LineMatcher) bool { + ectx = locateError(r, fe, func(m LineMatcher) int { if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber m.Position = text.Position{LineNumber: lno} return linematcher(m) } - return false + return -1 }) } else { - errorContext, contentPos = locateError(r, fe, linematcher) + ectx = locateError(r, fe, linematcher) } - if errorContext.ChromaLexer == "" { + if ectx.ChromaLexer == "" { if fe.fileType != "" { - errorContext.ChromaLexer = chromaLexerFromType(fe.fileType) + ectx.ChromaLexer = chromaLexerFromType(fe.fileType) } else { - errorContext.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) + ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) } } - fe.errorContext = errorContext + fe.errorContext = ectx - if contentPos.LineNumber > 0 { - fe.position.LineNumber = contentPos.LineNumber + if ectx.Position.LineNumber > 0 { + fe.position.LineNumber = ectx.Position.LineNumber + } + + if ectx.Position.ColumnNumber > 0 { + fe.position.ColumnNumber = ectx.Position.ColumnNumber } return fe @@ -144,10 +147,6 @@ func (e *fileError) Unwrap() error { // 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 { - if err == nil { - panic("err is nil") - } - // Filetype is used to determine the Chroma lexer to use. fileType, pos := extractFileTypePos(err) pos.Filename = name @@ -155,10 +154,17 @@ func NewFileError(name string, err error) FileError { _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) } - if pos.LineNumber < 0 { - panic(fmt.Sprintf("invalid line number: %d", pos.LineNumber)) - } + return &fileError{cause: err, fileType: fileType, position: pos} +} + +// 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 { + // Filetype is used to determine the Chroma lexer to use. + fileType, _ := extractFileTypePos(err) + if fileType == "" { + _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) + } return &fileError{cause: err, fileType: fileType, position: pos} } @@ -191,7 +197,7 @@ func extractFileTypePos(err error) (string, text.Position) { err = Cause(err) var fileType string - // Fall back to line/col 1:1 if we cannot find any better information. + // Default to line 1 col 1 if we don't find any better. pos := text.Position{ Offset: -1, LineNumber: 1, @@ -242,6 +248,18 @@ func UnwrapFileError(err error) FileError { return nil } +// UnwrapFileErrors tries to unwrap all FileError. +func UnwrapFileErrors(err error) []FileError { + var errs []FileError + for err != nil { + if v, ok := err.(FileError); ok { + errs = append(errs, v) + } + err = errors.Unwrap(err) + } + return errs +} + // UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext. func UnwrapFileErrorsWithErrorContext(err error) []FileError { var errs []FileError diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go index e6595aa28..41e244109 100644 --- a/common/herrors/file_error_test.go +++ b/common/herrors/file_error_test.go @@ -41,7 +41,7 @@ func TestNewFileError(t *testing.T) { fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2}) c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`) fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212}) - fe.UpdateContent(strings.NewReader(lines), SimpleLineMatcher) + fe.UpdateContent(strings.NewReader(lines), nil) c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`) errorContext := fe.ErrorContext() c.Assert(errorContext, qt.IsNotNil) diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go index 14c76ae45..c61c55a67 100644 --- a/common/loggers/loggers.go +++ b/common/loggers/loggers.go @@ -240,6 +240,11 @@ func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger { return newLogger(t, jww.LevelError, w, ioutil.Discard, false) } +// RemoveANSIColours removes all ANSI colours from the given string. +func RemoveANSIColours(s string) string { + return ansiColorRe.ReplaceAllString(s, "") +} + var ( ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m") errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)") diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go index 0c0cc859b..a7bd1ae12 100644 --- a/common/loggers/loggers_test.go +++ b/common/loggers/loggers_test.go @@ -46,3 +46,15 @@ func TestLoggerToWriterWithPrefix(t *testing.T) { c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n") } + +func TestRemoveANSIColours(t *testing.T) { + c := qt.New(t) + + c.Assert(RemoveANSIColours(""), qt.Equals, "") + c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "") + c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello") + c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello") + c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World") + c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!") + c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |") +} diff --git a/config/configLoader.go b/config/configLoader.go index 6bbad7002..008ebbfae 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, herrors.SimpleLineMatcher) + return nil, herrors.NewFileErrorFromFile(err, filename, filename, fs, nil) } return NewFrom(m), nil } diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index 0a7f66e6c..dff2082b6 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -77,7 +77,7 @@ kind = '{section,term}' } builders := make([]*IntegrationTestBuilder, b.N) - for i, _ := range builders { + for i := range builders { builders[i] = NewIntegrationTestBuilder(cfg) } diff --git a/hugolib/config.go b/hugolib/config.go index b2713758a..1e7cf6b06 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, herrors.SimpleLineMatcher) + return herrors.NewFileErrorFromFile(err, filename, filename, l.Fs, nil) } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 9f18f33bc..4026f58d3 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, herrors.SimpleLineMatcher) + return herrors.NewFileErrorFromFile(err, realFilename, realFilename, h.SourceSpec.Fs.Source, nil) } diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go index 8f983075d..ffbfe1c17 100644 --- a/hugolib/hugo_sites_build_errors_test.go +++ b/hugolib/hugo_sites_build_errors_test.go @@ -347,13 +347,75 @@ minify = true } -func TestErrorNested(t *testing.T) { +func TestErrorNestedRender(t *testing.T) { t.Parallel() files := ` -- config.toml -- +-- content/_index.md -- +--- +title: "Home" +--- +-- layouts/index.html -- +line 1 +line 2 +1{{ .Render "myview" }} +-- layouts/_default/myview.html -- +line 1 +12{{ partial "foo.html" . }} +line 4 +line 5 +-- layouts/partials/foo.html -- +line 1 +line 2 +123{{ .ThisDoesNotExist }} +line 4 +` + + b, err := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.IsNotNil) + errors := herrors.UnwrapFileErrorsWithErrorContext(err) + b.Assert(errors, qt.HasLen, 4) + b.Assert(errors[0].Position().LineNumber, qt.Equals, 3) + b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 4) + b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/index.html:3:4": execute of template failed`)) + b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "1{{ .Render \"myview\" }}"}) + b.Assert(errors[2].Position().LineNumber, qt.Equals, 2) + b.Assert(errors[2].Position().ColumnNumber, qt.Equals, 5) + b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) + + b.Assert(errors[3].Position().LineNumber, qt.Equals, 3) + b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6) + b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) + +} + +func TestErrorNestedShortocde(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- content/_index.md -- +--- +title: "Home" +--- + +## Hello +{{< hello >}} + -- layouts/index.html -- line 1 +line 2 +{{ .Content }} +line 5 +-- layouts/shortcodes/hello.html -- +line 1 12{{ partial "foo.html" . }} line 4 line 5 @@ -373,15 +435,183 @@ line 4 b.Assert(err, qt.IsNotNil) errors := herrors.UnwrapFileErrorsWithErrorContext(err) + + b.Assert(errors, qt.HasLen, 3) + + b.Assert(errors[0].Position().LineNumber, qt.Equals, 6) + b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 1) + b.Assert(errors[0].ErrorContext().ChromaLexer, qt.Equals, "md") + b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:6:1": failed to render shortcode "hello": failed to process shortcode: "/layouts/shortcodes/hello.html:2:5":`)) + b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"", "## Hello", "{{< hello >}}", ""}) + b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) + b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) + +} + +func TestErrorRenderHookHeading(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- content/_index.md -- +--- +title: "Home" +--- + +## Hello + +-- layouts/index.html -- +line 1 +line 2 +{{ .Content }} +line 5 +-- layouts/_default/_markup/render-heading.html -- +line 1 +12{{ .Levels }} +line 4 +line 5 +` + + b, err := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.IsNotNil) + errors := herrors.UnwrapFileErrorsWithErrorContext(err) + b.Assert(errors, qt.HasLen, 2) - fmt.Println(errors[0]) - b.Assert(errors[0].Position().LineNumber, qt.Equals, 2) - b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 5) - b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/index.html:2:5": execute of template failed`)) - b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) - b.Assert(errors[1].Position().LineNumber, qt.Equals, 3) - b.Assert(errors[1].Position().ColumnNumber, qt.Equals, 6) - b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) + b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:1:1": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`)) + +} + +func TestErrorRenderHookCodeblock(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +-- content/_index.md -- +--- +title: "Home"< |