From 5e8b222b6980d15fae51c859ca79b97cc679168d Mon Sep 17 00:00:00 2001 From: JT Olds Date: Tue, 16 Dec 2014 16:17:49 -0700 Subject: [PATCH 1/4] Add programmable reference overrides If a user provides a ReferenceOverride function, then reference ids will be passed to the given ReferenceOverride function first, before consulting the generated reference table. The goal here is to enable programmable support for "WikiWords"-style identifiers or other application-specific user-generated keywords. Example, writing documentation: The [Frobnosticator][] is a very important class in our codebase. While it is used to frobnosticate widgets in general, it can also be passed to the [WeeDoodler][] to interesting effect. This might be solveable with the HTML Renderer relative prefix, but I didn't see a good way of making a short link to 'Frobnosticator' relatively without having to write it twice. Maybe '' should work? Should Autolinks work for relative links? In addition, I wanted a little more richness. I plan to support Godoc links by prefixing references with a '!', like so: Check out the [Frobnosticator][] helper function [!util.Frobnosticate()][] The first link links to the Frobnosticator architectural overview documentation, whereas the second links to Godoc. Better advice on how to implement this sort of think with Blackfriday is highly desired. --- inline.go | 8 ++--- inline_test.go | 84 +++++++++++++++++++++++++++++++++++++++----------- markdown.go | 77 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 142 insertions(+), 27 deletions(-) diff --git a/inline.go b/inline.go index 651e4e8..b8b7659 100644 --- a/inline.go +++ b/inline.go @@ -384,9 +384,8 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { id = data[linkB:linkE] } - // find the reference with matching id (ids are case-insensitive) - key := string(bytes.ToLower(id)) - lr, ok := p.refs[key] + // find the reference with matching id + lr, ok := p.getRef(string(id)) if !ok { return 0 @@ -423,7 +422,6 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { } } - key := string(bytes.ToLower(id)) if t == linkInlineFootnote { // create a new reference noteId = len(p.notes) + 1 @@ -453,7 +451,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { title = ref.title } else { // find the reference with matching id - lr, ok := p.refs[key] + lr, ok := p.getRef(string(id)) if !ok { return 0 } diff --git a/inline_test.go b/inline_test.go index 7ad3f0d..1742370 100644 --- a/inline_test.go +++ b/inline_test.go @@ -20,19 +20,19 @@ import ( "strings" ) -func runMarkdownInline(input string, extensions, htmlFlags int, params HtmlRendererParameters) string { - extensions |= EXTENSION_AUTOLINK - extensions |= EXTENSION_STRIKETHROUGH +func runMarkdownInline(input string, opts Options, htmlFlags int, params HtmlRendererParameters) string { + opts.Extensions |= EXTENSION_AUTOLINK + opts.Extensions |= EXTENSION_STRIKETHROUGH htmlFlags |= HTML_USE_XHTML renderer := HtmlRendererWithParameters(htmlFlags, "", "", params) - return string(Markdown([]byte(input), renderer, extensions)) + return string(MarkdownOptions([]byte(input), renderer, opts)) } func doTestsInline(t *testing.T, tests []string) { - doTestsInlineParam(t, tests, 0, 0, HtmlRendererParameters{}) + doTestsInlineParam(t, tests, Options{}, 0, HtmlRendererParameters{}) } func doLinkTestsInline(t *testing.T, tests []string) { @@ -41,22 +41,22 @@ func doLinkTestsInline(t *testing.T, tests []string) { prefix := "http://localhost" params := HtmlRendererParameters{AbsolutePrefix: prefix} transformTests := transformLinks(tests, prefix) - doTestsInlineParam(t, transformTests, 0, 0, params) - doTestsInlineParam(t, transformTests, 0, commonHtmlFlags, params) + doTestsInlineParam(t, transformTests, Options{}, 0, params) + doTestsInlineParam(t, transformTests, Options{}, commonHtmlFlags, params) } func doSafeTestsInline(t *testing.T, tests []string) { - doTestsInlineParam(t, tests, 0, HTML_SAFELINK, HtmlRendererParameters{}) + doTestsInlineParam(t, tests, Options{}, 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, params) + doTestsInlineParam(t, transformTests, Options{}, HTML_SAFELINK, params) } -func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int, +func doTestsInlineParam(t *testing.T, tests []string, opts Options, htmlFlags int, params HtmlRendererParameters) { // catch and report panics var candidate string @@ -72,7 +72,7 @@ func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int, input := tests[i] candidate = input expected := tests[i+1] - actual := runMarkdownInline(candidate, extensions, htmlFlags, params) + actual := runMarkdownInline(candidate, opts, htmlFlags, params) if actual != expected { t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]", candidate, expected, actual) @@ -83,7 +83,7 @@ func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int, for start := 0; start < len(input); start++ { for end := start + 1; end <= len(input); end++ { candidate = input[start:end] - _ = runMarkdownInline(candidate, extensions, htmlFlags, params) + _ = runMarkdownInline(candidate, opts, htmlFlags, params) } } } @@ -157,6 +157,54 @@ func TestEmphasis(t *testing.T) { doTestsInline(t, tests) } +func TestReferenceOverride(t *testing.T) { + var tests = []string{ + "test [ref1][]\n", + "

