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