// // Blackfriday Markdown Processor // Available at http://github.com/russross/blackfriday // // Copyright © 2011 Russ Ross . // Distributed under the Simplified BSD License. // See README.md for details. // // // // HTML rendering backend // // package blackfriday import ( "bytes" "fmt" "strconv" "strings" ) const ( HTML_SKIP_HTML = 1 << iota HTML_SKIP_STYLE HTML_SKIP_IMAGES HTML_SKIP_LINKS HTML_SAFELINK HTML_TOC HTML_OMIT_CONTENTS HTML_COMPLETE_PAGE HTML_GITHUB_BLOCKCODE HTML_USE_XHTML HTML_USE_SMARTYPANTS HTML_SMARTYPANTS_FRACTIONS HTML_SMARTYPANTS_LATEX_DASHES ) type Html struct { flags int // HTML_* options closeTag string // how to end singleton tags: either " />\n" or ">\n" title string // document title css string // optional css file url (used with HTML_COMPLETE_PAGE) // table of contents data tocMarker int headerCount int currentLevel int toc *bytes.Buffer smartypants *SmartypantsRenderer } const ( xhtmlClose = " />\n" htmlClose = ">\n" ) func HtmlRenderer(flags int, title string, css string) Renderer { // configure the rendering engine closeTag := htmlClose if flags&HTML_USE_XHTML != 0 { closeTag = xhtmlClose } return &Html{ flags: flags, closeTag: closeTag, title: title, css: css, headerCount: 0, currentLevel: 0, toc: new(bytes.Buffer), smartypants: Smartypants(flags), } } func attrEscape(out *bytes.Buffer, src []byte) { org := 0 for i, ch := range src { // using if statements is a bit faster than a switch statement. // as the compiler improves, this should be unnecessary // this is only worthwhile because attrEscape is the single // largest CPU user in normal use if ch == '"' { if i > org { // copy all the normal characters since the last escape out.Write(src[org:i]) } org = i + 1 out.WriteString(""") continue } if ch == '&' { if i > org { out.Write(src[org:i]) } org = i + 1 out.WriteString("&") continue } if ch == '<' { if i > org { out.Write(src[org:i]) } org = i + 1 out.WriteString("<") continue } if ch == '>' { if i > org { out.Write(src[org:i]) } org = i + 1 out.WriteString(">") continue } } if org < len(src) { out.Write(src[org:]) } } func (options *Html) Header(out *bytes.Buffer, text func() bool, level int) { marker := out.Len() doubleSpace(out) if options.flags&HTML_TOC != 0 { // headerCount is incremented in htmlTocHeader out.WriteString(fmt.Sprintf("", level, options.headerCount)) } else { out.WriteString(fmt.Sprintf("", level)) } tocMarker := out.Len() if !text() { out.Truncate(marker) return } // are we building a table of contents? if options.flags&HTML_TOC != 0 { options.TocHeader(out.Bytes()[tocMarker:], level) } out.WriteString(fmt.Sprintf("\n", level)) } func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) { if options.flags&HTML_SKIP_HTML != 0 { return } doubleSpace(out) out.Write(text) out.WriteByte('\n') } func (options *Html) HRule(out *bytes.Buffer) { doubleSpace(out) out.WriteString("") } else { out.WriteString("\">") } attrEscape(out, text) out.WriteString("\n") } /* * GitHub style code block: * *

 *              ...
 *              
* * Unlike other parsers, we store the language identifier in the
,
 * and don't let the user generate custom classes.
 *
 * The language identifier in the 
 block gets postprocessed and all
 * the code inside gets syntax highlighted with Pygments. This is much safer
 * than letting the user specify a CSS class for highlighting.
 *
 * Note that we only generate HTML for the first specifier.
 * E.g.
 *              ~~~~ {.python .numbered}        =>      

 */