test ref1

\n", + + "test [my ref][ref1]\n", + "

test my ref

\n", + + "test [ref2][]\n\n[ref2]: http://www.leftalone.com/ (Ref left alone)\n", + "

test ref2

\n", + + "test [ref3][]\n\n[ref3]: http://www.leftalone.com/ (Ref left alone)\n", + "

test ref3

\n", + + "test [ref4][]\n\n[ref4]: http://zombo.com/ (You can do anything)\n", + "

test [ref4][]

\n", + + "test [!(*http.ServeMux).ServeHTTP][] complicated ref\n", + "

test !(*http.ServeMux).ServeHTTP complicated ref

\n", + } + doTestsInlineParam(t, tests, Options{ + ReferenceOverride: func(reference string) (rv *Reference, overridden bool) { + switch reference { + case "ref1": + // just an overriden reference exists without definition + return &Reference{ + Link: "http://www.ref1.com/", + Title: "Reference 1"}, true + case "ref2": + // overridden exists and reference defined + return &Reference{ + Link: "http://www.overridden.com/", + Title: "Reference Overridden"}, true + case "ref3": + // not overridden and reference defined + return nil, false + case "ref4": + // overridden missing and defined + return nil, true + case "!(*http.ServeMux).ServeHTTP": + return &Reference{ + Link: "http://localhost:6060/pkg/net/http/#ServeMux.ServeHTTP", + Title: "ServeHTTP docs"}, true + } + return nil, false + }}, 0, HtmlRendererParameters{}) +} + func TestStrong(t *testing.T) { var tests = []string{ "nothing inline\n", @@ -436,7 +484,7 @@ func TestNofollowLink(t *testing.T) { "[foo](/bar/)\n", "

foo

