Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
949 lines
24 KiB
949 lines
24 KiB
// |
|
// 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 == "" { |
|
params.FootnoteReturnLinkContents = `<sup>[return]</sup>` |
|
} |
|
|
|
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) |
|
escapeHTML(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) |
|
escapeHTML(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() |
|
}
|
|
|