Improve the Renderer interface

Improve Renderer to be less confusing. Fix documentation for it.

OmitContents flag got dropped along the way. First, it would fit poorly
into the new design and second, it's unclear how widely this feature is
used. But most importantly, it's trivial to roll your own with the v2
API: https://gist.github.com/rtfb/2693f6bfcc1760661e8d2fb832763a15

Fixes #368.
This commit is contained in:
Vytautas Šaltenis 2017-07-09 15:20:34 +03:00
parent 70c446a327
commit 479920a987
4 changed files with 85 additions and 108 deletions

View File

@ -134,7 +134,7 @@ All features of Sundown are supported, including:
know and send me the input that does it. know and send me the input that does it.
NOTE: "safety" in this context means *runtime safety only*. In order to NOTE: "safety" in this context means *runtime safety only*. In order to
protect yourself agains JavaScript injection in untrusted content, see protect yourself against JavaScript injection in untrusted content, see
[this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content).
* **Fast processing**. It is fast enough to render on-demand in * **Fast processing**. It is fast enough to render on-demand in

View File

@ -1606,36 +1606,6 @@ func TestTOC(t *testing.T) {
}) })
} }
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",
"",
}
doTestsParam(t, tests, TestParams{
HTMLFlags: UseXHTML | TOC | OmitContents,
})
// Now run again: make sure OmitContents implies TOC
doTestsParam(t, tests, TestParams{
HTMLFlags: UseXHTML | OmitContents,
})
}
func TestCompletePage(t *testing.T) { func TestCompletePage(t *testing.T) {
var tests = []string{ var tests = []string{
"*foo*", "*foo*",

115
html.go
View File

@ -45,7 +45,6 @@ const (
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 TOC // Generate a table of contents
OmitContents // Skip the main contents (for a standalone table of contents)
TagName = "[A-Za-z][A-Za-z0-9-]*" TagName = "[A-Za-z][A-Za-z0-9-]*"
AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
@ -819,55 +818,71 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
return GoToNext return GoToNext
} }
func (r *HTMLRenderer) writeDocumentHeader(w *bytes.Buffer) { // RenderHeader writes HTML document preamble and TOC if requested.
func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
r.writeDocumentHeader(w)
if r.Flags&TOC != 0 {
r.writeTOC(w, ast)
}
}
// RenderFooter writes HTML document footer.
func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
if r.Flags&CompletePage == 0 {
return
}
w.Write([]byte("\n</body>\n</html>\n"))
}
func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
if r.Flags&CompletePage == 0 { if r.Flags&CompletePage == 0 {
return return
} }
ending := "" ending := ""
if r.Flags&UseXHTML != 0 { if r.Flags&UseXHTML != 0 {
w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") w.Write([]byte("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "))
w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") w.Write([]byte("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"))
w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") w.Write([]byte("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"))
ending = " /" ending = " /"
} else { } else {
w.WriteString("<!DOCTYPE html>\n") w.Write([]byte("<!DOCTYPE html>\n"))
w.WriteString("<html>\n") w.Write([]byte("<html>\n"))
} }
w.WriteString("<head>\n") w.Write([]byte("<head>\n"))
w.WriteString(" <title>") w.Write([]byte(" <title>"))
if r.Flags&Smartypants != 0 { if r.Flags&Smartypants != 0 {
r.sr.Process(w, []byte(r.Title)) r.sr.Process(w, []byte(r.Title))
} else { } else {
escapeHTML(w, []byte(r.Title)) escapeHTML(w, []byte(r.Title))
} }
w.WriteString("</title>\n") w.Write([]byte("</title>\n"))
w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") w.Write([]byte(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v"))
w.WriteString(Version) w.Write([]byte(Version))
w.WriteString("\"") w.Write([]byte("\""))
w.WriteString(ending) w.Write([]byte(ending))
w.WriteString(">\n") w.Write([]byte(">\n"))
w.WriteString(" <meta charset=\"utf-8\"") w.Write([]byte(" <meta charset=\"utf-8\""))
w.WriteString(ending) w.Write([]byte(ending))
w.WriteString(">\n") w.Write([]byte(">\n"))
if r.CSS != "" { if r.CSS != "" {
w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") w.Write([]byte(" <link rel=\"stylesheet\" type=\"text/css\" href=\""))
escapeHTML(w, []byte(r.CSS)) escapeHTML(w, []byte(r.CSS))
w.WriteString("\"") w.Write([]byte("\""))
w.WriteString(ending) w.Write([]byte(ending))
w.WriteString(">\n") w.Write([]byte(">\n"))
} }
if r.Icon != "" { if r.Icon != "" {
w.WriteString(" <link rel=\"icon\" type=\"image/x-icon\" href=\"") w.Write([]byte(" <link rel=\"icon\" type=\"image/x-icon\" href=\""))
escapeHTML(w, []byte(r.Icon)) escapeHTML(w, []byte(r.Icon))
w.WriteString("\"") w.Write([]byte("\""))
w.WriteString(ending) w.Write([]byte(ending))
w.WriteString(">\n") w.Write([]byte(">\n"))
} }
w.WriteString("</head>\n") w.Write([]byte("</head>\n"))
w.WriteString("<body>\n\n") w.Write([]byte("<body>\n\n"))
} }
func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) { func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
inHeading := false inHeading := false
@ -880,24 +895,24 @@ func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) {
if entering { if entering {
node.HeadingID = fmt.Sprintf("toc_%d", headingCount) node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
if node.Level == tocLevel { if node.Level == tocLevel {
buf.WriteString("</li>\n\n<li>") buf.Write([]byte("</li>\n\n<li>"))
} else if node.Level < tocLevel { } else if node.Level < tocLevel {
for node.Level < tocLevel { for node.Level < tocLevel {
tocLevel-- tocLevel--
buf.WriteString("</li>\n</ul>") buf.Write([]byte("</li>\n</ul>"))
} }
buf.WriteString("</li>\n\n<li>") buf.Write([]byte("</li>\n\n<li>"))
} else { } else {
for node.Level > tocLevel { for node.Level > tocLevel {
tocLevel++ tocLevel++
buf.WriteString("\n<ul>\n<li>") buf.Write([]byte("\n<ul>\n<li>"))
} }
} }
fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
headingCount++ headingCount++
} else { } else {
buf.WriteString("</a>") buf.Write([]byte("</a>"))
} }
return GoToNext return GoToNext
} }
@ -910,39 +925,13 @@ func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) {
}) })
for ; tocLevel > 0; tocLevel-- { for ; tocLevel > 0; tocLevel-- {
buf.WriteString("</li>\n</ul>") buf.Write([]byte("</li>\n</ul>"))
} }
if buf.Len() > 0 { if buf.Len() > 0 {
w.WriteString("<nav>\n") w.Write([]byte("<nav>\n"))
w.Write(buf.Bytes()) w.Write(buf.Bytes())
w.WriteString("\n\n</nav>\n") w.Write([]byte("\n\n</nav>\n"))
} }
r.lastOutputLen = buf.Len() r.lastOutputLen = buf.Len()
} }
func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) {
if r.Flags&CompletePage == 0 {
return
}
w.WriteString("\n</body>\n</html>\n")
}
// Render walks the specified syntax (sub)tree and returns a HTML document.
func (r *HTMLRenderer) Render(ast *Node) []byte {
//println("render_Blackfriday")
//dump(ast)
var buf bytes.Buffer
r.writeDocumentHeader(&buf)
if r.Flags&TOC != 0 || r.Flags&OmitContents != 0 {
r.writeTOC(&buf, ast)
if r.Flags&OmitContents != 0 {
return buf.Bytes()
}
}
ast.Walk(func(node *Node, entering bool) WalkStatus {
return r.RenderNode(&buf, node, entering)
})
r.writeDocumentFooter(&buf)
return buf.Bytes()
}

