Merge pull request #86 from dimfeld/master Add absolute link transformation and footnote enhancements
@@ -41,6 +41,8 @@ HTML_USE_XHTML // generate XHTML output instead of HTML
HTML_USE_SMARTYPANTS // enable smart punctuation substitutions HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS) HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS) + HTML_ABSOLUTE_LINKS // convert all links to absolute links, using AbsolutePrefix + HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source ) var (@@ -54,6 +56,17 @@ // TODO: improve this regexp to catch all possible entities:
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`) ) +type HtmlRendererParameters struct { + // Prepend this text to each URL, if the HTML_ABSOLUTE_LINKS option is enabled. + AbsolutePrefix string + // Add this text to each footnote anchor, to ensure uniqueness. + FootnoteAnchorPrefix string + // Show this text inside the <a> tag for a footnote return link, if the + // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string + // <sup>[return]</sup> is used. + FootnoteReturnLinkContents string +} + // Html is a type that implements the Renderer interface for HTML output. // // Do not create this directly, instead use the HtmlRenderer function.@@ -62,6 +75,8 @@ flags int // HTML_* options
closeTag string // how to end singleton tags: either " />\n" or ">\n" title string // document title css string // optional css file url (used with HTML_COMPLETE_PAGE) + + parameters HtmlRendererParameters // table of contents data tocMarker int@@ -85,17 +100,27 @@ // title is the title of the document, and css is a URL for the document's
// stylesheet. // title and css are only used when HTML_COMPLETE_PAGE is selected. func HtmlRenderer(flags int, title string, css string) Renderer { + return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{}) +} + +func HtmlRendererWithParameters(flags int, title string, + css string, renderParameters HtmlRendererParameters) Renderer { // configure the rendering engine closeTag := htmlClose if flags&HTML_USE_XHTML != 0 { closeTag = xhtmlClose } + if renderParameters.FootnoteReturnLinkContents == "" { + renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>` + } + return &Html{ - flags: flags, - closeTag: closeTag, - title: title, - css: css, + flags: flags, + closeTag: closeTag, + title: title, + css: css, + parameters: renderParameters, headerCount: 0, currentLevel: 0,@@ -349,10 +374,22 @@ func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { doubleSpace(out) } - out.WriteString(`<li id="fn:`) - out.Write(slugify(name)) + slug := slugify(name) + out.WriteString(`<li id="`) + out.WriteString(`fn:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) out.WriteString(`">`) out.Write(text) + if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 { + out.WriteString(` <a class="footnote-return" href="#`) + out.WriteString(`fnref:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) + out.Write(slug) + out.WriteString(`">`) + out.WriteString(options.parameters.FootnoteReturnLinkContents) + out.WriteString(`</a>`) + } out.WriteString("</li>\n") }@@ -410,7 +447,10 @@
out.WriteString("<a href=\"") if kind == LINK_TYPE_EMAIL { out.WriteString("mailto:") + } else { + options.maybeWriteAbsolutePrefix(out, link) } + entityEscapeWithSkip(out, link, skipRanges) if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {@@ -459,12 +499,22 @@ out.Write(text)
out.WriteString("</em>") } +func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) { + if options.flags&HTML_ABSOLUTE_LINKS != 0 && isRelativeLink(link) { + out.WriteString(options.parameters.AbsolutePrefix) + if link[0] != '/' { + out.WriteByte('/') + } + } +} + func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { if options.flags&HTML_SKIP_IMAGES != 0 { return } out.WriteString("<img src=\"") + options.maybeWriteAbsolutePrefix(out, link) attrEscape(out, link) out.WriteString("\" alt=\"") if len(alt) > 0 {@@ -503,6 +553,7 @@ return
} out.WriteString("<a href=\"") + options.maybeWriteAbsolutePrefix(out, link) attrEscape(out, link) if len(title) > 0 { out.WriteString("\" title=\"")@@ -552,9 +603,13 @@ }
func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { slug := slugify(ref) - out.WriteString(`<sup class="footnote-ref" id="fnref:`) + out.WriteString(`<sup class="footnote-ref" id="`) + out.WriteString(`fnref:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) out.Write(slug) - out.WriteString(`"><a rel="footnote" href="#fn:`) + out.WriteString(`"><a rel="footnote" href="#`) + out.WriteString(`fn:`) + out.WriteString(options.parameters.FootnoteAnchorPrefix) out.Write(slug) out.WriteString(`">`) out.WriteString(strconv.Itoa(id))
@@ -14,29 +14,49 @@
package blackfriday import ( + "regexp" "testing" + + "strings" ) -func runMarkdownInline(input string, extensions, htmlFlags int) string { +func runMarkdownInline(input string, extensions, htmlFlags int, params HtmlRendererParameters) string { extensions |= EXTENSION_AUTOLINK extensions |= EXTENSION_STRIKETHROUGH htmlFlags |= HTML_USE_XHTML - renderer := HtmlRenderer(htmlFlags, "", "") + renderer := HtmlRendererWithParameters(htmlFlags, "", "", params) return string(Markdown([]byte(input), renderer, extensions)) } func doTestsInline(t *testing.T, tests []string) { - doTestsInlineParam(t, tests, 0, 0) + doTestsInlineParam(t, tests, 0, 0, HtmlRendererParameters{}) +} + +func doLinkTestsInline(t *testing.T, tests []string) { + doTestsInline(t, tests) + + prefix := "http://localhost" + params := HtmlRendererParameters{AbsolutePrefix: prefix} + transformTests := transformLinks(tests, prefix) + doTestsInlineParam(t, transformTests, 0, HTML_ABSOLUTE_LINKS, params) } func doSafeTestsInline(t *testing.T, tests []string) { - doTestsInlineParam(t, tests, 0, HTML_SAFELINK) + doTestsInlineParam(t, tests, 0, HTML_SAFELINK, HtmlRendererParameters{}) + + // All the links in this test should not have the prefix appended, so + // just rerun it with different parameters and the same expectations. + prefix := "http://localhost" + params := HtmlRendererParameters{AbsolutePrefix: prefix} + transformTests := transformLinks(tests, prefix) + doTestsInlineParam(t, transformTests, 0, HTML_SAFELINK|HTML_ABSOLUTE_LINKS, params) } -func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int) { +func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int, + params HtmlRendererParameters) { // catch and report panics var candidate string /*@@ -51,7 +71,7 @@ for i := 0; i+1 < len(tests); i += 2 {
input := tests[i] candidate = input expected := tests[i+1] - actual := runMarkdownInline(candidate, extensions, htmlFlags) + actual := runMarkdownInline(candidate, extensions, htmlFlags, params) if actual != expected { t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]", candidate, expected, actual)@@ -62,11 +82,25 @@ if !testing.Short() {
for start := 0; start < len(input); start++ { for end := start + 1; end <= len(input); end++ { candidate = input[start:end] - _ = runMarkdownInline(candidate, extensions, htmlFlags) + _ = runMarkdownInline(candidate, extensions, htmlFlags, params) } } } } +} + +func transformLinks(tests []string, prefix string) []string { + newTests := make([]string, len(tests)) + anchorRe := regexp.MustCompile(`<a href="/(.*?)"`) + imgRe := regexp.MustCompile(`<img src="/(.*?)"`) + for i, test := range tests { + if i%2 == 1 { + test = anchorRe.ReplaceAllString(test, `<a href="`+prefix+`/$1"`) + test = imgRe.ReplaceAllString(test, `<img src="`+prefix+`/$1"`) + } + newTests[i] = test + } + return newTests } func TestEmphasis(t *testing.T) {@@ -383,7 +417,8 @@
"[[t]](/t)\n", "<p><a href=\"/t\">[t]</a></p>\n", } - doTestsInline(t, tests) + doLinkTestsInline(t, tests) + } func TestNofollowLink(t *testing.T) {@@ -391,13 +426,14 @@ var tests = []string{
"[foo](http://bar.com/foo/)\n", "<p><a href=\"http://bar.com/foo/\" rel=\"nofollow\">foo</a></p>\n", } - doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS|HTML_SANITIZE_OUTPUT) + doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS|HTML_SANITIZE_OUTPUT, + HtmlRendererParameters{}) // HTML_SANITIZE_OUTPUT won't allow relative links, so test that separately: tests = []string{ "[foo](/bar/)\n", "<p><a href=\"/bar/\">foo</a></p>\n", } - doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS) + doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS, HtmlRendererParameters{}) } func TestHrefTargetBlank(t *testing.T) {@@ -409,7 +445,7 @@
"[foo](http://example.com)\n", "<p><a href=\"http://example.com\" target=\"_blank\">foo</a></p>\n", } - doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_HREF_TARGET_BLANK) + doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_HREF_TARGET_BLANK, HtmlRendererParameters{}) } func TestSafeInlineLink(t *testing.T) {@@ -465,7 +501,7 @@
"[ref]\n [ref]: /url/ \"title\"\n", "<p><a href=\"/url/\" title=\"title\">ref</a></p>\n", } - doTestsInline(t, tests) + doLinkTestsInline(t, tests) } func TestTags(t *testing.T) {@@ -592,13 +628,12 @@
"http://foo.com/viewtopic.php?param="18"", "<p><a href=\"http://foo.com/viewtopic.php?param="18"\">http://foo.com/viewtopic.php?param="18"</a></p>\n", } - doTestsInline(t, tests) + doLinkTestsInline(t, tests) } -func TestFootnotes(t *testing.T) { - tests := []string{ - "testing footnotes.[^a]\n\n[^a]: This is the note\n", - `<p>testing footnotes.<sup class="footnote-ref" id="fnref:a"><a rel="footnote" href="#fn:a">1</a></sup></p> +var footnoteTests = []string{ + "testing footnotes.[^a]\n\n[^a]: This is the note\n", + `<p>testing footnotes.<sup class="footnote-ref" id="fnref:a"><a rel="footnote" href="#fn:a">1</a></sup></p> <div class="footnotes"> <hr />@@ -610,7 +645,7 @@ </ol>
</div> `, - `testing long[^b] notes. + `testing long[^b] notes. [^b]: Paragraph 1@@ -622,7 +657,7 @@ Paragraph 3
No longer in the footnote `, - `<p>testing long<sup class="footnote-ref" id="fnref:b"><a rel="footnote" href="#fn:b">1</a></sup> notes.</p> + `<p>testing long<sup class="footnote-ref" id="fnref:b"><a rel="footnote" href="#fn:b">1</a></sup> notes.</p> <p>No longer in the footnote</p> <div class="footnotes">@@ -644,7 +679,7 @@ </ol>
</div> `, - `testing[^c] multiple[^d] notes. + `testing[^c] multiple[^d] notes. [^c]: this is [note] c@@ -658,7 +693,7 @@
[note]: /link/c `, - `<p>testing<sup class="footnote-ref" id="fnref:c"><a rel="footnote" href="#fn:c">1</a></sup> multiple<sup class="footnote-ref" id="fnref:d"><a rel="footnote" href="#fn:d">2</a></sup> notes.</p> + `<p>testing<sup class="footnote-ref" id="fnref:c"><a rel="footnote" href="#fn:c">1</a></sup> multiple<sup class="footnote-ref" id="fnref:d"><a rel="footnote" href="#fn:d">2</a></sup> notes.</p> <p>omg</p>@@ -676,8 +711,8 @@ </ol>
</div> `, - "testing inline^[this is the note] notes.\n", - `<p>testing inline<sup class="footnote-ref" id="fnref:this-is-the-note"><a rel="footnote" href="#fn:this-is-the-note">1</a></sup> notes.</p> + "testing inline^[this is the note] notes.\n", + `<p>testing inline<sup class="footnote-ref" id="fnref:this-is-the-note"><a rel="footnote" href="#fn:this-is-the-note">1</a></sup> notes.</p> <div class="footnotes"> <hr />@@ -688,8 +723,8 @@ </ol>
</div> `, - "testing multiple[^1] types^[inline note] of notes[^2]\n\n[^2]: the second deferred note\n[^1]: the first deferred note\n\n\twhich happens to be a block\n", - `<p>testing multiple<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup> types<sup class="footnote-ref" id="fnref:inline-note"><a rel="footnote" href="#fn:inline-note">2</a></sup> of notes<sup class="footnote-ref" id="fnref:2"><a rel="footnote" href="#fn:2">3</a></sup></p> + "testing multiple[^1] types^[inline note] of notes[^2]\n\n[^2]: the second deferred note\n[^1]: the first deferred note\n\n\twhich happens to be a block\n", + `<p>testing multiple<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup> types<sup class="footnote-ref" id="fnref:inline-note"><a rel="footnote" href="#fn:inline-note">2</a></sup> of notes<sup class="footnote-ref" id="fnref:2"><a rel="footnote" href="#fn:2">3</a></sup></p> <div class="footnotes"> <hr />@@ -706,13 +741,13 @@ </ol>
</div> `, - `This is a footnote[^1]^[and this is an inline footnote] + `This is a footnote[^1]^[and this is an inline footnote] [^1]: the footnote text. may be multiple paragraphs. `, - `<p>This is a footnote<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup><sup class="footnote-ref" id="fnref:and-this-is-an-i"><a rel="footnote" href="#fn:and-this-is-an-i">2</a></sup></p> + `<p>This is a footnote<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup><sup class="footnote-ref" id="fnref:and-this-is-an-i"><a rel="footnote" href="#fn:and-this-is-an-i">2</a></sup></p> <div class="footnotes"> <hr />@@ -727,9 +762,35 @@ </ol>
</div> `, - "empty footnote[^]\n\n[^]: fn text", - "<p>empty footnote<sup class=\"footnote-ref\" id=\"fnref:\"><a rel=\"footnote\" href=\"#fn:\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:\">fn text\n</li>\n</ol>\n</div>\n", + "empty footnote[^]\n\n[^]: fn text", + "<p>empty footnote<sup class=\"footnote-ref\" id=\"fnref:\"><a rel=\"footnote\" href=\"#fn:\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:\">fn text\n</li>\n</ol>\n</div>\n", +} + +func TestFootnotes(t *testing.T) { + doTestsInlineParam(t, footnoteTests, EXTENSION_FOOTNOTES, 0, HtmlRendererParameters{}) +} + +func TestFootnotesWithParameters(t *testing.T) { + tests := make([]string, len(footnoteTests)) + + prefix := "testPrefix" + returnText := "ret" + re := regexp.MustCompile(`(?ms)<li id="fn:(\S+?)">(.*?)</li>`) + + // Transform the test expectations to match the parameters we're using. + for i, test := range footnoteTests { + if i%2 == 1 { + test = strings.Replace(test, "fn:", "fn:"+prefix, -1) + test = strings.Replace(test, "fnref:", "fnref:"+prefix, -1) + test = re.ReplaceAllString(test, `<li id="fn:$1">$2 <a class="footnote-return" href="#fnref:$1">ret</a></li>`) + } + tests[i] = test } - doTestsInlineParam(t, tests, EXTENSION_FOOTNOTES, 0) + params := HtmlRendererParameters{ + FootnoteAnchorPrefix: prefix, + FootnoteReturnLinkContents: returnText, + } + + doTestsInlineParam(t, tests, EXTENSION_FOOTNOTES, HTML_FOOTNOTE_RETURN_LINKS, params) }
@@ -332,7 +332,7 @@ if p.flags&EXTENSION_FENCED_CODE != 0 {
// when last line was none blank and a fenced code block comes after if beg >= lastFencedCodeBlockEnd { // tmp var so we don't modify beyond bounds of `input` - var tmp = make([]byte, len(input[beg:]), len(input[beg:]) + 1) + var tmp = make([]byte, len(input[beg:]), len(input[beg:])+1) copy(tmp, input[beg:]) if i := p.fencedCode(&out, append(tmp, '\n'), false); i > 0 { if !lastLineWasBlank {
@@ -5,7 +5,7 @@ "testing"
) func doTestsSanitize(t *testing.T, tests []string) { - doTestsInlineParam(t, tests, 0, HTML_SKIP_STYLE|HTML_SANITIZE_OUTPUT) + doTestsInlineParam(t, tests, 0, HTML_SKIP_STYLE|HTML_SANITIZE_OUTPUT, HtmlRendererParameters{}) } func TestSanitizeRawHtmlTag(t *testing.T) {@@ -192,8 +192,8 @@ func TestSanitizeInlineLink(t *testing.T) {
tests := []string{ "[link](javascript:evil)", "<p><a>link</a></p>\n", - "[link](/abc)", - "<p><a href=\"/abc\">link</a></p>\n", + "[link](/abc)", + "<p><a href=\"/abc\">link</a></p>\n", } doTestsSanitize(t, tests) }