Implement TOC and OmitContents Move these two flags from HTML renderer's flags to extensions. Implement both since they were not yet implemented in the AST rewrite. Add tests. Note: the expected test strings differ very slightly from v1. The HTML produced by v2 has a few extra newlines compared to the old one, but it's now uniform with other sections of the generated document. If the newline placement gets cleaned up in the future, this will get fixed automatically, since the renderer is agnostic about the TOC list.
Vytautas Ĺ altenis vytas@rtfb.lt
Mon, 04 Apr 2016 10:14:49 +0300
4 files changed,
213 insertions(+),
7 deletions(-)
M
block_test.go
→
block_test.go
@@ -1522,3 +1522,82 @@ "<p>Some text</p>\n\n<!--\n\n<div><p>Commented</p>\n<span>html</span></div>\n-->\n",
} doTestsBlock(t, tests, 0) } + +func TestTOC(t *testing.T) { + var tests = []string{ + "# Title\n\n##Subtitle1\n\n##Subtitle2", + //"<nav>\n<ul>\n<li><a href=\"#toc_0\">Title</a>\n<ul>\n<li><a href=\"#toc_1\">Subtitle1</a></li>\n<li><a href=\"#toc_2\">Subtitle2</a></li>\n</ul></li>\n</ul>\n</nav>\n\n<h1 id=\"toc_0\">Title</h1>\n\n<h2 id=\"toc_1\">Subtitle1</h2>\n\n<h2 id=\"toc_2\">Subtitle2</h2>\n", + `<nav> + +<ul> +<li><a href="#toc_0">Title</a> +<ul> +<li><a href="#toc_1">Subtitle1</a></li> + +<li><a href="#toc_2">Subtitle2</a></li> +</ul></li> +</ul> + +</nav> + +<h1 id="toc_0">Title</h1> + +<h2 id="toc_1">Subtitle1</h2> + +<h2 id="toc_2">Subtitle2</h2> +`, + + "# Title\n\n##Subtitle\n\n#Title2", + //"<nav>\n<ul>\n<li><a href=\"#toc_0\">Title</a>\n<ul>\n<li><a href=\"#toc_1\">Subtitle</a></li>\n</ul></li>\n<li><a href=\"#toc_2\">Title2</a></li>\n</ul>\n</nav>\n\n<h1 id=\"toc_0\">Title</h1>\n\n<h2 id=\"toc_1\">Subtitle</h2>\n\n<h1 id=\"toc_2\">Title2</h1>\n", + `<nav> + +<ul> +<li><a href="#toc_0">Title</a> +<ul> +<li><a href="#toc_1">Subtitle</a></li> +</ul></li> + +<li><a href="#toc_2">Title2</a></li> +</ul> + +</nav> + +<h1 id="toc_0">Title</h1> + +<h2 id="toc_1">Subtitle</h2> + +<h1 id="toc_2">Title2</h1> +`, + + // Trigger empty TOC + "#", + "", + } + doTestsBlock(t, tests, TOC) +} + +func TestOmitContents(t *testing.T) { + var tests = []string{ + "# Title\n\n##Subtitle\n\n#Title2", + `<nav> + +<ul> +<li><a href="#toc_0">Title</a> +<ul> +<li><a href="#toc_1">Subtitle</a></li> +</ul></li> + +<li><a href="#toc_2">Title2</a></li> +</ul> + +</nav> +`, + + // Make sure OmitContents omits even with no TOC + "#\n\nfoo", + "", + } + doTestsBlock(t, tests, TOC|OmitContents) + // Now run again: make sure OmitContents implies TOC + doTestsBlock(t, tests, OmitContents) +}
M
html.go
→
html.go
@@ -38,8 +38,6 @@ Safelink // Only link to trusted protocols
NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" HrefTargetBlank // Add a blank target - TOC // Generate a table of contents - OmitContents // Skip the main contents (for a standalone table of contents) 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@@ -249,7 +247,7 @@
func (r *HTML) BeginHeader(level int, id string) { r.w.Newline() - if id == "" && r.flags&TOC != 0 { + if id == "" && r.extensions&TOC != 0 { id = fmt.Sprintf("toc_%d", r.headerCount) }@@ -272,7 +270,7 @@ }
func (r *HTML) EndHeader(level int, id string, header []byte) { // are we building a table of contents? - if r.flags&TOC != 0 { + if r.extensions&TOC != 0 { r.TocHeaderWithAnchor(header, level, id) }@@ -733,7 +731,7 @@ }
func (r *HTML) DocumentFooter() { // finalize and insert the table of contents - if r.flags&TOC != 0 { + if r.extensions&TOC != 0 { r.TocFinalize() // now we have to insert the table of contents into the document@@ -756,12 +754,12 @@ r.w.Write(r.toc.Bytes())
r.w.WriteString("</nav>\n") // corner case spacing issue - if r.flags&CompletePage == 0 && r.flags&OmitContents == 0 { + if r.flags&CompletePage == 0 && r.extensions&OmitContents == 0 { r.w.WriteByte('\n') } // write out everything that came after it - if r.flags&OmitContents == 0 { + if r.extensions&OmitContents == 0 { r.w.Write(temp.Bytes()) } }@@ -1389,6 +1387,54 @@ panic("Unknown node type " + node.Type.String())
} } +func (r *HTML) writeDocumentHeader(w *bytes.Buffer, sr *SPRenderer) { + if r.flags&CompletePage == 0 { + return + } + ending := "" + if r.flags&UseXHTML != 0 { + w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") + w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") + w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") + ending = " /" + } else { + w.WriteString("<!DOCTYPE html>\n") + w.WriteString("<html>\n") + } + w.WriteString("<head>\n") + w.WriteString(" <title>") + if r.extensions&Smartypants != 0 { + w.Write(sr.Process([]byte(r.title))) + } else { + w.Write(esc([]byte(r.title), false)) + } + w.WriteString("</title>\n") + w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") + w.WriteString(VERSION) + w.WriteString("\"") + w.WriteString(ending) + w.WriteString(">\n") + w.WriteString(" <meta charset=\"utf-8\"") + w.WriteString(ending) + w.WriteString(">\n") + if r.css != "" { + w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") + r.attrEscape([]byte(r.css)) + w.WriteString("\"") + w.WriteString(ending) + w.WriteString(">\n") + } + w.WriteString("</head>\n") + w.WriteString("<body>\n\n") +} + +func (r *HTML) writeDocumentFooter(w *bytes.Buffer) { + if r.flags&CompletePage == 0 { + return + } + w.WriteString("\n</body>\n</html>\n") +} + func (r *HTML) Render(ast *Node) []byte { //println("render_Blackfriday") //dump(ast)@@ -1404,8 +1450,10 @@ }
} }) var buff bytes.Buffer + r.writeDocumentHeader(&buff, sr) ast.Walk(func(node *Node, entering bool) { r.RenderNode(&buff, node, entering) }) + r.writeDocumentFooter(&buff) return buff.Bytes() }
M
markdown.go
→
markdown.go
@@ -54,6 +54,8 @@ 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 + TOC // Generate a table of contents + OmitContents // Skip the main contents (for a standalone table of contents) CommonHtmlFlags HTMLFlags = UseXHTML@@ -458,7 +460,70 @@ node.content = nil
} }) p.parseRefsToAST() + p.generateTOC() return p.doc +} + +func (p *parser) generateTOC() { + if p.flags&TOC == 0 && p.flags&OmitContents == 0 { + return + } + navNode := NewNode(HTMLBlock) + navNode.Literal = []byte("<nav>") + navNode.open = false + + var topList *Node + var listNode *Node + var lastItem *Node + headerCount := 0 + var currentLevel uint32 + p.doc.Walk(func(node *Node, entering bool) { + if entering && node.Type == Header { + if node.Level > currentLevel { + currentLevel++ + newList := NewNode(List) + if lastItem != nil { + lastItem.appendChild(newList) + listNode = newList + } else { + listNode = newList + topList = listNode + } + } + if node.Level < currentLevel { + finalizeList(listNode) + lastItem = listNode.Parent + listNode = lastItem.Parent + } + node.HeaderID = fmt.Sprintf("toc_%d", headerCount) + headerCount++ + lastItem = NewNode(Item) + listNode.appendChild(lastItem) + anchorNode := NewNode(Link) + anchorNode.Destination = []byte("#" + node.HeaderID) + lastItem.appendChild(anchorNode) + anchorNode.appendChild(text(node.FirstChild.Literal)) + } + }) + firstChild := p.doc.FirstChild + // Insert TOC only if there is anything to insert + if topList != nil { + finalizeList(topList) + firstChild.insertBefore(navNode) + firstChild.insertBefore(topList) + navCloseNode := NewNode(HTMLBlock) + navCloseNode.Literal = []byte("</nav>") + navCloseNode.open = false + firstChild.insertBefore(navCloseNode) + } + // Drop everything after the TOC if OmitContents was requested + if p.flags&OmitContents != 0 { + for firstChild != nil { + next := firstChild.Next + firstChild.unlink() + firstChild = next + } + } } func (p *parser) parseRefsToAST() {
M
node.go
→
node.go
@@ -157,6 +157,20 @@ n.LastChild = child
} } +func (n *Node) insertBefore(sibling *Node) { + sibling.unlink() + sibling.Prev = n.Prev + if sibling.Prev != nil { + sibling.Prev.Next = sibling + } + sibling.Next = n + n.Prev = sibling + sibling.Parent = n.Parent + if sibling.Prev == nil { + sibling.Parent.FirstChild = sibling + } +} + func (n *Node) isContainer() bool { switch n.Type { case Document: