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 }