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
'<Frobnosticator>' 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.
This commit is contained in:
JT Olds 2014-12-16 16:17:49 -07:00
parent 48aaef5fbf
commit 5e8b222b69
3 changed files with 142 additions and 27 deletions

View File

@ -384,9 +384,8 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
id = data[linkB:linkE] id = data[linkB:linkE]
} }
// find the reference with matching id (ids are case-insensitive) // find the reference with matching id
key := string(bytes.ToLower(id)) lr, ok := p.getRef(string(id))
lr, ok := p.refs[key]
if !ok { if !ok {
return 0 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 { if t == linkInlineFootnote {
// create a new reference // create a new reference
noteId = len(p.notes) + 1 noteId = len(p.notes) + 1
@ -453,7 +451,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
title = ref.title title = ref.title
} else { } else {
// find the reference with matching id // find the reference with matching id
lr, ok := p.refs[key] lr, ok := p.getRef(string(id))
if !ok { if !ok {
return 0 return 0
} }

View File

@ -20,19 +20,19 @@ import (
"strings" "strings"
) )
func runMarkdownInline(input string, extensions, htmlFlags int, params HtmlRendererParameters) string { func runMarkdownInline(input string, opts Options, htmlFlags int, params HtmlRendererParameters) string {
extensions |= EXTENSION_AUTOLINK opts.Extensions |= EXTENSION_AUTOLINK
extensions |= EXTENSION_STRIKETHROUGH opts.Extensions |= EXTENSION_STRIKETHROUGH
htmlFlags |= HTML_USE_XHTML htmlFlags |= HTML_USE_XHTML
renderer := HtmlRendererWithParameters(htmlFlags, "", "", params) 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) { 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) { func doLinkTestsInline(t *testing.T, tests []string) {
@ -41,22 +41,22 @@ func doLinkTestsInline(t *testing.T, tests []string) {
prefix := "http://localhost" prefix := "http://localhost"
params := HtmlRendererParameters{AbsolutePrefix: prefix} params := HtmlRendererParameters{AbsolutePrefix: prefix}
transformTests := transformLinks(tests, prefix) transformTests := transformLinks(tests, prefix)
doTestsInlineParam(t, transformTests, 0, 0, params) doTestsInlineParam(t, transformTests, Options{}, 0, params)
doTestsInlineParam(t, transformTests, 0, commonHtmlFlags, params) doTestsInlineParam(t, transformTests, Options{}, commonHtmlFlags, params)
} }
func doSafeTestsInline(t *testing.T, tests []string) { 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 // All the links in this test should not have the prefix appended, so
// just rerun it with different parameters and the same expectations. // just rerun it with different parameters and the same expectations.
prefix := "http://localhost" prefix := "http://localhost"
params := HtmlRendererParameters{AbsolutePrefix: prefix} params := HtmlRendererParameters{AbsolutePrefix: prefix}
transformTests := transformLinks(tests, 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) { params HtmlRendererParameters) {
// catch and report panics // catch and report panics
var candidate string var candidate string
@ -72,7 +72,7 @@ func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int,
input := tests[i] input := tests[i]
candidate = input candidate = input
expected := tests[i+1] expected := tests[i+1]
actual := runMarkdownInline(candidate, extensions, htmlFlags, params) actual := runMarkdownInline(candidate, opts, htmlFlags, params)
if actual != expected { if actual != expected {
t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]", t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]",
candidate, expected, actual) 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 start := 0; start < len(input); start++ {
for end := start + 1; end <= len(input); end++ { for end := start + 1; end <= len(input); end++ {
candidate = input[start: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) doTestsInline(t, tests)
} }
func TestReferenceOverride(t *testing.T) {
var tests = []string{
"test [ref1][]\n",
"<p>test <a href=\"http://www.ref1.com/\" title=\"Reference 1\">ref1</a></p>\n",
"test [my ref][ref1]\n",
"<p>test <a href=\"http://www.ref1.com/\" title=\"Reference 1\">my ref</a></p>\n",
"test [ref2][]\n\n[ref2]: http://www.leftalone.com/ (Ref left alone)\n",
"<p>test <a href=\"http://www.overridden.com/\" title=\"Reference Overridden\">ref2</a></p>\n",
"test [ref3][]\n\n[ref3]: http://www.leftalone.com/ (Ref left alone)\n",
"<p>test <a href=\"http://www.leftalone.com/\" title=\"Ref left alone\">ref3</a></p>\n",
"test [ref4][]\n\n[ref4]: http://zombo.com/ (You can do anything)\n",
"<p>test [ref4][]</p>\n",
"test [!(*http.ServeMux).ServeHTTP][] complicated ref\n",
"<p>test <a href=\"http://localhost:6060/pkg/net/http/#ServeMux.ServeHTTP\" title=\"ServeHTTP docs\">!(*http.ServeMux).ServeHTTP</a> complicated ref</p>\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) { func TestStrong(t *testing.T) {
var tests = []string{ var tests = []string{
"nothing inline\n", "nothing inline\n",
@ -436,7 +484,7 @@ func TestNofollowLink(t *testing.T) {
"[foo](/bar/)\n", "[foo](/bar/)\n",
"<p><a href=\"/bar/\">foo</a></p>\n", "<p><a href=\"/bar/\">foo</a></p>\n",
} }
doTestsInlineParam(t, tests, 0, HTML_SAFELINK|HTML_NOFOLLOW_LINKS, doTestsInlineParam(t, tests, Options{}, HTML_SAFELINK|HTML_NOFOLLOW_LINKS,
HtmlRendererParameters{}) HtmlRendererParameters{})
} }
@ -449,7 +497,7 @@ func TestHrefTargetBlank(t *testing.T) {
"[foo](http://example.com)\n", "[foo](http://example.com)\n",
"<p><a href=\"http://example.com\" target=\"_blank\">foo</a></p>\n", "<p><a href=\"http://example.com\" target=\"_blank\">foo</a></p>\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) { func TestSafeInlineLink(t *testing.T) {
@ -771,7 +819,7 @@ what happens here
} }
func TestFootnotes(t *testing.T) { 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) { func TestFootnotesWithParameters(t *testing.T) {
@ -796,7 +844,7 @@ func TestFootnotesWithParameters(t *testing.T) {
FootnoteReturnLinkContents: returnText, 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) { 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",
"<p>two pair of &ldquo;some&rdquo; quoted &ldquo;text&rdquo;.</p>\n"} "<p>two pair of &ldquo;some&rdquo; quoted &ldquo;text&rdquo;.</p>\n"}
doTestsInlineParam(t, tests, 0, HTML_USE_SMARTYPANTS, HtmlRendererParameters{}) doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS, HtmlRendererParameters{})
} }
func TestSmartAngledDoubleQuotes(t *testing.T) { 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",
"<p>two pair of &laquo;some&raquo; quoted &laquo;text&raquo;.</p>\n"} "<p>two pair of &laquo;some&raquo; quoted &laquo;text&raquo;.</p>\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{})
} }

View File

@ -20,6 +20,7 @@ package blackfriday
import ( import (
"bytes" "bytes"
"strings"
"unicode/utf8" "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. // This is constructed by the Markdown function.
type parser struct { type parser struct {
r Renderer r Renderer
refOverride ReferenceOverrideFunc
refs map[string]*reference refs map[string]*reference
inlineCallback [256]inlineParser inlineCallback [256]inlineParser
flags int flags int
@ -209,12 +211,70 @@ type parser struct {
notes []*reference 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 // 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. // MarkdownBasic is a convenience function for simple rendering.
// It processes markdown input with no extensions enabled. // It processes markdown input with no extensions enabled.
func MarkdownBasic(input []byte) []byte { func MarkdownBasic(input []byte) []byte {
@ -223,9 +283,7 @@ func MarkdownBasic(input []byte) []byte {
renderer := HtmlRenderer(htmlFlags, "", "") renderer := HtmlRenderer(htmlFlags, "", "")
// set up the parser // set up the parser
extensions := 0 return MarkdownOptions(input, renderer, Options{Extensions: 0})
return Markdown(input, renderer, extensions)
} }
// Call Markdown with most useful extensions enabled // Call Markdown with most useful extensions enabled
@ -250,7 +308,8 @@ func MarkdownBasic(input []byte) []byte {
func MarkdownCommon(input []byte) []byte { func MarkdownCommon(input []byte) []byte {
// set up the HTML renderer // set up the HTML renderer
renderer := HtmlRenderer(commonHtmlFlags, "", "") renderer := HtmlRenderer(commonHtmlFlags, "", "")
return Markdown(input, renderer, commonExtensions) return MarkdownOptions(input, renderer, Options{
Extensions: commonExtensions})
} }
// Markdown is the main rendering function. // 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 // To use the supplied Html or LaTeX renderers, see HtmlRenderer and
// LatexRenderer, respectively. // LatexRenderer, respectively.
func Markdown(input []byte, renderer Renderer, extensions int) []byte { 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 // no point in parsing if we can't render
if renderer == nil { if renderer == nil {
return nil return nil
} }
extensions := opts.Extensions
// fill in the render structure // fill in the render structure
p := new(parser) p := new(parser)
p.r = renderer p.r = renderer
p.flags = extensions p.flags = extensions
p.refOverride = opts.ReferenceOverride
p.refs = make(map[string]*reference) p.refs = make(map[string]*reference)
p.maxNesting = 16 p.maxNesting = 16
p.insideLink = false p.insideLink = false