mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-29 22:29:56 +02:00
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
This commit is contained in:
115
markup/goldmark/codeblocks/integration_test.go
Normal file
115
markup/goldmark/codeblocks/integration_test.go
Normal file
@@ -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>",
|
||||
)
|
||||
}
|
159
markup/goldmark/codeblocks/render.go
Normal file
159
markup/goldmark/codeblocks/render.go
Normal file
@@ -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
|
||||
}
|
53
markup/goldmark/codeblocks/transform.go
Normal file
53
markup/goldmark/codeblocks/transform.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>")
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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 | safeHTML }}|{{ end }}|
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Content }}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
NeedsOsFS: false,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", `
|
||||
plain: |class: Smith & Wesson|id: heading-attribute-which-needs-escaping|
|
||||
safeHTML: |class: Smith & Wesson|id: heading-attribute-which-needs-escaping|
|
||||
`)
|
||||
}
|
||||
|
||||
// Issue 9504
|
||||
func TestLinkInTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -132,6 +189,84 @@ title: "p1"
|
||||
)
|
||||
}
|
||||
|
||||
func TestHighlight(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/single.html --
|
||||
{{ .Content }}
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
|
||||
## Code Fences
|
||||
|
||||
§§§bash
|
||||
LINE1
|
||||
§§§
|
||||
|
||||
## Code Fences No Lexer
|
||||
|
||||
§§§moo
|
||||
LINE1
|
||||
§§§
|
||||
|
||||
## Code Fences Simple Attributes
|
||||
|
||||
§§A§bash { .myclass id="myid" }
|
||||
LINE1
|
||||
§§A§
|
||||
|
||||
## Code Fences Line Numbers
|
||||
|
||||
§§§bash {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
|
||||
LINE1
|
||||
LINE2
|
||||
LINE3
|
||||
LINE4
|
||||
LINE5
|
||||
LINE6
|
||||
LINE7
|
||||
LINE8
|
||||
§§§
|
||||
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
// Code fences
|
||||
files = strings.ReplaceAll(files, "§§§", "```")
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\">LINE1\n</span></span></code></pre></div>",
|
||||
"Code Fences No Lexer</h2>\n<pre tabindex=\"0\"><code class=\"language-moo\" data-lang=\"moo\">LINE1\n</code></pre>",
|
||||
"lnt",
|
||||
)
|
||||
}
|
||||
|
||||
func BenchmarkRenderHooks(b *testing.B) {
|
||||
files := `
|
||||
-- config.toml --
|
||||
|
81
markup/goldmark/internal/render/context.go
Normal file
81
markup/goldmark/internal/render/context.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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 render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/bits"
|
||||
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
)
|
||||
|
||||
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 Context struct {
|
||||
*BufWriter
|
||||
positions []int
|
||||
ContextData
|
||||
}
|
||||
|
||||
func (ctx *Context) PushPos(n int) {
|
||||
ctx.positions = append(ctx.positions, n)
|
||||
}
|
||||
|
||||
func (ctx *Context) PopPos() int {
|
||||
i := len(ctx.positions) - 1
|
||||
p := ctx.positions[i]
|
||||
ctx.positions = ctx.positions[:i]
|
||||
return p
|
||||
}
|
||||
|
||||
type ContextData 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)
|
||||
}
|
@@ -16,11 +16,10 @@ package goldmark
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"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"
|
||||
@@ -44,28 +43,6 @@ func newLinks() goldmark.Extender {
|
||||
return &links{}
|
||||
}
|
||||
|
||||
type attributesHolder struct {
|
||||
// What we get from Goldmark.
|
||||
astAttributes []ast.Attribute
|
||||
|
||||
// What we send to the the render hooks.
|
||||
attributesInit sync.Once
|
||||
attributes map[string]string
|
||||
}
|
||||
|
||||
func (a *attributesHolder) Attributes() map[string]string {
|
||||
a.attributesInit.Do(func() {
|
||||
a.attributes = make(map[string]string)
|
||||
for _, attr := range a.astAttributes {
|
||||
if strings.HasPrefix(string(attr.Name), "on") {
|
||||
continue
|
||||
}
|
||||
a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
|
||||
}
|
||||
})
|
||||
return a.attributes
|
||||
}
|
||||
|
||||
type linkContext struct {
|
||||
page interface{}
|
||||
destination string
|
||||
@@ -104,7 +81,7 @@ type headingContext struct {
|
||||
anchor string
|
||||
text string
|
||||
plainText string
|
||||
*attributesHolder
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
|
||||
func (ctx headingContext) Page() interface{} {
|
||||
@@ -143,52 +120,17 @@ func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
|
||||
reg.Register(ast.KindHeading, r.renderHeading)
|
||||
}
|
||||
|
||||
func (r *hookedRenderer) renderAttributesForNode(w util.BufWriter, node ast.Node) {
|
||||
renderAttributes(w, false, node.Attributes()...)
|
||||
}
|
||||
|
||||
// Attributes with special meaning that does not make sense to render in HTML.
|
||||
var attributeExcludes = map[string]bool{
|
||||
"hl_lines": true,
|
||||
"hl_style": true,
|
||||
"linenos": true,
|
||||
"linenostart": true,
|
||||
}
|
||||
|
||||
func renderAttributes(w util.BufWriter, skipClass bool, attributes ...ast.Attribute) {
|
||||
for _, attr := range attributes {
|
||||
if skipClass && bytes.Equal(attr.Name, []byte("class")) {
|
||||
continue
|
||||
}
|
||||
|
||||
a := strings.ToLower(string(attr.Name))
|
||||
if attributeExcludes[a] || strings.HasPrefix(a, "on") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = w.WriteString(" ")
|
||||
_, _ = w.Write(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
|
||||
switch v := attr.Value.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(util.EscapeHTML(v))
|
||||
default:
|
||||
w.WriteString(cast.ToString(v))
|
||||
}
|
||||
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Image)
|
||||
var h hooks.Renderers
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.ImageRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -197,15 +139,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
|
||||
err := h.ImageRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -216,7 +158,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
},
|
||||
)
|
||||
|
||||
ctx.AddIdentity(h.ImageRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -250,12 +192,15 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
|
||||
|
||||
func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
var h hooks.Renderers
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.LinkRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -264,15 +209,15 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
|
||||
err := h.LinkRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -286,7 +231,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
|
||||
// but for now it's important that it's not .GetIdentity() that's added here,
|
||||
// to make sure we search the entire chain on changes.
|
||||
ctx.AddIdentity(h.LinkRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -319,12 +264,15 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
}
|
||||
|
||||
n := node.(*ast.AutoLink)
|
||||
var h hooks.Renderers
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.LinkRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -337,7 +285,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
url = "mailto:" + url
|
||||
}
|
||||
|
||||
err := h.LinkRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -350,7 +298,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
|
||||
// but for now it's important that it's not .GetIdentity() that's added here,
|
||||
// to make sure we search the entire chain on changes.
|
||||
ctx.AddIdentity(h.LinkRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -383,12 +331,15 @@ func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte,
|
||||
|
||||
func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Heading)
|
||||
var h hooks.Renderers
|
||||
var hr hooks.HeadingRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.HeadingRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
hr = h.(hooks.HeadingRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -397,11 +348,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
// All ast.Heading nodes are guaranteed to have an attribute called "id"
|
||||
@@ -409,7 +360,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
anchori, _ := n.AttributeString("id")
|
||||
anchor := anchori.([]byte)
|
||||
|
||||
err := h.HeadingRenderer.RenderHeading(
|
||||
err := hr.RenderHeading(
|
||||
w,
|
||||
headingContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -417,11 +368,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
anchor: string(anchor),
|
||||
text: string(text),
|
||||
plainText: string(n.Text(source)),
|
||||
attributesHolder: &attributesHolder{astAttributes: n.Attributes()},
|
||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
},
|
||||
)
|
||||
|
||||
ctx.AddIdentity(h.HeadingRenderer)
|
||||
ctx.AddIdentity(hr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -432,7 +383,7 @@ func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, n
|
||||
_, _ = w.WriteString("<h")
|
||||
_ = w.WriteByte("0123456"[n.Level])
|
||||
if n.Attributes() != nil {
|
||||
r.renderAttributesForNode(w, node)
|
||||
attributes.RenderASTAttributes(w, node.Attributes()...)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var nopGetRenderer = func(t hooks.RendererType, id interface{}) interface{} { return nil }
|
||||
|
||||
func TestToc(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
@@ -58,7 +61,7 @@ And then some.
|
||||
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), RenderTOC: true})
|
||||
b, err := conv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
|
||||
c.Assert(err, qt.IsNil)
|
||||
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3, false)
|
||||
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
|
||||
@@ -108,7 +111,7 @@ func TestEscapeToc(t *testing.T) {
|
||||
"# `echo codeblock`",
|
||||
}, "\n")
|
||||
// content := ""
|
||||
b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
|
||||
b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
|
||||
c.Assert(err, qt.IsNil)
|
||||
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
|
||||
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
|
||||
@@ -120,7 +123,7 @@ func TestEscapeToc(t *testing.T) {
|
||||
</ul>
|
||||
</nav>`, qt.Commentf(got))
|
||||
|
||||
b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
|
||||
b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
|
||||
c.Assert(err, qt.IsNil)
|
||||
got = b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
|
||||
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
|
||||
|
Reference in New Issue
Block a user