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/goldmark | |
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/goldmark')
-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 |
9 files changed, 633 insertions, 239 deletions
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 | 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 -- diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go new file mode 100644 index 000000000..b18983ef3 --- /dev/null +++ b/markup/goldmark/internal/render/context.go @@ -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) +} |