func (options *Html) BlockCodeGithub(out *bytes.Buffer, text []byte, lang string) {
	doubleSpace(out)

	// parse out the language name
	count := 0
	for _, elt := range strings.Fields(lang) {
		if elt[0] == '.' {
			elt = elt[1:]
		}
		if len(elt) == 0 {
			continue
		}
		out.WriteString("
")
		count++
		break
	}

	if count == 0 {
		out.WriteString("
")
	}

	attrEscape(out, text)
	out.WriteString("
\n") } func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) { doubleSpace(out) out.WriteString("
\n") out.Write(text) out.WriteString("
\n") } func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { doubleSpace(out) out.WriteString("\n\n") out.Write(header) out.WriteString("\n\n\n") out.Write(body) out.WriteString("\n
\n") } func (options *Html) TableRow(out *bytes.Buffer, text []byte) { doubleSpace(out) out.WriteString("\n") out.Write(text) out.WriteString("\n\n") } func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) { doubleSpace(out) switch align { case TABLE_ALIGNMENT_LEFT: out.WriteString("") case TABLE_ALIGNMENT_RIGHT: out.WriteString("") case TABLE_ALIGNMENT_CENTER: out.WriteString("") default: out.WriteString("") } out.Write(text) out.WriteString("") } func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) { marker := out.Len() doubleSpace(out) if flags&LIST_TYPE_ORDERED != 0 { out.WriteString("
    ") } else { out.WriteString("
      ") } if !text() { out.Truncate(marker) return } if flags&LIST_TYPE_ORDERED != 0 { out.WriteString("
\n") } else { out.WriteString("\n") } } func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) { if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { doubleSpace(out) } out.WriteString("
  • ") out.Write(text) out.WriteString("
  • \n") } func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) { marker := out.Len() doubleSpace(out) out.WriteString("

    ") if !text() { out.Truncate(marker) return } out.WriteString("

    \n") } func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) { if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL { // mark it but don't link it if it is not a safe link: no smartypants out.WriteString("") attrEscape(out, link) out.WriteString("") return } out.WriteString("") // Pretty print: if we get an email address as // an actual URI, e.g. `mailto:foo@bar.com`, we don't // want to print the `mailto:` prefix switch { case bytes.HasPrefix(link, []byte("mailto://")): attrEscape(out, link[len("mailto://"):]) case bytes.HasPrefix(link, []byte("mailto:")): attrEscape(out, link[len("mailto:"):]) default: attrEscape(out, link) } out.WriteString("") } func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) { out.WriteString("") attrEscape(out, text) out.WriteString("") } func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) { out.WriteString("") out.Write(text) out.WriteString("") } func (options *Html) Emphasis(out *bytes.Buffer, text []byte) { if len(text) == 0 { return } out.WriteString("") out.Write(text) out.WriteString("") } func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { if options.flags&HTML_SKIP_IMAGES != 0 { return } out.WriteString("\"") 0 { attrEscape(out, alt) } if len(title) > 0 { out.WriteString("\" title=\"") attrEscape(out, title) } out.WriteByte('"') out.WriteString(options.closeTag) return } func (options *Html) LineBreak(out *bytes.Buffer) { out.WriteString("") attrEscape(out, content) out.WriteString("") return } if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) { // write the link text out but don't link it, just mark it with typewriter font out.WriteString("") attrEscape(out, content) out.WriteString("") return } out.WriteString(" 0 { out.WriteString("\" title=\"") attrEscape(out, title) } out.WriteString("\">") out.Write(content) out.WriteString("") return } func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) { if options.flags&HTML_SKIP_HTML != 0 { return } if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") { return } if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") { return } if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") { return } out.Write(text) } func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) { out.WriteString("") out.Write(text) out.WriteString("") } func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) { out.WriteString("") out.Write(text) out.WriteString("") } func (options *Html) Entity(out *bytes.Buffer, entity []byte) { out.Write(entity) } func (options *Html) NormalText(out *bytes.Buffer, text []byte) { if options.flags&HTML_USE_SMARTYPANTS != 0 { options.Smartypants(out, text) } else { attrEscape(out, text) } } func (options *Html) Smartypants(out *bytes.Buffer, text []byte) { smrt := smartypantsData{false, false} // first do normal entity escaping var escaped bytes.Buffer attrEscape(&escaped, text) text = escaped.Bytes() mark := 0 for i := 0; i < len(text); i++ { if action := options.smartypants[text[i]]; action != nil { if i > mark { out.Write(text[mark:i]) } previousChar := byte(0) if i > 0 { previousChar = text[i-1] } i += action(out, &smrt, previousChar, text[i:]) mark = i + 1 } } if mark < len(text) { out.Write(text[mark:]) } } func (options *Html) DocumentHeader(out *bytes.Buffer) { if options.flags&HTML_COMPLETE_PAGE == 0 { return } ending := "" if options.flags&HTML_USE_XHTML != 0 { out.WriteString("\n") out.WriteString("\n") ending = " /" } else { out.WriteString("\n") out.WriteString("\n") } out.WriteString("\n") out.WriteString(" ") options.NormalText(out, []byte(options.title)) out.WriteString("\n") out.WriteString(" \n") out.WriteString(" \n") if options.css != "" { out.WriteString(" \n") } out.WriteString("\n") out.WriteString("\n") options.tocMarker = out.Len() } func (options *Html) DocumentFooter(out *bytes.Buffer) { // finalize and insert the table of contents if options.flags&HTML_TOC != 0 { options.TocFinalize() // now we have to insert the table of contents into the document var temp bytes.Buffer // start by making a copy of everything after the document header temp.Write(out.Bytes()[options.tocMarker:]) // now clear the copied material from the main output buffer out.Truncate(options.tocMarker) // corner case spacing issue if options.flags&HTML_COMPLETE_PAGE != 0 { out.WriteByte('\n') } // insert the table of contents out.Write(options.toc.Bytes()) // corner case spacing issue if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 { out.WriteByte('\n') } // write out everything that came after it if options.flags&HTML_OMIT_CONTENTS == 0 { out.Write(temp.Bytes()) } } if options.flags&HTML_COMPLETE_PAGE != 0 { out.WriteString("\n\n") out.WriteString("\n") } } func (options *Html) TocHeader(text []byte, level int) { for level > options.currentLevel { switch { case bytes.HasSuffix(options.toc.Bytes(), []byte("\n")): // this sublist can nest underneath a header size := options.toc.Len() options.toc.Truncate(size - len("\n")) case options.currentLevel > 0: options.toc.WriteString("
  • ") } if options.toc.Len() > 0 { options.toc.WriteByte('\n') } options.toc.WriteString("
      \n") options.currentLevel++ } for level < options.currentLevel { options.toc.WriteString("
    ") if options.currentLevel > 1 { options.toc.WriteString("
  • \n") } options.currentLevel-- } options.toc.WriteString("
  • ") options.headerCount++ options.toc.Write(text) options.toc.WriteString("
  • \n") } func (options *Html) TocFinalize() { for options.currentLevel > 1 { options.toc.WriteString("\n") options.currentLevel-- } if options.currentLevel > 0 { options.toc.WriteString("\n") } } func isHtmlTag(tag []byte, tagname string) bool { i := 0 if i < len(tag) && tag[0] != '<' { return false } i++ for i < len(tag) && isspace(tag[i]) { i++ } if i < len(tag) && tag[i] == '/' { i++ } for i < len(tag) && isspace(tag[i]) { i++ } j := i for ; i < len(tag); i, j = i+1, j+1 { if j >= len(tagname) { break } if tag[i] != tagname[j] { return false } } if i == len(tag) { return false } return isspace(tag[i]) || tag[i] == '>' } func doubleSpace(out *bytes.Buffer) { if out.Len() > 0 { out.WriteByte('\n') } }