From b0b1b76dc9c7edad15924785d01684e36f3e6cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 14 Mar 2023 12:45:09 +0100 Subject: [PATCH] markup/goldmark: Fail on invalid Markdown attributes --- common/herrors/error_locator.go | 10 +++++ common/herrors/file_error.go | 14 +++++++ hugolib/page.go | 8 +++- .../goldmark/codeblocks/integration_test.go | 39 +++++++++++++++++++ markup/goldmark/codeblocks/render.go | 28 ++++++++----- 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index c7e2d2f06f5..7624bab9876 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -61,6 +61,16 @@ var OffsetMatcher = func(m LineMatcher) int { return -1 } +// ContainsMatcher is a line matcher that matches by line content. +func ContainsMatcher(text string) func(m LineMatcher) int { + return func(m LineMatcher) int { + if idx := strings.Index(m.Line, text); idx != -1 { + return idx + 1 + } + 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 e6baaf6e37e..9273b2a8041 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -392,3 +392,17 @@ func extractPosition(e error) (pos text.Position) { } return } + +// TextSegmentError is an error with a text segment attached. +type TextSegmentError struct { + Segment string + Err error +} + +func (e TextSegmentError) Unwrap() error { + return e.Err +} + +func (e TextSegmentError) Error() string { + return e.Err.Error() +} diff --git a/hugolib/page.go b/hugolib/page.go index a80b28a3e25..ebc29df4765 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -617,7 +617,13 @@ func (p *pageState) wrapError(err error) error { } } - return herrors.NewFileErrorFromFile(err, filename, p.s.SourceSpec.Fs.Source, herrors.NopLineMatcher) + lineMatcher := herrors.NopLineMatcher + + if textSegmentErr, ok := err.(*herrors.TextSegmentError); ok { + lineMatcher = herrors.ContainsMatcher(textSegmentErr.Segment) + } + + return herrors.NewFileErrorFromFile(err, filename, p.s.SourceSpec.Fs.Source, lineMatcher) } diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go index 29ff0dc7d8c..7f02018784b 100644 --- a/markup/goldmark/codeblocks/integration_test.go +++ b/markup/goldmark/codeblocks/integration_test.go @@ -18,6 +18,8 @@ import ( "strings" "testing" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/hugolib" ) @@ -384,3 +386,40 @@ Common } } + +// Issue 10835 +func TestAttributesValidation(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term"] +-- content/p1.md -- +--- +title: "p1" +--- + +## Issue 10835 + +§§§bash { color=red dimensions=300x200 } +Hello, World! +§§§ + +-- layouts/index.html -- +-- layouts/_default/single.html -- +{{ .Content }} +-- layouts/_default/_markup/render-codeblock.html -- +Attributes: {{ .Attributes }}|Type: {{ .Type }}| +` + + b, err := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.Not(qt.IsNil)) + b.Assert(err.Error(), qt.Contains, "p1.md:7:9\": failed to parse Markdown attributes; you may need to quote the values") + +} diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go index cf5a0f2966c..5f053d2787b 100644 --- a/markup/goldmark/codeblocks/render.go +++ b/markup/goldmark/codeblocks/render.go @@ -15,6 +15,7 @@ package codeblocks import ( "bytes" + "errors" "fmt" "strings" "sync" @@ -101,7 +102,10 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No } // IsDefaultCodeBlockRendererProvider - attrs := getAttributes(n.b, info) + attrs, attrStr, err := getAttributes(n.b, info) + if err != nil { + return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr} + } cbctx := &codeBlockContext{ page: ctx.DocumentContext().Document, lang: lang, @@ -123,7 +127,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No cr := renderer.(hooks.CodeBlockRenderer) - err := cr.RenderCodeblock( + err = cr.RenderCodeblock( ctx.RenderContext().Ctx, w, cbctx, @@ -182,30 +186,36 @@ func getLang(node *ast.FencedCodeBlock, src []byte) string { return lang } -func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute { +func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ([]ast.Attribute, string, error) { if node.Attributes() != nil { - return node.Attributes() + return node.Attributes(), "", nil } if infostr != nil { attrStartIdx := -1 + attrEndIdx := -1 for idx, char := range infostr { - if char == '{' { + if attrEndIdx == -1 && char == '{' { attrStartIdx = idx + } + if attrStartIdx != -1 && char == '}' { + attrEndIdx = idx break } } - if attrStartIdx != -1 { + if attrStartIdx != -1 && attrEndIdx != -1 { n := ast.NewTextBlock() // dummy node for storing attributes - attrStr := infostr[attrStartIdx:] + attrStr := infostr[attrStartIdx : attrEndIdx+1] if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { for _, attr := range attrs { n.SetAttribute(attr.Name, attr.Value) } - return n.Attributes() + return n.Attributes(), "", nil + } else { + return nil, string(attrStr), errors.New("failed to parse Markdown attributes; you may need to quote the values") } } } - return nil + return nil, "", nil }