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