View File

@ -134,22 +134,33 @@ var blockTags = map[string]struct{}{
"video": struct{}{}, "video": struct{}{},
} }
// Renderer is the rendering interface. // Renderer is the rendering interface. This is mostly of interest if you are
// This is mostly of interest if you are implementing a new rendering format. // implementing a new rendering format.
// //
// When a byte slice is provided, it contains the (rendered) contents of the // Only an HTML implementation is provided in this repository, see the README
// element. // for external implementations.
//
// When a callback is provided instead, it will write the contents of the
// respective element directly to the output buffer and return true on success.
// If the callback returns false, the rendering function should reset the
// output buffer as though it had never been called.
//
// Only an HTML implementation is provided in this repository,
// see the README for external implementations.
type Renderer interface { type Renderer interface {
Render(ast *Node) []byte // RenderNode is the main rendering method. It will be called once for
// every leaf node and twice for every non-leaf node (first with
// entering=true, then with entering=false). The method should write its
// rendition of the node to the supplied writer w.
RenderNode(w io.Writer, node *Node, entering bool) WalkStatus RenderNode(w io.Writer, node *Node, entering bool) WalkStatus
// RenderHeader is a method that allows the renderer to produce some
// content preceding the main body of the output document. The header is
// understood in the broad sense here. For example, the default HTML
// renderer will write not only the HTML document preamble, but also the
// table of contents if it was requested.
//
// The method will be passed an entire document tree, in case a particular
// implementation needs to inspect it to produce output.
//
// The output should be written to the supplied writer w. If your
// implementation has no header to write, supply an empty implementation.
RenderHeader(w io.Writer, ast *Node)
// RenderFooter is a symmetric counterpart of RenderHeader.
RenderFooter(w io.Writer, ast *Node)
} }
// Callback functions for inline parsing. One such function is defined // Callback functions for inline parsing. One such function is defined
@ -374,7 +385,14 @@ func Run(input []byte, opts ...Option) []byte {
optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)}
optList = append(optList, opts...) optList = append(optList, opts...)
parser := New(optList...) parser := New(optList...)
return parser.renderer.Render(parser.Parse(input)) ast := parser.Parse(input)
var buf bytes.Buffer
parser.renderer.RenderHeader(&buf, ast)
ast.Walk(func(node *Node, entering bool) WalkStatus {
return parser.renderer.RenderNode(&buf, node, entering)
})
parser.renderer.RenderFooter(&buf, ast)
return buf.Bytes()
} }
// Parse is an entry point to the parsing part of Blackfriday. It takes an // Parse is an entry point to the parsing part of Blackfriday. It takes an