render.go (4987B)
1 // Copyright 2022 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package codeblocks
15
16 import (
17 "bytes"
18 "fmt"
19 "sync"
20
21 "github.com/alecthomas/chroma/v2/lexers"
22 "github.com/gohugoio/hugo/common/herrors"
23 htext "github.com/gohugoio/hugo/common/text"
24 "github.com/gohugoio/hugo/markup/converter/hooks"
25 "github.com/gohugoio/hugo/markup/goldmark/internal/render"
26 "github.com/gohugoio/hugo/markup/internal/attributes"
27 "github.com/yuin/goldmark"
28 "github.com/yuin/goldmark/ast"
29 "github.com/yuin/goldmark/parser"
30 "github.com/yuin/goldmark/renderer"
31 "github.com/yuin/goldmark/text"
32 "github.com/yuin/goldmark/util"
33 )
34
35 type (
36 codeBlocksExtension struct{}
37 htmlRenderer struct{}
38 )
39
40 func New() goldmark.Extender {
41 return &codeBlocksExtension{}
42 }
43
44 func (e *codeBlocksExtension) Extend(m goldmark.Markdown) {
45 m.Parser().AddOptions(
46 parser.WithASTTransformers(
47 util.Prioritized(&Transformer{}, 100),
48 ),
49 )
50 m.Renderer().AddOptions(renderer.WithNodeRenderers(
51 util.Prioritized(newHTMLRenderer(), 100),
52 ))
53 }
54
55 func newHTMLRenderer() renderer.NodeRenderer {
56 r := &htmlRenderer{}
57 return r
58 }
59
60 func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
61 reg.Register(KindCodeBlock, r.renderCodeBlock)
62 }
63
64 func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
65 ctx := w.(*render.Context)
66
67 if entering {
68 return ast.WalkContinue, nil
69 }
70
71 n := node.(*codeBlock)
72 lang := string(n.b.Language(src))
73 renderer := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
74 if renderer == nil {
75 return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
76 }
77
78 ordinal := n.ordinal
79
80 var buff bytes.Buffer
81
82 l := n.b.Lines().Len()
83 for i := 0; i < l; i++ {
84 line := n.b.Lines().At(i)
85 buff.Write(line.Value(src))
86 }
87
88 s := htext.Chomp(buff.String())
89
90 var info []byte
91 if n.b.Info != nil {
92 info = n.b.Info.Segment.Value(src)
93 }
94
95 attrtp := attributes.AttributesOwnerCodeBlockCustom
96 if isd, ok := renderer.(hooks.IsDefaultCodeBlockRendererProvider); (ok && isd.IsDefaultCodeBlockRenderer()) || lexers.Get(lang) != nil {
97 // We say that this is a Chroma code block if it's the default code block renderer
98 // or if the language is supported by Chroma.
99 attrtp = attributes.AttributesOwnerCodeBlockChroma
100 }
101
102 // IsDefaultCodeBlockRendererProvider
103 attrs := getAttributes(n.b, info)
104 cbctx := &codeBlockContext{
105 page: ctx.DocumentContext().Document,
106 lang: lang,
107 code: s,
108 ordinal: ordinal,
109 AttributesHolder: attributes.New(attrs, attrtp),
110 }
111
112 cbctx.createPos = func() htext.Position {
113 if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
114 return resolver.ResolvePosition(cbctx)
115 }
116 return htext.Position{
117 Filename: ctx.DocumentContext().Filename,
118 LineNumber: 1,
119 ColumnNumber: 1,
120 }
121 }
122
123 cr := renderer.(hooks.CodeBlockRenderer)
124
125 err := cr.RenderCodeblock(
126 w,
127 cbctx,
128 )
129
130 ctx.AddIdentity(cr)
131
132 if err != nil {
133 return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
134 }
135
136 return ast.WalkContinue, nil
137 }
138
139 type codeBlockContext struct {
140 page any
141 lang string
142 code string
143 ordinal int
144
145 // This is only used in error situations and is expensive to create,
146 // to deleay creation until needed.
147 pos htext.Position
148 posInit sync.Once
149 createPos func() htext.Position
150
151 *attributes.AttributesHolder
152 }
153
154 func (c *codeBlockContext) Page() any {
155 return c.page
156 }
157
158 func (c *codeBlockContext) Type() string {
159 return c.lang
160 }
161
162 func (c *codeBlockContext) Inner() string {
163 return c.code
164 }
165
166 func (c *codeBlockContext) Ordinal() int {
167 return c.ordinal
168 }
169
170 func (c *codeBlockContext) Position() htext.Position {
171 c.posInit.Do(func() {
172 c.pos = c.createPos()
173 })
174 return c.pos
175 }
176
177 func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute {
178 if node.Attributes() != nil {
179 return node.Attributes()
180 }
181 if infostr != nil {
182 attrStartIdx := -1
183
184 for idx, char := range infostr {
185 if char == '{' {
186 attrStartIdx = idx
187 break
188 }
189 }
190
191 if attrStartIdx > 0 {
192 n := ast.NewTextBlock() // dummy node for storing attributes
193 attrStr := infostr[attrStartIdx:]
194 if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
195 for _, attr := range attrs {
196 n.SetAttribute(attr.Name, attr.Value)
197 }
198 return n.Attributes()
199 }
200 }
201 }
202 return nil
203 }