refactor HTMLRenderer.RenderNode; change Walk to be more like in go/ast package

This commit is contained in:
Krzysztof Kowalczyk 2018-01-26 18:54:34 -08:00
parent 80758f072a
commit 41105f379c
4 changed files with 454 additions and 385 deletions

View File

@ -307,29 +307,29 @@ func appendLanguageAttr(attrs []string, info []byte) []string {
return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
} }
func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { func (r *HTMLRenderer) tag(w io.Writer, name string, attrs []string) {
w.Write(name) io.WriteString(w, name)
if len(attrs) > 0 { if len(attrs) > 0 {
w.Write(spaceBytes) w.Write(spaceBytes)
w.Write([]byte(strings.Join(attrs, " "))) io.WriteString(w, strings.Join(attrs, " "))
} }
w.Write(gtBytes) w.Write(gtBytes)
r.lastOutputLen = 1 r.lastOutputLen = 1
} }
func footnoteRef(prefix string, node *LinkData) []byte { func footnoteRef(prefix string, node *LinkData) string {
urlFrag := prefix + string(slugify(node.Destination)) urlFrag := prefix + string(slugify(node.Destination))
anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID) anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) return fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)
} }
func footnoteItem(prefix string, slug []byte) []byte { func footnoteItem(prefix string, slug []byte) string {
return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) return fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)
} }
func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>` const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) return fmt.Sprintf(format, prefix, slug, returnLink)
} }
func itemOpenCR(node *Node) bool { func itemOpenCR(node *Node) bool {
@ -365,13 +365,20 @@ func cellAlignment(align CellAlignFlags) string {
} }
} }
func (r *HTMLRenderer) out(w io.Writer, text []byte) { func (r *HTMLRenderer) out(w io.Writer, d []byte) {
r.lastOutputLen = len(d)
if r.disableTags > 0 { if r.disableTags > 0 {
w.Write(htmlTagRe.ReplaceAll(text, []byte{})) d = htmlTagRe.ReplaceAll(d, []byte{})
} else {
w.Write(text)
} }
r.lastOutputLen = len(text) w.Write(d)
}
func (r *HTMLRenderer) outs(w io.Writer, s string) {
r.lastOutputLen = len(s)
if r.disableTags > 0 {
s = htmlTagRe.ReplaceAllString(s, "")
}
io.WriteString(w, s)
} }
func (r *HTMLRenderer) cr(w io.Writer) { func (r *HTMLRenderer) cr(w io.Writer) {
@ -386,92 +393,405 @@ var (
spaceBytes = []byte{' '} spaceBytes = []byte{' '}
) )
var ( func headingOpenTagFromLevel(level int) string {
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) {
switch level { switch level {
case 1: case 1:
return h1Tag, h1CloseTag return "<h1"
case 2: case 2:
return h2Tag, h2CloseTag return "<h2"
case 3: case 3:
return h3Tag, h3CloseTag return "<h3"
case 4: case 4:
return h4Tag, h4CloseTag return "<h4"
case 5: case 5:
return h5Tag, h5CloseTag return "<h5"
default: default:
return h6Tag, h6CloseTag return "<h6"
}
}
func headingCloseTagFromLevel(level int) string {
switch level {
case 1:
return "</h1>"
case 2:
return "</h2>"
case 3:
return "</h3>"
case 4:
return "</h4>"
case 5:
return "</h5>"
default:
return "</h6>"
} }
} }
func (r *HTMLRenderer) outHRTag(w io.Writer) { func (r *HTMLRenderer) outHRTag(w io.Writer) {
if r.params.Flags&UseXHTML == 0 { if r.params.Flags&UseXHTML == 0 {
r.out(w, hrTag) r.out(w, []byte("<hr>"))
} else { } else {
r.out(w, hrXHTMLTag) r.out(w, []byte("<hr />"))
}
}
func (r *HTMLRenderer) text(w io.Writer, node *Node, nodeData *TextData) {
if r.params.Flags&Smartypants != 0 {
var tmp bytes.Buffer
escapeHTML(&tmp, node.Literal)
r.sr.Process(w, tmp.Bytes())
} else {
if isLinkData(node.Parent.Data) {
escLink(w, node.Literal)
} else {
escapeHTML(w, node.Literal)
}
}
}
func (r *HTMLRenderer) hardBreak(w io.Writer, node *Node, nodeData *HardbreakData) {
s := "<br>"
if r.params.Flags&UseXHTML != 0 {
s = "<br />"
}
r.outs(w, s)
r.cr(w)
}
func (r *HTMLRenderer) openOrCloseTag(w io.Writer, isOpen bool, openTag string, closeTag string) {
if isOpen {
r.outs(w, openTag)
} else {
r.outs(w, closeTag)
}
}
func (r *HTMLRenderer) crOpenOrCloseTag(w io.Writer, isOpen bool, openTag string, closeTag string) {
if isOpen {
r.cr(w)
r.outs(w, openTag)
} else {
r.outs(w, closeTag)
r.cr(w)
}
}
func (r *HTMLRenderer) span(w io.Writer, node *Node, nodeData *HTMLSpanData) {
if r.params.Flags&SkipHTML != 0 {
return
}
r.out(w, node.Literal)
}
func (r *HTMLRenderer) link(w io.Writer, node *Node, nodeData *LinkData, entering bool) {
var attrs []string
// mark it but don't link it if it is not a safe link: no smartypants
dest := nodeData.Destination
if needSkipLink(r.params.Flags, dest) {
r.openOrCloseTag(w, entering, "<tt>", "</tt>")
return
}
if !entering {
if nodeData.NoteID == 0 {
r.out(w, []byte("</a>"))
}
return
}
// entering
dest = r.addAbsPrefix(dest)
var hrefBuf bytes.Buffer
hrefBuf.WriteString("href=\"")
escLink(&hrefBuf, dest)
hrefBuf.WriteByte('"')
attrs = append(attrs, hrefBuf.String())
if nodeData.NoteID != 0 {
r.outs(w, footnoteRef(r.params.FootnoteAnchorPrefix, nodeData))
return
}
attrs = appendLinkAttrs(attrs, r.params.Flags, dest)
if len(nodeData.Title) > 0 {
var titleBuff bytes.Buffer
titleBuff.WriteString("title=\"")
escapeHTML(&titleBuff, nodeData.Title)
titleBuff.WriteByte('"')
attrs = append(attrs, titleBuff.String())
}
r.tag(w, "<a", attrs)
}
func (r *HTMLRenderer) imageEnter(w io.Writer, node *Node, nodeData *ImageData) {
dest := nodeData.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++
}
func (r *HTMLRenderer) imageExit(w io.Writer, node *Node, nodeData *ImageData) {
r.disableTags--
if r.disableTags == 0 {
if nodeData.Title != nil {
r.out(w, []byte(`" title="`))
escapeHTML(w, nodeData.Title)
}
r.out(w, []byte(`" />`))
}
}
func (r *HTMLRenderer) paragraphEnter(w io.Writer, node *Node, nodeData *ParagraphData) {
// TODO: untangle this clusterfuck about when the newlines need
// to be added and when not.
if node.Prev != nil {
switch node.Prev.Data.(type) {
case *HTMLBlockData, *ListData, *ParagraphData, *HeadingData, *CodeBlockData, *BlockQuoteData, *HorizontalRuleData:
r.cr(w)
}
}
if isBlockQuoteData(node.Parent.Data) && node.Prev == nil {
r.cr(w)
}
r.out(w, []byte("<p>"))
}
func (r *HTMLRenderer) paragraphExit(w io.Writer, node *Node, nodeData *ParagraphData) {
r.out(w, []byte("</p>"))
if !(isItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
}
func (r *HTMLRenderer) paragraph(w io.Writer, node *Node, nodeData *ParagraphData, entering bool) {
if skipParagraphTags(node) {
return
}
if entering {
r.paragraphEnter(w, node, nodeData)
} else {
r.paragraphExit(w, node, nodeData)
}
}
func (r *HTMLRenderer) image(w io.Writer, node *Node, nodeData *ImageData, entering bool) {
if entering {
r.imageEnter(w, node, nodeData)
} else {
r.imageExit(w, node, nodeData)
}
}
func (r *HTMLRenderer) code(w io.Writer, node *Node, nodeData *CodeData) {
r.outs(w, "<code>")
escapeHTML(w, node.Literal)
r.outs(w, "</code>")
}
func (r *HTMLRenderer) htmlBlock(w io.Writer, node *Node, nodeData *HTMLBlockData) {
if r.params.Flags&SkipHTML != 0 {
return
}
r.cr(w)
r.out(w, node.Literal)
r.cr(w)
}
func (r *HTMLRenderer) heading(w io.Writer, node *Node, nodeData *HeadingData, entering bool) {
if !entering {
closeTag := headingCloseTagFromLevel(nodeData.Level)
r.outs(w, closeTag)
if !(isItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
return
}
// entering
var attrs []string
if nodeData.IsTitleblock {
attrs = append(attrs, `class="title"`)
}
if nodeData.HeadingID != "" {
id := r.ensureUniqueHeadingID(nodeData.HeadingID)
if r.params.HeadingIDPrefix != "" {
id = r.params.HeadingIDPrefix + id
}
if r.params.HeadingIDSuffix != "" {
id = id + r.params.HeadingIDSuffix
}
attrID := `id="` + id + `"`
attrs = append(attrs, attrID)
}
r.cr(w)
openTag := headingOpenTagFromLevel(nodeData.Level)
r.tag(w, openTag, attrs)
}
func (r *HTMLRenderer) horizontalRule(w io.Writer) {
r.cr(w)
r.outHRTag(w)
r.cr(w)
}
func (r *HTMLRenderer) listEnter(w io.Writer, node *Node, nodeData *ListData) {
// TODO: attrs don't seem to be set
var attrs []string
openTag := "<ul"
if nodeData.ListFlags&ListTypeOrdered != 0 {
openTag = "<ol"
}
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = "<dl"
}
if nodeData.IsFootnotesList {
r.outs(w, "\n<div class=\"footnotes\">\n\n")
r.outHRTag(w)
r.cr(w)
}
r.cr(w)
if isItemData(node.Parent.Data) {
grand := node.Parent.Parent
if isListTight(grand.Data) {
r.cr(w)
}
}
r.tag(w, openTag, attrs)
r.cr(w)
}
func (r *HTMLRenderer) listExit(w io.Writer, node *Node, nodeData *ListData) {
closeTag := "</ul>"
if nodeData.ListFlags&ListTypeOrdered != 0 {
closeTag = "</ol>"
}
if nodeData.ListFlags&ListTypeDefinition != 0 {
closeTag = "</dl>"
}
r.outs(w, closeTag)
//cr(w)
//if node.parent.Type != Item {
// cr(w)
//}
if isItemData(node.Parent.Data) && node.Next != nil {
r.cr(w)
}
if isDocumentData(node.Parent.Data) || isBlockQuoteData(node.Parent.Data) {
r.cr(w)
}
if nodeData.IsFootnotesList {
r.outs(w, "\n</div>\n")
}
}
func (r *HTMLRenderer) list(w io.Writer, node *Node, nodeData *ListData, entering bool) {
if entering {
r.listEnter(w, node, nodeData)
} else {
r.listExit(w, node, nodeData)
}
}
func (r *HTMLRenderer) item(w io.Writer, node *Node, nodeData *ItemData, entering bool) {
if entering {
openTag := "<li>"
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = "<dd>"
}
if nodeData.ListFlags&ListTypeTerm != 0 {
openTag = "<dt>"
}
if itemOpenCR(node) {
r.cr(w)
}
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
r.outs(w, footnoteItem(r.params.FootnoteAnchorPrefix, slug))
return
}
r.outs(w, openTag)
} else {
closeTag := "</li>"
if nodeData.ListFlags&ListTypeDefinition != 0 {
closeTag = "</dd>"
}
if nodeData.ListFlags&ListTypeTerm != 0 {
closeTag = "</dt>"
}
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
if r.params.Flags&FootnoteReturnLinks != 0 {
r.outs(w, footnoteReturnLink(r.params.FootnoteAnchorPrefix, r.params.FootnoteReturnLinkContents, slug))
}
}
r.outs(w, closeTag)
r.cr(w)
}
}
func (r *HTMLRenderer) codeBlock(w io.Writer, node *Node, nodeData *CodeBlockData) {
var attrs []string
attrs = appendLanguageAttr(attrs, nodeData.Info)
r.cr(w)
r.outs(w, "<pre>")
r.tag(w, "<code", attrs)
escapeHTML(w, node.Literal)
r.outs(w, "</code>")
r.outs(w, "</pre>")
if !isItemData(node.Parent.Data) {
r.cr(w)
}
}
func (r *HTMLRenderer) tableCell(w io.Writer, node *Node, nodeData *TableCellData, entering bool) {
if !entering {
closeTag := "</td>"
if nodeData.IsHeader {
closeTag = "</th>"
}
r.outs(w, closeTag)
r.cr(w)
return
}
// entering
var attrs []string
openTag := "<td"
if nodeData.IsHeader {
openTag = "<th"
}
align := cellAlignment(nodeData.Align)
if align != "" {
attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
}
if node.Prev == nil {
r.cr(w)
}
r.tag(w, openTag, attrs)
}
func (r *HTMLRenderer) tableBody(w io.Writer, node *Node, nodeData *TableBodyData, entering bool) {
if entering {
r.cr(w)
r.outs(w, "<tbody>")
// XXX: this is to adhere to a rather silly test. Should fix test.
if node.FirstChild == nil {
r.cr(w)
}
} else {
r.outs(w, "</tbody>")
r.cr(w)
} }
} }
@ -486,328 +806,59 @@ func (r *HTMLRenderer) outHRTag(w io.Writer) {
// The typical behavior is to return GoToNext, which asks for the usual // The typical behavior is to return GoToNext, which asks for the usual
// traversal to the next node. // traversal to the next node.
func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
attrs := []string{}
switch nodeData := node.Data.(type) { switch nodeData := node.Data.(type) {
case *TextData: case *TextData:
if r.params.Flags&Smartypants != 0 { r.text(w, node, nodeData)
var tmp bytes.Buffer
escapeHTML(&tmp, node.Literal)
r.sr.Process(w, tmp.Bytes())
} else {
if isLinkData(node.Parent.Data) {
escLink(w, node.Literal)
} else {
escapeHTML(w, node.Literal)
}
}
case *SoftbreakData: case *SoftbreakData:
r.cr(w) r.cr(w)
// TODO: make it configurable via out(renderer.softbreak) // TODO: make it configurable via out(renderer.softbreak)
case *HardbreakData: case *HardbreakData:
if r.params.Flags&UseXHTML == 0 { r.hardBreak(w, node, nodeData)
r.out(w, brTag)
} else {
r.out(w, brXHTMLTag)
}
r.cr(w)
case *EmphData: case *EmphData:
if entering { r.openOrCloseTag(w, entering, "<em>", "</em>")
r.out(w, emTag)
} else {
r.out(w, emCloseTag)
}
case *StrongData: case *StrongData:
if entering { r.openOrCloseTag(w, entering, "<strong>", "</strong>")
r.out(w, strongTag)
} else {
r.out(w, strongCloseTag)
}
case *DelData: case *DelData:
if entering { r.openOrCloseTag(w, entering, "<del>", "</del>")
r.out(w, delTag)
} else {
r.out(w, delCloseTag)
}
case *HTMLSpanData: case *HTMLSpanData:
if r.params.Flags&SkipHTML != 0 { r.span(w, node, nodeData)
break
}
r.out(w, node.Literal)
case *LinkData: case *LinkData:
// mark it but don't link it if it is not a safe link: no smartypants r.link(w, node, nodeData, entering)
dest := nodeData.Destination
if needSkipLink(r.params.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 nodeData.NoteID != 0 {
r.out(w, footnoteRef(r.params.FootnoteAnchorPrefix, nodeData))
break
}
attrs = appendLinkAttrs(attrs, r.params.Flags, dest)
if len(nodeData.Title) > 0 {
var titleBuff bytes.Buffer
titleBuff.WriteString("title=\"")
escapeHTML(&titleBuff, nodeData.Title)
titleBuff.WriteByte('"')
attrs = append(attrs, titleBuff.String())
}
r.tag(w, aTag, attrs)
} else {
if nodeData.NoteID != 0 {
break
}
r.out(w, aCloseTag)
}
}
case *ImageData: case *ImageData:
if r.params.Flags&SkipImages != 0 { if r.params.Flags&SkipImages != 0 {
return SkipChildren return SkipChildren
} }
if entering { r.image(w, node, nodeData, entering)
dest := nodeData.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 nodeData.Title != nil {
r.out(w, []byte(`" title="`))
escapeHTML(w, nodeData.Title)
}
r.out(w, []byte(`" />`))
}
}
case *CodeData: case *CodeData:
r.out(w, codeTag) r.code(w, node, nodeData)
escapeHTML(w, node.Literal)
r.out(w, codeCloseTag)
case *DocumentData: case *DocumentData:
// do nothing
case *ParagraphData: case *ParagraphData:
if skipParagraphTags(node) { r.paragraph(w, node, nodeData, entering)
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.Data.(type) {
case *HTMLBlockData, *ListData, *ParagraphData, *HeadingData, *CodeBlockData, *BlockQuoteData, *HorizontalRuleData:
r.cr(w)
}
}
if isBlockQuoteData(node.Parent.Data) && node.Prev == nil {
r.cr(w)
}
r.out(w, pTag)
} else {
r.out(w, pCloseTag)
if !(isItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
}
case *BlockQuoteData: case *BlockQuoteData:
if entering { r.crOpenOrCloseTag(w, entering, "<blockquote>", "</blockquote>")
r.cr(w)
r.out(w, blockquoteTag)
} else {
r.out(w, blockquoteCloseTag)
r.cr(w)
}
case *HTMLBlockData: case *HTMLBlockData:
if r.params.Flags&SkipHTML != 0 { r.htmlBlock(w, node, nodeData)
break
}
r.cr(w)
r.out(w, node.Literal)
r.cr(w)
case *HeadingData: case *HeadingData:
openTag, closeTag := headingTagsFromLevel(nodeData.Level) r.heading(w, node, nodeData, entering)
if entering {
if nodeData.IsTitleblock {
attrs = append(attrs, `class="title"`)
}
if nodeData.HeadingID != "" {
id := r.ensureUniqueHeadingID(nodeData.HeadingID)
if r.params.HeadingIDPrefix != "" {
id = r.params.HeadingIDPrefix + id
}
if r.params.HeadingIDSuffix != "" {
id = id + r.params.HeadingIDSuffix
}
attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
}
r.cr(w)
r.tag(w, openTag, attrs)
} else {
r.out(w, closeTag)
if !(isItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
}
case *HorizontalRuleData: case *HorizontalRuleData:
r.cr(w) r.horizontalRule(w)
r.outHRTag(w)
r.cr(w)
case *ListData: case *ListData:
openTag := ulTag r.list(w, node, nodeData, entering)
closeTag := ulCloseTag
if nodeData.ListFlags&ListTypeOrdered != 0 {
openTag = olTag
closeTag = olCloseTag
}
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = dlTag
closeTag = dlCloseTag
}
if entering {
if nodeData.IsFootnotesList {
r.out(w, footnotesDivBytes)
r.outHRTag(w)
r.cr(w)
}
r.cr(w)
if isItemData(node.Parent.Data) {
grand := node.Parent.Parent
if isListTight(grand.Data) {
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 isItemData(node.Parent.Data) && node.Next != nil {
r.cr(w)
}
if isDocumentData(node.Parent.Data) || isBlockQuoteData(node.Parent.Data) {
r.cr(w)
}
if nodeData.IsFootnotesList {
r.out(w, footnotesCloseDivBytes)
}
}
case *ItemData: case *ItemData:
openTag := liTag r.item(w, node, nodeData, entering)
closeTag := liCloseTag
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = ddTag
closeTag = ddCloseTag
}
if nodeData.ListFlags&ListTypeTerm != 0 {
openTag = dtTag
closeTag = dtCloseTag
}
if entering {
if itemOpenCR(node) {
r.cr(w)
}
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
r.out(w, footnoteItem(r.params.FootnoteAnchorPrefix, slug))
break
}
r.out(w, openTag)
} else {
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
if r.params.Flags&FootnoteReturnLinks != 0 {
r.out(w, footnoteReturnLink(r.params.FootnoteAnchorPrefix, r.params.FootnoteReturnLinkContents, slug))
}
}
r.out(w, closeTag)
r.cr(w)
}
case *CodeBlockData: case *CodeBlockData:
attrs = appendLanguageAttr(attrs, nodeData.Info) r.codeBlock(w, node, nodeData)
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 !isItemData(node.Parent.Data) {
r.cr(w)
}
case *TableData: case *TableData:
if entering { r.crOpenOrCloseTag(w, entering, "<table>", "</table>")
r.cr(w)
r.out(w, tableTag)
} else {
r.out(w, tableCloseTag)
r.cr(w)
}
case *TableCellData: case *TableCellData:
openTag := tdTag r.tableCell(w, node, nodeData, entering)
closeTag := tdCloseTag
if nodeData.IsHeader {
openTag = thTag
closeTag = thCloseTag
}
if entering {
align := cellAlignment(nodeData.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 *TableHeadData: case *TableHeadData:
if entering { r.crOpenOrCloseTag(w, entering, "<thead>", "</thead>")
r.cr(w)
r.out(w, theadTag)
} else {
r.out(w, theadCloseTag)
r.cr(w)
}
case *TableBodyData: case *TableBodyData:
if entering { r.tableBody(w, node, nodeData, 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 *TableRowData: case *TableRowData:
if entering { r.crOpenOrCloseTag(w, entering, "<tr>", "</tr>")
r.cr(w)
r.out(w, trTag)
} else {
r.out(w, trCloseTag)
r.cr(w)
}
default: default:
//panic("Unknown node type " + node.Type.String()) //panic("Unknown node type " + node.Type.String())
panic(fmt.Sprintf("Unknown node type %T", node.Data)) panic(fmt.Sprintf("Unknown node type %T", node.Data))
@ -886,7 +937,7 @@ func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
tocLevel := 0 tocLevel := 0
headingCount := 0 headingCount := 0
ast.Walk(func(node *Node, entering bool) WalkStatus { ast.WalkFunc(func(node *Node, entering bool) WalkStatus {
if nodeData, ok := node.Data.(*HeadingData); ok && !nodeData.IsTitleblock { if nodeData, ok := node.Data.(*HeadingData); ok && !nodeData.IsTitleblock {
inHeading = entering inHeading = entering
if entering { if entering {

20
node.go
View File

@ -300,14 +300,24 @@ const (
// NodeVisitor is a callback to be called when traversing the syntax tree. // NodeVisitor is a callback to be called when traversing the syntax tree.
// Called twice for every node: once with entering=true when the branch is // Called twice for every node: once with entering=true when the branch is
// first visited, then with entering=false after all the children are done. // first visited, then with entering=false after all the children are done.
type NodeVisitor func(node *Node, entering bool) WalkStatus type NodeVisitor interface {
Visit(node *Node, entering bool) WalkStatus
}
// NodeVisitorFunc casts a function to match NodeVisitor interface
type NodeVisitorFunc func(node *Node, entering bool) WalkStatus
// Visit calls visitor function
func (f NodeVisitorFunc) Visit(node *Node, entering bool) WalkStatus {
return f(node, entering)
}
// Walk is a convenience method that instantiates a walker and starts a // Walk is a convenience method that instantiates a walker and starts a
// traversal of subtree rooted at n. // traversal of subtree rooted at n.
func (n *Node) Walk(visitor NodeVisitor) { func (n *Node) Walk(visitor NodeVisitor) {
w := newNodeWalker(n) w := newNodeWalker(n)
for w.current != nil { for w.current != nil {
status := visitor(w.current, w.entering) status := visitor.Visit(w.current, w.entering)
switch status { switch status {
case GoToNext: case GoToNext:
w.next() w.next()
@ -320,6 +330,12 @@ func (n *Node) Walk(visitor NodeVisitor) {
} }
} }
// WalkFunc is like Walk but accepts just a callback function
func (n *Node) WalkFunc(f NodeVisitorFunc) {
visitor := NodeVisitorFunc(f)
n.Walk(visitor)
}
type nodeWalker struct { type nodeWalker struct {
current *Node current *Node
root *Node root *Node

View File

@ -238,7 +238,7 @@ func (p *Parser) Render(renderer Renderer) []byte {
var buf bytes.Buffer var buf bytes.Buffer
ast := p.Doc ast := p.Doc
renderer.RenderHeader(&buf, ast) renderer.RenderHeader(&buf, ast)
ast.Walk(func(node *Node, entering bool) WalkStatus { ast.WalkFunc(func(node *Node, entering bool) WalkStatus {
return renderer.RenderNode(&buf, node, entering) return renderer.RenderNode(&buf, node, entering)
}) })
renderer.RenderFooter(&buf, ast) renderer.RenderFooter(&buf, ast)
@ -257,7 +257,7 @@ func (p *Parser) Parse(input []byte) *Node {
p.finalize(p.tip) p.finalize(p.tip)
} }
// Walk the tree again and process inline markdown in each block // Walk the tree again and process inline markdown in each block
p.Doc.Walk(func(node *Node, entering bool) WalkStatus { p.Doc.WalkFunc(func(node *Node, entering bool) WalkStatus {
switch node.Data.(type) { switch node.Data.(type) {
case *ParagraphData, *HeadingData, *TableCellData: case *ParagraphData, *HeadingData, *TableCellData:
p.inline(node, node.content) p.inline(node, node.content)
@ -302,7 +302,7 @@ func (p *Parser) parseRefsToAST() {
above := block.Parent above := block.Parent
finalizeList(block, d) finalizeList(block, d)
p.tip = above p.tip = above
block.Walk(func(node *Node, entering bool) WalkStatus { block.WalkFunc(func(node *Node, entering bool) WalkStatus {
switch node.Data.(type) { switch node.Data.(type) {
case *ParagraphData, *HeadingData: case *ParagraphData, *HeadingData:
p.inline(node, node.content) p.inline(node, node.content)

View File

@ -6,6 +6,8 @@
[x] simplify oliPrefix() [x] simplify oliPrefix()
[ ] rename item => listItem
[ ] speed: Node is probably too fat [ ] speed: Node is probably too fat
[ ] speed: Node could be allocated from a slice and pointers could be replaces with int32 integers. Node would have to keep track of the allocator or we can change the api to pass both Parser (which is allocator of nodes) and Node (which would be a typedef for int) [ ] speed: Node could be allocated from a slice and pointers could be replaces with int32 integers. Node would have to keep track of the allocator or we can change the api to pass both Parser (which is allocator of nodes) and Node (which would be a typedef for int)