| // |
| // Blackfriday Markdown Processor |
| // Available at http://github.com/russross/blackfriday |
| // |
| // Copyright © 2011 Russ Ross <russ@russross.com>. |
| // Distributed under the Simplified BSD License. |
| // See README.md for details. |
| // |
| |
| // |
| // |
| // HTML rendering backend |
| // |
| // |
| |
| package blackfriday |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "regexp" |
| "strings" |
| ) |
| |
| // HTMLFlags control optional behavior of HTML renderer. |
| type HTMLFlags int |
| |
| // HTML renderer configuration options. |
| const ( |
| HTMLFlagsNone HTMLFlags = 0 |
| SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks |
| SkipImages // Skip embedded images |
| SkipLinks // Skip all links |
| Safelink // Only link to trusted protocols |
| NofollowLinks // Only link with rel="nofollow" |
| NoreferrerLinks // Only link with rel="noreferrer" |
| NoopenerLinks // Only link with rel="noopener" |
| HrefTargetBlank // Add a blank target |
| CompletePage // Generate a complete HTML page |
| UseXHTML // Generate XHTML output instead of HTML |
| FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source |
| Smartypants // Enable smart punctuation substitutions |
| SmartypantsFractions // Enable smart fractions (with Smartypants) |
| SmartypantsDashes // Enable smart dashes (with Smartypants) |
| SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) |
| SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering |
| SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) |
| TOC // Generate a table of contents |
| ) |
| |
| var ( |
| htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) |
| ) |
| |
| const ( |
| htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + |
| processingInstruction + "|" + declaration + "|" + cdata + ")" |
| closeTag = "</" + tagName + "\\s*[>]" |
| openTag = "<" + tagName + attribute + "*" + "\\s*/?>" |
| attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" |
| attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" |
| attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" |
| attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" |
| cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>" |
| declaration = "<![A-Z]+" + "\\s+[^>]*>" |
| doubleQuotedValue = "\"[^\"]*\"" |
| htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->" |
| processingInstruction = "[<][?].*?[?][>]" |
| singleQuotedValue = "'[^']*'" |
| tagName = "[A-Za-z][A-Za-z0-9-]*" |
| unquotedValue = "[^\"'=<>`\\x00-\\x20]+" |
| ) |
| |
| // HTMLRendererParameters is a collection of supplementary parameters tweaking |
| // the behavior of various parts of HTML renderer. |
| type HTMLRendererParameters struct { |
| // Prepend this text to each relative URL. |
| AbsolutePrefix string |
| // Add this text to each footnote anchor, to ensure uniqueness. |
| FootnoteAnchorPrefix string |
| // Show this text inside the <a> tag for a footnote return link, if the |
| // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string |
| // <sup>[return]</sup> is used. |
| FootnoteReturnLinkContents string |
| // If set, add this text to the front of each Heading ID, to ensure |
| // uniqueness. |
| HeadingIDPrefix string |
| // If set, add this text to the back of each Heading ID, to ensure uniqueness. |
| HeadingIDSuffix string |
| // Increase heading levels: if the offset is 1, <h1> becomes <h2> etc. |
| // Negative offset is also valid. |
| // Resulting levels are clipped between 1 and 6. |
| HeadingLevelOffset int |
| |
| Title string // Document title (used if CompletePage is set) |
| CSS string // Optional CSS file URL (used if CompletePage is set) |
| Icon string // Optional icon file URL (used if CompletePage is set) |
| |
| Flags HTMLFlags // Flags allow customizing this renderer's behavior |
| } |
| |
| // HTMLRenderer is a type that implements the Renderer interface for HTML output. |
| // |
| // Do not create this directly, instead use the NewHTMLRenderer function. |
| type HTMLRenderer struct { |
| HTMLRendererParameters |
| |
| closeTag string // how to end singleton tags: either " />" or ">" |
| |
| // Track heading IDs to prevent ID collision in a single generation. |
| headingIDs map[string]int |
| |
| lastOutputLen int |
| disableTags int |
| |
| sr *SPRenderer |
| } |
| |
| const ( |
| xhtmlClose = " />" |
| htmlClose = ">" |
| ) |
| |
| // NewHTMLRenderer creates and configures an HTMLRenderer object, which |
| // satisfies the Renderer interface. |
| func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { |
| // configure the rendering engine |
| closeTag := htmlClose |
| if params.Flags&UseXHTML != 0 { |
| closeTag = xhtmlClose |
| } |
| |
| if params.FootnoteReturnLinkContents == "" { |
| // U+FE0E is VARIATION SELECTOR-15. |
| // It suppresses automatic emoji presentation of the preceding |
| // U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS. |
| params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>" |
| } |
| |
| return &HTMLRenderer{ |
| HTMLRendererParameters: params, |
| |
| closeTag: closeTag, |
| headingIDs: make(map[string]int), |
| |
| sr: NewSmartypantsRenderer(params.Flags), |
| } |
| } |
| |
| func isHTMLTag(tag []byte, tagname string) bool { |
| found, _ := findHTMLTagPos(tag, tagname) |
| return found |
| } |
| |
| // Look for a character, but ignore it when it's in any kind of quotes, it |
| // might be JavaScript |
| func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { |
| inSingleQuote := false |
| inDoubleQuote := false |
| inGraveQuote := false |
| i := start |
| for i < len(html) { |
| switch { |
| case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: |
| return i |
| case html[i] == '\'': |
| inSingleQuote = !inSingleQuote |
| case html[i] == '"': |
| inDoubleQuote = !inDoubleQuote |
| case html[i] == '`': |
| inGraveQuote = !inGraveQuote |
| } |
| i++ |
| } |
| return start |
| } |
| |
| func findHTMLTagPos(tag []byte, tagname string) (bool, int) { |
| i := 0 |
| if i < len(tag) && tag[0] != '<' { |
| return false, -1 |
| } |
| i++ |
| i = skipSpace(tag, i) |
| |
| if i < len(tag) && tag[i] == '/' { |
| i++ |
| } |
| |
| i = skipSpace(tag, i) |
| j := 0 |
| for ; i < len(tag); i, j = i+1, j+1 { |
| if j >= len(tagname) { |
| break |
| } |
| |
| if strings.ToLower(string(tag[i]))[0] != tagname[j] { |
| return false, -1 |
| } |
| } |
| |
| if i == len(tag) { |
| return false, -1 |
| } |
| |
| rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') |
| if rightAngle >= i { |
| return true, rightAngle |
| } |
| |
| return false, -1 |
| } |
| |
| func skipSpace(tag []byte, i int) int { |
| for i < len(tag) && isspace(tag[i]) { |
| i++ |
| } |
| return i |
| } |
| |
| func isRelativeLink(link []byte) (yes bool) { |
| // a tag begin with '#' |
| if link[0] == '#' { |
| return true |
| } |
| |
| // link begin with '/' but not '//', the second maybe a protocol relative link |
| if len(link) >= 2 && link[0] == '/' && link[1] != '/' { |
| return true |
| } |
| |
| // only the root '/' |
| if len(link) == 1 && link[0] == '/' { |
| return true |
| } |
| |
| // current directory : begin with "./" |
| if bytes.HasPrefix(link, []byte("./")) { |
| return true |
| } |
| |
| // parent directory : begin with "../" |
| if bytes.HasPrefix(link, []byte("../")) { |
| return true |
| } |
| |
| return false |
| } |
| |
| func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { |
| for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { |
| tmp := fmt.Sprintf("%s-%d", id, count+1) |
| |
| if _, tmpFound := r.headingIDs[tmp]; !tmpFound { |
| r.headingIDs[id] = count + 1 |
| id = tmp |
| } else { |
| id = id + "-1" |
| } |
| } |
| |
| if _, found := r.headingIDs[id]; !found { |
| r.headingIDs[id] = 0 |
| } |
| |
| return id |
| } |
| |
| func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { |
| if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { |
| newDest := r.AbsolutePrefix |
| if link[0] != '/' { |
| newDest += "/" |
| } |
| newDest += string(link) |
| return []byte(newDest) |
| } |
| return link |
| } |
| |
| func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { |
| if isRelativeLink(link) { |
| return attrs |
| } |
| val := []string{} |
| if flags&NofollowLinks != 0 { |
| val = append(val, "nofollow") |
| } |
| if flags&NoreferrerLinks != 0 { |
| val = append(val, "noreferrer") |
| } |
| if flags&NoopenerLinks != 0 { |
| val = append(val, "noopener") |
| } |
| if flags&HrefTargetBlank != 0 { |
| attrs = append(attrs, "target=\"_blank\"") |
| } |
| if len(val) == 0 { |
| return attrs |
| } |
| attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) |
| return append(attrs, attr) |
| } |
| |
| func isMailto(link []byte) bool { |
| return bytes.HasPrefix(link, []byte("mailto:")) |
| } |
| |
| func needSkipLink(flags HTMLFlags, dest []byte) bool { |
| if flags&SkipLinks != 0 { |
| return true |
| } |
| return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) |
| } |
| |
| func isSmartypantable(node *Node) bool { |
| pt := node.Parent.Type |
| return pt != Link && pt != CodeBlock && pt != Code |
| } |
| |
| func appendLanguageAttr(attrs []string, info []byte) []string { |
| if len(info) == 0 { |
| return attrs |
| } |
| endOfLang := bytes.IndexAny(info, "\t ") |
| if endOfLang < 0 { |
| endOfLang = len(info) |
| } |
| return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) |
| } |
| |
| func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { |
| w.Write(name) |
| if len(attrs) > 0 { |
| w.Write(spaceBytes) |
| w.Write([]byte(strings.Join(attrs, " "))) |
| } |
| w.Write(gtBytes) |
| r.lastOutputLen = 1 |
| } |
| |
| func footnoteRef(prefix string, node *Node) []byte { |
| urlFrag := prefix + string(slugify(node.Destination)) |
| anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID) |
| return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) |
| } |
| |
| func footnoteItem(prefix string, slug []byte) []byte { |
| return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) |
| } |
| |
| func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { |
| const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>` |
| return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) |
| } |
| |
| func itemOpenCR(node *Node) bool { |
| if node.Prev == nil { |
| return false |
| } |
| ld := node.Parent.ListData |
| return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 |
| } |
| |
| func skipParagraphTags(node *Node) bool { |
| grandparent := node.Parent.Parent |
| if grandparent == nil || grandparent.Type != List { |
| return false |
| } |
| tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 |
| return grandparent.Type == List && tightOrTerm |
| } |
| |
| func cellAlignment(align CellAlignFlags) string { |
| switch align { |
| case TableAlignmentLeft: |
| return "left" |
| case TableAlignmentRight: |
| return "right" |
| case TableAlignmentCenter: |
| return "center" |
| default: |
| return "" |
| } |
| } |
| |
| func (r *HTMLRenderer) out(w io.Writer, text []byte) { |
| if r.disableTags > 0 { |
| w.Write(htmlTagRe.ReplaceAll(text, []byte{})) |
| } else { |
| w.Write(text) |
| } |
| r.lastOutputLen = len(text) |
| } |
| |
| func (r *HTMLRenderer) cr(w io.Writer) { |
| if r.lastOutputLen > 0 { |
| r.out(w, nlBytes) |
| } |
| } |
| |
| var ( |
| nlBytes = []byte{'\n'} |
| gtBytes = []byte{'>'} |
| spaceBytes = []byte{' '} |
| ) |
| |
| var ( |
| brTag = []byte("<br>") |
| brXHTMLTag = []byte("<br />") |
| emTag = []byte("<em>") |
| emCloseTag = []byte("</em>") |
| strongTag = []byte("<strong>") |
| strongCloseTag = []byte("</strong>") |
| delTag = []byte("<del>") |
| delCloseTag = []byte("</del>") |
| ttTag = []byte("<tt>") |
| ttCloseTag = []byte("</tt>") |
| aTag = []byte("<a") |
| aCloseTag = []byte("</a>") |
| preTag = []byte("<pre>") |
| preCloseTag = []byte("</pre>") |
| codeTag = []byte("<code>") |
| codeCloseTag = []byte("</code>") |
| pTag = []byte("<p>") |
| pCloseTag = []byte("</p>") |
| blockquoteTag = []byte("<blockquote>") |
| blockquoteCloseTag = []byte("</blockquote>") |
| hrTag = []byte("<hr>") |
| hrXHTMLTag = []byte("<hr />") |
| ulTag = []byte("<ul>") |
| ulCloseTag = []byte("</ul>") |
| olTag = []byte("<ol>") |
| olCloseTag = []byte("</ol>") |
| dlTag = []byte("<dl>") |
| dlCloseTag = []byte("</dl>") |
| liTag = []byte("<li>") |
| liCloseTag = []byte("</li>") |
| ddTag = []byte("<dd>") |
| ddCloseTag = []byte("</dd>") |
| dtTag = []byte("<dt>") |
| dtCloseTag = []byte("</dt>") |
| tableTag = []byte("<table>") |
| tableCloseTag = []byte("</table>") |
| tdTag = []byte("<td") |
| tdCloseTag = []byte("</td>") |
| thTag = []byte("<th") |
| thCloseTag = []byte("</th>") |
| theadTag = []byte("<thead>") |
| theadCloseTag = []byte("</thead>") |
| tbodyTag = []byte("<tbody>") |
| tbodyCloseTag = []byte("</tbody>") |
| trTag = []byte("<tr>") |
| trCloseTag = []byte("</tr>") |
| h1Tag = []byte("<h1") |
| h1CloseTag = []byte("</h1>") |
| h2Tag = []byte("<h2") |
| h2CloseTag = []byte("</h2>") |
| h3Tag = []byte("<h3") |
| h3CloseTag = []byte("</h3>") |
| h4Tag = []byte("<h4") |
| h4CloseTag = []byte("</h4>") |
| h5Tag = []byte("<h5") |
| h5CloseTag = []byte("</h5>") |
| h6Tag = []byte("<h6") |
| h6CloseTag = []byte("</h6>") |
| |
| footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n") |
| footnotesCloseDivBytes = []byte("\n</div>\n") |
| ) |
| |
| func headingTagsFromLevel(level int) ([]byte, []byte) { |
| if level <= 1 { |
| return h1Tag, h1CloseTag |
| } |
| switch level { |
| case 2: |
| return h2Tag, h2CloseTag |
| case 3: |
| return h3Tag, h3CloseTag |
| case 4: |
| return h4Tag, h4CloseTag |
| case 5: |
| return h5Tag, h5CloseTag |
| } |
| return h6Tag, h6CloseTag |
| } |
| |
| func (r *HTMLRenderer) outHRTag(w io.Writer) { |
| if r.Flags&UseXHTML == 0 { |
| r.out(w, hrTag) |
| } else { |
| r.out(w, hrXHTMLTag) |
| } |
| } |
| |
| // RenderNode is a default renderer of a single node of a syntax tree. For |
| // block nodes it will be called twice: first time with entering=true, second |
| // time with entering=false, so that it could know when it's working on an open |
| // tag and when on close. It writes the result to w. |
| // |
| // The return value is a way to tell the calling walker to adjust its walk |
| // pattern: e.g. it can terminate the traversal by returning Terminate. Or it |
| // can ask the walker to skip a subtree of this node by returning SkipChildren. |
| // The typical behavior is to return GoToNext, which asks for the usual |
| // traversal to the next node. |
| func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { |
| attrs := []string{} |
| switch node.Type { |
| case Text: |
| if r.Flags&Smartypants != 0 { |
| var tmp bytes.Buffer |
| escapeHTML(&tmp, node.Literal) |
| r.sr.Process(w, tmp.Bytes()) |
| } else { |
| if node.Parent.Type == Link { |
| escLink(w, node.Literal) |
| } else { |
| escapeHTML(w, node.Literal) |
| } |
| } |
| case Softbreak: |
| r.cr(w) |
| // TODO: make it configurable via out(renderer.softbreak) |
| case Hardbreak: |
| if r.Flags&UseXHTML == 0 { |
| r.out(w, brTag) |
| } else { |
| r.out(w, brXHTMLTag) |
| } |
| r.cr(w) |
| case Emph: |
| if entering { |
| r.out(w, emTag) |
| } else { |
| r.out(w, emCloseTag) |
| } |
| case Strong: |
| if entering { |
| r.out(w, strongTag) |
| } else { |
| r.out(w, strongCloseTag) |
| } |
| case Del: |
| if entering { |
| r.out(w, delTag) |
| } else { |
| r.out(w, delCloseTag) |
| } |
| case HTMLSpan: |
| if r.Flags&SkipHTML != 0 { |
| break |
| } |
| r.out(w, node.Literal) |
| case Link: |
| // mark it but don't link it if it is not a safe link: no smartypants |
| dest := node.LinkData.Destination |
| if needSkipLink(r.Flags, dest) { |
| if entering { |
| r.out(w, ttTag) |
| } else { |
| r.out(w, ttCloseTag) |
| } |
| } else { |
| if entering { |
| dest = r.addAbsPrefix(dest) |
| var hrefBuf bytes.Buffer |
| hrefBuf.WriteString("href=\"") |
| escLink(&hrefBuf, dest) |
| hrefBuf.WriteByte('"') |
| attrs = append(attrs, hrefBuf.String()) |
| if node.NoteID != 0 { |
| r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) |
| break |
| } |
| attrs = appendLinkAttrs(attrs, r.Flags, dest) |
| if len(node.LinkData.Title) > 0 { |
| var titleBuff bytes.Buffer |
| titleBuff.WriteString("title=\"") |
| escapeHTML(&titleBuff, node.LinkData.Title) |
| titleBuff.WriteByte('"') |
| attrs = append(attrs, titleBuff.String()) |
| } |
| r.tag(w, aTag, attrs) |
| } else { |
| if node.NoteID != 0 { |
| break |
| } |
| r.out(w, aCloseTag) |
| } |
| } |
| case Image: |
| if r.Flags&SkipImages != 0 { |
| return SkipChildren |
| } |
| if entering { |
| dest := node.LinkData.Destination |
| dest = r.addAbsPrefix(dest) |
| if r.disableTags == 0 { |
| //if options.safe && potentiallyUnsafe(dest) { |
| //out(w, `<img src="" alt="`) |
| //} else { |
| r.out(w, []byte(`<img src="`)) |
| escLink(w, dest) |
| r.out(w, []byte(`" alt="`)) |
| //} |
| } |
| r.disableTags++ |
| } else { |
| r.disableTags-- |
| if r.disableTags == 0 { |
| if node.LinkData.Title != nil { |
| r.out(w, []byte(`" title="`)) |
| escapeHTML(w, node.LinkData.Title) |
| } |
| r.out(w, []byte(`" />`)) |
| } |
| } |
| case Code: |
| r.out(w, codeTag) |
| escapeAllHTML(w, node.Literal) |
| r.out(w, codeCloseTag) |
| case Document: |
| break |
| case Paragraph: |
| if skipParagraphTags(node) { |
| break |
| } |
| if entering { |
| // TODO: untangle this clusterfuck about when the newlines need |
| // to be added and when not. |
| if node.Prev != nil { |
| switch node.Prev.Type { |
| case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: |
| r.cr(w) |
| } |
| } |
| if node.Parent.Type == BlockQuote && node.Prev == nil { |
| r.cr(w) |
| } |
| r.out(w, pTag) |
| } else { |
| r.out(w, pCloseTag) |
| if !(node.Parent.Type == Item && node.Next == nil) { |
| r.cr(w) |
| } |
| } |
| case BlockQuote: |
| if entering { |
| r.cr(w) |
| r.out(w, blockquoteTag) |
| } else { |
| r.out(w, blockquoteCloseTag) |
| r.cr(w) |
| } |
| case HTMLBlock: |
| if r.Flags&SkipHTML != 0 { |
| break |
| } |
| r.cr(w) |
| r.out(w, node.Literal) |
| r.cr(w) |
| case Heading: |
| headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level |
| openTag, closeTag := headingTagsFromLevel(headingLevel) |
| if entering { |
| if node.IsTitleblock { |
| attrs = append(attrs, `class="title"`) |
| } |
| if node.HeadingID != "" { |
| id := r.ensureUniqueHeadingID(node.HeadingID) |
| if r.HeadingIDPrefix != "" { |
| id = r.HeadingIDPrefix + id |
| } |
| if r.HeadingIDSuffix != "" { |
| id = id + r.HeadingIDSuffix |
| } |
| attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) |
| } |
| r.cr(w) |
| r.tag(w, openTag, attrs) |
| } else { |
| r.out(w, closeTag) |
| if !(node.Parent.Type == Item && node.Next == nil) { |
| r.cr(w) |
| } |
| } |
| case HorizontalRule: |
| r.cr(w) |
| r.outHRTag(w) |
| r.cr(w) |
| case List: |
| openTag := ulTag |
| closeTag := ulCloseTag |
| if node.ListFlags&ListTypeOrdered != 0 { |
| openTag = olTag |
| closeTag = olCloseTag |
| } |
| if node.ListFlags&ListTypeDefinition != 0 { |
| openTag = dlTag |
| closeTag = dlCloseTag |
| } |
| if entering { |
| if node.IsFootnotesList { |
| r.out(w, footnotesDivBytes) |
| r.outHRTag(w) |
| r.cr(w) |
| } |
| r.cr(w) |
| if node.Parent.Type == Item && node.Parent.Parent.Tight { |
| r.cr(w) |
| } |
| r.tag(w, openTag[:len(openTag)-1], attrs) |
| r.cr(w) |
| } else { |
| r.out(w, closeTag) |
| //cr(w) |
| //if node.parent.Type != Item { |
| // cr(w) |
| //} |
| if node.Parent.Type == Item && node.Next != nil { |
| r.cr(w) |
| } |
| if node.Parent.Type == Document || node.Parent.Type == BlockQuote { |
| r.cr(w) |
| } |
| if node.IsFootnotesList { |
| r.out(w, footnotesCloseDivBytes) |
| } |
| } |
| case Item: |
| openTag := liTag |
| closeTag := liCloseTag |
| if node.ListFlags&ListTypeDefinition != 0 { |
| openTag = ddTag |
| closeTag = ddCloseTag |
| } |
| if node.ListFlags&ListTypeTerm != 0 { |
| openTag = dtTag |
| closeTag = dtCloseTag |
| } |
| if entering { |
| if itemOpenCR(node) { |
| r.cr(w) |
| } |
| if node.ListData.RefLink != nil { |
| slug := slugify(node.ListData.RefLink) |
| r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) |
| break |
| } |
| r.out(w, openTag) |
| } else { |
| if node.ListData.RefLink != nil { |
| slug := slugify(node.ListData.RefLink) |
| if r.Flags&FootnoteReturnLinks != 0 { |
| r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) |
| } |
| } |
| r.out(w, closeTag) |
| r.cr(w) |
| } |
| case CodeBlock: |
| attrs = appendLanguageAttr(attrs, node.Info) |
| r.cr(w) |
| r.out(w, preTag) |
| r.tag(w, codeTag[:len(codeTag)-1], attrs) |
| escapeAllHTML(w, node.Literal) |
| r.out(w, codeCloseTag) |
| r.out(w, preCloseTag) |
| if node.Parent.Type != Item { |
| r.cr(w) |
| } |
| case Table: |
| if entering { |
| r.cr(w) |
| r.out(w, tableTag) |
| } else { |
| r.out(w, tableCloseTag) |
| r.cr(w) |
| } |
| case TableCell: |
| openTag := tdTag |
| closeTag := tdCloseTag |
| if node.IsHeader { |
| openTag = thTag |
| closeTag = thCloseTag |
| } |
| if entering { |
| align := cellAlignment(node.Align) |
| if align != "" { |
| attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) |
| } |
| if node.Prev == nil { |
| r.cr(w) |
| } |
| r.tag(w, openTag, attrs) |
| } else { |
| r.out(w, closeTag) |
| r.cr(w) |
| } |
| case TableHead: |
| if entering { |
| r.cr(w) |
| r.out(w, theadTag) |
| } else { |
| r.out(w, theadCloseTag) |
| r.cr(w) |
| } |
| case TableBody: |
| if entering { |
| r.cr(w) |
| r.out(w, tbodyTag) |
| // XXX: this is to adhere to a rather silly test. Should fix test. |
| if node.FirstChild == nil { |
| r.cr(w) |
| } |
| } else { |
| r.out(w, tbodyCloseTag) |
| r.cr(w) |
| } |
| case TableRow: |
| if entering { |
| r.cr(w) |
| r.out(w, trTag) |
| } else { |
| r.out(w, trCloseTag) |
| r.cr(w) |
| } |
| default: |
| panic("Unknown node type " + node.Type.String()) |
| } |
| return GoToNext |
| } |
| |
| // RenderHeader writes HTML document preamble and TOC if requested. |
| func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { |
| r.writeDocumentHeader(w) |
| if r.Flags&TOC != 0 { |
| r.writeTOC(w, ast) |
| } |
| } |
| |
| // RenderFooter writes HTML document footer. |
| func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { |
| if r.Flags&CompletePage == 0 { |
| return |
| } |
| io.WriteString(w, "\n</body>\n</html>\n") |
| } |
| |
| func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { |
| if r.Flags&CompletePage == 0 { |
| return |
| } |
| ending := "" |
| if r.Flags&UseXHTML != 0 { |
| io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") |
| io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") |
| io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") |
| ending = " /" |
| } else { |
| io.WriteString(w, "<!DOCTYPE html>\n") |
| io.WriteString(w, "<html>\n") |
| } |
| io.WriteString(w, "<head>\n") |
| io.WriteString(w, " <title>") |
| if r.Flags&Smartypants != 0 { |
| r.sr.Process(w, []byte(r.Title)) |
| } else { |
| escapeHTML(w, []byte(r.Title)) |
| } |
| io.WriteString(w, "</title>\n") |
| io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") |
| io.WriteString(w, Version) |
| io.WriteString(w, "\"") |
| io.WriteString(w, ending) |
| io.WriteString(w, ">\n") |
| io.WriteString(w, " <meta charset=\"utf-8\"") |
| io.WriteString(w, ending) |
| io.WriteString(w, ">\n") |
| if r.CSS != "" { |
| io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"") |
| escapeHTML(w, []byte(r.CSS)) |
| io.WriteString(w, "\"") |
| io.WriteString(w, ending) |
| io.WriteString(w, ">\n") |
| } |
| if r.Icon != "" { |
| io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"") |
| escapeHTML(w, []byte(r.Icon)) |
| io.WriteString(w, "\"") |
| io.WriteString(w, ending) |
| io.WriteString(w, ">\n") |
| } |
| io.WriteString(w, "</head>\n") |
| io.WriteString(w, "<body>\n\n") |
| } |
| |
| func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { |
| buf := bytes.Buffer{} |
| |
| inHeading := false |
| tocLevel := 0 |
| headingCount := 0 |
| |
| ast.Walk(func(node *Node, entering bool) WalkStatus { |
| if node.Type == Heading && !node.HeadingData.IsTitleblock { |
| inHeading = entering |
| if entering { |
| node.HeadingID = fmt.Sprintf("toc_%d", headingCount) |
| if node.Level == tocLevel { |
| buf.WriteString("</li>\n\n<li>") |
| } else if node.Level < tocLevel { |
| for node.Level < tocLevel { |
| tocLevel-- |
| buf.WriteString("</li>\n</ul>") |
| } |
| buf.WriteString("</li>\n\n<li>") |
| } else { |
| for node.Level > tocLevel { |
| tocLevel++ |
| buf.WriteString("\n<ul>\n<li>") |
| } |
| } |
| |
| fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) |
| headingCount++ |
| } else { |
| buf.WriteString("</a>") |
| } |
| return GoToNext |
| } |
| |
| if inHeading { |
| return r.RenderNode(&buf, node, entering) |
| } |
| |
| return GoToNext |
| }) |
| |
| for ; tocLevel > 0; tocLevel-- { |
| buf.WriteString("</li>\n</ul>") |
| } |
| |
| if buf.Len() > 0 { |
| io.WriteString(w, "<nav>\n") |
| w.Write(buf.Bytes()) |
| io.WriteString(w, "\n\n</nav>\n") |
| } |
| r.lastOutputLen = buf.Len() |
| } |