From 479920a987fe3ccb2929da9370b4b34cd6053f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytautas=20=C5=A0altenis?= Date: Sun, 9 Jul 2017 15:20:34 +0300 Subject: [PATCH 1/2] 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. --- README.md | 2 +- block_test.go | 30 ------------- html.go | 115 +++++++++++++++++++++++--------------------------- markdown.go | 46 ++++++++++++++------ 4 files changed, 85 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 400572fd..9ce73342 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ All features of Sundown are supported, including: know and send me the input that does it. 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). * **Fast processing**. It is fast enough to render on-demand in diff --git a/block_test.go b/block_test.go index eaad6d43..0a2a4d84 100644 --- a/block_test.go +++ b/block_test.go @@ -1606,36 +1606,6 @@ func TestTOC(t *testing.T) { }) } -func TestOmitContents(t *testing.T) { - var tests = []string{ - "# Title\n\n##Subtitle\n\n#Title2", - ` -`, - - // 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) { var tests = []string{ "*foo*", diff --git a/html.go b/html.go index a47b7409..5e56b837 100644 --- a/html.go +++ b/html.go @@ -45,7 +45,6 @@ const ( 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) TagName = "[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 } -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\n\n")) +} + +func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { if r.Flags&CompletePage == 0 { return } ending := "" if r.Flags&UseXHTML != 0 { - w.WriteString("\n") - w.WriteString("\n") + w.Write([]byte("\n")) + w.Write([]byte("\n")) ending = " /" } else { - w.WriteString("\n") - w.WriteString("\n") + w.Write([]byte("\n")) + w.Write([]byte("\n")) } - w.WriteString("\n") - w.WriteString(" ") + w.Write([]byte("<head>\n")) + w.Write([]byte(" <title>")) if r.Flags&Smartypants != 0 { r.sr.Process(w, []byte(r.Title)) } else { escapeHTML(w, []byte(r.Title)) } - w.WriteString("\n") - w.WriteString(" \n") - w.WriteString(" \n") + w.Write([]byte("\n")) + w.Write([]byte(" \n")) + w.Write([]byte(" \n")) if r.CSS != "" { - w.WriteString(" \n") + w.Write([]byte("\"")) + w.Write([]byte(ending)) + w.Write([]byte(">\n")) } if r.Icon != "" { - w.WriteString(" \n") + w.Write([]byte("\"")) + w.Write([]byte(ending)) + w.Write([]byte(">\n")) } - w.WriteString("\n") - w.WriteString("\n\n") + w.Write([]byte("\n")) + w.Write([]byte("\n\n")) } -func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) { +func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { buf := bytes.Buffer{} inHeading := false @@ -880,24 +895,24 @@ func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) { if entering { node.HeadingID = fmt.Sprintf("toc_%d", headingCount) if node.Level == tocLevel { - buf.WriteString("\n\n
  • ") + buf.Write([]byte("
  • \n\n
  • ")) } else if node.Level < tocLevel { for node.Level < tocLevel { tocLevel-- - buf.WriteString("
  • \n") + buf.Write([]byte("\n")) } - buf.WriteString("\n\n
  • ") + buf.Write([]byte("
  • \n\n
  • ")) } else { for node.Level > tocLevel { tocLevel++ - buf.WriteString("\n")) } if buf.Len() > 0 { - w.WriteString("\n")) } r.lastOutputLen = buf.Len() } - -func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) { - if r.Flags&CompletePage == 0 { - return - } - w.WriteString("\n\n\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() -} diff --git a/markdown.go b/markdown.go index 9c70e22e..d3756b59 100644 --- a/markdown.go +++ b/markdown.go @@ -134,22 +134,33 @@ var blockTags = map[string]struct{}{ "video": struct{}{}, } -// Renderer is the rendering interface. -// This is mostly of interest if you are implementing a new rendering format. +// Renderer is the rendering interface. This is mostly of interest if you are +// implementing a new rendering format. // -// When a byte slice is provided, it contains the (rendered) contents of the -// element. -// -// 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. +// Only an HTML implementation is provided in this repository, see the README +// for external implementations. 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 + + // 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 @@ -374,7 +385,14 @@ func Run(input []byte, opts ...Option) []byte { optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} optList = append(optList, opts...) 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 From 427717f9916ccbf0bea4cecc949b42355bf318f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytautas=20=C5=A0altenis?= Date: Mon, 10 Jul 2017 19:11:30 +0300 Subject: [PATCH 2/2] Use io.WriteString instead of w.Write([]byte(str)) --- html.go | 70 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/html.go b/html.go index 5e56b837..f1a60de2 100644 --- a/html.go +++ b/html.go @@ -831,7 +831,7 @@ func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { if r.Flags&CompletePage == 0 { return } - w.Write([]byte("\n\n\n")) + io.WriteString(w, "\n\n\n") } func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { @@ -840,46 +840,46 @@ func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { } ending := "" if r.Flags&UseXHTML != 0 { - w.Write([]byte("\n")) - w.Write([]byte("\n")) + io.WriteString(w, "\n") + io.WriteString(w, "\n") ending = " /" } else { - w.Write([]byte("\n")) - w.Write([]byte("\n")) + io.WriteString(w, "\n") + io.WriteString(w, "\n") } - w.Write([]byte("\n")) - w.Write([]byte(" ")) + io.WriteString(w, "<head>\n") + io.WriteString(w, " <title>") if r.Flags&Smartypants != 0 { r.sr.Process(w, []byte(r.Title)) } else { escapeHTML(w, []byte(r.Title)) } - w.Write([]byte("\n")) - w.Write([]byte(" \n")) - w.Write([]byte(" \n")) + io.WriteString(w, "\n") + io.WriteString(w, " \n") + io.WriteString(w, " \n") if r.CSS != "" { - w.Write([]byte(" \n")) + io.WriteString(w, "\"") + io.WriteString(w, ending) + io.WriteString(w, ">\n") } if r.Icon != "" { - w.Write([]byte(" \n")) + io.WriteString(w, "\"") + io.WriteString(w, ending) + io.WriteString(w, ">\n") } - w.Write([]byte("\n")) - w.Write([]byte("\n\n")) + io.WriteString(w, "\n") + io.WriteString(w, "\n\n") } func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { @@ -895,24 +895,24 @@ func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { if entering { node.HeadingID = fmt.Sprintf("toc_%d", headingCount) if node.Level == tocLevel { - buf.Write([]byte("
  • \n\n
  • ")) + buf.WriteString("
  • \n\n
  • ") } else if node.Level < tocLevel { for node.Level < tocLevel { tocLevel-- - buf.Write([]byte("
  • \n")) + buf.WriteString("\n") } - buf.Write([]byte("\n\n
  • ")) + buf.WriteString("
  • \n\n
  • ") } else { for node.Level > tocLevel { tocLevel++ - buf.Write([]byte("\n") } if buf.Len() > 0 { - w.Write([]byte("\n") } r.lastOutputLen = buf.Len() }