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