| // | |
| // 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() | |
| } |