1package html
  2
  3import (
  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.
 15type 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.
 24func 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.
 35func (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.
 51type Option interface {
 52	SetHTMLOption(*Config)
 53}
 54
 55// TextWriter is an option name used in WithWriter.
 56const optTextWriter renderer.OptionName = "Writer"
 57
 58type withWriter struct {
 59	value Writer
 60}
 61
 62func (o *withWriter) SetConfig(c *renderer.Config) {
 63	c.Options[optTextWriter] = o.value
 64}
 65
 66func (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.
 72func 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.
 80const optHardWraps renderer.OptionName = "HardWraps"
 81
 82type withHardWraps struct {
 83}
 84
 85func (o *withHardWraps) SetConfig(c *renderer.Config) {
 86	c.Options[optHardWraps] = true
 87}
 88
 89func (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>'.
 95func WithHardWraps() interface {
 96	renderer.Option
 97	Option
 98} {
 99	return &withHardWraps{}
100}
101
102// EastAsianLineBreaks is an option name used in WithEastAsianLineBreaks.
103const optEastAsianLineBreaks renderer.OptionName = "EastAsianLineBreaks"
104
105type withEastAsianLineBreaks struct {
106}
107
108func (o *withEastAsianLineBreaks) SetConfig(c *renderer.Config) {
109	c.Options[optEastAsianLineBreaks] = true
110}
111
112func (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.
118func WithEastAsianLineBreaks() interface {
119	renderer.Option
120	Option
121} {
122	return &withEastAsianLineBreaks{}
123}
124
125// XHTML is an option name used in WithXHTML.
126const optXHTML renderer.OptionName = "XHTML"
127
128type withXHTML struct {
129}
130
131func (o *withXHTML) SetConfig(c *renderer.Config) {
132	c.Options[optXHTML] = true
133}
134
135func (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.
141func WithXHTML() interface {
142	Option
143	renderer.Option
144} {
145	return &withXHTML{}
146}
147
148// Unsafe is an option name used in WithUnsafe.
149const optUnsafe renderer.OptionName = "Unsafe"
150
151type withUnsafe struct {
152}
153
154func (o *withUnsafe) SetConfig(c *renderer.Config) {
155	c.Options[optUnsafe] = true
156}
157
158func (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.
164func 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.
173type Renderer struct {
174	Config
175}
176
177// NewRenderer returns a new Renderer with given options.
178func 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 .
190func (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
217func (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.
226var 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
256func (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
262var HeadingAttributeFilter = GlobalAttributeFilter
263
264func (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
282var BlockquoteAttributeFilter = GlobalAttributeFilter.Extend(
283	[]byte("cite"),
284)
285
286func (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
301func (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
311func (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
329func (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.
355var ListAttributeFilter = GlobalAttributeFilter.Extend(
356	[]byte("start"),
357	[]byte("reversed"),
358	[]byte("type"),
359)
360
361func (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.
386var ListItemAttributeFilter = GlobalAttributeFilter.Extend(
387	[]byte("value"),
388)
389
390func (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.
412var ParagraphAttributeFilter = GlobalAttributeFilter
413
414func (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
429func (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.
439var 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
447func (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.
464var 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
476func (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.
501var CodeAttributeFilter = GlobalAttributeFilter
502
503func (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.
529var EmphasisAttributeFilter = GlobalAttributeFilter
530
531func (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
552func (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.
576var 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
593func (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
621func (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
638func (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
676func (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
693var 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.
698func 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.
715type 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
728var replacementCharacter = []byte("\ufffd")
729
730// A WriterConfig struct has configurations for the HTML based writers.
731type 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.
737type WriterOption func(*WriterConfig)
738
739// WithEscapedSpace is a WriterOption indicates that a '\' escaped half-space(0x20) should not be rendered.
740func WithEscapedSpace() WriterOption {
741	return func(c *WriterConfig) {
742		c.EscapedSpace = true
743	}
744}
745
746type defaultWriter struct {
747	WriterConfig
748}
749
750// NewWriter returns a new Writer.
751func NewWriter(opts ...WriterOption) Writer {
752	w := &defaultWriter{}
753	for _, opt := range opts {
754		opt(&w.WriterConfig)
755	}
756	return w
757}
758
759func 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
770func (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
787func (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
805func (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.
891var DefaultWriter = NewWriter()
892
893var bDataImage = []byte("data:image/")
894var bPng = []byte("png;")
895var bGif = []byte("gif;")
896var bJpeg = []byte("jpeg;")
897var bWebp = []byte("webp;")
898var bSvg = []byte("svg+xml;")
899var bJs = []byte("javascript:")
900var bVb = []byte("vbscript:")
901var bFile = []byte("file:")
902var bData = []byte("data:")
903
904// IsDangerousURL returns true if the given url seems a potentially dangerous url,
905// otherwise false.
906func 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
920func 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}