html.go (24702B)
1 package html 2 3 import ( 4 "bytes" 5 "fmt" 6 "strconv" 7 "unicode/utf8" 8 9 "github.com/yuin/goldmark/ast" 10 "github.com/yuin/goldmark/renderer" 11 "github.com/yuin/goldmark/util" 12 ) 13 14 // A Config struct has configurations for the HTML based renderers. 15 type Config struct { 16 Writer Writer 17 HardWraps bool 18 EastAsianLineBreaks bool 19 XHTML bool 20 Unsafe bool 21 } 22 23 // NewConfig returns a new Config with defaults. 24 func NewConfig() Config { 25 return Config{ 26 Writer: DefaultWriter, 27 HardWraps: false, 28 EastAsianLineBreaks: false, 29 XHTML: false, 30 Unsafe: false, 31 } 32 } 33 34 // SetOption implements renderer.NodeRenderer.SetOption. 35 func (c *Config) SetOption(name renderer.OptionName, value interface{}) { 36 switch name { 37 case optHardWraps: 38 c.HardWraps = value.(bool) 39 case optEastAsianLineBreaks: 40 c.EastAsianLineBreaks = value.(bool) 41 case optXHTML: 42 c.XHTML = value.(bool) 43 case optUnsafe: 44 c.Unsafe = value.(bool) 45 case optTextWriter: 46 c.Writer = value.(Writer) 47 } 48 } 49 50 // An Option interface sets options for HTML based renderers. 51 type Option interface { 52 SetHTMLOption(*Config) 53 } 54 55 // TextWriter is an option name used in WithWriter. 56 const optTextWriter renderer.OptionName = "Writer" 57 58 type withWriter struct { 59 value Writer 60 } 61 62 func (o *withWriter) SetConfig(c *renderer.Config) { 63 c.Options[optTextWriter] = o.value 64 } 65 66 func (o *withWriter) SetHTMLOption(c *Config) { 67 c.Writer = o.value 68 } 69 70 // WithWriter is a functional option that allow you to set the given writer to 71 // the renderer. 72 func WithWriter(writer Writer) interface { 73 renderer.Option 74 Option 75 } { 76 return &withWriter{writer} 77 } 78 79 // HardWraps is an option name used in WithHardWraps. 80 const optHardWraps renderer.OptionName = "HardWraps" 81 82 type withHardWraps struct { 83 } 84 85 func (o *withHardWraps) SetConfig(c *renderer.Config) { 86 c.Options[optHardWraps] = true 87 } 88 89 func (o *withHardWraps) SetHTMLOption(c *Config) { 90 c.HardWraps = true 91 } 92 93 // WithHardWraps is a functional option that indicates whether softline breaks 94 // should be rendered as '<br>'. 95 func WithHardWraps() interface { 96 renderer.Option 97 Option 98 } { 99 return &withHardWraps{} 100 } 101 102 // EastAsianLineBreaks is an option name used in WithEastAsianLineBreaks. 103 const optEastAsianLineBreaks renderer.OptionName = "EastAsianLineBreaks" 104 105 type withEastAsianLineBreaks struct { 106 } 107 108 func (o *withEastAsianLineBreaks) SetConfig(c *renderer.Config) { 109 c.Options[optEastAsianLineBreaks] = true 110 } 111 112 func (o *withEastAsianLineBreaks) SetHTMLOption(c *Config) { 113 c.EastAsianLineBreaks = true 114 } 115 116 // WithEastAsianLineBreaks is a functional option that indicates whether softline breaks 117 // between east asian wide characters should be ignored. 118 func WithEastAsianLineBreaks() interface { 119 renderer.Option 120 Option 121 } { 122 return &withEastAsianLineBreaks{} 123 } 124 125 // XHTML is an option name used in WithXHTML. 126 const optXHTML renderer.OptionName = "XHTML" 127 128 type withXHTML struct { 129 } 130 131 func (o *withXHTML) SetConfig(c *renderer.Config) { 132 c.Options[optXHTML] = true 133 } 134 135 func (o *withXHTML) SetHTMLOption(c *Config) { 136 c.XHTML = true 137 } 138 139 // WithXHTML is a functional option indicates that nodes should be rendered in 140 // xhtml instead of HTML5. 141 func WithXHTML() interface { 142 Option 143 renderer.Option 144 } { 145 return &withXHTML{} 146 } 147 148 // Unsafe is an option name used in WithUnsafe. 149 const optUnsafe renderer.OptionName = "Unsafe" 150 151 type withUnsafe struct { 152 } 153 154 func (o *withUnsafe) SetConfig(c *renderer.Config) { 155 c.Options[optUnsafe] = true 156 } 157 158 func (o *withUnsafe) SetHTMLOption(c *Config) { 159 c.Unsafe = true 160 } 161 162 // WithUnsafe is a functional option that renders dangerous contents 163 // (raw htmls and potentially dangerous links) as it is. 164 func WithUnsafe() interface { 165 renderer.Option 166 Option 167 } { 168 return &withUnsafe{} 169 } 170 171 // A Renderer struct is an implementation of renderer.NodeRenderer that renders 172 // nodes as (X)HTML. 173 type Renderer struct { 174 Config 175 } 176 177 // NewRenderer returns a new Renderer with given options. 178 func NewRenderer(opts ...Option) renderer.NodeRenderer { 179 r := &Renderer{ 180 Config: NewConfig(), 181 } 182 183 for _, opt := range opts { 184 opt.SetHTMLOption(&r.Config) 185 } 186 return r 187 } 188 189 // RegisterFuncs implements NodeRenderer.RegisterFuncs . 190 func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 191 // blocks 192 193 reg.Register(ast.KindDocument, r.renderDocument) 194 reg.Register(ast.KindHeading, r.renderHeading) 195 reg.Register(ast.KindBlockquote, r.renderBlockquote) 196 reg.Register(ast.KindCodeBlock, r.renderCodeBlock) 197 reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) 198 reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) 199 reg.Register(ast.KindList, r.renderList) 200 reg.Register(ast.KindListItem, r.renderListItem) 201 reg.Register(ast.KindParagraph, r.renderParagraph) 202 reg.Register(ast.KindTextBlock, r.renderTextBlock) 203 reg.Register(ast.KindThematicBreak, r.renderThematicBreak) 204 205 // inlines 206 207 reg.Register(ast.KindAutoLink, r.renderAutoLink) 208 reg.Register(ast.KindCodeSpan, r.renderCodeSpan) 209 reg.Register(ast.KindEmphasis, r.renderEmphasis) 210 reg.Register(ast.KindImage, r.renderImage) 211 reg.Register(ast.KindLink, r.renderLink) 212 reg.Register(ast.KindRawHTML, r.renderRawHTML) 213 reg.Register(ast.KindText, r.renderText) 214 reg.Register(ast.KindString, r.renderString) 215 } 216 217 func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) { 218 l := n.Lines().Len() 219 for i := 0; i < l; i++ { 220 line := n.Lines().At(i) 221 r.Writer.RawWrite(w, line.Value(source)) 222 } 223 } 224 225 // GlobalAttributeFilter defines attribute names which any elements can have. 226 var GlobalAttributeFilter = util.NewBytesFilter( 227 []byte("accesskey"), 228 []byte("autocapitalize"), 229 []byte("autofocus"), 230 []byte("class"), 231 []byte("contenteditable"), 232 []byte("dir"), 233 []byte("draggable"), 234 []byte("enterkeyhint"), 235 []byte("hidden"), 236 []byte("id"), 237 []byte("inert"), 238 []byte("inputmode"), 239 []byte("is"), 240 []byte("itemid"), 241 []byte("itemprop"), 242 []byte("itemref"), 243 []byte("itemscope"), 244 []byte("itemtype"), 245 []byte("lang"), 246 []byte("part"), 247 []byte("role"), 248 []byte("slot"), 249 []byte("spellcheck"), 250 []byte("style"), 251 []byte("tabindex"), 252 []byte("title"), 253 []byte("translate"), 254 ) 255 256 func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 257 // nothing to do 258 return ast.WalkContinue, nil 259 } 260 261 // HeadingAttributeFilter defines attribute names which heading elements can have 262 var HeadingAttributeFilter = GlobalAttributeFilter 263 264 func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 265 n := node.(*ast.Heading) 266 if entering { 267 _, _ = w.WriteString("<h") 268 _ = w.WriteByte("0123456"[n.Level]) 269 if n.Attributes() != nil { 270 RenderAttributes(w, node, HeadingAttributeFilter) 271 } 272 _ = w.WriteByte('>') 273 } else { 274 _, _ = w.WriteString("</h") 275 _ = w.WriteByte("0123456"[n.Level]) 276 _, _ = w.WriteString(">\n") 277 } 278 return ast.WalkContinue, nil 279 } 280 281 // BlockquoteAttributeFilter defines attribute names which blockquote elements can have 282 var BlockquoteAttributeFilter = GlobalAttributeFilter.Extend( 283 []byte("cite"), 284 ) 285 286 func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 287 if entering { 288 if n.Attributes() != nil { 289 _, _ = w.WriteString("<blockquote") 290 RenderAttributes(w, n, BlockquoteAttributeFilter) 291 _ = w.WriteByte('>') 292 } else { 293 _, _ = w.WriteString("<blockquote>\n") 294 } 295 } else { 296 _, _ = w.WriteString("</blockquote>\n") 297 } 298 return ast.WalkContinue, nil 299 } 300 301 func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 302 if entering { 303 _, _ = w.WriteString("<pre><code>") 304 r.writeLines(w, source, n) 305 } else { 306 _, _ = w.WriteString("</code></pre>\n") 307 } 308 return ast.WalkContinue, nil 309 } 310 311 func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 312 n := node.(*ast.FencedCodeBlock) 313 if entering { 314 _, _ = w.WriteString("<pre><code") 315 language := n.Language(source) 316 if language != nil { 317 _, _ = w.WriteString(" class=\"language-") 318 r.Writer.Write(w, language) 319 _, _ = w.WriteString("\"") 320 } 321 _ = w.WriteByte('>') 322 r.writeLines(w, source, n) 323 } else { 324 _, _ = w.WriteString("</code></pre>\n") 325 } 326 return ast.WalkContinue, nil 327 } 328 329 func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 330 n := node.(*ast.HTMLBlock) 331 if entering { 332 if r.Unsafe { 333 l := n.Lines().Len() 334 for i := 0; i < l; i++ { 335 line := n.Lines().At(i) 336 r.Writer.SecureWrite(w, line.Value(source)) 337 } 338 } else { 339 _, _ = w.WriteString("<!-- raw HTML omitted -->\n") 340 } 341 } else { 342 if n.HasClosure() { 343 if r.Unsafe { 344 closure := n.ClosureLine 345 r.Writer.SecureWrite(w, closure.Value(source)) 346 } else { 347 _, _ = w.WriteString("<!-- raw HTML omitted -->\n") 348 } 349 } 350 } 351 return ast.WalkContinue, nil 352 } 353 354 // ListAttributeFilter defines attribute names which list elements can have. 355 var ListAttributeFilter = GlobalAttributeFilter.Extend( 356 []byte("start"), 357 []byte("reversed"), 358 []byte("type"), 359 ) 360 361 func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 362 n := node.(*ast.List) 363 tag := "ul" 364 if n.IsOrdered() { 365 tag = "ol" 366 } 367 if entering { 368 _ = w.WriteByte('<') 369 _, _ = w.WriteString(tag) 370 if n.IsOrdered() && n.Start != 1 { 371 fmt.Fprintf(w, " start=\"%d\"", n.Start) 372 } 373 if n.Attributes() != nil { 374 RenderAttributes(w, n, ListAttributeFilter) 375 } 376 _, _ = w.WriteString(">\n") 377 } else { 378 _, _ = w.WriteString("</") 379 _, _ = w.WriteString(tag) 380 _, _ = w.WriteString(">\n") 381 } 382 return ast.WalkContinue, nil 383 } 384 385 // ListItemAttributeFilter defines attribute names which list item elements can have. 386 var ListItemAttributeFilter = GlobalAttributeFilter.Extend( 387 []byte("value"), 388 ) 389 390 func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 391 if entering { 392 if n.Attributes() != nil { 393 _, _ = w.WriteString("<li") 394 RenderAttributes(w, n, ListItemAttributeFilter) 395 _ = w.WriteByte('>') 396 } else { 397 _, _ = w.WriteString("<li>") 398 } 399 fc := n.FirstChild() 400 if fc != nil { 401 if _, ok := fc.(*ast.TextBlock); !ok { 402 _ = w.WriteByte('\n') 403 } 404 } 405 } else { 406 _, _ = w.WriteString("</li>\n") 407 } 408 return ast.WalkContinue, nil 409 } 410 411 // ParagraphAttributeFilter defines attribute names which paragraph elements can have. 412 var ParagraphAttributeFilter = GlobalAttributeFilter 413 414 func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 415 if entering { 416 if n.Attributes() != nil { 417 _, _ = w.WriteString("<p") 418 RenderAttributes(w, n, ParagraphAttributeFilter) 419 _ = w.WriteByte('>') 420 } else { 421 _, _ = w.WriteString("<p>") 422 } 423 } else { 424 _, _ = w.WriteString("</p>\n") 425 } 426 return ast.WalkContinue, nil 427 } 428 429 func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 430 if !entering { 431 if _, ok := n.NextSibling().(ast.Node); ok && n.FirstChild() != nil { 432 _ = w.WriteByte('\n') 433 } 434 } 435 return ast.WalkContinue, nil 436 } 437 438 // ThematicAttributeFilter defines attribute names which hr elements can have. 439 var ThematicAttributeFilter = GlobalAttributeFilter.Extend( 440 []byte("align"), // [Deprecated] 441 []byte("color"), // [Not Standardized] 442 []byte("noshade"), // [Deprecated] 443 []byte("size"), // [Deprecated] 444 []byte("width"), // [Deprecated] 445 ) 446 447 func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 448 if !entering { 449 return ast.WalkContinue, nil 450 } 451 _, _ = w.WriteString("<hr") 452 if n.Attributes() != nil { 453 RenderAttributes(w, n, ThematicAttributeFilter) 454 } 455 if r.XHTML { 456 _, _ = w.WriteString(" />\n") 457 } else { 458 _, _ = w.WriteString(">\n") 459 } 460 return ast.WalkContinue, nil 461 } 462 463 // LinkAttributeFilter defines attribute names which link elements can have. 464 var LinkAttributeFilter = GlobalAttributeFilter.Extend( 465 []byte("download"), 466 // []byte("href"), 467 []byte("hreflang"), 468 []byte("media"), 469 []byte("ping"), 470 []byte("referrerpolicy"), 471 []byte("rel"), 472 []byte("shape"), 473 []byte("target"), 474 ) 475 476 func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 477 n := node.(*ast.AutoLink) 478 if !entering { 479 return ast.WalkContinue, nil 480 } 481 _, _ = w.WriteString(`<a href="`) 482 url := n.URL(source) 483 label := n.Label(source) 484 if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { 485 _, _ = w.WriteString("mailto:") 486 } 487 _, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false))) 488 if n.Attributes() != nil { 489 _ = w.WriteByte('"') 490 RenderAttributes(w, n, LinkAttributeFilter) 491 _ = w.WriteByte('>') 492 } else { 493 _, _ = w.WriteString(`">`) 494 } 495 _, _ = w.Write(util.EscapeHTML(label)) 496 _, _ = w.WriteString(`</a>`) 497 return ast.WalkContinue, nil 498 } 499 500 // CodeAttributeFilter defines attribute names which code elements can have. 501 var CodeAttributeFilter = GlobalAttributeFilter 502 503 func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 504 if entering { 505 if n.Attributes() != nil { 506 _, _ = w.WriteString("<code") 507 RenderAttributes(w, n, CodeAttributeFilter) 508 _ = w.WriteByte('>') 509 } else { 510 _, _ = w.WriteString("<code>") 511 } 512 for c := n.FirstChild(); c != nil; c = c.NextSibling() { 513 segment := c.(*ast.Text).Segment 514 value := segment.Value(source) 515 if bytes.HasSuffix(value, []byte("\n")) { 516 r.Writer.RawWrite(w, value[:len(value)-1]) 517 r.Writer.RawWrite(w, []byte(" ")) 518 } else { 519 r.Writer.RawWrite(w, value) 520 } 521 } 522 return ast.WalkSkipChildren, nil 523 } 524 _, _ = w.WriteString("</code>") 525 return ast.WalkContinue, nil 526 } 527 528 // EmphasisAttributeFilter defines attribute names which emphasis elements can have. 529 var EmphasisAttributeFilter = GlobalAttributeFilter 530 531 func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 532 n := node.(*ast.Emphasis) 533 tag := "em" 534 if n.Level == 2 { 535 tag = "strong" 536 } 537 if entering { 538 _ = w.WriteByte('<') 539 _, _ = w.WriteString(tag) 540 if n.Attributes() != nil { 541 RenderAttributes(w, n, EmphasisAttributeFilter) 542 } 543 _ = w.WriteByte('>') 544 } else { 545 _, _ = w.WriteString("</") 546 _, _ = w.WriteString(tag) 547 _ = w.WriteByte('>') 548 } 549 return ast.WalkContinue, nil 550 } 551 552 func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 553 n := node.(*ast.Link) 554 if entering { 555 _, _ = w.WriteString("<a href=\"") 556 if r.Unsafe || !IsDangerousURL(n.Destination) { 557 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) 558 } 559 _ = w.WriteByte('"') 560 if n.Title != nil { 561 _, _ = w.WriteString(` title="`) 562 r.Writer.Write(w, n.Title) 563 _ = w.WriteByte('"') 564 } 565 if n.Attributes() != nil { 566 RenderAttributes(w, n, LinkAttributeFilter) 567 } 568 _ = w.WriteByte('>') 569 } else { 570 _, _ = w.WriteString("</a>") 571 } 572 return ast.WalkContinue, nil 573 } 574 575 // ImageAttributeFilter defines attribute names which image elements can have. 576 var ImageAttributeFilter = GlobalAttributeFilter.Extend( 577 []byte("align"), 578 []byte("border"), 579 []byte("crossorigin"), 580 []byte("decoding"), 581 []byte("height"), 582 []byte("importance"), 583 []byte("intrinsicsize"), 584 []byte("ismap"), 585 []byte("loading"), 586 []byte("referrerpolicy"), 587 []byte("sizes"), 588 []byte("srcset"), 589 []byte("usemap"), 590 []byte("width"), 591 ) 592 593 func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 594 if !entering { 595 return ast.WalkContinue, nil 596 } 597 n := node.(*ast.Image) 598 _, _ = w.WriteString("<img src=\"") 599 if r.Unsafe || !IsDangerousURL(n.Destination) { 600 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) 601 } 602 _, _ = w.WriteString(`" alt="`) 603 _, _ = w.Write(nodeToHTMLText(n, source)) 604 _ = w.WriteByte('"') 605 if n.Title != nil { 606 _, _ = w.WriteString(` title="`) 607 r.Writer.Write(w, n.Title) 608 _ = w.WriteByte('"') 609 } 610 if n.Attributes() != nil { 611 RenderAttributes(w, n, ImageAttributeFilter) 612 } 613 if r.XHTML { 614 _, _ = w.WriteString(" />") 615 } else { 616 _, _ = w.WriteString(">") 617 } 618 return ast.WalkSkipChildren, nil 619 } 620 621 func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 622 if !entering { 623 return ast.WalkSkipChildren, nil 624 } 625 if r.Unsafe { 626 n := node.(*ast.RawHTML) 627 l := n.Segments.Len() 628 for i := 0; i < l; i++ { 629 segment := n.Segments.At(i) 630 _, _ = w.Write(segment.Value(source)) 631 } 632 return ast.WalkSkipChildren, nil 633 } 634 _, _ = w.WriteString("<!-- raw HTML omitted -->") 635 return ast.WalkSkipChildren, nil 636 } 637 638 func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 639 if !entering { 640 return ast.WalkContinue, nil 641 } 642 n := node.(*ast.Text) 643 segment := n.Segment 644 if n.IsRaw() { 645 r.Writer.RawWrite(w, segment.Value(source)) 646 } else { 647 value := segment.Value(source) 648 r.Writer.Write(w, value) 649 if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) { 650 if r.XHTML { 651 _, _ = w.WriteString("<br />\n") 652 } else { 653 _, _ = w.WriteString("<br>\n") 654 } 655 } else if n.SoftLineBreak() { 656 if r.EastAsianLineBreaks && len(value) != 0 { 657 sibling := node.NextSibling() 658 if sibling != nil && sibling.Kind() == ast.KindText { 659 if siblingText := sibling.(*ast.Text).Text(source); len(siblingText) != 0 { 660 thisLastRune := util.ToRune(value, len(value)-1) 661 siblingFirstRune, _ := utf8.DecodeRune(siblingText) 662 if !(util.IsEastAsianWideRune(thisLastRune) && 663 util.IsEastAsianWideRune(siblingFirstRune)) { 664 _ = w.WriteByte('\n') 665 } 666 } 667 } 668 } else { 669 _ = w.WriteByte('\n') 670 } 671 } 672 } 673 return ast.WalkContinue, nil 674 } 675 676 func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 677 if !entering { 678 return ast.WalkContinue, nil 679 } 680 n := node.(*ast.String) 681 if n.IsCode() { 682 _, _ = w.Write(n.Value) 683 } else { 684 if n.IsRaw() { 685 r.Writer.RawWrite(w, n.Value) 686 } else { 687 r.Writer.Write(w, n.Value) 688 } 689 } 690 return ast.WalkContinue, nil 691 } 692 693 var dataPrefix = []byte("data-") 694 695 // RenderAttributes renders given node's attributes. 696 // You can specify attribute names to render by the filter. 697 // If filter is nil, RenderAttributes renders all attributes. 698 func RenderAttributes(w util.BufWriter, node ast.Node, filter util.BytesFilter) { 699 for _, attr := range node.Attributes() { 700 if filter != nil && !filter.Contains(attr.Name) { 701 if !bytes.HasPrefix(attr.Name, dataPrefix) { 702 continue 703 } 704 } 705 _, _ = w.WriteString(" ") 706 _, _ = w.Write(attr.Name) 707 _, _ = w.WriteString(`="`) 708 // TODO: convert numeric values to strings 709 _, _ = w.Write(util.EscapeHTML(attr.Value.([]byte))) 710 _ = w.WriteByte('"') 711 } 712 } 713 714 // A Writer interface writes textual contents to a writer. 715 type Writer interface { 716 // Write writes the given source to writer with resolving references and unescaping 717 // backslash escaped characters. 718 Write(writer util.BufWriter, source []byte) 719 720 // RawWrite writes the given source to writer without resolving references and 721 // unescaping backslash escaped characters. 722 RawWrite(writer util.BufWriter, source []byte) 723 724 // SecureWrite writes the given source to writer with replacing insecure characters. 725 SecureWrite(writer util.BufWriter, source []byte) 726 } 727 728 var replacementCharacter = []byte("\ufffd") 729 730 // A WriterConfig struct has configurations for the HTML based writers. 731 type WriterConfig struct { 732 // EscapedSpace is an option that indicates that a '\' escaped half-space(0x20) should not be rendered. 733 EscapedSpace bool 734 } 735 736 // A WriterOption interface sets options for HTML based writers. 737 type WriterOption func(*WriterConfig) 738 739 // WithEscapedSpace is a WriterOption indicates that a '\' escaped half-space(0x20) should not be rendered. 740 func WithEscapedSpace() WriterOption { 741 return func(c *WriterConfig) { 742 c.EscapedSpace = true 743 } 744 } 745 746 type defaultWriter struct { 747 WriterConfig 748 } 749 750 // NewWriter returns a new Writer. 751 func NewWriter(opts ...WriterOption) Writer { 752 w := &defaultWriter{} 753 for _, opt := range opts { 754 opt(&w.WriterConfig) 755 } 756 return w 757 } 758 759 func escapeRune(writer util.BufWriter, r rune) { 760 if r < 256 { 761 v := util.EscapeHTMLByte(byte(r)) 762 if v != nil { 763 _, _ = writer.Write(v) 764 return 765 } 766 } 767 _, _ = writer.WriteRune(util.ToValidRune(r)) 768 } 769 770 func (d *defaultWriter) SecureWrite(writer util.BufWriter, source []byte) { 771 n := 0 772 l := len(source) 773 for i := 0; i < l; i++ { 774 if source[i] == '\u0000' { 775 _, _ = writer.Write(source[i-n : i]) 776 n = 0 777 _, _ = writer.Write(replacementCharacter) 778 continue 779 } 780 n++ 781 } 782 if n != 0 { 783 _, _ = writer.Write(source[l-n:]) 784 } 785 } 786 787 func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) { 788 n := 0 789 l := len(source) 790 for i := 0; i < l; i++ { 791 v := util.EscapeHTMLByte(source[i]) 792 if v != nil { 793 _, _ = writer.Write(source[i-n : i]) 794 n = 0 795 _, _ = writer.Write(v) 796 continue 797 } 798 n++ 799 } 800 if n != 0 { 801 _, _ = writer.Write(source[l-n:]) 802 } 803 } 804 805 func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { 806 escaped := false 807 var ok bool 808 limit := len(source) 809 n := 0 810 for i := 0; i < limit; i++ { 811 c := source[i] 812 if escaped { 813 if util.IsPunct(c) { 814 d.RawWrite(writer, source[n:i-1]) 815 n = i 816 escaped = false 817 continue 818 } 819 if d.EscapedSpace && c == ' ' { 820 d.RawWrite(writer, source[n:i-1]) 821 n = i + 1 822 escaped = false 823 continue 824 } 825 } 826 if c == '\x00' { 827 d.RawWrite(writer, source[n:i]) 828 d.RawWrite(writer, replacementCharacter) 829 n = i + 1 830 escaped = false 831 continue 832 } 833 if c == '&' { 834 pos := i 835 next := i + 1 836 if next < limit && source[next] == '#' { 837 nnext := next + 1 838 if nnext < limit { 839 nc := source[nnext] 840 // code point like #x22; 841 if nnext < limit && nc == 'x' || nc == 'X' { 842 start := nnext + 1 843 i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal) 844 if ok && i < limit && source[i] == ';' && i-start < 7 { 845 v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32) 846 d.RawWrite(writer, source[n:pos]) 847 n = i + 1 848 escapeRune(writer, rune(v)) 849 continue 850 } 851 // code point like #1234; 852 } else if nc >= '0' && nc <= '9' { 853 start := nnext 854 i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric) 855 if ok && i < limit && i-start < 8 && source[i] == ';' { 856 v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 10, 32) 857 d.RawWrite(writer, source[n:pos]) 858 n = i + 1 859 escapeRune(writer, rune(v)) 860 continue 861 } 862 } 863 } 864 } else { 865 start := next 866 i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsAlphaNumeric) 867 // entity reference 868 if ok && i < limit && source[i] == ';' { 869 name := util.BytesToReadOnlyString(source[start:i]) 870 entity, ok := util.LookUpHTML5EntityByName(name) 871 if ok { 872 d.RawWrite(writer, source[n:pos]) 873 n = i + 1 874 d.RawWrite(writer, entity.Characters) 875 continue 876 } 877 } 878 } 879 i = next - 1 880 } 881 if c == '\\' { 882 escaped = true 883 continue 884 } 885 escaped = false 886 } 887 d.RawWrite(writer, source[n:]) 888 } 889 890 // DefaultWriter is a default instance of the Writer. 891 var DefaultWriter = NewWriter() 892 893 var bDataImage = []byte("data:image/") 894 var bPng = []byte("png;") 895 var bGif = []byte("gif;") 896 var bJpeg = []byte("jpeg;") 897 var bWebp = []byte("webp;") 898 var bSvg = []byte("svg+xml;") 899 var bJs = []byte("javascript:") 900 var bVb = []byte("vbscript:") 901 var bFile = []byte("file:") 902 var bData = []byte("data:") 903 904 // IsDangerousURL returns true if the given url seems a potentially dangerous url, 905 // otherwise false. 906 func IsDangerousURL(url []byte) bool { 907 if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 { 908 v := url[11:] 909 if bytes.HasPrefix(v, bPng) || bytes.HasPrefix(v, bGif) || 910 bytes.HasPrefix(v, bJpeg) || bytes.HasPrefix(v, bWebp) || 911 bytes.HasPrefix(v, bSvg) { 912 return false 913 } 914 return true 915 } 916 return bytes.HasPrefix(url, bJs) || bytes.HasPrefix(url, bVb) || 917 bytes.HasPrefix(url, bFile) || bytes.HasPrefix(url, bData) 918 } 919 920 func nodeToHTMLText(n ast.Node, source []byte) []byte { 921 var buf bytes.Buffer 922 for c := n.FirstChild(); c != nil; c = c.NextSibling() { 923 if s, ok := c.(*ast.String); ok && s.IsCode() { 924 buf.Write(s.Text(source)) 925 } else if !c.HasChildren() { 926 buf.Write(util.EscapeHTML(c.Text(source))) 927 } else { 928 buf.Write(nodeToHTMLText(c, source)) 929 } 930 } 931 return buf.Bytes() 932 }