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)
|
||||
}
|
||||
|
||||
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"
|
||||
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()
|
||||
}
|
||||
|
|
65
markdown.go
65
markdown.go
|
@ -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
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 {
|
||||
switch n.Type {
|
||||
case Document:
|
||||
|
|
Loading…
Reference in New Issue
Block a user