all repos — grayfriday @ be0fb4602b8113ab16b70ab34fe48c0573bd65ef

blackfriday fork with a few changes

html.go (view raw)

  1//
  2// Black Friday Markdown Processor
  3// Originally based on http://github.com/tanoku/upskirt
  4// by Russ Ross <russ@russross.com>
  5//
  6
  7//
  8//
  9// HTML rendering backend
 10//
 11//
 12
 13package blackfriday
 14
 15import (
 16	"bytes"
 17	"fmt"
 18	"strconv"
 19)
 20
 21const (
 22	HTML_SKIP_HTML = 1 << iota
 23	HTML_SKIP_STYLE
 24	HTML_SKIP_IMAGES
 25	HTML_SKIP_LINKS
 26	HTML_EXPAND_TABS
 27	HTML_SAFELINK
 28	HTML_TOC
 29	HTML_HARD_WRAP
 30	HTML_GITHUB_BLOCKCODE
 31	HTML_USE_XHTML
 32	HTML_USE_SMARTYPANTS
 33	HTML_SMARTYPANTS_FRACTIONS
 34	HTML_SMARTYPANTS_LATEX_DASHES
 35)
 36
 37type htmlOptions struct {
 38	flags    int
 39	closeTag string // how to end singleton tags: usually " />\n", possibly ">\n"
 40	tocData  struct {
 41		headerCount  int
 42		currentLevel int
 43	}
 44	smartypants *SmartypantsRenderer
 45}
 46
 47var xhtmlClose = " />\n"
 48var htmlClose = ">\n"
 49
 50func HtmlRenderer(flags int) *Renderer {
 51	// configure the rendering engine
 52	r := new(Renderer)
 53	if flags&HTML_GITHUB_BLOCKCODE == 0 {
 54		r.BlockCode = htmlBlockCode
 55	} else {
 56		r.BlockCode = htmlBlockCodeGithub
 57	}
 58	r.BlockQuote = htmlBlockQuote
 59	if flags&HTML_SKIP_HTML == 0 {
 60		r.BlockHtml = htmlRawBlock
 61	}
 62	r.Header = htmlHeader
 63	r.HRule = htmlHRule
 64	r.List = htmlList
 65	r.ListItem = htmlListItem
 66	r.Paragraph = htmlParagraph
 67	r.Table = htmlTable
 68	r.TableRow = htmlTableRow
 69	r.TableCell = htmlTableCell
 70
 71	r.AutoLink = htmlAutoLink
 72	r.CodeSpan = htmlCodeSpan
 73	r.DoubleEmphasis = htmlDoubleEmphasis
 74	r.Emphasis = htmlEmphasis
 75	if flags&HTML_SKIP_IMAGES == 0 {
 76		r.Image = htmlImage
 77	}
 78	r.LineBreak = htmlLineBreak
 79	if flags&HTML_SKIP_LINKS == 0 {
 80		r.Link = htmlLink
 81	}
 82	r.RawHtmlTag = htmlRawTag
 83	r.TripleEmphasis = htmlTripleEmphasis
 84	r.StrikeThrough = htmlStrikeThrough
 85
 86	var cb *SmartypantsRenderer
 87	if flags&HTML_USE_SMARTYPANTS == 0 {
 88		r.NormalText = htmlNormalText
 89	} else {
 90		cb = Smartypants(flags)
 91		r.NormalText = htmlSmartypants
 92	}
 93
 94	closeTag := htmlClose
 95	if flags&HTML_USE_XHTML != 0 {
 96		closeTag = xhtmlClose
 97	}
 98	r.Opaque = &htmlOptions{flags: flags, closeTag: closeTag, smartypants: cb}
 99	return r
100}
101
102func HtmlTocRenderer(flags int) *Renderer {
103	// configure the rendering engine
104	r := new(Renderer)
105	r.Header = htmlTocHeader
106
107	r.CodeSpan = htmlCodeSpan
108	r.DoubleEmphasis = htmlDoubleEmphasis
109	r.Emphasis = htmlEmphasis
110	r.TripleEmphasis = htmlTripleEmphasis
111	r.StrikeThrough = htmlStrikeThrough
112
113	r.DocumentFooter = htmlTocFinalize
114
115	closeTag := ">\n"
116	if flags&HTML_USE_XHTML != 0 {
117		closeTag = " />\n"
118	}
119	r.Opaque = &htmlOptions{flags: flags | HTML_TOC, closeTag: closeTag}
120	return r
121}
122
123func attrEscape(out *bytes.Buffer, src []byte) {
124	for i := 0; i < len(src); i++ {
125		// directly copy normal characters
126		org := i
127		for i < len(src) && src[i] != '<' && src[i] != '>' && src[i] != '&' && src[i] != '"' {
128			i++
129		}
130		if i > org {
131			out.Write(src[org:i])
132		}
133
134		// escape a character
135		if i >= len(src) {
136			break
137		}
138		switch src[i] {
139		case '<':
140			out.WriteString("&lt;")
141		case '>':
142			out.WriteString("&gt;")
143		case '&':
144			out.WriteString("&amp;")
145		case '"':
146			out.WriteString("&quot;")
147		}
148	}
149}
150
151func htmlHeader(out *bytes.Buffer, text []byte, level int, opaque interface{}) {
152	options := opaque.(*htmlOptions)
153
154	if out.Len() > 0 {
155		out.WriteByte('\n')
156	}
157
158	if options.flags&HTML_TOC != 0 {
159		out.WriteString(fmt.Sprintf("<h%d id=\"toc_%d\">", level, options.tocData.headerCount))
160		options.tocData.headerCount++
161	} else {
162		out.WriteString(fmt.Sprintf("<h%d>", level))
163	}
164
165	out.Write(text)
166	out.WriteString(fmt.Sprintf("</h%d>\n", level))
167}
168
169func htmlRawBlock(out *bytes.Buffer, text []byte, opaque interface{}) {
170	sz := len(text)
171	for sz > 0 && text[sz-1] == '\n' {
172		sz--
173	}
174	org := 0
175	for org < sz && text[org] == '\n' {
176		org++
177	}
178	if org >= sz {
179		return
180	}
181	if out.Len() > 0 {
182		out.WriteByte('\n')
183	}
184	out.Write(text[org:sz])
185	out.WriteByte('\n')
186}
187
188func htmlHRule(out *bytes.Buffer, opaque interface{}) {
189	options := opaque.(*htmlOptions)
190
191	if out.Len() > 0 {
192		out.WriteByte('\n')
193	}
194	out.WriteString("<hr")
195	out.WriteString(options.closeTag)
196}
197
198func htmlBlockCode(out *bytes.Buffer, text []byte, lang string, opaque interface{}) {
199	if out.Len() > 0 {
200		out.WriteByte('\n')
201	}
202
203	if lang != "" {
204		out.WriteString("<pre><code class=\"")
205
206		for i, cls := 0, 0; i < len(lang); i, cls = i+1, cls+1 {
207			for i < len(lang) && isspace(lang[i]) {
208				i++
209			}
210
211			if i < len(lang) {
212				org := i
213				for i < len(lang) && !isspace(lang[i]) {
214					i++
215				}
216
217				if lang[org] == '.' {
218					org++
219				}
220
221				if cls > 0 {
222					out.WriteByte(' ')
223				}
224				attrEscape(out, []byte(lang[org:]))
225			}
226		}
227
228		out.WriteString("\">")
229	} else {
230		out.WriteString("<pre><code>")
231	}
232
233	if len(text) > 0 {
234		attrEscape(out, text)
235	}
236
237	out.WriteString("</code></pre>\n")
238}
239
240/*
241 * GitHub style code block:
242 *
243 *              <pre lang="LANG"><code>
244 *              ...
245 *              </pre></code>
246 *
247 * Unlike other parsers, we store the language identifier in the <pre>,
248 * and don't let the user generate custom classes.
249 *
250 * The language identifier in the <pre> block gets postprocessed and all
251 * the code inside gets syntax highlighted with Pygments. This is much safer
252 * than letting the user specify a CSS class for highlighting.
253 *
254 * Note that we only generate HTML for the first specifier.
255 * E.g.
256 *              ~~~~ {.python .numbered}        =>      <pre lang="python"><code>
257 */
258func htmlBlockCodeGithub(out *bytes.Buffer, text []byte, lang string, opaque interface{}) {
259	if out.Len() > 0 {
260		out.WriteByte('\n')
261	}
262
263	if len(lang) > 0 {
264		out.WriteString("<pre lang=\"")
265
266		i := 0
267		for i < len(lang) && !isspace(lang[i]) {
268			i++
269		}
270
271		if lang[0] == '.' {
272			attrEscape(out, []byte(lang[1:i]))
273		} else {
274			attrEscape(out, []byte(lang[:i]))
275		}
276
277		out.WriteString("\"><code>")
278	} else {
279		out.WriteString("<pre><code>")
280	}
281
282	if len(text) > 0 {
283		attrEscape(out, text)
284	}
285
286	out.WriteString("</code></pre>\n")
287}
288
289
290func htmlBlockQuote(out *bytes.Buffer, text []byte, opaque interface{}) {
291	out.WriteString("<blockquote>\n")
292	out.Write(text)
293	out.WriteString("</blockquote>")
294}
295
296func htmlTable(out *bytes.Buffer, header []byte, body []byte, columnData []int, opaque interface{}) {
297	if out.Len() > 0 {
298		out.WriteByte('\n')
299	}
300	out.WriteString("<table><thead>\n")
301	out.Write(header)
302	out.WriteString("\n</thead><tbody>\n")
303	out.Write(body)
304	out.WriteString("\n</tbody></table>")
305}
306
307func htmlTableRow(out *bytes.Buffer, text []byte, opaque interface{}) {
308	if out.Len() > 0 {
309		out.WriteByte('\n')
310	}
311	out.WriteString("<tr>\n")
312	out.Write(text)
313	out.WriteString("\n</tr>")
314}
315
316func htmlTableCell(out *bytes.Buffer, text []byte, align int, opaque interface{}) {
317	if out.Len() > 0 {
318		out.WriteByte('\n')
319	}
320	switch align {
321	case TABLE_ALIGNMENT_LEFT:
322		out.WriteString("<td align=\"left\">")
323	case TABLE_ALIGNMENT_RIGHT:
324		out.WriteString("<td align=\"right\">")
325	case TABLE_ALIGNMENT_CENTER:
326		out.WriteString("<td align=\"center\">")
327	default:
328		out.WriteString("<td>")
329	}
330
331	out.Write(text)
332	out.WriteString("</td>")
333}
334
335func htmlList(out *bytes.Buffer, text []byte, flags int, opaque interface{}) {
336	if out.Len() > 0 {
337		out.WriteByte('\n')
338	}
339	if flags&LIST_TYPE_ORDERED != 0 {
340		out.WriteString("<ol>\n")
341	} else {
342		out.WriteString("<ul>\n")
343	}
344	out.Write(text)
345	if flags&LIST_TYPE_ORDERED != 0 {
346		out.WriteString("</ol>\n")
347	} else {
348		out.WriteString("</ul>\n")
349	}
350}
351
352func htmlListItem(out *bytes.Buffer, text []byte, flags int, opaque interface{}) {
353	out.WriteString("<li>")
354	size := len(text)
355	for size > 0 && text[size-1] == '\n' {
356		size--
357	}
358	out.Write(text[:size])
359	out.WriteString("</li>\n")
360}
361
362func htmlParagraph(out *bytes.Buffer, text []byte, opaque interface{}) {
363	options := opaque.(*htmlOptions)
364	i := 0
365
366	if out.Len() > 0 {
367		out.WriteByte('\n')
368	}
369
370	if len(text) == 0 {
371		return
372	}
373
374	for i < len(text) && isspace(text[i]) {
375		i++
376	}
377
378	if i == len(text) {
379		return
380	}
381
382	out.WriteString("<p>")
383	if options.flags&HTML_HARD_WRAP != 0 {
384		for i < len(text) {
385			org := i
386			for i < len(text) && text[i] != '\n' {
387				i++
388			}
389
390			if i > org {
391				out.Write(text[org:i])
392			}
393
394			if i >= len(text) {
395				break
396			}
397
398			out.WriteString("<br>")
399			out.WriteString(options.closeTag)
400			i++
401		}
402	} else {
403		out.Write(text[i:])
404	}
405	out.WriteString("</p>\n")
406}
407
408func htmlAutoLink(out *bytes.Buffer, link []byte, kind int, opaque interface{}) int {
409	options := opaque.(*htmlOptions)
410
411	if len(link) == 0 {
412		return 0
413	}
414	if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL {
415		return 0
416	}
417
418	out.WriteString("<a href=\"")
419	if kind == LINK_TYPE_EMAIL {
420		out.WriteString("mailto:")
421	}
422	attrEscape(out, link)
423	out.WriteString("\">")
424
425	/*
426	 * Pretty print: if we get an email address as
427	 * an actual URI, e.g. `mailto:foo@bar.com`, we don't
428	 * want to print the `mailto:` prefix
429	 */
430	switch {
431	case bytes.HasPrefix(link, []byte("mailto://")):
432		attrEscape(out, link[9:])
433	case bytes.HasPrefix(link, []byte("mailto:")):
434		attrEscape(out, link[7:])
435	default:
436		attrEscape(out, link)
437	}
438
439	out.WriteString("</a>")
440
441	return 1
442}
443
444func htmlCodeSpan(out *bytes.Buffer, text []byte, opaque interface{}) int {
445	out.WriteString("<code>")
446	attrEscape(out, text)
447	out.WriteString("</code>")
448	return 1
449}
450
451func htmlDoubleEmphasis(out *bytes.Buffer, text []byte, opaque interface{}) int {
452	if len(text) == 0 {
453		return 0
454	}
455	out.WriteString("<strong>")
456	out.Write(text)
457	out.WriteString("</strong>")
458	return 1
459}
460
461func htmlEmphasis(out *bytes.Buffer, text []byte, opaque interface{}) int {
462	if len(text) == 0 {
463		return 0
464	}
465	out.WriteString("<em>")
466	out.Write(text)
467	out.WriteString("</em>")
468	return 1
469}
470
471func htmlImage(out *bytes.Buffer, link []byte, title []byte, alt []byte, opaque interface{}) int {
472	options := opaque.(*htmlOptions)
473	if len(link) == 0 {
474		return 0
475	}
476	out.WriteString("<img src=\"")
477	attrEscape(out, link)
478	out.WriteString("\" alt=\"")
479	if len(alt) > 0 {
480		attrEscape(out, alt)
481	}
482	if len(title) > 0 {
483		out.WriteString("\" title=\"")
484		attrEscape(out, title)
485	}
486
487	out.WriteByte('"')
488	out.WriteString(options.closeTag)
489	return 1
490}
491
492func htmlLineBreak(out *bytes.Buffer, opaque interface{}) int {
493	options := opaque.(*htmlOptions)
494	out.WriteString("<br")
495	out.WriteString(options.closeTag)
496	return 1
497}
498
499func htmlLink(out *bytes.Buffer, link []byte, title []byte, content []byte, opaque interface{}) int {
500	options := opaque.(*htmlOptions)
501
502	if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) {
503		return 0
504	}
505
506	out.WriteString("<a href=\"")
507	attrEscape(out, link)
508	if len(title) > 0 {
509		out.WriteString("\" title=\"")
510		attrEscape(out, title)
511	}
512	out.WriteString("\">")
513	out.Write(content)
514	out.WriteString("</a>")
515	return 1
516}
517
518func htmlRawTag(out *bytes.Buffer, text []byte, opaque interface{}) int {
519	options := opaque.(*htmlOptions)
520	if options.flags&HTML_SKIP_HTML != 0 {
521		return 1
522	}
523	if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") {
524		return 1
525	}
526	if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") {
527		return 1
528	}
529	if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
530		return 1
531	}
532	out.Write(text)
533	return 1
534}
535
536func htmlTripleEmphasis(out *bytes.Buffer, text []byte, opaque interface{}) int {
537	if len(text) == 0 {
538		return 0
539	}
540	out.WriteString("<strong><em>")
541	out.Write(text)
542	out.WriteString("</em></strong>")
543	return 1
544}
545
546func htmlStrikeThrough(out *bytes.Buffer, text []byte, opaque interface{}) int {
547	if len(text) == 0 {
548		return 0
549	}
550	out.WriteString("<del>")
551	out.Write(text)
552	out.WriteString("</del>")
553	return 1
554}
555
556func htmlNormalText(out *bytes.Buffer, text []byte, opaque interface{}) {
557	attrEscape(out, text)
558}
559
560func htmlTocHeader(out *bytes.Buffer, text []byte, level int, opaque interface{}) {
561	options := opaque.(*htmlOptions)
562	for level > options.tocData.currentLevel {
563		if options.tocData.currentLevel > 0 {
564			out.WriteString("<li>")
565		}
566		out.WriteString("<ul>\n")
567		options.tocData.currentLevel++
568	}
569
570	for level < options.tocData.currentLevel {
571		out.WriteString("</ul>")
572		if options.tocData.currentLevel > 1 {
573			out.WriteString("</li>\n")
574		}
575		options.tocData.currentLevel--
576	}
577
578	out.WriteString("<li><a href=\"#toc_")
579	out.WriteString(strconv.Itoa(options.tocData.headerCount))
580	out.WriteString("\">")
581	options.tocData.headerCount++
582
583	if len(text) > 0 {
584		out.Write(text)
585	}
586	out.WriteString("</a></li>\n")
587}
588
589func htmlTocFinalize(out *bytes.Buffer, opaque interface{}) {
590	options := opaque.(*htmlOptions)
591	for options.tocData.currentLevel > 1 {
592		out.WriteString("</ul></li>\n")
593		options.tocData.currentLevel--
594	}
595
596	if options.tocData.currentLevel > 0 {
597		out.WriteString("</ul>\n")
598	}
599}
600
601func isHtmlTag(tag []byte, tagname string) bool {
602	i := 0
603	if i < len(tag) && tag[0] != '<' {
604		return false
605	}
606	i++
607	for i < len(tag) && isspace(tag[i]) {
608		i++
609	}
610
611	if i < len(tag) && tag[i] == '/' {
612		i++
613	}
614
615	for i < len(tag) && isspace(tag[i]) {
616		i++
617	}
618
619	tag_i := i
620	for ; i < len(tag); i, tag_i = i+1, tag_i+1 {
621		if tag_i >= len(tagname) {
622			break
623		}
624
625		if tag[i] != tagname[tag_i] {
626			return false
627		}
628	}
629
630	if i == len(tag) {
631		return false
632	}
633
634	return isspace(tag[i]) || tag[i] == '>'
635}