\n", } - doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS, + doTestsInlineParam(t, tests, Options{}, HTML_SAFELINK|HTML_NOFOLLOW_LINKS, HtmlRendererParameters{}) } @@ -449,7 +497,7 @@ func TestHrefTargetBlank(t *testing.T) { "[foo](http://example.com)\n", "

foo

\n", } - doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_HREF_TARGET_BLANK, HtmlRendererParameters{}) + doTestsInlineParam(t, tests, Options{}, HTML_SAFELINK|HTML_HREF_TARGET_BLANK, HtmlRendererParameters{}) } func TestSafeInlineLink(t *testing.T) { @@ -771,7 +819,7 @@ what happens here } func TestFootnotes(t *testing.T) { - doTestsInlineParam(t, footnoteTests, EXTENSION_FOOTNOTES, 0, HtmlRendererParameters{}) + doTestsInlineParam(t, footnoteTests, Options{Extensions: EXTENSION_FOOTNOTES}, 0, HtmlRendererParameters{}) } func TestFootnotesWithParameters(t *testing.T) { @@ -796,7 +844,7 @@ func TestFootnotesWithParameters(t *testing.T) { FootnoteReturnLinkContents: returnText, } - doTestsInlineParam(t, tests, EXTENSION_FOOTNOTES, HTML_FOOTNOTE_RETURN_LINKS, params) + doTestsInlineParam(t, tests, Options{Extensions: EXTENSION_FOOTNOTES}, HTML_FOOTNOTE_RETURN_LINKS, params) } func TestSmartDoubleQuotes(t *testing.T) { @@ -808,7 +856,7 @@ func TestSmartDoubleQuotes(t *testing.T) { "two pair of \"some\" quoted \"text\".\n", "

two pair of “some” quoted “text”.

\n"} - doTestsInlineParam(t, tests, 0, HTML_USE_SMARTYPANTS, HtmlRendererParameters{}) + doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS, HtmlRendererParameters{}) } func TestSmartAngledDoubleQuotes(t *testing.T) { @@ -820,5 +868,5 @@ func TestSmartAngledDoubleQuotes(t *testing.T) { "two pair of \"some\" quoted \"text\".\n", "

two pair of «some» quoted «text».

\n"} - doTestsInlineParam(t, tests, 0, HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_ANGLED_QUOTES, HtmlRendererParameters{}) + doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_ANGLED_QUOTES, HtmlRendererParameters{}) } diff --git a/markdown.go b/markdown.go index d07e37c..976f95f 100644 --- a/markdown.go +++ b/markdown.go @@ -20,6 +20,7 @@ package blackfriday import ( "bytes" + "strings" "unicode/utf8" ) @@ -196,6 +197,7 @@ type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) in // This is constructed by the Markdown function. type parser struct { r Renderer + refOverride ReferenceOverrideFunc refs map[string]*reference inlineCallback [256]inlineParser flags int @@ -209,12 +211,70 @@ type parser struct { notes []*reference } +func (p *parser) getRef(refid string) (ref *reference, found bool) { + if p.refOverride != nil { + r, overridden := p.refOverride(refid) + if overridden { + if r == nil { + return nil, false + } + return &reference{ + link: []byte(r.Link), + title: []byte(r.Title), + noteId: 0, + hasBlock: false}, true + } + } + // refs are case insensitive + ref, found = p.refs[strings.ToLower(refid)] + return ref, found +} + // // // Public interface // // +// Reference represents the details of a link. +// See the documentation in Options for more details on use-case. +type Reference struct { + // Link is usually the URL the reference points to. + Link string + // Title is the alternate text describing the link in more detail. + Title string +} + +// ReferenceOverrideFunc is expected to be called with a reference string and +// return either a valid Reference type that the reference string maps to or +// nil. If overridden is false, the default reference logic will be executed. +// See the documentation in Options for more details on use-case. +type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) + +// Options represents configurable overrides and callbacks (in addition to the +// extension flag set) for configuring a Markdown parse. +type Options struct { + // Extensions is a flag set of bit-wise ORed extension bits. See the + // EXTENSION_* flags defined in this package. + Extensions int + + // ReferenceOverride is an optional function callback that is called every + // time a reference is resolved. + // + // In Markdown, the link reference syntax can be made to resolve a link to + // a reference instead of an inline URL, in one of the following ways: + // + // * [link text][refid] + // * [refid][] + // + // Usually, the refid is defined at the bottom of the Markdown document. If + // this override function is provided, the refid is passed to the override + // function first, before consulting the defined refids at the bottom. If + // the override function indicates an override did not occur, the refids at + // the bottom will be used to fill in the link details. + ReferenceOverride ReferenceOverrideFunc +} + // MarkdownBasic is a convenience function for simple rendering. // It processes markdown input with no extensions enabled. func MarkdownBasic(input []byte) []byte { @@ -223,9 +283,7 @@ func MarkdownBasic(input []byte) []byte { renderer := HtmlRenderer(htmlFlags, "", "") // set up the parser - extensions := 0 - - return Markdown(input, renderer, extensions) + return MarkdownOptions(input, renderer, Options{Extensions: 0}) } // Call Markdown with most useful extensions enabled @@ -250,7 +308,8 @@ func MarkdownBasic(input []byte) []byte { func MarkdownCommon(input []byte) []byte { // set up the HTML renderer renderer := HtmlRenderer(commonHtmlFlags, "", "") - return Markdown(input, renderer, commonExtensions) + return MarkdownOptions(input, renderer, Options{ + Extensions: commonExtensions}) } // Markdown is the main rendering function. @@ -261,15 +320,25 @@ func MarkdownCommon(input []byte) []byte { // To use the supplied Html or LaTeX renderers, see HtmlRenderer and // LatexRenderer, respectively. func Markdown(input []byte, renderer Renderer, extensions int) []byte { + return MarkdownOptions(input, renderer, Options{ + Extensions: extensions}) +} + +// MarkdownOptions is just like Markdown but takes additional options through +// the Options struct. +func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte { // no point in parsing if we can't render if renderer == nil { return nil } + extensions := opts.Extensions + // fill in the render structure p := new(parser) p.r = renderer p.flags = extensions + p.refOverride = opts.ReferenceOverride p.refs = make(map[string]*reference) p.maxNesting = 16 p.insideLink = false From 8e10236be5dd3b43b5a38f041e500dec837b6df5 Mon Sep 17 00:00:00 2001 From: JT Olds Date: Thu, 18 Dec 2014 17:36:46 -0700 Subject: [PATCH 2/4] support replacing [refid][] syntax link content with alternate content --- inline.go | 19 ++++++++++++++----- markdown.go | 7 ++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/inline.go b/inline.go index b8b7659..72b64d8 100644 --- a/inline.go +++ b/inline.go @@ -211,10 +211,10 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] var ( - i = 1 - noteId int - title, link []byte - textHasNl = false + i = 1 + noteId int + title, link, alt_content []byte + textHasNl = false ) if t == linkDeferredFootnote { @@ -350,6 +350,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // reference style link case i < len(data) && data[i] == '[': var id []byte + alt_content_considered := false // look for the id i++ @@ -379,6 +380,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { id = b.Bytes() } else { id = data[1:txtE] + alt_content_considered = true } } else { id = data[linkB:linkE] @@ -394,6 +396,9 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // keep link and title from reference link = lr.link title = lr.title + if alt_content_considered { + alt_content = lr.text + } i++ // shortcut reference style link or reference or inline footnote @@ -503,7 +508,11 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // call the relevant rendering function switch t { case linkNormal: - p.r.Link(out, uLink, title, content.Bytes()) + if len(alt_content) > 0 { + p.r.Link(out, uLink, title, alt_content) + } else { + p.r.Link(out, uLink, title, content.Bytes()) + } case linkImg: outSize := out.Len() diff --git a/markdown.go b/markdown.go index 976f95f..42ae4d1 100644 --- a/markdown.go +++ b/markdown.go @@ -222,7 +222,8 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) { link: []byte(r.Link), title: []byte(r.Title), noteId: 0, - hasBlock: false}, true + hasBlock: false, + text: []byte(r.Text)}, true } } // refs are case insensitive @@ -243,6 +244,9 @@ type Reference struct { Link string // Title is the alternate text describing the link in more detail. Title string + // Text is the optional text to override the ref with if the syntax used was + // [refid][] + Text string } // ReferenceOverrideFunc is expected to be called with a reference string and @@ -508,6 +512,7 @@ type reference struct { title []byte noteId int // 0 if not a footnote ref hasBlock bool + text []byte } // Check whether or not data starts with a reference link. From 62f0018e2f0ff83c09bec0ca5cd167744d546bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytautas=20=C5=A0altenis?= Date: Wed, 6 May 2015 15:55:04 +0300 Subject: [PATCH 3/4] Replace snake_case with mixedCase --- inline.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/inline.go b/inline.go index 15137a1..3f39b52 100644 --- a/inline.go +++ b/inline.go @@ -216,10 +216,10 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { data = data[offset:] var ( - i = 1 - noteId int - title, link, alt_content []byte - textHasNl = false + i = 1 + noteId int + title, link, altContent []byte + textHasNl = false ) if t == linkDeferredFootnote { @@ -355,7 +355,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // reference style link case i < len(data)-1 && data[i] == '[' && data[i+1] != '^': var id []byte - alt_content_considered := false + altContentConsidered := false // look for the id i++ @@ -385,7 +385,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { id = b.Bytes() } else { id = data[1:txtE] - alt_content_considered = true + altContentConsidered = true } } else { id = data[linkB:linkE] @@ -401,8 +401,8 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // keep link and title from reference link = lr.link title = lr.title - if alt_content_considered { - alt_content = lr.text + if altContentConsidered { + altContent = lr.text } i++ @@ -513,8 +513,8 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { // call the relevant rendering function switch t { case linkNormal: - if len(alt_content) > 0 { - p.r.Link(out, uLink, title, alt_content) + if len(altContent) > 0 { + p.r.Link(out, uLink, title, altContent) } else { p.r.Link(out, uLink, title, content.Bytes()) } From 314ce8fe44fab9ebdfdbfa5658c8bba4c5bb3cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytautas=20=C5=A0altenis?= Date: Wed, 6 May 2015 15:57:15 +0300 Subject: [PATCH 4/4] Add a missing test case for reference override Exercise link text override. --- inline_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/inline_test.go b/inline_test.go index 100be52..9ba94e8 100644 --- a/inline_test.go +++ b/inline_test.go @@ -176,6 +176,9 @@ func TestReferenceOverride(t *testing.T) { "test [!(*http.ServeMux).ServeHTTP][] complicated ref\n", "

test !(*http.ServeMux).ServeHTTP complicated ref

\n", + + "test [ref5][]\n", + "

test Moo

\n", } doTestsInlineParam(t, tests, Options{ ReferenceOverride: func(reference string) (rv *Reference, overridden bool) { @@ -200,6 +203,12 @@ func TestReferenceOverride(t *testing.T) { return &Reference{ Link: "http://localhost:6060/pkg/net/http/#ServeMux.ServeHTTP", Title: "ServeHTTP docs"}, true + case "ref5": + return &Reference{ + Link: "http://www.ref5.com/", + Title: "Reference 5", + Text: "Moo", + }, true } return nil, false }}, 0, HtmlRendererParameters{})