all repos — grayfriday @ d7f18785f101248fbe020ef33c79b1c818afd7e7

blackfriday fork with a few changes

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
commit

d7f18785f101248fbe020ef33c79b1c818afd7e7

parent

0b697962488f48c6035509a15ce6ff5f8f114039

4 files changed, 213 insertions(+), 7 deletions(-)

jump to
M block_test.goblock_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.gohtml.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.gomarkdown.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.gonode.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: