diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-02-17 13:04:00 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-02-24 18:59:50 +0100 |
commit | 08fdca9d9365eaf1e496a12e2af5e18617bd0e66 (patch) | |
tree | 6c6942d1b74a4160d93a997860bafd52b92025f5 /markup | |
parent | 2c20f5bc00b604e72b3b7e401fbdbf9447fe3470 (diff) |
Add Markdown diagrams and render hooks for code blocks
You can now create custom hook templates for code blocks, either one for all (`render-codeblock.html`) or for a given code language (e.g. `render-codeblock-go.html`).
We also used this new hook to add support for diagrams in Hugo:
* Goat (Go ASCII Tool) is built-in and enabled by default; just create a fenced code block with the language `goat` and start draw your Ascii diagrams.
* Another popular alternative for diagrams in Markdown, Mermaid (supported by GitHub), can also be implemented with a simple template. See the Hugo documentation for more information.
Updates #7765
Closes #9538
Fixes #9553
Fixes #8520
Fixes #6702
Fixes #9558
Diffstat (limited to 'markup')
-rw-r--r-- | markup/converter/converter.go | 10 | ||||
-rw-r--r-- | markup/converter/hooks/hooks.go | 100 | ||||
-rw-r--r-- | markup/goldmark/codeblocks/integration_test.go | 115 | ||||
-rw-r--r-- | markup/goldmark/codeblocks/render.go | 159 | ||||
-rw-r--r-- | markup/goldmark/codeblocks/transform.go | 53 | ||||
-rw-r--r-- | markup/goldmark/convert.go | 146 | ||||
-rw-r--r-- | markup/goldmark/convert_test.go | 25 | ||||
-rw-r--r-- | markup/goldmark/integration_test.go | 141 | ||||
-rw-r--r-- | markup/goldmark/internal/render/context.go | 81 | ||||
-rw-r--r-- | markup/goldmark/render_hooks.go | 143 | ||||
-rw-r--r-- | markup/goldmark/toc_test.go | 9 | ||||
-rw-r--r-- | markup/highlight/config.go | 99 | ||||
-rw-r--r-- | markup/highlight/highlight.go | 178 | ||||
-rw-r--r-- | markup/internal/attributes/attributes.go | 219 | ||||
-rw-r--r-- | markup/markup.go | 13 | ||||
-rw-r--r-- | markup/org/convert.go | 3 |
16 files changed, 1130 insertions, 364 deletions
diff --git a/markup/converter/converter.go b/markup/converter/converter.go index 180208a7b..30addfec6 100644 --- a/markup/converter/converter.go +++ b/markup/converter/converter.go @@ -21,6 +21,7 @@ import ( "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/markup/highlight" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/spf13/afero" @@ -34,7 +35,7 @@ type ProviderConfig struct { ContentFs afero.Fs Logger loggers.Logger Exec *hexec.Exec - Highlight func(code, lang, optsStr string) (string, error) + highlight.Highlighter } // ProviderProvider creates converter providers. @@ -127,9 +128,10 @@ type DocumentContext struct { // RenderContext holds contextual information about the content to render. type RenderContext struct { - Src []byte - RenderTOC bool - RenderHooks hooks.Renderers + Src []byte + RenderTOC bool + + GetRenderer hooks.GetRendererFunc } var FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks") diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go index d36dad288..987cb1dc3 100644 --- a/markup/converter/hooks/hooks.go +++ b/markup/converter/hooks/hooks.go @@ -14,15 +14,17 @@ package hooks import ( - "fmt" "io" - "strings" + "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/internal/attributes" ) +var _ AttributesOptionsSliceProvider = (*attributes.AttributesHolder)(nil) + type AttributesProvider interface { - Attributes() map[string]string + Attributes() map[string]interface{} } type LinkContext interface { @@ -33,11 +35,30 @@ type LinkContext interface { PlainText() string } +type CodeblockContext interface { + AttributesProvider + Options() map[string]interface{} + Lang() string + Code() string + Ordinal() int + Page() interface{} +} + +type AttributesOptionsSliceProvider interface { + AttributesSlice() []attributes.Attribute + OptionsSlice() []attributes.Attribute +} + type LinkRenderer interface { RenderLink(w io.Writer, ctx LinkContext) error identity.Provider } +type CodeBlockRenderer interface { + RenderCodeblock(w hugio.FlexiWriter, ctx CodeblockContext) error + identity.Provider +} + // HeadingContext contains accessors to all attributes that a HeadingRenderer // can use to render a heading. type HeadingContext interface { @@ -63,70 +84,13 @@ type HeadingRenderer interface { identity.Provider } -type Renderers struct { - LinkRenderer LinkRenderer - ImageRenderer LinkRenderer - HeadingRenderer HeadingRenderer -} - -func (r Renderers) Eq(other interface{}) bool { - ro, ok := other.(Renderers) - if !ok { - return false - } - - if r.IsZero() || ro.IsZero() { - return r.IsZero() && ro.IsZero() - } - - var b1, b2 bool - b1, b2 = r.ImageRenderer == nil, ro.ImageRenderer == nil - if (b1 || b2) && (b1 != b2) { - return false - } - if !b1 && r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() { - return false - } - - b1, b2 = r.LinkRenderer == nil, ro.LinkRenderer == nil - if (b1 || b2) && (b1 != b2) { - return false - } - if !b1 && r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() { - return false - } - - b1, b2 = r.HeadingRenderer == nil, ro.HeadingRenderer == nil - if (b1 || b2) && (b1 != b2) { - return false - } - if !b1 && r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() { - return false - } - - return true -} - -func (r Renderers) IsZero() bool { - return r.HeadingRenderer == nil && r.LinkRenderer == nil && r.ImageRenderer == nil -} +type RendererType int -func (r Renderers) String() string { - if r.IsZero() { - return "<zero>" - } - - var sb strings.Builder - - if r.LinkRenderer != nil { - sb.WriteString(fmt.Sprintf("LinkRenderer<%s>|", r.LinkRenderer.GetIdentity())) - } - if r.HeadingRenderer != nil { - sb.WriteString(fmt.Sprintf("HeadingRenderer<%s>|", r.HeadingRenderer.GetIdentity())) - } - if r.ImageRenderer != nil { - sb.WriteString(fmt.Sprintf("ImageRenderer<%s>|", r.ImageRenderer.GetIdentity())) - } +const ( + LinkRendererType RendererType = iota + 1 + ImageRendererType + HeadingRendererType + CodeBlockRendererType +) - return sb.String() -} +type GetRendererFunc func(t RendererType, id interface{}) interface{} diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go new file mode 100644 index 000000000..d662b3911 --- /dev/null +++ b/markup/goldmark/codeblocks/integration_test.go @@ -0,0 +1,115 @@ +// Copyright 2022 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 codeblocks_test + +import ( + "strings" + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestCodeblocks(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +[markup] + [markup.highlight] + anchorLineNos = false + codeFences = true + guessSyntax = false + hl_Lines = '' + lineAnchors = '' + lineNoStart = 1 + lineNos = false + lineNumbersInTable = true + noClasses = false + style = 'monokai' + tabWidth = 4 +-- layouts/_default/_markup/render-codeblock-goat.html -- +{{ $diagram := diagrams.Goat .Code }} +Goat SVG:{{ substr $diagram.SVG 0 100 | safeHTML }} }}| +Goat Attribute: {{ .Attributes.width}}| +-- layouts/_default/_markup/render-codeblock-go.html -- +Go Code: {{ .Code | safeHTML }}| +Go Language: {{ .Lang }}| +-- layouts/_default/single.html -- +{{ .Content }} +-- content/p1.md -- +--- +title: "p1" +--- + +## Ascii Diagram + +CODE_FENCEgoat { width="600" } +---> +CODE_FENCE + +## Go Code + +CODE_FENCEgo +fmt.Println("Hello, World!"); +CODE_FENCE + +## Golang Code + +CODE_FENCEgolang +fmt.Println("Hello, Golang!"); +CODE_FENCE + +## Bash Code + +CODE_FENCEbash { linenos=inline,hl_lines=[2,"5-6"],linenostart=32 class=blue } +echo "l1"; +echo "l2"; +echo "l3"; +echo "l4"; +echo "l5"; +echo "l6"; +echo "l7"; +echo "l8"; +CODE_FENCE +` + + files = strings.ReplaceAll(files, "CODE_FENCE", "```") + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + NeedsOsFS: false, + }, + ).Build() + + b.AssertFileContent("public/p1/index.html", ` +Goat SVG:<svg class='diagram' +Goat Attribute: 600| + +Go Language: go| +Go Code: fmt.Println("Hello, World!"); + +Go Code: fmt.Println("Hello, Golang!"); +Go Language: golang| + + + `, + "Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'", + "Goat Attribute: 600|", + "<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|", + "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|", + "<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"l1"</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>", + ) +} diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go new file mode 100644 index 000000000..59d142e23 --- /dev/null +++ b/markup/goldmark/codeblocks/render.go @@ -0,0 +1,159 @@ +// Copyright 2022 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 codeblocks + +import ( + "bytes" + "fmt" + + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/markup/goldmark/internal/render" + "github.com/gohugoio/hugo/markup/internal/attributes" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type ( + diagrams struct{} + htmlRenderer struct{} +) + +func New() goldmark.Extender { + return &diagrams{} +} + +func (e *diagrams) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithASTTransformers( + util.Prioritized(&Transformer{}, 100), + ), + ) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(newHTMLRenderer(), 100), + )) +} + +func newHTMLRenderer() renderer.NodeRenderer { + r := &htmlRenderer{} + return r +} + +func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindCodeBlock, r.renderCodeBlock) +} + +func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + ctx := w.(*render.Context) + + if entering { + return ast.WalkContinue, nil + } + + n := node.(*codeBlock) + lang := string(n.b.Language(src)) + ordinal := n.ordinal + + var buff bytes.Buffer + + l := n.b.Lines().Len() + for i := 0; i < l; i++ { + line := n.b.Lines().At(i) + buff.Write(line.Value(src)) + } + text := buff.String() + + var info []byte + if n.b.Info != nil { + info = n.b.Info.Segment.Value(src) + } + attrs := getAttributes(n.b, info) + + v := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang) + if v == nil { + return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang) + } + + cr := v.(hooks.CodeBlockRenderer) + + err := cr.RenderCodeblock( + w, + codeBlockContext{ + page: ctx.DocumentContext().Document, + lang: lang, + code: text, + ordinal: ordinal, + AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock), + }, + ) + + ctx.AddIdentity(cr) + + return ast.WalkContinue, err +} + +type codeBlockContext struct { + page interface{} + lang string + code string + ordinal int + *attributes.AttributesHolder +} + +func (c codeBlockContext) Page() interface{} { + return c.page +} + +func (c codeBlockContext) Lang() string { + return c.lang +} + +func (c codeBlockContext) Code() string { + return c.code +} + +func (c codeBlockContext) Ordinal() int { + return c.ordinal +} + +func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute { + if node.Attributes() != nil { + return node.Attributes() + } + if infostr != nil { + attrStartIdx := -1 + + for idx, char := range infostr { + if char == '{' { + attrStartIdx = idx + break + } + } + + if attrStartIdx > 0 { + n := ast.NewTextBlock() // dummy node for storing attributes + attrStr := infostr[attrStartIdx:] + if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { + for _, attr := range attrs { + n.SetAttribute(attr.Name, attr.Value) + } + return n.Attributes() + } + } + } + return nil +} diff --git a/markup/goldmark/codeblocks/transform.go b/markup/goldmark/codeblocks/transform.go new file mode 100644 index 000000000..791e99a5c --- /dev/null +++ b/markup/goldmark/codeblocks/transform.go @@ -0,0 +1,53 @@ +package codeblocks + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" +) + +// Kind is the kind of an Hugo code block. +var KindCodeBlock = ast.NewNodeKind("HugoCodeBlock") + +// Its raw contents are the plain text of the code block. +type codeBlock struct { + ast.BaseBlock + ordinal int + b *ast.FencedCodeBlock +} + +func (*codeBlock) Kind() ast.NodeKind { return KindCodeBlock } + +func (*codeBlock) IsRaw() bool { return true } + +func (b *codeBlock) Dump(src []byte, level int) { +} + +type Transformer struct{} + +// Transform transforms the provided Markdown AST. +func (*Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser.Context) { + var codeBlocks []*ast.FencedCodeBlock + + ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) { + if !enter { + return ast.WalkContinue, nil + } + + cb, ok := node.(*ast.FencedCodeBlock) + if !ok { + return ast.WalkContinue, nil + } + + codeBlocks = append(codeBlocks, cb) + return ast.WalkContinue, nil + }) + + for i, cb := range codeBlocks { + b := &codeBlock{b: cb, ordinal: i} + parent := cb.Parent() + if parent != nil { + parent.ReplaceChild(parent, cb, b) + } + } +} diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go index c547fe1e0..4c1641a0b 100644 --- a/markup/goldmark/convert.go +++ b/markup/goldmark/convert.go @@ -17,12 +17,12 @@ package goldmark import ( "bytes" "fmt" - "math/bits" "path/filepath" "runtime/debug" + "github.com/gohugoio/hugo/markup/goldmark/codeblocks" "github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes" - "github.com/yuin/goldmark/ast" + "github.com/gohugoio/hugo/markup/goldmark/internal/render" "github.com/gohugoio/hugo/identity" @@ -32,16 +32,13 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/markup/highlight" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/yuin/goldmark" - hl "github.com/yuin/goldmark-highlighting" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" ) // Provider is the package entry point. @@ -104,7 +101,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown { ) if mcfg.Highlight.CodeFences { - extensions = append(extensions, newHighlighting(mcfg.Highlight)) + extensions = append(extensions, codeblocks.New()) } if cfg.Extensions.Table { @@ -178,65 +175,6 @@ func (c converterResult) GetIdentities() identity.Identities { return c.ids } -type bufWriter struct { - *bytes.Buffer -} - -const maxInt = 1<<(bits.UintSize-1) - 1 - -func (b *bufWriter) Available() int { - return maxInt -} - -func (b *bufWriter) Buffered() int { - return b.Len() -} - -func (b *bufWriter) Flush() error { - return nil -} - -type renderContext struct { - *bufWriter - positions []int - renderContextData -} - -func (ctx *renderContext) pushPos(n int) { - ctx.positions = append(ctx.positions, n) -} - -func (ctx *renderContext) popPos() int { - i := len(ctx.positions) - 1 - p := ctx.positions[i] - ctx.positions = ctx.positions[:i] - return p -} - -type renderContextData interface { - RenderContext() converter.RenderContext - DocumentContext() converter.DocumentContext - AddIdentity(id identity.Provider) -} - -type renderContextDataHolder struct { - rctx converter.RenderContext - dctx converter.DocumentContext - ids identity.Manager -} - -func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext { - return ctx.rctx -} - -func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext { - return ctx.dctx -} - -func (ctx *renderContextDataHolder) AddIdentity(id identity.Provider) { - ctx.ids.Add(id) -} - var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"} func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) { @@ -251,7 +189,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert } }() - buf := &bufWriter{Buffer: &bytes.Buffer{}} + buf := &render.BufWriter{Buffer: &bytes.Buffer{}} result = buf pctx := c.newParserContext(ctx) reader := text.NewReader(ctx.Src) @@ -261,15 +199,15 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert parser.WithContext(pctx), ) - rcx := &renderContextDataHolder{ - rctx: ctx, - dctx: c.ctx, - ids: identity.NewManager(converterIdentity), + rcx := &render.RenderContextDataHolder{ + Rctx: ctx, + Dctx: c.ctx, + IDs: identity.NewManager(converterIdentity), } - w := &renderContext{ - bufWriter: buf, - renderContextData: rcx, + w := &render.Context{ + BufWriter: buf, + ContextData: rcx, } if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil { @@ -278,7 +216,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert return converterResult{ Result: buf, - ids: rcx.ids.GetIdentities(), + ids: rcx.IDs.GetIdentities(), toc: pctx.TableOfContents(), }, nil } @@ -309,63 +247,3 @@ func (p *parserContext) TableOfContents() tableofcontents.Root { } return tableofcontents.Root{} } - -func newHighlighting(cfg highlight.Config) goldmark.Extender { - return hl.NewHighlighting( - hl.WithStyle(cfg.Style), - hl.WithGuessLanguage(cfg.GuessSyntax), - hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()), - hl.WithFormatOptions( - cfg.ToHTMLOptions()..., - ), - - hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) { - var language string - if l, hasLang := ctx.Language(); hasLang { - language = string(l) - } - - if ctx.Highlighted() { - if entering { - writeDivStart(w, ctx) - } else { - writeDivEnd(w) - } - } else { - if entering { - highlight.WritePreStart(w, language, "") - } else { - highlight.WritePreEnd(w) - } - } - }), - ) -} - -func writeDivStart(w util.BufWriter, ctx hl.CodeBlockContext) { - w.WriteString(`<div class="highlight`) - - var attributes []ast.Attribute - if ctx.Attributes() != nil { - attributes = ctx.Attributes().All() - } - - if attributes != nil { - class, found := ctx.Attributes().GetString("class") - if found { - w.WriteString(" ") - w.Write(util.EscapeHTML(class.([]byte))) - - } - _, _ = w.WriteString("\"") - renderAttributes(w, true, attributes...) - } else { - _, _ = w.WriteString("\"") - } - - w.WriteString(">") -} - -func writeDivEnd(w util.BufWriter) { - w.WriteString("</div>") -} diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go index 684f22c54..ecb308eba 100644 --- a/markup/goldmark/convert_test.go +++ b/markup/goldmark/convert_test.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cast" + "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/markup/highlight" @@ -41,9 +42,18 @@ func convert(c *qt.C, mconf markup_config.Config, content string) converter.Resu }, ) c.Assert(err, qt.IsNil) + h := highlight.New(mconf.Highlight) + + getRenderer := func(t hooks.RendererType, id interface{}) interface{} { + if t == hooks.CodeBlockRendererType { + return h + } + return nil + } + conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"}) c.Assert(err, qt.IsNil) - b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)}) + b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content), GetRenderer: getRenderer}) c.Assert(err, qt.IsNil) return b @@ -372,12 +382,21 @@ LINE5 }, ) + h := highlight.New(conf) + + getRenderer := func(t hooks.RendererType, id interface{}) interface{} { + if t == hooks.CodeBlockRendererType { + return h + } + return nil + } + content := "```" + language + "\n" + code + "\n```" c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - b, err := conv.Convert(converter.RenderContext{Src: []byte(content)}) + b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer}) c.Assert(err, qt.IsNil) return string(b.Bytes()) @@ -391,7 +410,7 @@ LINE5 // TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func. c.Assert(result, qt.Equals, "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"Hugo Rocks!"</span>\n</span></span></code></pre></div>") result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown") - c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo "Hugo Rocks!"\n</code></pre>") + c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo "Hugo Rocks!"\n</code></pre>") }) c.Run("Highlight lines, default config", func(c *qt.C) { diff --git a/markup/goldmark/integration_test.go b/markup/goldmark/integration_test.go index 4ace04f75..f1fa745c5 100644 --- a/markup/goldmark/integration_test.go +++ b/markup/goldmark/integration_test.go @@ -36,12 +36,12 @@ func TestAttributeExclusion(t *testing.T) { --- title: "p1" --- -## Heading {class="a" onclick="alert('heading')" linenos="inline"} +## Heading {class="a" onclick="alert('heading')"} > Blockquote -{class="b" ondblclick="alert('blockquote')" LINENOS="inline"} +{class="b" ondblclick="alert('blockquote')"} -~~~bash {id="c" onmouseover="alert('code fence')"} +~~~bash {id="c" onmouseover="alert('code fence')" LINENOS=true} foo ~~~ -- layouts/_default/single.html -- @@ -96,6 +96,63 @@ title: "p1" `) } +func TestAttributesDefaultRenderer(t *testing.T) { + t.Parallel() + + files := ` +-- content/p1.md -- +--- +title: "p1" +--- +## Heading Attribute Which Needs Escaping { class="a < b" } +-- layouts/_default/single.html -- +{{ .Content }} +` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{ + T: t, + TxtarString: files, + NeedsOsFS: false, + }, + ).Build() + + b.AssertFileContent("public/p1/index.html", ` +class="a < b" + `) +} + +// Issue 9558. +func TestAttributesHookNoEscape(t *testing.T) { + t.Parallel() + + files := ` +-- content/p1.md -- +--- +title: "p1" +--- +## Heading Attribute Which Needs Escaping { class="Smith & Wesson" } +-- layouts/_default/_markup/render-heading.html -- +plain: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v }}|{{ end }}| +safeHTML: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v | safeHT |