diff --git a/commands/gen.go b/commands/gen.go
index 83b4d637c66..fad392578d6 100644
--- a/commands/gen.go
+++ b/commands/gen.go
@@ -27,6 +27,7 @@ import (
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
+ "github.com/goccy/go-yaml"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers"
@@ -35,7 +36,6 @@ import (
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
- "gopkg.in/yaml.v2"
)
func newGenCommand() *genCommand {
diff --git a/commands/server.go b/commands/server.go
index c2fee68b239..219d97acd28 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -1160,7 +1160,6 @@ func chmodFilter(dst, src os.FileInfo) bool {
}
func cleanErrorLog(content string) string {
- content = strings.ReplaceAll(content, "\n", " ")
content = logReplacer.Replace(content)
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
index 32a6f0081f8..f83a6eefb3d 100644
--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -112,11 +112,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
fe.errorContext = ectx
- if ectx.Position.LineNumber > 0 {
+ if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
fe.position.LineNumber = ectx.Position.LineNumber
}
- if ectx.Position.ColumnNumber > 0 {
+ if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
@@ -181,6 +181,7 @@ func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
+
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
@@ -238,7 +239,9 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
- return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
+ fe := NewFileErrorFromName(err, realFilename)
+ fe = fe.UpdateContent(f, linematcher)
+ return fe
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -306,13 +309,9 @@ func extractFileTypePos(err error) (string, text.Position) {
}
// Look in the error message for the line number.
- for _, handle := range lineNumberExtractors {
- lno, col := handle(err)
- if lno > 0 {
- pos.ColumnNumber = col
- pos.LineNumber = lno
- break
- }
+ if lno, col := commonLineNumberExtractor(err); lno > 0 {
+ pos.ColumnNumber = col
+ pos.LineNumber = lno
}
if fileType == "" && pos.Filename != "" {
diff --git a/common/herrors/line_number_extractors.go b/common/herrors/line_number_extractors.go
index f70a2691fc3..121506bb07a 100644
--- a/common/herrors/line_number_extractors.go
+++ b/common/herrors/line_number_extractors.go
@@ -19,17 +19,27 @@ import (
)
var lineNumberExtractors = []lineNumberExtractor{
+ // YAML parse errors.
+ newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
+
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
- // YAML parse errors
- newLineNumberErrHandlerFromRegexp(`line (\d+):`),
-
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
+func commonLineNumberExtractor(e error) (int, int) {
+ for _, handler := range lineNumberExtractors {
+ lno, col := handler(e)
+ if lno > 0 {
+ return lno, col
+ }
+ }
+ return 0, 0
+}
+
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
diff --git a/common/herrors/line_number_extractors_test.go b/common/herrors/line_number_extractors_test.go
new file mode 100644
index 00000000000..7209ac9aa07
--- /dev/null
+++ b/common/herrors/line_number_extractors_test.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package herrors
+
+import (
+ "errors"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestCommonLineNumberExtractor(t *testing.T) {
+ t.Parallel()
+
+ c := qt.New(t)
+
+ lno, col := commonLineNumberExtractor(errors.New("[4:9] value is not allowed in this context"))
+ c.Assert(lno, qt.Equals, 4)
+ c.Assert(col, qt.Equals, 9)
+}
diff --git a/go.mod b/go.mod
index b12a889a9c6..d33c3355fd7 100644
--- a/go.mod
+++ b/go.mod
@@ -36,6 +36,7 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/gobuffalo/flect v1.0.3
github.com/gobwas/glob v0.2.3
+ github.com/goccy/go-yaml v1.14.0
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e
github.com/gohugoio/hashstructure v0.1.0
github.com/gohugoio/httpcache v0.7.0
@@ -83,7 +84,6 @@ require (
golang.org/x/text v0.19.0
golang.org/x/tools v0.26.0
google.golang.org/api v0.191.0
- gopkg.in/yaml.v2 v2.4.0
)
require (
@@ -164,6 +164,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index fcd88e55ace..91461cd3f0c 100644
--- a/go.sum
+++ b/go.sum
@@ -227,6 +227,8 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/goccy/go-yaml v1.14.0 h1:G/NDXJvf1CX0FshjxKn2AOL0MnrxsSJNpY9FpvMRblw=
+github.com/goccy/go-yaml v1.14.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/hashstructure v0.1.0 h1:kBSTMLMyTXbrJVAxaKI+wv30MMJJxn9Q8kfQtJaZ400=
diff --git a/hugolib/frontmatter_test.go b/hugolib/frontmatter_test.go
index 3a2080b0ec1..c4cbfa72ea7 100644
--- a/hugolib/frontmatter_test.go
+++ b/hugolib/frontmatter_test.go
@@ -40,7 +40,7 @@ Strings: {{ printf "%T" .Params.strings }} {{ range .Params.strings }}Strings: {
b.Build()
- b.AssertFileContent("public/post/one/index.html", "Ints: []interface {} Int: 1 (int)|Int: 2 (int)|Int: 3 (int)|")
- b.AssertFileContent("public/post/one/index.html", "Mixed: []interface {} Mixed: 1 (string)|Mixed: 2 (int)|Mixed: 3 (int)|")
+ b.AssertFileContent("public/post/one/index.html", "Ints: []interface {} Int: 1 (uint64)|Int: 2 (uint64)|Int: 3 (uint64)|")
+ b.AssertFileContent("public/post/one/index.html", "Mixed: []interface {} Mixed: 1 (string)|Mixed: 2 (uint64)|Mixed: 3 (uint64)|")
b.AssertFileContent("public/post/one/index.html", "Strings: []string Strings: 1 (string)|Strings: 2 (string)|Strings: 3 (string)|")
}
diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go
index 71afe676772..27ab990760c 100644
--- a/hugolib/hugo_sites_build_errors_test.go
+++ b/hugolib/hugo_sites_build_errors_test.go
@@ -476,7 +476,7 @@ line 5
errors := herrors.UnwrapFileErrorsWithErrorContext(err)
b.Assert(errors, qt.HasLen, 3)
- 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`))
+ b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:2:5": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`))
}
func TestErrorRenderHookCodeblock(t *testing.T) {
@@ -645,3 +645,35 @@ Home.
b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.html:2:3`))
b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`)
}
+
+func TestErrorFrontmatterYAMLSyntax(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- content/_index.md --
+
+
+
+
+
+---
+line1: 'value1'
+x
+line2: 'value2'
+line3: 'value3'
+---
+`
+
+ b, err := TestE(t, files)
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, "> 3 |")
+ fe := herrors.UnwrapFileError(err)
+ b.Assert(fe, qt.Not(qt.IsNil))
+ pos := fe.Position()
+ b.Assert(pos.Filename, qt.Contains, filepath.FromSlash("content/_index.md"))
+ b.Assert(fe.ErrorContext(), qt.Not(qt.IsNil))
+ b.Assert(pos.LineNumber, qt.Equals, 9)
+ b.Assert(pos.ColumnNumber, qt.Equals, 1)
+}
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index 4ec91f7b5cd..b6bfe76e88f 100644
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -283,23 +283,20 @@ func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser
var err error
c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f)
if err != nil {
- if fe, ok := err.(herrors.FileError); ok {
- pos := fe.Position()
+ fe := herrors.UnwrapFileError(err)
+ if fe == nil {
+ fe = herrors.NewFileError(err)
+ }
+ pos := fe.Position()
- // Offset the starting position of front matter.
- offset := iter.LineNumber(source) - 1
- if f == metadecoders.YAML {
- offset -= 1
- }
- pos.LineNumber += offset
+ // Offset the starting position of front matter.
+ offset := iter.LineNumber(source) - 1
- fe.UpdatePosition(pos)
- fe.SetFilename("") // It will be set later.
+ pos.LineNumber += offset
- return fe
- } else {
- return err
- }
+ fe.UpdatePosition(pos)
+ fe.SetFilename("") // It will be set later.
+ return fe
}
return nil
diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go
index 96c2c0f96e2..0b6234d3b07 100644
--- a/hugolib/pages_capture.go
+++ b/hugolib/pages_capture.go
@@ -123,7 +123,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error {
numPages, numResources, err := c.m.AddFi(fi, c.buildConfig)
if err != nil {
- return hugofs.AddFileInfoToError(err, fi, c.fs)
+ return hugofs.AddFileInfoToError(err, fi, c.h.SourceFs)
}
numFilesProcessedTotal.Add(1)
numPagesProcessedTotal.Add(numPages)
diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go
index 9ede538d233..78a134708d5 100644
--- a/langs/i18n/translationProvider.go
+++ b/langs/i18n/translationProvider.go
@@ -20,9 +20,9 @@ import (
"github.com/gohugoio/hugo/common/paths"
+ yaml "github.com/goccy/go-yaml"
"github.com/gohugoio/hugo/common/herrors"
"golang.org/x/text/language"
- yaml "gopkg.in/yaml.v2"
"github.com/gohugoio/go-i18n/v2/i18n"
"github.com/gohugoio/hugo/helpers"
diff --git a/parser/frontmatter.go b/parser/frontmatter.go
index 18e55f9ad4f..398aecc3005 100644
--- a/parser/frontmatter.go
+++ b/parser/frontmatter.go
@@ -22,8 +22,6 @@ import (
toml "github.com/pelletier/go-toml/v2"
- yaml "gopkg.in/yaml.v2"
-
xml "github.com/clbanning/mxj/v2"
)
@@ -39,7 +37,7 @@ func InterfaceToConfig(in any, format metadecoders.Format, w io.Writer) error {
switch format {
case metadecoders.YAML:
- b, err := yaml.Marshal(in)
+ b, err := metadecoders.MarshalYAML(in)
if err != nil {
return err
}
diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go
index 5dac23f0328..4615c704c75 100644
--- a/parser/metadecoders/decoder.go
+++ b/parser/metadecoders/decoder.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -28,10 +28,10 @@ import (
"github.com/niklasfasching/go-org/org"
xml "github.com/clbanning/mxj/v2"
+ yaml "github.com/goccy/go-yaml"
toml "github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
"github.com/spf13/cast"
- yaml "gopkg.in/yaml.v2"
)
// Decoder provides some configuration options for the decoders.
@@ -164,35 +164,7 @@ func (d Decoder) UnmarshalTo(data []byte, f Format, v any) error {
case TOML:
err = toml.Unmarshal(data, v)
case YAML:
- err = yaml.Unmarshal(data, v)
- if err != nil {
- return toFileError(f, data, fmt.Errorf("failed to unmarshal YAML: %w", err))
- }
-
- // To support boolean keys, the YAML package unmarshals maps to
- // map[interface{}]interface{}. Here we recurse through the result
- // and change all maps to map[string]interface{} like we would've
- // gotten from `json`.
- var ptr any
- switch vv := v.(type) {
- case *map[string]any:
- ptr = *vv
- case *any:
- ptr = *vv
- default:
- // Not a map.
- }
-
- if ptr != nil {
- if mm, changed := stringifyMapKeys(ptr); changed {
- switch vv := v.(type) {
- case *map[string]any:
- *vv = mm.(map[string]any)
- case *any:
- *vv = mm
- }
- }
- }
+ return yaml.Unmarshal(data, v)
case CSV:
return d.unmarshalCSV(data, v)
diff --git a/parser/metadecoders/decoder_test.go b/parser/metadecoders/decoder_test.go
index 49f7868cc18..0f596b1e42c 100644
--- a/parser/metadecoders/decoder_test.go
+++ b/parser/metadecoders/decoder_test.go
@@ -91,8 +91,8 @@ func TestUnmarshalToMap(t *testing.T) {
{`a = "b"`, TOML, expect},
{`a: "b"`, YAML, expect},
// Make sure we get all string keys, even for YAML
- {"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]any{"a": "Easy!", "b": map[string]any{"c": 2, "d": []any{3, 4}}}},
- {"a:\n true: 1\n false: 2", YAML, map[string]any{"a": map[string]any{"true": 1, "false": 2}}},
+ {"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]any{"a": "Easy!", "b": map[string]any{"c": uint64(2), "d": []any{uint64(3), uint64(4)}}}},
+ {"a:\n true: 1\n false: 2", YAML, map[string]any{"a": map[string]any{"true": uint64(1), "false": uint64(2)}}},
{`{ "a": "b" }`, JSON, expect},
{`b`, XML, expect},
{`#+a: b`, ORG, expect},
@@ -137,7 +137,7 @@ func TestUnmarshalToInterface(t *testing.T) {
{[]byte(`a: "b"`), YAML, expect},
{[]byte(`b`), XML, expect},
{[]byte(`a,b,c`), CSV, [][]string{{"a", "b", "c"}}},
- {[]byte("a: Easy!\nb:\n c: 2\n d: [3, 4]"), YAML, map[string]any{"a": "Easy!", "b": map[string]any{"c": 2, "d": []any{3, 4}}}},
+ {[]byte("a: Easy!\nb:\n c: 2\n d: [3, 4]"), YAML, map[string]any{"a": "Easy!", "b": map[string]any{"c": uint64(2), "d": []any{uint64(3), uint64(4)}}}},
// errors
{[]byte(`a = "`), TOML, false},
} {
@@ -170,7 +170,7 @@ func TestUnmarshalStringTo(t *testing.T) {
{"32", int64(1234), int64(32)},
{"32", int(1234), int(32)},
{"3.14159", float64(1), float64(3.14159)},
- {"[3,7,9]", []any{}, []any{3, 7, 9}},
+ {"[3,7,9]", []any{}, []any{uint64(3), uint64(7), uint64(9)}},
{"[3.1,7.2,9.3]", []any{}, []any{3.1, 7.2, 9.3}},
} {
msg := qt.Commentf("%d: %T", i, test.to)
diff --git a/parser/metadecoders/encoder.go b/parser/metadecoders/encoder.go
new file mode 100644
index 00000000000..a18da443a1f
--- /dev/null
+++ b/parser/metadecoders/encoder.go
@@ -0,0 +1,25 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadecoders
+
+import yaml "github.com/goccy/go-yaml"
+
+var yamlEncodeOptions = []yaml.EncodeOption{
+ yaml.UseSingleQuote(true),
+}
+
+// MarshalYAML marshals the given value to YAML.
+var MarshalYAML = func(v any) ([]byte, error) {
+ return yaml.MarshalWithOptions(v, yamlEncodeOptions...)
+}
diff --git a/testscripts/commands/server__error_recovery_edit_content.txt b/testscripts/commands/server__error_recovery_edit_content.txt
index f5ea7e94baf..e640cd8f383 100644
--- a/testscripts/commands/server__error_recovery_edit_content.txt
+++ b/testscripts/commands/server__error_recovery_edit_content.txt
@@ -8,7 +8,7 @@ waitServer
httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'
replace $WORK/content/p1/index.md 'title:' 'titlecolon'
-httpget ${HUGOTEST_BASEURL_0}p1/ 'failed'
+httpget ${HUGOTEST_BASEURL_0}p1/ 'Error'
replace $WORK/content/p1/index.md 'titlecolon' 'title:'
httpget ${HUGOTEST_BASEURL_0}p1/ 'Title: P1'