mirror of
https://github.com/russross/blackfriday.git
synced 2024-03-22 13:40:34 +08:00
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:
parent
0b69796248
commit
d7f18785f1
|
@ -1522,3 +1522,82 @@ func TestBlockComments(t *testing.T) {
|
||||||
}
|
}
|
||||||
doTestsBlock(t, tests, 0)
|
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
62
html.go
|
@ -38,8 +38,6 @@ const (
|
||||||
NofollowLinks // Only link with rel="nofollow"
|
NofollowLinks // Only link with rel="nofollow"
|
||||||
NoreferrerLinks // Only link with rel="noreferrer"
|
NoreferrerLinks // Only link with rel="noreferrer"
|
||||||
HrefTargetBlank // Add a blank target
|
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
|
CompletePage // Generate a complete HTML page
|
||||||
UseXHTML // Generate XHTML output instead of HTML
|
UseXHTML // Generate XHTML output instead of HTML
|
||||||
FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
|
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) {
|
func (r *HTML) BeginHeader(level int, id string) {
|
||||||
r.w.Newline()
|
r.w.Newline()
|
||||||
|
|
||||||
if id == "" && r.flags&TOC != 0 {
|
if id == "" && r.extensions&TOC != 0 {
|
||||||
id = fmt.Sprintf("toc_%d", r.headerCount)
|
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) {
|
func (r *HTML) EndHeader(level int, id string, header []byte) {
|
||||||
// are we building a table of contents?
|
// are we building a table of contents?
|
||||||
if r.flags&TOC != 0 {
|
if r.extensions&TOC != 0 {
|
||||||
r.TocHeaderWithAnchor(header, level, id)
|
r.TocHeaderWithAnchor(header, level, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -733,7 +731,7 @@ func (r *HTML) DocumentHeader() {
|
||||||
|
|
||||||
func (r *HTML) DocumentFooter() {
|
func (r *HTML) DocumentFooter() {
|
||||||
// finalize and insert the table of contents
|
// finalize and insert the table of contents
|
||||||
if r.flags&TOC != 0 {
|
if r.extensions&TOC != 0 {
|
||||||
r.TocFinalize()
|
r.TocFinalize()
|
||||||
|
|
||||||
// now we have to insert the table of contents into the document
|
// 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")
|
r.w.WriteString("</nav>\n")
|
||||||
|
|
||||||
// corner case spacing issue
|
// 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')
|
r.w.WriteByte('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// write out everything that came after it
|
// write out everything that came after it
|
||||||
if r.flags&OmitContents == 0 {
|
if r.extensions&OmitContents == 0 {
|
||||||
r.w.Write(temp.Bytes())
|
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 {
|
func (r *HTML) Render(ast *Node) []byte {
|
||||||
//println("render_Blackfriday")
|
//println("render_Blackfriday")
|
||||||
//dump(ast)
|
//dump(ast)
|
||||||
|
@ -1404,8 +1450,10 @@ func (r *HTML) Render(ast *Node) []byte {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
|
r.writeDocumentHeader(&buff, sr)
|
||||||
ast.Walk(func(node *Node, entering bool) {
|
ast.Walk(func(node *Node, entering bool) {
|
||||||
r.RenderNode(&buff, node, entering)
|
r.RenderNode(&buff, node, entering)
|
||||||
})
|
})
|
||||||
|
r.writeDocumentFooter(&buff)
|
||||||
return buff.Bytes()
|
return buff.Bytes()
|
||||||
}
|
}
|
||||||
|
|
65
markdown.go
65
markdown.go
|
@ -54,6 +54,8 @@ const (
|
||||||
SmartypantsDashes // Enable smart dashes (with Smartypants)
|
SmartypantsDashes // Enable smart dashes (with Smartypants)
|
||||||
SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
|
SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
|
||||||
SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
|
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
|
CommonHtmlFlags HTMLFlags = UseXHTML
|
||||||
|
|
||||||
|
@ -458,9 +460,72 @@ func Parse(input []byte, opts Options) *Node {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
p.parseRefsToAST()
|
p.parseRefsToAST()
|
||||||
|
p.generateTOC()
|
||||||
return p.doc
|
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() {
|
func (p *parser) parseRefsToAST() {
|
||||||
if p.flags&Footnotes == 0 || len(p.notes) == 0 {
|
if p.flags&Footnotes == 0 || len(p.notes) == 0 {
|
||||||
return
|
return
|
||||||
|
|
14
node.go
14
node.go
|
@ -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 {
|
func (n *Node) isContainer() bool {
|
||||||
switch n.Type {
|
switch n.Type {
|
||||||
case Document:
|
case Document:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user