diff --git a/block_test.go b/block_test.go index 9f813a7..e6e9f98 100644 --- a/block_test.go +++ b/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)) } diff --git a/example/main.go b/example/main.go index 4347feb..ade1458 100644 --- a/example/main.go +++ b/example/main.go @@ -16,15 +16,17 @@ 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 var page, xhtml, latex, smartypants, latexdashes, fractions bool @@ -128,7 +130,12 @@ func main() { 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 @@ func main() { out = os.Stdout } - if page { - // if it starts with an

, make that the title - title := "" - if bytes.HasPrefix(output, []byte("

")) { - 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("

")) { - title = string(output[len("

") : end-len("

")]) - } - } - - ending := "" - if xhtml { - fmt.Fprint(out, "") - fmt.Fprintln(out, "") - ending = " /" - } else { - fmt.Fprint(out, "") - fmt.Fprintln(out, "") - } - fmt.Fprintln(out, "") - fmt.Fprintf(out, " %s\n", title) - fmt.Fprintf(out, " \n", - blackfriday.VERSION, ending) - fmt.Fprintf(out, " \n", - ending) - if css != "" { - fmt.Fprintf(out, " \n", css) - } - fmt.Fprintln(out, "") - fmt.Fprintln(out, "") - } if _, err = out.Write(output); err != nil { fmt.Fprintln(os.Stderr, "Error writing output:", err) os.Exit(-1) } - if page { - fmt.Fprintln(out, "") - fmt.Fprintln(out, "") - } +} + +// try to guess the title from the input buffer +// just check if it starts with an

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)) } diff --git a/html.go b/html.go index 83fb5ea..85ffee1 100644 --- a/html.go +++ b/html.go @@ -28,6 +28,7 @@ const ( HTML_SKIP_LINKS HTML_SAFELINK HTML_TOC + HTML_COMPLETE_PAGE HTML_GITHUB_BLOCKCODE HTML_USE_XHTML HTML_USE_SMARTYPANTS @@ -36,19 +37,23 @@ const ( ) 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 @@ func HtmlRenderer(flags int) *Renderer { 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 -} - -func HtmlTocRenderer(flags int) *Renderer { - // configure the rendering engine - r := new(Renderer) - r.Header = htmlTocHeader - - r.CodeSpan = htmlCodeSpan - r.DoubleEmphasis = htmlDoubleEmphasis - r.Emphasis = htmlEmphasis - r.TripleEmphasis = htmlTripleEmphasis - r.StrikeThrough = htmlStrikeThrough - - r.DocumentFooter = htmlTocFinalize - - closeTag := ">\n" - if flags&HTML_USE_XHTML != 0 { - closeTag = " />\n" + var toc *bytes.Buffer + if flags&HTML_TOC != 0 { + toc = new(bytes.Buffer) + } + + r.Opaque = &htmlOptions{ + flags: flags, + closeTag: closeTag, + title: title, + css: css, + + headerCount: 0, + currentLevel: 0, + toc: toc, + + smartypants: Smartypants(flags), } - r.Opaque = &htmlOptions{flags: flags | HTML_TOC, closeTag: closeTag} return r } @@ -159,8 +164,8 @@ func htmlHeader(out *bytes.Buffer, text func() bool, level int, opaque interface } if options.flags&HTML_TOC != 0 { - out.WriteString(fmt.Sprintf("", level, options.tocData.headerCount)) - options.tocData.headerCount++ + out.WriteString(fmt.Sprintf("", level, options.headerCount)) + options.headerCount++ } else { out.WriteString(fmt.Sprintf("", level)) } @@ -169,6 +174,12 @@ func htmlHeader(out *bytes.Buffer, text func() bool, level int, opaque interface 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("\n", level)) } @@ -553,6 +564,10 @@ func htmlStrikeThrough(out *bytes.Buffer, text []byte, opaque interface{}) bool 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 @@ func htmlNormalText(out *bytes.Buffer, text []byte, opaque interface{}) { } } -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("
  • ") + for level > options.currentLevel { + if options.currentLevel > 0 { + options.toc.WriteString("
  • ") } - out.WriteString("
      \n") - options.tocData.currentLevel++ + options.toc.WriteString("
        \n") + options.currentLevel++ } - for level < options.tocData.currentLevel { - out.WriteString("
      ") - if options.tocData.currentLevel > 1 { - out.WriteString("\n") + for level < options.currentLevel { + options.toc.WriteString("
    ") + if options.currentLevel > 1 { + options.toc.WriteString("
  • \n") } - options.tocData.currentLevel-- + options.currentLevel-- } - out.WriteString("
  • ") - options.tocData.headerCount++ + options.toc.WriteString("
  • ") + options.headerCount++ - if !text() { - out.Truncate(marker) + options.toc.Write(text) + + options.toc.WriteString("
  • \n") +} + +func htmlDocumentHeader(out *bytes.Buffer, opaque interface{}) { + options := opaque.(*htmlOptions) + if options.flags&HTML_COMPLETE_PAGE == 0 { return } - out.WriteString("\n") + + ending := "" + if options.flags&HTML_USE_XHTML != 0 { + out.WriteString("\n") + out.WriteString("\n") + ending = " /" + } else { + out.WriteString("\n") + out.WriteString("\n") + } + out.WriteString("\n") + out.WriteString(" ") + htmlNormalText(out, []byte(options.title), opaque) + out.WriteString("\n") + out.WriteString(" \n") + out.WriteString(" \n") + if options.css != "" { + out.WriteString(" \n") + } + out.WriteString("\n") + out.WriteString("\n") +} + +func htmlDocumentFooter(out *bytes.Buffer, opaque interface{}) { + options := opaque.(*htmlOptions) + if options.flags&HTML_COMPLETE_PAGE == 0 { + return + } + + out.WriteString("\n\n") + out.WriteString("\n") } func htmlTocFinalize(out *bytes.Buffer, opaque interface{}) { options := opaque.(*htmlOptions) - for options.tocData.currentLevel > 1 { + for options.currentLevel > 1 { out.WriteString("\n") - options.tocData.currentLevel-- + options.currentLevel-- } - if options.tocData.currentLevel > 0 { + if options.currentLevel > 0 { out.WriteString("\n") } } diff --git a/inline_test.go b/inline_test.go index 1fcb6a2..ba60e01 100644 --- a/inline_test.go +++ b/inline_test.go @@ -25,7 +25,7 @@ func runMarkdownInline(input string) string { htmlFlags := 0 htmlFlags |= HTML_USE_XHTML - renderer := HtmlRenderer(htmlFlags) + renderer := HtmlRenderer(htmlFlags, "", "") return string(Markdown([]byte(input), renderer, extensions)) } diff --git a/markdown.go b/markdown.go index c943183..5efc9e9 100644 --- a/markdown.go +++ b/markdown.go @@ -166,7 +166,7 @@ type Parser struct { 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 @@ func MarkdownCommon(input []byte) []byte { 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 diff --git a/upskirtref_test.go b/upskirtref_test.go index 4790bfd..2dedc66 100644 --- a/upskirtref_test.go +++ b/upskirtref_test.go @@ -20,7 +20,7 @@ import ( ) func runMarkdownReference(input string) string { - renderer := HtmlRenderer(0) + renderer := HtmlRenderer(0, "", "") return string(Markdown([]byte(input), renderer, 0)) }