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