complete page rendering is now an option in the library
Russ Ross russ@dixie.edu
Wed, 29 Jun 2011 10:08:56 -0600
6 files changed,
172 insertions(+),
95 deletions(-)
M
block_test.go
→
block_test.go
@@ -21,7 +21,7 @@ func runMarkdownBlock(input string, extensions int) string {
htmlFlags := 0 htmlFlags |= HTML_USE_XHTML - renderer := HtmlRenderer(htmlFlags) + renderer := HtmlRenderer(htmlFlags, "", "") return string(Markdown([]byte(input), renderer, extensions)) }
M
example/main.go
→
example/main.go
@@ -16,14 +16,16 @@
package main import ( - "bytes" "flag" "fmt" "io/ioutil" "github.com/russross/blackfriday" "os" "runtime/pprof" + "strings" ) + +const DEFAULT_TITLE = "" func main() { // parse command-line options@@ -128,7 +130,12 @@ }
if latexdashes { htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES } - renderer = blackfriday.HtmlRenderer(htmlFlags) + title := "" + if page { + htmlFlags |= blackfriday.HTML_COMPLETE_PAGE + title = getTitle(input) + } + renderer = blackfriday.HtmlRenderer(htmlFlags, title, css) } // parse and render@@ -149,49 +156,57 @@ } else {
out = os.Stdout } - if page { - // if it starts with an <h1>, make that the title - title := "" - if bytes.HasPrefix(output, []byte("<h1>")) { - end := 0 - // we know the buffer ends with a newline, so no need to check bounds - for output[end] != '\n' { - end++ - } - if bytes.HasSuffix(output[:end], []byte("</h1>")) { - title = string(output[len("<h1>") : end-len("</h1>")]) - } - } - - ending := "" - if xhtml { - fmt.Fprint(out, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") - fmt.Fprintln(out, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">") - fmt.Fprintln(out, "<html xmlns=\"http://www.w3.org/1999/xhtml\">") - ending = " /" - } else { - fmt.Fprint(out, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" ") - fmt.Fprintln(out, "\"http://www.w3.org/TR/html4/strict.dtd\">") - fmt.Fprintln(out, "<html>") - } - fmt.Fprintln(out, "<head>") - fmt.Fprintf(out, " <title>%s</title>\n", title) - fmt.Fprintf(out, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v%s\"%s>\n", - blackfriday.VERSION, ending) - fmt.Fprintf(out, " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"%s>\n", - ending) - if css != "" { - fmt.Fprintf(out, " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n", css) - } - fmt.Fprintln(out, "</head>") - fmt.Fprintln(out, "<body>") - } if _, err = out.Write(output); err != nil { fmt.Fprintln(os.Stderr, "Error writing output:", err) os.Exit(-1) } - if page { - fmt.Fprintln(out, "</body>") - fmt.Fprintln(out, "</html>") +} + +// try to guess the title from the input buffer +// just check if it starts with an <h1> element and use that +func getTitle(input []byte) string { + i := 0 + + // skip blank lines + for i < len(input) && (input[i] == '\n' || input[i] == '\r') { + i++ + } + if i >= len(input) { + return DEFAULT_TITLE + } + if input[i] == '\r' && i+1 < len(input) && input[i+1] == '\n' { + i++ + } + + // find the first line + start := i + for i < len(input) && input[i] != '\n' && input[i] != '\r' { + i++ + } + line1 := input[start:i] + if input[i] == '\r' && i+1 < len(input) && input[i+1] == '\n' { + i++ + } + i++ + + // check for a prefix header + if len(line1) >= 3 && line1[0] == '#' && (line1[1] == ' ' || line1[1] == '\t') { + return strings.TrimSpace(string(line1[2:])) + } + + // check for an underlined header + if i >= len(input) || input[i] != '=' { + return DEFAULT_TITLE + } + for i < len(input) && input[i] == '=' { + i++ + } + for i < len(input) && (input[i] == ' ' || input[i] == '\t') { + i++ + } + if i >= len(input) || (input[i] != '\n' && input[i] != '\r') { + return DEFAULT_TITLE } + + return strings.TrimSpace(string(line1)) }
M
html.go
→
html.go
@@ -28,6 +28,7 @@ HTML_SKIP_IMAGES
HTML_SKIP_LINKS HTML_SAFELINK HTML_TOC + HTML_COMPLETE_PAGE HTML_GITHUB_BLOCKCODE HTML_USE_XHTML HTML_USE_SMARTYPANTS@@ -36,19 +37,23 @@ HTML_SMARTYPANTS_LATEX_DASHES
) type htmlOptions struct { - flags int + flags int // HTML_* options closeTag string // how to end singleton tags: either " />\n" or ">\n" - tocData struct { - headerCount int - currentLevel int - } + title string // document title + css string // optional css file url (used with HTML_COMPLETE_PAGE) + + // table of contents data + headerCount int + currentLevel int + toc *bytes.Buffer + smartypants *SmartypantsRenderer } var xhtmlClose = " />\n" var htmlClose = ">\n" -func HtmlRenderer(flags int) *Renderer { +func HtmlRenderer(flags int, title string, css string) *Renderer { // configure the rendering engine r := new(Renderer) r.BlockCode = htmlBlockCode@@ -73,34 +78,34 @@ r.Link = htmlLink
r.RawHtmlTag = htmlRawTag r.TripleEmphasis = htmlTripleEmphasis r.StrikeThrough = htmlStrikeThrough + + r.Entity = htmlEntity r.NormalText = htmlNormalText + r.DocumentHeader = htmlDocumentHeader + r.DocumentFooter = htmlDocumentFooter + closeTag := htmlClose if flags&HTML_USE_XHTML != 0 { closeTag = xhtmlClose } - r.Opaque = &htmlOptions{flags: flags, closeTag: closeTag, smartypants: Smartypants(flags)} - return r -} + var toc *bytes.Buffer + if flags&HTML_TOC != 0 { + toc = new(bytes.Buffer) + } -func HtmlTocRenderer(flags int) *Renderer { - // configure the rendering engine - r := new(Renderer) - r.Header = htmlTocHeader + r.Opaque = &htmlOptions{ + flags: flags, + closeTag: closeTag, + title: title, + css: css, - r.CodeSpan = htmlCodeSpan - r.DoubleEmphasis = htmlDoubleEmphasis - r.Emphasis = htmlEmphasis - r.TripleEmphasis = htmlTripleEmphasis - r.StrikeThrough = htmlStrikeThrough - - r.DocumentFooter = htmlTocFinalize + headerCount: 0, + currentLevel: 0, + toc: toc, - closeTag := ">\n" - if flags&HTML_USE_XHTML != 0 { - closeTag = " />\n" + smartypants: Smartypants(flags), } - r.Opaque = &htmlOptions{flags: flags | HTML_TOC, closeTag: closeTag} return r }@@ -159,8 +164,8 @@ out.WriteByte('\n')
} if options.flags&HTML_TOC != 0 { - out.WriteString(fmt.Sprintf("<h%d id=\"toc_%d\">", level, options.tocData.headerCount)) - options.tocData.headerCount++ + out.WriteString(fmt.Sprintf("<h%d id=\"toc_%d\">", level, options.headerCount)) + options.headerCount++ } else { out.WriteString(fmt.Sprintf("<h%d>", level)) }@@ -169,6 +174,12 @@ if !text() {
out.Truncate(marker) return } + + // are we building a table of contents? + if options.flags&HTML_TOC != 0 { + htmlTocHeader(out.Bytes()[marker:], level, opaque) + } + out.WriteString(fmt.Sprintf("</h%d>\n", level)) }@@ -553,6 +564,10 @@ out.WriteString("</del>")
return true } +func htmlEntity(out *bytes.Buffer, entity []byte, opaque interface{}) { + out.Write(entity) +} + func htmlNormalText(out *bytes.Buffer, text []byte, opaque interface{}) { options := opaque.(*htmlOptions) if options.flags&HTML_USE_SMARTYPANTS != 0 {@@ -562,46 +577,93 @@ attrEscape(out, text)
} } -func htmlTocHeader(out *bytes.Buffer, text func() bool, level int, opaque interface{}) { +func htmlTocHeader(text []byte, level int, opaque interface{}) { options := opaque.(*htmlOptions) - marker := out.Len() - for level > options.tocData.currentLevel { - if options.tocData.currentLevel > 0 { - out.WriteString("<li>") + for level > options.currentLevel { + if options.currentLevel > 0 { + options.toc.WriteString("<li>") } - out.WriteString("<ul>\n") - options.tocData.currentLevel++ + options.toc.WriteString("<ul>\n") + options.currentLevel++ } - for level < options.tocData.currentLevel { - out.WriteString("</ul>") - if options.tocData.currentLevel > 1 { - out.WriteString("</li>\n") + for level < options.currentLevel { + options.toc.WriteString("</ul>") + if options.currentLevel > 1 { + options.toc.WriteString("</li>\n") } - options.tocData.currentLevel-- + options.currentLevel-- } - out.WriteString("<li><a href=\"#toc_") - out.WriteString(strconv.Itoa(options.tocData.headerCount)) - out.WriteString("\">") - options.tocData.headerCount++ + options.toc.WriteString("<li><a href=\"#toc_") + options.toc.WriteString(strconv.Itoa(options.headerCount)) + options.toc.WriteString("\">") + options.headerCount++ + + options.toc.Write(text) + + options.toc.WriteString("</a></li>\n") +} + +func htmlDocumentHeader(out *bytes.Buffer, opaque interface{}) { + options := opaque.(*htmlOptions) + if options.flags&HTML_COMPLETE_PAGE == 0 { + return + } + + ending := "" + if options.flags&HTML_USE_XHTML != 0 { + out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") + out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") + out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") + ending = " /" + } else { + out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" ") + out.WriteString("\"http://www.w3.org/TR/html4/strict.dtd\">\n") + out.WriteString("<html>\n") + } + out.WriteString("<head>\n") + out.WriteString(" <title>") + htmlNormalText(out, []byte(options.title), opaque) + out.WriteString("</title>\n") + out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") + out.WriteString(VERSION) + out.WriteString("\"") + out.WriteString(ending) + out.WriteString(">\n") + out.WriteString(" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"") + out.WriteString(ending) + out.WriteString(">\n") + if options.css != "" { + out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") + attrEscape(out, []byte(options.css)) + out.WriteString("\"") + out.WriteString(ending) + out.WriteString(">\n") + } + out.WriteString("</head>\n") + out.WriteString("<body>\n") +} - if !text() { - out.Truncate(marker) +func htmlDocumentFooter(out *bytes.Buffer, opaque interface{}) { + options := opaque.(*htmlOptions) + if options.flags&HTML_COMPLETE_PAGE == 0 { return } - out.WriteString("</a></li>\n") + + out.WriteString("\n</body>\n") + out.WriteString("</html>\n") } func htmlTocFinalize(out *bytes.Buffer, opaque interface{}) { options := opaque.(*htmlOptions) - for options.tocData.currentLevel > 1 { + for options.currentLevel > 1 { out.WriteString("</ul></li>\n") - options.tocData.currentLevel-- + options.currentLevel-- } - if options.tocData.currentLevel > 0 { + if options.currentLevel > 0 { out.WriteString("</ul>\n") } }
M
inline_test.go
→
inline_test.go
@@ -25,7 +25,7 @@
htmlFlags := 0 htmlFlags |= HTML_USE_XHTML - renderer := HtmlRenderer(htmlFlags) + renderer := HtmlRenderer(htmlFlags, "", "") return string(Markdown([]byte(input), renderer, extensions)) }
M
markdown.go
→
markdown.go
@@ -166,7 +166,7 @@ // Call Markdown with no extensions
func MarkdownBasic(input []byte) []byte { // set up the HTML renderer htmlFlags := HTML_USE_XHTML - renderer := HtmlRenderer(htmlFlags) + renderer := HtmlRenderer(htmlFlags, "", "") // set up the parser extensions := 0@@ -182,7 +182,7 @@ htmlFlags |= HTML_USE_XHTML
htmlFlags |= HTML_USE_SMARTYPANTS htmlFlags |= HTML_SMARTYPANTS_FRACTIONS htmlFlags |= HTML_SMARTYPANTS_LATEX_DASHES - renderer := HtmlRenderer(htmlFlags) + renderer := HtmlRenderer(htmlFlags, "", "") // set up the parser extensions := 0
M
upskirtref_test.go
→
upskirtref_test.go
@@ -20,7 +20,7 @@ "testing"
) func runMarkdownReference(input string) string { - renderer := HtmlRenderer(0) + renderer := HtmlRenderer(0, "", "") return string(Markdown([]byte(input), renderer, 0)) }