github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/markup/goldmark/render_hooks.go (about) 1 // Copyright 2019 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 goldmark 15 16 import ( 17 "bytes" 18 "strings" 19 20 "github.com/gohugoio/hugo/markup/converter/hooks" 21 "github.com/gohugoio/hugo/markup/goldmark/internal/render" 22 "github.com/gohugoio/hugo/markup/internal/attributes" 23 24 "github.com/yuin/goldmark" 25 "github.com/yuin/goldmark/ast" 26 "github.com/yuin/goldmark/renderer" 27 "github.com/yuin/goldmark/renderer/html" 28 "github.com/yuin/goldmark/util" 29 ) 30 31 var _ renderer.SetOptioner = (*hookedRenderer)(nil) 32 33 func newLinkRenderer() renderer.NodeRenderer { 34 r := &hookedRenderer{ 35 Config: html.Config{ 36 Writer: html.DefaultWriter, 37 }, 38 } 39 return r 40 } 41 42 func newLinks() goldmark.Extender { 43 return &links{} 44 } 45 46 type linkContext struct { 47 page interface{} 48 destination string 49 title string 50 text string 51 plainText string 52 } 53 54 func (ctx linkContext) Destination() string { 55 return ctx.destination 56 } 57 58 func (ctx linkContext) Resolved() bool { 59 return false 60 } 61 62 func (ctx linkContext) Page() interface{} { 63 return ctx.page 64 } 65 66 func (ctx linkContext) Text() string { 67 return ctx.text 68 } 69 70 func (ctx linkContext) PlainText() string { 71 return ctx.plainText 72 } 73 74 func (ctx linkContext) Title() string { 75 return ctx.title 76 } 77 78 type headingContext struct { 79 page interface{} 80 level int 81 anchor string 82 text string 83 plainText string 84 *attributes.AttributesHolder 85 } 86 87 func (ctx headingContext) Page() interface{} { 88 return ctx.page 89 } 90 91 func (ctx headingContext) Level() int { 92 return ctx.level 93 } 94 95 func (ctx headingContext) Anchor() string { 96 return ctx.anchor 97 } 98 99 func (ctx headingContext) Text() string { 100 return ctx.text 101 } 102 103 func (ctx headingContext) PlainText() string { 104 return ctx.plainText 105 } 106 107 type hookedRenderer struct { 108 html.Config 109 } 110 111 func (r *hookedRenderer) SetOption(name renderer.OptionName, value interface{}) { 112 r.Config.SetOption(name, value) 113 } 114 115 // RegisterFuncs implements NodeRenderer.RegisterFuncs. 116 func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 117 reg.Register(ast.KindLink, r.renderLink) 118 reg.Register(ast.KindAutoLink, r.renderAutoLink) 119 reg.Register(ast.KindImage, r.renderImage) 120 reg.Register(ast.KindHeading, r.renderHeading) 121 } 122 123 func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 124 n := node.(*ast.Image) 125 var lr hooks.LinkRenderer 126 127 ctx, ok := w.(*render.Context) 128 if ok { 129 h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil) 130 ok = h != nil 131 if ok { 132 lr = h.(hooks.LinkRenderer) 133 } 134 } 135 136 if !ok { 137 return r.renderImageDefault(w, source, node, entering) 138 } 139 140 if entering { 141 // Store the current pos so we can capture the rendered text. 142 ctx.PushPos(ctx.Buffer.Len()) 143 return ast.WalkContinue, nil 144 } 145 146 pos := ctx.PopPos() 147 text := ctx.Buffer.Bytes()[pos:] 148 ctx.Buffer.Truncate(pos) 149 150 err := lr.RenderLink( 151 w, 152 linkContext{ 153 page: ctx.DocumentContext().Document, 154 destination: string(n.Destination), 155 title: string(n.Title), 156 text: string(text), 157 plainText: string(n.Text(source)), 158 }, 159 ) 160 161 ctx.AddIdentity(lr) 162 163 return ast.WalkContinue, err 164 } 165 166 // Fall back to the default Goldmark render funcs. Method below borrowed from: 167 // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404 168 func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 169 if !entering { 170 return ast.WalkContinue, nil 171 } 172 n := node.(*ast.Image) 173 _, _ = w.WriteString("<img src=\"") 174 if r.Unsafe || !html.IsDangerousURL(n.Destination) { 175 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) 176 } 177 _, _ = w.WriteString(`" alt="`) 178 _, _ = w.Write(n.Text(source)) 179 _ = w.WriteByte('"') 180 if n.Title != nil { 181 _, _ = w.WriteString(` title="`) 182 r.Writer.Write(w, n.Title) 183 _ = w.WriteByte('"') 184 } 185 if r.XHTML { 186 _, _ = w.WriteString(" />") 187 } else { 188 _, _ = w.WriteString(">") 189 } 190 return ast.WalkSkipChildren, nil 191 } 192 193 func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 194 n := node.(*ast.Link) 195 var lr hooks.LinkRenderer 196 197 ctx, ok := w.(*render.Context) 198 if ok { 199 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil) 200 ok = h != nil 201 if ok { 202 lr = h.(hooks.LinkRenderer) 203 } 204 } 205 206 if !ok { 207 return r.renderLinkDefault(w, source, node, entering) 208 } 209 210 if entering { 211 // Store the current pos so we can capture the rendered text. 212 ctx.PushPos(ctx.Buffer.Len()) 213 return ast.WalkContinue, nil 214 } 215 216 pos := ctx.PopPos() 217 text := ctx.Buffer.Bytes()[pos:] 218 ctx.Buffer.Truncate(pos) 219 220 err := lr.RenderLink( 221 w, 222 linkContext{ 223 page: ctx.DocumentContext().Document, 224 destination: string(n.Destination), 225 title: string(n.Title), 226 text: string(text), 227 plainText: string(n.Text(source)), 228 }, 229 ) 230 231 // TODO(bep) I have a working branch that fixes these rather confusing identity types, 232 // but for now it's important that it's not .GetIdentity() that's added here, 233 // to make sure we search the entire chain on changes. 234 ctx.AddIdentity(lr) 235 236 return ast.WalkContinue, err 237 } 238 239 // Fall back to the default Goldmark render funcs. Method below borrowed from: 240 // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404 241 func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 242 n := node.(*ast.Link) 243 if entering { 244 _, _ = w.WriteString("<a href=\"") 245 if r.Unsafe || !html.IsDangerousURL(n.Destination) { 246 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) 247 } 248 _ = w.WriteByte('"') 249 if n.Title != nil { 250 _, _ = w.WriteString(` title="`) 251 r.Writer.Write(w, n.Title) 252 _ = w.WriteByte('"') 253 } 254 _ = w.WriteByte('>') 255 } else { 256 _, _ = w.WriteString("</a>") 257 } 258 return ast.WalkContinue, nil 259 } 260 261 func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 262 if !entering { 263 return ast.WalkContinue, nil 264 } 265 266 n := node.(*ast.AutoLink) 267 var lr hooks.LinkRenderer 268 269 ctx, ok := w.(*render.Context) 270 if ok { 271 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil) 272 ok = h != nil 273 if ok { 274 lr = h.(hooks.LinkRenderer) 275 } 276 } 277 278 if !ok { 279 return r.renderAutoLinkDefault(w, source, node, entering) 280 } 281 282 url := string(n.URL(source)) 283 label := string(n.Label(source)) 284 if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") { 285 url = "mailto:" + url 286 } 287 288 err := lr.RenderLink( 289 w, 290 linkContext{ 291 page: ctx.DocumentContext().Document, 292 destination: url, 293 text: label, 294 plainText: label, 295 }, 296 ) 297 298 // TODO(bep) I have a working branch that fixes these rather confusing identity types, 299 // but for now it's important that it's not .GetIdentity() that's added here, 300 // to make sure we search the entire chain on changes. 301 ctx.AddIdentity(lr) 302 303 return ast.WalkContinue, err 304 } 305 306 // Fall back to the default Goldmark render funcs. Method below borrowed from: 307 // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439 308 func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 309 n := node.(*ast.AutoLink) 310 if !entering { 311 return ast.WalkContinue, nil 312 } 313 _, _ = w.WriteString(`<a href="`) 314 url := n.URL(source) 315 label := n.Label(source) 316 if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { 317 _, _ = w.WriteString("mailto:") 318 } 319 _, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false))) 320 if n.Attributes() != nil { 321 _ = w.WriteByte('"') 322 html.RenderAttributes(w, n, html.LinkAttributeFilter) 323 _ = w.WriteByte('>') 324 } else { 325 _, _ = w.WriteString(`">`) 326 } 327 _, _ = w.Write(util.EscapeHTML(label)) 328 _, _ = w.WriteString(`</a>`) 329 return ast.WalkContinue, nil 330 } 331 332 func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 333 n := node.(*ast.Heading) 334 var hr hooks.HeadingRenderer 335 336 ctx, ok := w.(*render.Context) 337 if ok { 338 h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil) 339 ok = h != nil 340 if ok { 341 hr = h.(hooks.HeadingRenderer) 342 } 343 } 344 345 if !ok { 346 return r.renderHeadingDefault(w, source, node, entering) 347 } 348 349 if entering { 350 // Store the current pos so we can capture the rendered text. 351 ctx.PushPos(ctx.Buffer.Len()) 352 return ast.WalkContinue, nil 353 } 354 355 pos := ctx.PopPos() 356 text := ctx.Buffer.Bytes()[pos:] 357 ctx.Buffer.Truncate(pos) 358 // All ast.Heading nodes are guaranteed to have an attribute called "id" 359 // that is an array of bytes that encode a valid string. 360 anchori, _ := n.AttributeString("id") 361 anchor := anchori.([]byte) 362 363 err := hr.RenderHeading( 364 w, 365 headingContext{ 366 page: ctx.DocumentContext().Document, 367 level: n.Level, 368 anchor: string(anchor), 369 text: string(text), 370 plainText: string(n.Text(source)), 371 AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral), 372 }, 373 ) 374 375 ctx.AddIdentity(hr) 376 377 return ast.WalkContinue, err 378 } 379 380 func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 381 n := node.(*ast.Heading) 382 if entering { 383 _, _ = w.WriteString("<h") 384 _ = w.WriteByte("0123456"[n.Level]) 385 if n.Attributes() != nil { 386 attributes.RenderASTAttributes(w, node.Attributes()...) 387 } 388 _ = w.WriteByte('>') 389 } else { 390 _, _ = w.WriteString("</h") 391 _ = w.WriteByte("0123456"[n.Level]) 392 _, _ = w.WriteString(">\n") 393 } 394 return ast.WalkContinue, nil 395 } 396 397 type links struct{} 398 399 // Extend implements goldmark.Extender. 400 func (e *links) Extend(m goldmark.Markdown) { 401 m.Renderer().AddOptions(renderer.WithNodeRenderers( 402 util.Prioritized(newLinkRenderer(), 100), 403 )) 404 }