github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/markup/goldmark/codeblocks/render.go (about) 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 "strings" 20 "sync" 21 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/highlight/chromalexers" 27 "github.com/gohugoio/hugo/markup/internal/attributes" 28 "github.com/yuin/goldmark" 29 "github.com/yuin/goldmark/ast" 30 "github.com/yuin/goldmark/parser" 31 "github.com/yuin/goldmark/renderer" 32 "github.com/yuin/goldmark/text" 33 "github.com/yuin/goldmark/util" 34 ) 35 36 type ( 37 codeBlocksExtension struct{} 38 htmlRenderer struct{} 39 ) 40 41 func New() goldmark.Extender { 42 return &codeBlocksExtension{} 43 } 44 45 func (e *codeBlocksExtension) Extend(m goldmark.Markdown) { 46 m.Parser().AddOptions( 47 parser.WithASTTransformers( 48 util.Prioritized(&Transformer{}, 100), 49 ), 50 ) 51 m.Renderer().AddOptions(renderer.WithNodeRenderers( 52 util.Prioritized(newHTMLRenderer(), 100), 53 )) 54 } 55 56 func newHTMLRenderer() renderer.NodeRenderer { 57 r := &htmlRenderer{} 58 return r 59 } 60 61 func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 62 reg.Register(KindCodeBlock, r.renderCodeBlock) 63 } 64 65 func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 66 ctx := w.(*render.Context) 67 68 if entering { 69 return ast.WalkContinue, nil 70 } 71 72 n := node.(*codeBlock) 73 lang := getLang(n.b, src) 74 renderer := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang) 75 if renderer == nil { 76 return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang) 77 } 78 79 ordinal := n.ordinal 80 81 var buff bytes.Buffer 82 83 l := n.b.Lines().Len() 84 for i := 0; i < l; i++ { 85 line := n.b.Lines().At(i) 86 buff.Write(line.Value(src)) 87 } 88 89 s := htext.Chomp(buff.String()) 90 91 var info []byte 92 if n.b.Info != nil { 93 info = n.b.Info.Segment.Value(src) 94 } 95 96 attrtp := attributes.AttributesOwnerCodeBlockCustom 97 if isd, ok := renderer.(hooks.IsDefaultCodeBlockRendererProvider); (ok && isd.IsDefaultCodeBlockRenderer()) || chromalexers.Get(lang) != nil { 98 // We say that this is a Chroma code block if it's the default code block renderer 99 // or if the language is supported by Chroma. 100 attrtp = attributes.AttributesOwnerCodeBlockChroma 101 } 102 103 // IsDefaultCodeBlockRendererProvider 104 attrs := getAttributes(n.b, info) 105 cbctx := &codeBlockContext{ 106 page: ctx.DocumentContext().Document, 107 lang: lang, 108 code: s, 109 ordinal: ordinal, 110 AttributesHolder: attributes.New(attrs, attrtp), 111 } 112 113 cbctx.createPos = func() htext.Position { 114 if resolver, ok := renderer.(hooks.ElementPositionResolver); ok { 115 return resolver.ResolvePosition(cbctx) 116 } 117 return htext.Position{ 118 Filename: ctx.DocumentContext().Filename, 119 LineNumber: 1, 120 ColumnNumber: 1, 121 } 122 } 123 124 cr := renderer.(hooks.CodeBlockRenderer) 125 126 err := cr.RenderCodeblock( 127 ctx.RenderContext().Ctx, 128 w, 129 cbctx, 130 ) 131 132 ctx.AddIdentity(cr) 133 134 if err != nil { 135 return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos()) 136 } 137 138 return ast.WalkContinue, nil 139 } 140 141 type codeBlockContext struct { 142 page any 143 lang string 144 code string 145 ordinal int 146 147 // This is only used in error situations and is expensive to create, 148 // to deleay creation until needed. 149 pos htext.Position 150 posInit sync.Once 151 createPos func() htext.Position 152 153 *attributes.AttributesHolder 154 } 155 156 func (c *codeBlockContext) Page() any { 157 return c.page 158 } 159 160 func (c *codeBlockContext) Type() string { 161 return c.lang 162 } 163 164 func (c *codeBlockContext) Inner() string { 165 return c.code 166 } 167 168 func (c *codeBlockContext) Ordinal() int { 169 return c.ordinal 170 } 171 172 func (c *codeBlockContext) Position() htext.Position { 173 c.posInit.Do(func() { 174 c.pos = c.createPos() 175 }) 176 return c.pos 177 } 178 179 func getLang(node *ast.FencedCodeBlock, src []byte) string { 180 langWithAttributes := string(node.Language(src)) 181 lang, _, _ := strings.Cut(langWithAttributes, "{") 182 return lang 183 } 184 185 func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute { 186 if node.Attributes() != nil { 187 return node.Attributes() 188 } 189 if infostr != nil { 190 attrStartIdx := -1 191 192 for idx, char := range infostr { 193 if char == '{' { 194 attrStartIdx = idx 195 break 196 } 197 } 198 199 if attrStartIdx != -1 { 200 n := ast.NewTextBlock() // dummy node for storing attributes 201 attrStr := infostr[attrStartIdx:] 202 if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { 203 for _, attr := range attrs { 204 n.SetAttribute(attr.Name, attr.Value) 205 } 206 return n.Attributes() 207 } 208 } 209 } 210 return nil 211 }