From e81d1d11386a0ee9550ce7e2725fb003e15c6c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytautas=20=C5=A0altenis?= Date: Thu, 2 Feb 2017 09:35:10 +0200 Subject: [PATCH] Change the public interface to use functional options Convert the most important Blackfriday's function, Markdown(), to accept functional options (as per this Dave Cheney's post: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) --- block.go | 58 ++++++------- block_test.go | 4 +- helpers_test.go | 18 ++-- inline.go | 34 ++++---- inline_test.go | 70 ++++++++------- markdown.go | 222 ++++++++++++++++++++++++++---------------------- ref_test.go | 2 +- 7 files changed, 212 insertions(+), 196 deletions(-) diff --git a/block.go b/block.go index 9d3a003..43df826 100644 --- a/block.go +++ b/block.go @@ -34,7 +34,7 @@ var ( // Parse block-level data. // Note: this function and many that it calls assume that // the input buffer ends with a newline. -func (p *parser) block(data []byte) { +func (p *Parser) block(data []byte) { // this is called recursively: enforce a maximum depth if p.nesting >= p.maxNesting { return @@ -197,14 +197,14 @@ func (p *parser) block(data []byte) { p.nesting-- } -func (p *parser) addBlock(typ NodeType, content []byte) *Node { +func (p *Parser) addBlock(typ NodeType, content []byte) *Node { p.closeUnmatchedBlocks() container := p.addChild(typ, 0) container.content = content return container } -func (p *parser) isPrefixHeader(data []byte) bool { +func (p *Parser) isPrefixHeader(data []byte) bool { if data[0] != '#' { return false } @@ -221,7 +221,7 @@ func (p *parser) isPrefixHeader(data []byte) bool { return true } -func (p *parser) prefixHeader(data []byte) int { +func (p *Parser) prefixHeader(data []byte) int { level := 0 for level < 6 && level < len(data) && data[level] == '#' { level++ @@ -267,7 +267,7 @@ func (p *parser) prefixHeader(data []byte) int { return skip } -func (p *parser) isUnderlinedHeader(data []byte) int { +func (p *Parser) isUnderlinedHeader(data []byte) int { // test of level 1 header if data[0] == '=' { i := skipChar(data, 1, '=') @@ -291,7 +291,7 @@ func (p *parser) isUnderlinedHeader(data []byte) int { return 0 } -func (p *parser) titleBlock(data []byte, doRender bool) int { +func (p *Parser) titleBlock(data []byte, doRender bool) int { if data[0] != '%' { return 0 } @@ -315,7 +315,7 @@ func (p *parser) titleBlock(data []byte, doRender bool) int { return consumed } -func (p *parser) html(data []byte, doRender bool) int { +func (p *Parser) html(data []byte, doRender bool) int { var i, j int // identify the opening tag @@ -419,7 +419,7 @@ func finalizeHTMLBlock(block *Node) { } // HTML comment, lax form -func (p *parser) htmlComment(data []byte, doRender bool) int { +func (p *Parser) htmlComment(data []byte, doRender bool) int { i := p.inlineHTMLComment(data) // needs to end with a blank line if j := p.isEmpty(data[i:]); j > 0 { @@ -439,7 +439,7 @@ func (p *parser) htmlComment(data []byte, doRender bool) int { } // HR, which is the only self-closing block tag considered -func (p *parser) htmlHr(data []byte, doRender bool) int { +func (p *Parser) htmlHr(data []byte, doRender bool) int { if len(data) < 4 { return 0 } @@ -472,7 +472,7 @@ func (p *parser) htmlHr(data []byte, doRender bool) int { return 0 } -func (p *parser) htmlFindTag(data []byte) (string, bool) { +func (p *Parser) htmlFindTag(data []byte) (string, bool) { i := 0 for i < len(data) && isalnum(data[i]) { i++ @@ -484,7 +484,7 @@ func (p *parser) htmlFindTag(data []byte) (string, bool) { return "", false } -func (p *parser) htmlFindEnd(tag string, data []byte) int { +func (p *Parser) htmlFindEnd(tag string, data []byte) int { // assume data[0] == '<' && data[1] == '/' already tested if tag == "hr" { return 2 @@ -519,7 +519,7 @@ func (p *parser) htmlFindEnd(tag string, data []byte) int { return i + skip } -func (*parser) isEmpty(data []byte) int { +func (*Parser) isEmpty(data []byte) int { // it is okay to call isEmpty on an empty buffer if len(data) == 0 { return 0 @@ -537,7 +537,7 @@ func (*parser) isEmpty(data []byte) int { return i } -func (*parser) isHRule(data []byte) bool { +func (*Parser) isHRule(data []byte) bool { i := 0 // skip up to three spaces @@ -667,7 +667,7 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. -func (p *parser) fencedCodeBlock(data []byte, doRender bool) int { +func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int { var syntax string beg, marker := isFenceLine(data, &syntax, "") if beg == 0 || beg >= len(data) { @@ -739,7 +739,7 @@ func finalizeCodeBlock(block *Node) { block.content = nil } -func (p *parser) table(data []byte) int { +func (p *Parser) table(data []byte) int { table := p.addBlock(Table, nil) i, columns := p.tableHeader(data) if i == 0 { @@ -782,7 +782,7 @@ func isBackslashEscaped(data []byte, i int) bool { return backslashes&1 == 1 } -func (p *parser) tableHeader(data []byte) (size int, columns []CellAlignFlags) { +func (p *Parser) tableHeader(data []byte) (size int, columns []CellAlignFlags) { i := 0 colCount := 1 for i = 0; i < len(data) && data[i] != '\n'; i++ { @@ -895,7 +895,7 @@ func (p *parser) tableHeader(data []byte) (size int, columns []CellAlignFlags) { return } -func (p *parser) tableRow(data []byte, columns []CellAlignFlags, header bool) { +func (p *Parser) tableRow(data []byte, columns []CellAlignFlags, header bool) { p.addBlock(TableRow, nil) i, col := 0, 0 @@ -939,7 +939,7 @@ func (p *parser) tableRow(data []byte, columns []CellAlignFlags, header bool) { } // returns blockquote prefix length -func (p *parser) quotePrefix(data []byte) int { +func (p *Parser) quotePrefix(data []byte) int { i := 0 for i < 3 && i < len(data) && data[i] == ' ' { i++ @@ -955,7 +955,7 @@ func (p *parser) quotePrefix(data []byte) int { // blockquote ends with at least one blank line // followed by something without a blockquote prefix -func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { +func (p *Parser) terminateBlockquote(data []byte, beg, end int) bool { if p.isEmpty(data[beg:]) <= 0 { return false } @@ -966,7 +966,7 @@ func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { } // parse a blockquote fragment -func (p *parser) quote(data []byte) int { +func (p *Parser) quote(data []byte) int { block := p.addBlock(BlockQuote, nil) var raw bytes.Buffer beg, end := 0, 0 @@ -1004,7 +1004,7 @@ func (p *parser) quote(data []byte) int { } // returns prefix length for block code -func (p *parser) codePrefix(data []byte) int { +func (p *Parser) codePrefix(data []byte) int { if len(data) >= 1 && data[0] == '\t' { return 1 } @@ -1014,7 +1014,7 @@ func (p *parser) codePrefix(data []byte) int { return 0 } -func (p *parser) code(data []byte) int { +func (p *Parser) code(data []byte) int { var work bytes.Buffer i := 0 @@ -1064,7 +1064,7 @@ func (p *parser) code(data []byte) int { } // returns unordered list item prefix -func (p *parser) uliPrefix(data []byte) int { +func (p *Parser) uliPrefix(data []byte) int { i := 0 // start with up to 3 spaces for i < len(data) && i < 3 && data[i] == ' ' { @@ -1082,7 +1082,7 @@ func (p *parser) uliPrefix(data []byte) int { } // returns ordered list item prefix -func (p *parser) oliPrefix(data []byte) int { +func (p *Parser) oliPrefix(data []byte) int { i := 0 // start with up to 3 spaces @@ -1107,7 +1107,7 @@ func (p *parser) oliPrefix(data []byte) int { } // returns definition list item prefix -func (p *parser) dliPrefix(data []byte) int { +func (p *Parser) dliPrefix(data []byte) int { if len(data) < 2 { return 0 } @@ -1123,7 +1123,7 @@ func (p *parser) dliPrefix(data []byte) int { } // parse ordered or unordered list block -func (p *parser) list(data []byte, flags ListType) int { +func (p *Parser) list(data []byte, flags ListType) int { i := 0 flags |= ListItemBeginningOfList block := p.addBlock(List, nil) @@ -1191,7 +1191,7 @@ func finalizeList(block *Node) { // Parse a single list item. // Assumes initial prefix is already removed if this is a sublist. -func (p *parser) listItem(data []byte, flags *ListType) int { +func (p *Parser) listItem(data []byte, flags *ListType) int { // keep track of the indentation of the first line itemIndent := 0 if data[0] == '\t' { @@ -1383,7 +1383,7 @@ gatherlines: } // render a single paragraph that has already been parsed out -func (p *parser) renderParagraph(data []byte) { +func (p *Parser) renderParagraph(data []byte) { if len(data) == 0 { return } @@ -1408,7 +1408,7 @@ func (p *parser) renderParagraph(data []byte) { p.addBlock(Paragraph, data[beg:end]) } -func (p *parser) paragraph(data []byte) int { +func (p *Parser) paragraph(data []byte) int { // prev: index of 1st char of previous line // line: index of 1st char of current line // i: index of cursor/end of current line diff --git a/block_test.go b/block_test.go index 0a944a1..b1554f9 100644 --- a/block_test.go +++ b/block_test.go @@ -253,7 +253,7 @@ func TestPrefixHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) { } doTestsParam(t, tests, TestParams{ - Options: Options{Extensions: HeaderIDs}, + extensions: HeaderIDs, HTMLFlags: UseXHTML, HTMLRendererParameters: parameters, }) @@ -365,7 +365,7 @@ func TestPrefixAutoHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) { } doTestsParam(t, tests, TestParams{ - Options: Options{Extensions: AutoHeaderIDs}, + extensions: AutoHeaderIDs, HTMLFlags: UseXHTML, HTMLRendererParameters: parameters, }) diff --git a/helpers_test.go b/helpers_test.go index ecefe2f..d2639d2 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -21,7 +21,8 @@ import ( ) type TestParams struct { - Options + extensions Extensions + referenceOverride ReferenceOverrideFunc HTMLFlags HTMLRendererParameters } @@ -45,13 +46,15 @@ func execRecoverableTestSuite(t *testing.T, tests []string, params TestParams, s func runMarkdown(input string, params TestParams) string { params.HTMLRendererParameters.Flags = params.HTMLFlags renderer := NewHTMLRenderer(params.HTMLRendererParameters) - return string(Markdown([]byte(input), renderer, params.Options)) + return string(Markdown([]byte(input), WithRenderer(renderer), + WithExtensions(params.extensions), + WithRefOverride(params.referenceOverride))) } // doTests runs full document tests using MarkdownCommon configuration. func doTests(t *testing.T, tests []string) { doTestsParam(t, tests, TestParams{ - Options: DefaultOptions, + extensions: CommonExtensions, HTMLRendererParameters: HTMLRendererParameters{ Flags: CommonHTMLFlags, }, @@ -60,8 +63,8 @@ func doTests(t *testing.T, tests []string) { func doTestsBlock(t *testing.T, tests []string, extensions Extensions) { doTestsParam(t, tests, TestParams{ - Options: Options{Extensions: extensions}, - HTMLFlags: UseXHTML, + extensions: extensions, + HTMLFlags: UseXHTML, }) } @@ -124,8 +127,7 @@ func doSafeTestsInline(t *testing.T, tests []string) { } func doTestsInlineParam(t *testing.T, tests []string, params TestParams) { - params.Options.Extensions |= Autolink - params.Options.Extensions |= Strikethrough + params.extensions |= Autolink | Strikethrough params.HTMLFlags |= UseXHTML doTestsParam(t, tests, params) } @@ -145,7 +147,7 @@ func transformLinks(tests []string, prefix string) []string { } func doTestsReference(t *testing.T, files []string, flag Extensions) { - params := TestParams{Options: Options{Extensions: flag}} + params := TestParams{extensions: flag} execRecoverableTestSuite(t, files, params, func(candidate *string) { for _, basename := range files { filename := filepath.Join("testdata", basename+".text") diff --git a/inline.go b/inline.go index 2a1db7a..67ca219 100644 --- a/inline.go +++ b/inline.go @@ -32,7 +32,7 @@ var ( // data is the complete block being rendered // offset is the number of valid chars before the current cursor -func (p *parser) inline(currBlock *Node, data []byte) { +func (p *Parser) inline(currBlock *Node, data []byte) { // handlers might call us recursively: enforce a maximum depth if p.nesting >= p.maxNesting || len(data) == 0 { return @@ -69,7 +69,7 @@ func (p *parser) inline(currBlock *Node, data []byte) { } // single and double emphasis parsing -func emphasis(p *parser, data []byte, offset int) (int, *Node) { +func emphasis(p *Parser, data []byte, offset int) (int, *Node) { data = data[offset:] c := data[0] @@ -114,7 +114,7 @@ func emphasis(p *parser, data []byte, offset int) (int, *Node) { return 0, nil } -func codeSpan(p *parser, data []byte, offset int) (int, *Node) { +func codeSpan(p *Parser, data []byte, offset int) (int, *Node) { data = data[offset:] nb := 0 @@ -161,7 +161,7 @@ func codeSpan(p *parser, data []byte, offset int) (int, *Node) { } // newline preceded by two spaces becomes
-func maybeLineBreak(p *parser, data []byte, offset int) (int, *Node) { +func maybeLineBreak(p *Parser, data []byte, offset int) (int, *Node) { origOffset := offset for offset < len(data) && data[offset] == ' ' { offset++ @@ -177,7 +177,7 @@ func maybeLineBreak(p *parser, data []byte, offset int) (int, *Node) { } // newline without two spaces works when HardLineBreak is enabled -func lineBreak(p *parser, data []byte, offset int) (int, *Node) { +func lineBreak(p *Parser, data []byte, offset int) (int, *Node) { if p.flags&HardLineBreak != 0 { return 1, NewNode(Hardbreak) } @@ -200,14 +200,14 @@ func isReferenceStyleLink(data []byte, pos int, t linkType) bool { return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' } -func maybeImage(p *parser, data []byte, offset int) (int, *Node) { +func maybeImage(p *Parser, data []byte, offset int) (int, *Node) { if offset < len(data)-1 && data[offset+1] == '[' { return link(p, data, offset) } return 0, nil } -func maybeInlineFootnote(p *parser, data []byte, offset int) (int, *Node) { +func maybeInlineFootnote(p *Parser, data []byte, offset int) (int, *Node) { if offset < len(data)-1 && data[offset+1] == '[' { return link(p, data, offset) } @@ -215,7 +215,7 @@ func maybeInlineFootnote(p *parser, data []byte, offset int) (int, *Node) { } // '[': parse a link or an image or a footnote -func link(p *parser, data []byte, offset int) (int, *Node) { +func link(p *Parser, data []byte, offset int) (int, *Node) { // no links allowed inside regular links, footnote, and deferred footnotes if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { return 0, nil @@ -573,7 +573,7 @@ func link(p *parser, data []byte, offset int) (int, *Node) { return i, linkNode } -func (p *parser) inlineHTMLComment(data []byte) int { +func (p *Parser) inlineHTMLComment(data []byte) int { if len(data) < 5 { return 0 } @@ -613,7 +613,7 @@ const ( ) // '<' when tags or autolinks are allowed -func leftAngle(p *parser, data []byte, offset int) (int, *Node) { +func leftAngle(p *Parser, data []byte, offset int) (int, *Node) { data = data[offset:] altype, end := tagLength(data) if size := p.inlineHTMLComment(data); size > 0 { @@ -646,7 +646,7 @@ func leftAngle(p *parser, data []byte, offset int) (int, *Node) { // '\\' backslash escape var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") -func escape(p *parser, data []byte, offset int) (int, *Node) { +func escape(p *Parser, data []byte, offset int) (int, *Node) { data = data[offset:] if len(data) > 1 { @@ -686,7 +686,7 @@ func unescapeText(ob *bytes.Buffer, src []byte) { // '&' escaped when it doesn't belong to an entity // valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; -func entity(p *parser, data []byte, offset int) (int, *Node) { +func entity(p *Parser, data []byte, offset int) (int, *Node) { data = data[offset:] end := 1 @@ -748,7 +748,7 @@ var protocolPrefixes = [][]byte{ const shortestPrefix = 6 // len("ftp://"), the shortest of the above -func maybeAutoLink(p *parser, data []byte, offset int) (int, *Node) { +func maybeAutoLink(p *Parser, data []byte, offset int) (int, *Node) { // quick check to rule out most false hits if p.insideLink || len(data) < offset+shortestPrefix { return 0, nil @@ -765,7 +765,7 @@ func maybeAutoLink(p *parser, data []byte, offset int) (int, *Node) { return 0, nil } -func autoLink(p *parser, data []byte, offset int) (int, *Node) { +func autoLink(p *Parser, data []byte, offset int) (int, *Node) { // Now a more expensive check to see if we're not inside an anchor element anchorStart := offset offsetFromAnchor := 0 @@ -1095,7 +1095,7 @@ func helperFindEmphChar(data []byte, c byte) int { return 0 } -func helperEmphasis(p *parser, data []byte, c byte) (int, *Node) { +func helperEmphasis(p *Parser, data []byte, c byte) (int, *Node) { i := 0 // skip one symbol if coming from emph3 @@ -1135,7 +1135,7 @@ func helperEmphasis(p *parser, data []byte, c byte) (int, *Node) { return 0, nil } -func helperDoubleEmphasis(p *parser, data []byte, c byte) (int, *Node) { +func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, *Node) { i := 0 for i < len(data) { @@ -1159,7 +1159,7 @@ func helperDoubleEmphasis(p *parser, data []byte, c byte) (int, *Node) { return 0, nil } -func helperTripleEmphasis(p *parser, data []byte, offset int, c byte) (int, *Node) { +func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, *Node) { i := 0 origData := data data = data[offset:] diff --git a/inline_test.go b/inline_test.go index 847980a..168ef17 100644 --- a/inline_test.go +++ b/inline_test.go @@ -100,38 +100,36 @@ func TestReferenceOverride(t *testing.T) { "

test Moo

\n", } doTestsInlineParam(t, tests, TestParams{ - Options: Options{ - ReferenceOverride: func(reference string) (rv *Reference, overridden bool) { - switch reference { - case "ref1": - // just an overridden 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 - case "ref5": - return &Reference{ - Link: "http://www.ref5.com/", - Title: "Reference 5", - Text: "Moo", - }, true - } + referenceOverride: func(reference string) (rv *Reference, overridden bool) { + switch reference { + case "ref1": + // just an overridden 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 + case "ref5": + return &Reference{ + Link: "http://www.ref5.com/", + Title: "Reference 5", + Text: "Moo", + }, true + } + return nil, false }, }) } @@ -343,8 +341,8 @@ func TestLineBreak(t *testing.T) { "this has an \nextra space\n", "

this has an
\nextra space

\n", } - doTestsInlineParam(t, tests, TestParams{Options: Options{ - Extensions: BackslashLineBreak}}) + doTestsInlineParam(t, tests, TestParams{ + extensions: BackslashLineBreak}) } func TestInlineLink(t *testing.T) { @@ -932,7 +930,7 @@ what happens here func TestFootnotes(t *testing.T) { doTestsInlineParam(t, footnoteTests, TestParams{ - Options: Options{Extensions: Footnotes}, + extensions: Footnotes, }) } @@ -959,7 +957,7 @@ func TestFootnotesWithParameters(t *testing.T) { } doTestsInlineParam(t, tests, TestParams{ - Options: Options{Extensions: Footnotes}, + extensions: Footnotes, HTMLFlags: FootnoteReturnLinks, HTMLRendererParameters: params, }) @@ -989,7 +987,7 @@ func TestNestedFootnotes(t *testing.T) { `, } - doTestsInlineParam(t, tests, TestParams{Options: Options{Extensions: Footnotes}}) + doTestsInlineParam(t, tests, TestParams{extensions: Footnotes}) } func TestInlineComments(t *testing.T) { diff --git a/markdown.go b/markdown.go index 9c3fdb0..b34d021 100644 --- a/markdown.go +++ b/markdown.go @@ -57,9 +57,9 @@ const ( // DefaultOptions is a convenience variable with all the options that are // enabled by default. -var DefaultOptions = Options{ - Extensions: CommonExtensions, -} +// var DefaultOptions = Options{ +// Extensions: CommonExtensions, +// } // ListType contains bitwise or'ed flags for list and list item objects. type ListType int @@ -160,11 +160,11 @@ type Renderer interface { // Callback functions for inline parsing. One such function is defined // for each character that triggers a response when parsing inline data. -type inlineParser func(p *parser, data []byte, offset int) (int, *Node) +type inlineParser func(p *Parser, data []byte, offset int) (int, *Node) // Parser holds runtime state used by the parser. // This is constructed by the Markdown function. -type parser struct { +type Parser struct { refOverride ReferenceOverrideFunc refs map[string]*reference inlineCallback [256]inlineParser @@ -185,7 +185,7 @@ type parser struct { allClosed bool } -func (p *parser) getRef(refid string) (ref *reference, found bool) { +func (p *Parser) getRef(refid string) (ref *reference, found bool) { if p.refOverride != nil { r, overridden := p.refOverride(refid) if overridden { @@ -205,17 +205,17 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) { return ref, found } -func (p *parser) finalize(block *Node) { +func (p *Parser) finalize(block *Node) { above := block.Parent block.open = false p.tip = above } -func (p *parser) addChild(node NodeType, offset uint32) *Node { +func (p *Parser) addChild(node NodeType, offset uint32) *Node { return p.addExistingChild(NewNode(node), offset) } -func (p *parser) addExistingChild(node *Node, offset uint32) *Node { +func (p *Parser) addExistingChild(node *Node, offset uint32) *Node { for !p.tip.canContain(node.Type) { p.finalize(p.tip) } @@ -224,7 +224,7 @@ func (p *parser) addExistingChild(node *Node, offset uint32) *Node { return node } -func (p *parser) closeUnmatchedBlocks() { +func (p *Parser) closeUnmatchedBlocks() { if !p.allClosed { for p.oldTip != p.lastMatchedContainer { parent := p.oldTip.Parent @@ -259,108 +259,46 @@ type Reference struct { // 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 - // Extensions flags defined in this package. - Extensions Extensions - - // 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 +// Processor contains all the state necessary for Blackfriday to operate. +type Processor struct { + r Renderer + extensions Extensions + referenceOverride ReferenceOverrideFunc } -// MarkdownBasic is a convenience function for simple rendering. -// It processes markdown input with no extensions enabled. -func MarkdownBasic(input []byte) []byte { - // set up the HTML renderer - renderer := NewHTMLRenderer(HTMLRendererParameters{ - Flags: UseXHTML, - }) - - // set up the parser - return Markdown(input, renderer, Options{}) -} - -// MarkdownCommon is a convenience function for simple rendering. It calls -// Markdown with most useful extensions enabled, including: -// -// * Smartypants processing with smart fractions and LaTeX dashes -// -// * Intra-word emphasis suppression -// -// * Tables -// -// * Fenced code blocks -// -// * Autolinking -// -// * Strikethrough support -// -// * Strict header parsing -// -// * Custom Header IDs -func MarkdownCommon(input []byte) []byte { - // set up the HTML renderer - renderer := NewHTMLRenderer(HTMLRendererParameters{ - Flags: CommonHTMLFlags, - }) - return Markdown(input, renderer, DefaultOptions) -} - -// Markdown is the main rendering function. -// It parses and renders a block of markdown-encoded text. -// The supplied Renderer is used to format the output, and extensions dictates -// which non-standard extensions are enabled. -// -// To use the supplied HTML renderer, see NewHTMLRenderer. -func Markdown(input []byte, renderer Renderer, options Options) []byte { - if renderer == nil { - return nil +// DefaultProcessor creates the processor tuned to the most common behavior. +func DefaultProcessor() *Processor { + return &Processor{ + r: NewHTMLRenderer(HTMLRendererParameters{ + Flags: CommonHTMLFlags, + }), + extensions: CommonExtensions, } - return renderer.Render(Parse(input, options)) } -// Parse is an entry point to the parsing part of Blackfriday. It takes an -// input markdown document and produces a syntax tree for its contents. This -// tree can then be rendered with a default or custom renderer, or -// analyzed/transformed by the caller to whatever non-standard needs they have. -func Parse(input []byte, opts Options) *Node { - extensions := opts.Extensions - - // fill in the render structure - p := new(parser) - p.flags = extensions - p.refOverride = opts.ReferenceOverride +// NewParser constructs a Parser. You can use the same With* functions as for +// Markdown() to customize parser's behavior. +func (proc *Processor) NewParser(opts ...Option) *Parser { + for _, opt := range opts { + opt(proc) + } + var p Parser + p.flags = proc.extensions + p.refOverride = proc.referenceOverride p.refs = make(map[string]*reference) p.maxNesting = 16 p.insideLink = false - docNode := NewNode(Document) p.doc = docNode p.tip = docNode p.oldTip = docNode p.lastMatchedContainer = docNode p.allClosed = true - // register inline parsers p.inlineCallback[' '] = maybeLineBreak p.inlineCallback['*'] = emphasis p.inlineCallback['_'] = emphasis - if extensions&Strikethrough != 0 { + if proc.extensions&Strikethrough != 0 { p.inlineCallback['~'] = emphasis } p.inlineCallback['`'] = codeSpan @@ -371,8 +309,7 @@ func Parse(input []byte, opts Options) *Node { p.inlineCallback['&'] = entity p.inlineCallback['!'] = maybeImage p.inlineCallback['^'] = maybeInlineFootnote - - if extensions&Autolink != 0 { + if proc.extensions&Autolink != 0 { p.inlineCallback['h'] = maybeAutoLink p.inlineCallback['m'] = maybeAutoLink p.inlineCallback['f'] = maybeAutoLink @@ -380,11 +317,90 @@ func Parse(input []byte, opts Options) *Node { p.inlineCallback['M'] = maybeAutoLink p.inlineCallback['F'] = maybeAutoLink } - - if extensions&Footnotes != 0 { + if proc.extensions&Footnotes != 0 { p.notes = make([]*reference, 0) } + return &p +} +// Option customizes Processor's default behavior. +type Option func(*Processor) + +// WithRenderer allows you to override the default renderer. +func WithRenderer(r Renderer) Option { + return func(p *Processor) { + p.r = r + } +} + +// WithExtensions allows you to pick some of the many extensions provided by +// Blackfriday. You can bitwise OR them. +func WithExtensions(e Extensions) Option { + return func(p *Processor) { + p.extensions = e + } +} + +// WithNoExtensions turns off all extensions and custom behavior. +func WithNoExtensions() Option { + return func(p *Processor) { + p.extensions = NoExtensions + p.r = NewHTMLRenderer(HTMLRendererParameters{ + Flags: HTMLFlagsNone, + }) + } +} + +// WithRefOverride sets 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. +func WithRefOverride(o ReferenceOverrideFunc) Option { + return func(p *Processor) { + p.referenceOverride = o + } +} + +// Markdown is the main entry point to Blackfriday. It parses and renders a +// block of markdown-encoded text. +// +// The simplest invocation of Markdown takes one argument, input: +// output := Markdown(input) +// This will parse the input with CommonExtensions enabled and render it with +// the default HTMLRenderer (with CommonHTMLFlags). +// +// Variadic arguments opts can customize the default behavior. Since Processor +// type does not contain exported fields, you can not use it directly. Instead, +// use the With* functions. For example, this will call the most basic +// functionality, with no extensions: +// output := Markdown(input, WithNoExtensions()) +// +// You can use any number of With* arguments, even contradicting ones. They +// will be applied in order of appearance and the latter will override the +// former: +// output := Markdown(input, WithNoExtensions(), WithExtensions(exts), +// WithRenderer(yourRenderer)) +func Markdown(input []byte, opts ...Option) []byte { + p := DefaultProcessor() + parser := p.NewParser(opts...) + return p.r.Render(parser.Parse(input)) +} + +// Parse is an entry point to the parsing part of Blackfriday. It takes an +// input markdown document and produces a syntax tree for its contents. This +// tree can then be rendered with a default or custom renderer, or +// analyzed/transformed by the caller to whatever non-standard needs they have. +func (p *Parser) Parse(input []byte) *Node { p.block(input) // Walk the tree and finish up some of unfinished blocks for p.tip != nil { @@ -402,7 +418,7 @@ func Parse(input []byte, opts Options) *Node { return p.doc } -func (p *parser) parseRefsToAST() { +func (p *Parser) parseRefsToAST() { if p.flags&Footnotes == 0 || len(p.notes) == 0 { return } @@ -527,7 +543,7 @@ func (r *reference) String() string { // (in the render struct). // Returns the number of bytes to skip to move past it, // or zero if the first line is not a reference. -func isReference(p *parser, data []byte, tabSize int) int { +func isReference(p *Parser, data []byte, tabSize int) int { // up to 3 optional leading spaces if len(data) < 4 { return 0 @@ -630,7 +646,7 @@ func isReference(p *parser, data []byte, tabSize int) int { return lineEnd } -func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { +func scanLinkRef(p *Parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { // link: whitespace-free sequence, optionally between angle brackets if data[i] == '<' { i++ @@ -701,13 +717,13 @@ func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffse return } -// The first bit of this logic is the same as (*parser).listItem, but the rest +// The first bit of this logic is the same as Parser.listItem, but the rest // is much simpler. This function simply finds the entire block and shifts it // over by one tab if it is indeed a block (just returns the line if it's not). // blockEnd is the end of the section in the input buffer, and contents is the // extracted text that was shifted over one tab. It will need to be rendered at // the end of the document. -func scanFootnote(p *parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { +func scanFootnote(p *Parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { if i == 0 || len(data) == 0 { return } diff --git a/ref_test.go b/ref_test.go index a9b63d9..4375f54 100644 --- a/ref_test.go +++ b/ref_test.go @@ -80,7 +80,7 @@ func TestReference_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { var benchResultAnchor string func BenchmarkReference(b *testing.B) { - params := TestParams{Options: Options{Extensions: CommonExtensions}} + params := TestParams{extensions: CommonExtensions} files := []string{ "Amps and angle encoding", "Auto links",