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.
This commit is contained in:
Vytautas Šaltenis 2016-04-04 10:14:49 +03:00
parent 0b69796248
commit d7f18785f1
4 changed files with 213 additions and 7 deletions

View File

@ -1522,3 +1522,82 @@ func TestBlockComments(t *testing.T) {
}
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)
}

62
html.go
View File

@ -38,8 +38,6 @@ const (
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) TitleBlock(text []byte) {
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) BeginHeader(level int, id string) {
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) DocumentHeader() {
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 @@ func (r *HTML) DocumentFooter() {
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 @@ func (r *HTML) RenderNode(w io.Writer, node *Node, entering bool) {
}
}
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 @@ func (r *HTML) Render(ast *Node) []byte {
}
})
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()
}

View File

@ -54,6 +54,8 @@ const (
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,9 +460,72 @@ func Parse(input []byte, opts Options) *Node {
}
})
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() {
if p.flags&Footnotes == 0 || len(p.notes) == 0 {
return

14
node.go
View File

@ -157,6 +157,20 @@ func (n *Node) appendChild(child *Node) {
}
}
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: