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'