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)
This commit is contained in:
Vytautas Šaltenis 2017-02-02 09:35:10 +02:00
parent d04a53c644
commit e81d1d1138
7 changed files with 212 additions and 196 deletions

View File

@ -34,7 +34,7 @@ var (
// Parse block-level data. // Parse block-level data.
// Note: this function and many that it calls assume that // Note: this function and many that it calls assume that
// the input buffer ends with a newline. // 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 // this is called recursively: enforce a maximum depth
if p.nesting >= p.maxNesting { if p.nesting >= p.maxNesting {
return return
@ -197,14 +197,14 @@ func (p *parser) block(data []byte) {
p.nesting-- p.nesting--
} }
func (p *parser) addBlock(typ NodeType, content []byte) *Node { func (p *Parser) addBlock(typ NodeType, content []byte) *Node {
p.closeUnmatchedBlocks() p.closeUnmatchedBlocks()
container := p.addChild(typ, 0) container := p.addChild(typ, 0)
container.content = content container.content = content
return container return container
} }
func (p *parser) isPrefixHeader(data []byte) bool { func (p *Parser) isPrefixHeader(data []byte) bool {
if data[0] != '#' { if data[0] != '#' {
return false return false
} }
@ -221,7 +221,7 @@ func (p *parser) isPrefixHeader(data []byte) bool {
return true return true
} }
func (p *parser) prefixHeader(data []byte) int { func (p *Parser) prefixHeader(data []byte) int {
level := 0 level := 0
for level < 6 && level < len(data) && data[level] == '#' { for level < 6 && level < len(data) && data[level] == '#' {
level++ level++
@ -267,7 +267,7 @@ func (p *parser) prefixHeader(data []byte) int {
return skip return skip
} }
func (p *parser) isUnderlinedHeader(data []byte) int { func (p *Parser) isUnderlinedHeader(data []byte) int {
// test of level 1 header // test of level 1 header
if data[0] == '=' { if data[0] == '=' {
i := skipChar(data, 1, '=') i := skipChar(data, 1, '=')
@ -291,7 +291,7 @@ func (p *parser) isUnderlinedHeader(data []byte) int {
return 0 return 0
} }
func (p *parser) titleBlock(data []byte, doRender bool) int { func (p *Parser) titleBlock(data []byte, doRender bool) int {
if data[0] != '%' { if data[0] != '%' {
return 0 return 0
} }
@ -315,7 +315,7 @@ func (p *parser) titleBlock(data []byte, doRender bool) int {
return consumed return consumed
} }
func (p *parser) html(data []byte, doRender bool) int { func (p *Parser) html(data []byte, doRender bool) int {
var i, j int var i, j int
// identify the opening tag // identify the opening tag
@ -419,7 +419,7 @@ func finalizeHTMLBlock(block *Node) {
} }
// HTML comment, lax form // 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) i := p.inlineHTMLComment(data)
// needs to end with a blank line // needs to end with a blank line
if j := p.isEmpty(data[i:]); j > 0 { 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 // 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 { if len(data) < 4 {
return 0 return 0
} }
@ -472,7 +472,7 @@ func (p *parser) htmlHr(data []byte, doRender bool) int {
return 0 return 0
} }
func (p *parser) htmlFindTag(data []byte) (string, bool) { func (p *Parser) htmlFindTag(data []byte) (string, bool) {
i := 0 i := 0
for i < len(data) && isalnum(data[i]) { for i < len(data) && isalnum(data[i]) {
i++ i++
@ -484,7 +484,7 @@ func (p *parser) htmlFindTag(data []byte) (string, bool) {
return "", false 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 // assume data[0] == '<' && data[1] == '/' already tested
if tag == "hr" { if tag == "hr" {
return 2 return 2
@ -519,7 +519,7 @@ func (p *parser) htmlFindEnd(tag string, data []byte) int {
return i + skip 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 // it is okay to call isEmpty on an empty buffer
if len(data) == 0 { if len(data) == 0 {
return 0 return 0
@ -537,7 +537,7 @@ func (*parser) isEmpty(data []byte) int {
return i return i
} }
func (*parser) isHRule(data []byte) bool { func (*Parser) isHRule(data []byte) bool {
i := 0 i := 0
// skip up to three spaces // 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, // 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. // 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. // 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 var syntax string
beg, marker := isFenceLine(data, &syntax, "") beg, marker := isFenceLine(data, &syntax, "")
if beg == 0 || beg >= len(data) { if beg == 0 || beg >= len(data) {
@ -739,7 +739,7 @@ func finalizeCodeBlock(block *Node) {
block.content = nil block.content = nil
} }
func (p *parser) table(data []byte) int { func (p *Parser) table(data []byte) int {
table := p.addBlock(Table, nil) table := p.addBlock(Table, nil)
i, columns := p.tableHeader(data) i, columns := p.tableHeader(data)
if i == 0 { if i == 0 {
@ -782,7 +782,7 @@ func isBackslashEscaped(data []byte, i int) bool {
return backslashes&1 == 1 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 i := 0
colCount := 1 colCount := 1
for i = 0; i < len(data) && data[i] != '\n'; i++ { 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 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) p.addBlock(TableRow, nil)
i, col := 0, 0 i, col := 0, 0
@ -939,7 +939,7 @@ func (p *parser) tableRow(data []byte, columns []CellAlignFlags, header bool) {
} }
// returns blockquote prefix length // returns blockquote prefix length
func (p *parser) quotePrefix(data []byte) int { func (p *Parser) quotePrefix(data []byte) int {
i := 0 i := 0
for i < 3 && i < len(data) && data[i] == ' ' { for i < 3 && i < len(data) && data[i] == ' ' {
i++ i++
@ -955,7 +955,7 @@ func (p *parser) quotePrefix(data []byte) int {
// blockquote ends with at least one blank line // blockquote ends with at least one blank line
// followed by something without a blockquote prefix // 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 { if p.isEmpty(data[beg:]) <= 0 {
return false return false
} }
@ -966,7 +966,7 @@ func (p *parser) terminateBlockquote(data []byte, beg, end int) bool {
} }
// parse a blockquote fragment // parse a blockquote fragment
func (p *parser) quote(data []byte) int { func (p *Parser) quote(data []byte) int {
block := p.addBlock(BlockQuote, nil) block := p.addBlock(BlockQuote, nil)
var raw bytes.Buffer var raw bytes.Buffer
beg, end := 0, 0 beg, end := 0, 0
@ -1004,7 +1004,7 @@ func (p *parser) quote(data []byte) int {
} }
// returns prefix length for block code // 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' { if len(data) >= 1 && data[0] == '\t' {
return 1 return 1
} }
@ -1014,7 +1014,7 @@ func (p *parser) codePrefix(data []byte) int {
return 0 return 0
} }
func (p *parser) code(data []byte) int { func (p *Parser) code(data []byte) int {
var work bytes.Buffer var work bytes.Buffer
i := 0 i := 0
@ -1064,7 +1064,7 @@ func (p *parser) code(data []byte) int {
} }
// returns unordered list item prefix // returns unordered list item prefix
func (p *parser) uliPrefix(data []byte) int { func (p *Parser) uliPrefix(data []byte) int {
i := 0 i := 0
// start with up to 3 spaces // start with up to 3 spaces
for i < len(data) && i < 3 && data[i] == ' ' { for i < len(data) && i < 3 && data[i] == ' ' {
@ -1082,7 +1082,7 @@ func (p *parser) uliPrefix(data []byte) int {
} }
// returns ordered list item prefix // returns ordered list item prefix
func (p *parser) oliPrefix(data []byte) int { func (p *Parser) oliPrefix(data []byte) int {
i := 0 i := 0
// start with up to 3 spaces // start with up to 3 spaces
@ -1107,7 +1107,7 @@ func (p *parser) oliPrefix(data []byte) int {
} }
// returns definition list item prefix // returns definition list item prefix
func (p *parser) dliPrefix(data []byte) int { func (p *Parser) dliPrefix(data []byte) int {
if len(data) < 2 { if len(data) < 2 {
return 0 return 0
} }
@ -1123,7 +1123,7 @@ func (p *parser) dliPrefix(data []byte) int {
} }
// parse ordered or unordered list block // 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 i := 0
flags |= ListItemBeginningOfList flags |= ListItemBeginningOfList
block := p.addBlock(List, nil) block := p.addBlock(List, nil)
@ -1191,7 +1191,7 @@ func finalizeList(block *Node) {
// Parse a single list item. // Parse a single list item.
// Assumes initial prefix is already removed if this is a sublist. // 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 // keep track of the indentation of the first line
itemIndent := 0 itemIndent := 0
if data[0] == '\t' { if data[0] == '\t' {
@ -1383,7 +1383,7 @@ gatherlines:
} }
// render a single paragraph that has already been parsed out // 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 { if len(data) == 0 {
return return
} }
@ -1408,7 +1408,7 @@ func (p *parser) renderParagraph(data []byte) {
p.addBlock(Paragraph, data[beg:end]) 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 // prev: index of 1st char of previous line
// line: index of 1st char of current line // line: index of 1st char of current line
// i: index of cursor/end of current line // i: index of cursor/end of current line

View File

@ -253,7 +253,7 @@ func TestPrefixHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
} }
doTestsParam(t, tests, TestParams{ doTestsParam(t, tests, TestParams{
Options: Options{Extensions: HeaderIDs}, extensions: HeaderIDs,
HTMLFlags: UseXHTML, HTMLFlags: UseXHTML,
HTMLRendererParameters: parameters, HTMLRendererParameters: parameters,
}) })
@ -365,7 +365,7 @@ func TestPrefixAutoHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
} }
doTestsParam(t, tests, TestParams{ doTestsParam(t, tests, TestParams{
Options: Options{Extensions: AutoHeaderIDs}, extensions: AutoHeaderIDs,
HTMLFlags: UseXHTML, HTMLFlags: UseXHTML,
HTMLRendererParameters: parameters, HTMLRendererParameters: parameters,
}) })

View File

@ -21,7 +21,8 @@ import (
) )
type TestParams struct { type TestParams struct {
Options extensions Extensions
referenceOverride ReferenceOverrideFunc
HTMLFlags HTMLFlags
HTMLRendererParameters HTMLRendererParameters
} }
@ -45,13 +46,15 @@ func execRecoverableTestSuite(t *testing.T, tests []string, params TestParams, s
func runMarkdown(input string, params TestParams) string { func runMarkdown(input string, params TestParams) string {
params.HTMLRendererParameters.Flags = params.HTMLFlags params.HTMLRendererParameters.Flags = params.HTMLFlags
renderer := NewHTMLRenderer(params.HTMLRendererParameters) 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. // doTests runs full document tests using MarkdownCommon configuration.
func doTests(t *testing.T, tests []string) { func doTests(t *testing.T, tests []string) {
doTestsParam(t, tests, TestParams{ doTestsParam(t, tests, TestParams{
Options: DefaultOptions, extensions: CommonExtensions,
HTMLRendererParameters: HTMLRendererParameters{ HTMLRendererParameters: HTMLRendererParameters{
Flags: CommonHTMLFlags, Flags: CommonHTMLFlags,
}, },
@ -60,8 +63,8 @@ func doTests(t *testing.T, tests []string) {
func doTestsBlock(t *testing.T, tests []string, extensions Extensions) { func doTestsBlock(t *testing.T, tests []string, extensions Extensions) {
doTestsParam(t, tests, TestParams{ doTestsParam(t, tests, TestParams{
Options: Options{Extensions: extensions}, extensions: extensions,
HTMLFlags: UseXHTML, HTMLFlags: UseXHTML,
}) })
} }
@ -124,8 +127,7 @@ func doSafeTestsInline(t *testing.T, tests []string) {
} }
func doTestsInlineParam(t *testing.T, tests []string, params TestParams) { func doTestsInlineParam(t *testing.T, tests []string, params TestParams) {
params.Options.Extensions |= Autolink params.extensions |= Autolink | Strikethrough
params.Options.Extensions |= Strikethrough
params.HTMLFlags |= UseXHTML params.HTMLFlags |= UseXHTML
doTestsParam(t, tests, params) doTestsParam(t, tests, params)
} }
@ -145,7 +147,7 @@ func transformLinks(tests []string, prefix string) []string {
} }
func doTestsReference(t *testing.T, files []string, flag Extensions) { 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) { execRecoverableTestSuite(t, files, params, func(candidate *string) {
for _, basename := range files { for _, basename := range files {
filename := filepath.Join("testdata", basename+".text") filename := filepath.Join("testdata", basename+".text")

View File

@ -32,7 +32,7 @@ var (
// data is the complete block being rendered // data is the complete block being rendered
// offset is the number of valid chars before the current cursor // 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 // handlers might call us recursively: enforce a maximum depth
if p.nesting >= p.maxNesting || len(data) == 0 { if p.nesting >= p.maxNesting || len(data) == 0 {
return return
@ -69,7 +69,7 @@ func (p *parser) inline(currBlock *Node, data []byte) {
} }
// single and double emphasis parsing // 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:] data = data[offset:]
c := data[0] c := data[0]
@ -114,7 +114,7 @@ func emphasis(p *parser, data []byte, offset int) (int, *Node) {
return 0, nil 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:] data = data[offset:]
nb := 0 nb := 0
@ -161,7 +161,7 @@ func codeSpan(p *parser, data []byte, offset int) (int, *Node) {
} }
// newline preceded by two spaces becomes <br> // newline preceded by two spaces becomes <br>
func maybeLineBreak(p *parser, data []byte, offset int) (int, *Node) { func maybeLineBreak(p *Parser, data []byte, offset int) (int, *Node) {
origOffset := offset origOffset := offset
for offset < len(data) && data[offset] == ' ' { for offset < len(data) && data[offset] == ' ' {
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 // 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 { if p.flags&HardLineBreak != 0 {
return 1, NewNode(Hardbreak) 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] != '^' 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] == '[' { if offset < len(data)-1 && data[offset+1] == '[' {
return link(p, data, offset) return link(p, data, offset)
} }
return 0, nil 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] == '[' { if offset < len(data)-1 && data[offset+1] == '[' {
return link(p, data, offset) 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 // '[': 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 // 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] == '^') { if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') {
return 0, nil return 0, nil
@ -573,7 +573,7 @@ func link(p *parser, data []byte, offset int) (int, *Node) {
return i, linkNode return i, linkNode
} }
func (p *parser) inlineHTMLComment(data []byte) int { func (p *Parser) inlineHTMLComment(data []byte) int {
if len(data) < 5 { if len(data) < 5 {
return 0 return 0
} }
@ -613,7 +613,7 @@ const (
) )
// '<' when tags or autolinks are allowed // '<' 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:] data = data[offset:]
altype, end := tagLength(data) altype, end := tagLength(data)
if size := p.inlineHTMLComment(data); size > 0 { if size := p.inlineHTMLComment(data); size > 0 {
@ -646,7 +646,7 @@ func leftAngle(p *parser, data []byte, offset int) (int, *Node) {
// '\\' backslash escape // '\\' backslash escape
var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") 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:] data = data[offset:]
if len(data) > 1 { if len(data) > 1 {
@ -686,7 +686,7 @@ func unescapeText(ob *bytes.Buffer, src []byte) {
// '&' escaped when it doesn't belong to an entity // '&' escaped when it doesn't belong to an entity
// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; // 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:] data = data[offset:]
end := 1 end := 1
@ -748,7 +748,7 @@ var protocolPrefixes = [][]byte{
const shortestPrefix = 6 // len("ftp://"), the shortest of the above 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 // quick check to rule out most false hits
if p.insideLink || len(data) < offset+shortestPrefix { if p.insideLink || len(data) < offset+shortestPrefix {
return 0, nil return 0, nil
@ -765,7 +765,7 @@ func maybeAutoLink(p *parser, data []byte, offset int) (int, *Node) {
return 0, nil 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 // Now a more expensive check to see if we're not inside an anchor element
anchorStart := offset anchorStart := offset
offsetFromAnchor := 0 offsetFromAnchor := 0
@ -1095,7 +1095,7 @@ func helperFindEmphChar(data []byte, c byte) int {
return 0 return 0
} }
func helperEmphasis(p *parser, data []byte, c byte) (int, *Node) { func helperEmphasis(p *Parser, data []byte, c byte) (int, *Node) {
i := 0 i := 0
// skip one symbol if coming from emph3 // skip one symbol if coming from emph3
@ -1135,7 +1135,7 @@ func helperEmphasis(p *parser, data []byte, c byte) (int, *Node) {
return 0, nil 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 i := 0
for i < len(data) { for i < len(data) {
@ -1159,7 +1159,7 @@ func helperDoubleEmphasis(p *parser, data []byte, c byte) (int, *Node) {
return 0, nil 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 i := 0
origData := data origData := data
data = data[offset:] data = data[offset:]

View File

@ -100,38 +100,36 @@ func TestReferenceOverride(t *testing.T) {
"<p>test <a href=\"http://www.ref5.com/\" title=\"Reference 5\">Moo</a></p>\n", "<p>test <a href=\"http://www.ref5.com/\" title=\"Reference 5\">Moo</a></p>\n",
} }
doTestsInlineParam(t, tests, TestParams{ doTestsInlineParam(t, tests, TestParams{
Options: Options{ referenceOverride: func(reference string) (rv *Reference, overridden bool) {
ReferenceOverride: func(reference string) (rv *Reference, overridden bool) { switch reference {
switch reference { case "ref1":
case "ref1": // just an overridden reference exists without definition
// just an overridden reference exists without definition return &Reference{
return &Reference{ Link: "http://www.ref1.com/",
Link: "http://www.ref1.com/", Title: "Reference 1"}, true
Title: "Reference 1"}, true case "ref2":
case "ref2": // overridden exists and reference defined
// overridden exists and reference defined return &Reference{
return &Reference{ Link: "http://www.overridden.com/",
Link: "http://www.overridden.com/", Title: "Reference Overridden"}, true
Title: "Reference Overridden"}, true case "ref3":
case "ref3": // not overridden and reference defined
// 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 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",
"<p>this has an<br />\nextra space</p>\n", "<p>this has an<br />\nextra space</p>\n",
} }
doTestsInlineParam(t, tests, TestParams{Options: Options{ doTestsInlineParam(t, tests, TestParams{
Extensions: BackslashLineBreak}}) extensions: BackslashLineBreak})
} }
func TestInlineLink(t *testing.T) { func TestInlineLink(t *testing.T) {
@ -932,7 +930,7 @@ what happens here
func TestFootnotes(t *testing.T) { func TestFootnotes(t *testing.T) {
doTestsInlineParam(t, footnoteTests, TestParams{ doTestsInlineParam(t, footnoteTests, TestParams{
Options: Options{Extensions: Footnotes}, extensions: Footnotes,
}) })
} }
@ -959,7 +957,7 @@ func TestFootnotesWithParameters(t *testing.T) {
} }
doTestsInlineParam(t, tests, TestParams{ doTestsInlineParam(t, tests, TestParams{
Options: Options{Extensions: Footnotes}, extensions: Footnotes,
HTMLFlags: FootnoteReturnLinks, HTMLFlags: FootnoteReturnLinks,
HTMLRendererParameters: params, HTMLRendererParameters: params,
}) })
@ -989,7 +987,7 @@ func TestNestedFootnotes(t *testing.T) {
</div> </div>
`, `,
} }
doTestsInlineParam(t, tests, TestParams{Options: Options{Extensions: Footnotes}}) doTestsInlineParam(t, tests, TestParams{extensions: Footnotes})
} }
func TestInlineComments(t *testing.T) { func TestInlineComments(t *testing.T) {

View File

@ -57,9 +57,9 @@ const (
// DefaultOptions is a convenience variable with all the options that are // DefaultOptions is a convenience variable with all the options that are
// enabled by default. // enabled by default.
var DefaultOptions = Options{ // var DefaultOptions = Options{
Extensions: CommonExtensions, // Extensions: CommonExtensions,
} // }
// ListType contains bitwise or'ed flags for list and list item objects. // ListType contains bitwise or'ed flags for list and list item objects.
type ListType int type ListType int
@ -160,11 +160,11 @@ type Renderer interface {
// Callback functions for inline parsing. One such function is defined // Callback functions for inline parsing. One such function is defined
// for each character that triggers a response when parsing inline data. // 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. // Parser holds runtime state used by the parser.
// This is constructed by the Markdown function. // This is constructed by the Markdown function.
type parser struct { type Parser struct {
refOverride ReferenceOverrideFunc refOverride ReferenceOverrideFunc
refs map[string]*reference refs map[string]*reference
inlineCallback [256]inlineParser inlineCallback [256]inlineParser
@ -185,7 +185,7 @@ type parser struct {
allClosed bool 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 { if p.refOverride != nil {
r, overridden := p.refOverride(refid) r, overridden := p.refOverride(refid)
if overridden { if overridden {
@ -205,17 +205,17 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) {
return ref, found return ref, found
} }
func (p *parser) finalize(block *Node) { func (p *Parser) finalize(block *Node) {
above := block.Parent above := block.Parent
block.open = false block.open = false
p.tip = above 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) 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) { for !p.tip.canContain(node.Type) {
p.finalize(p.tip) p.finalize(p.tip)
} }
@ -224,7 +224,7 @@ func (p *parser) addExistingChild(node *Node, offset uint32) *Node {
return node return node
} }
func (p *parser) closeUnmatchedBlocks() { func (p *Parser) closeUnmatchedBlocks() {
if !p.allClosed { if !p.allClosed {
for p.oldTip != p.lastMatchedContainer { for p.oldTip != p.lastMatchedContainer {
parent := p.oldTip.Parent parent := p.oldTip.Parent
@ -259,108 +259,46 @@ type Reference struct {
// See the documentation in Options for more details on use-case. // See the documentation in Options for more details on use-case.
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool)
// Options represents configurable overrides and callbacks (in addition to the // Processor contains all the state necessary for Blackfriday to operate.
// extension flag set) for configuring a Markdown parse. type Processor struct {
type Options struct { r Renderer
// Extensions is a flag set of bit-wise ORed extension bits. See the extensions Extensions
// Extensions flags defined in this package. referenceOverride ReferenceOverrideFunc
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
} }
// MarkdownBasic is a convenience function for simple rendering. // DefaultProcessor creates the processor tuned to the most common behavior.
// It processes markdown input with no extensions enabled. func DefaultProcessor() *Processor {
func MarkdownBasic(input []byte) []byte { return &Processor{
// set up the HTML renderer r: NewHTMLRenderer(HTMLRendererParameters{
renderer := NewHTMLRenderer(HTMLRendererParameters{ Flags: CommonHTMLFlags,
Flags: UseXHTML, }),
}) extensions: CommonExtensions,
// 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
} }
return renderer.Render(Parse(input, options))
} }
// Parse is an entry point to the parsing part of Blackfriday. It takes an // NewParser constructs a Parser. You can use the same With* functions as for
// input markdown document and produces a syntax tree for its contents. This // Markdown() to customize parser's behavior.
// tree can then be rendered with a default or custom renderer, or func (proc *Processor) NewParser(opts ...Option) *Parser {
// analyzed/transformed by the caller to whatever non-standard needs they have. for _, opt := range opts {
func Parse(input []byte, opts Options) *Node { opt(proc)
extensions := opts.Extensions }
var p Parser
// fill in the render structure p.flags = proc.extensions
p := new(parser) p.refOverride = proc.referenceOverride
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
docNode := NewNode(Document) docNode := NewNode(Document)
p.doc = docNode p.doc = docNode
p.tip = docNode p.tip = docNode
p.oldTip = docNode p.oldTip = docNode
p.lastMatchedContainer = docNode p.lastMatchedContainer = docNode
p.allClosed = true p.allClosed = true
// register inline parsers // register inline parsers
p.inlineCallback[' '] = maybeLineBreak p.inlineCallback[' '] = maybeLineBreak
p.inlineCallback['*'] = emphasis p.inlineCallback['*'] = emphasis
p.inlineCallback['_'] = emphasis p.inlineCallback['_'] = emphasis
if extensions&Strikethrough != 0 { if proc.extensions&Strikethrough != 0 {
p.inlineCallback['~'] = emphasis p.inlineCallback['~'] = emphasis
} }
p.inlineCallback['`'] = codeSpan p.inlineCallback['`'] = codeSpan
@ -371,8 +309,7 @@ func Parse(input []byte, opts Options) *Node {
p.inlineCallback['&'] = entity p.inlineCallback['&'] = entity
p.inlineCallback['!'] = maybeImage p.inlineCallback['!'] = maybeImage
p.inlineCallback['^'] = maybeInlineFootnote p.inlineCallback['^'] = maybeInlineFootnote
if proc.extensions&Autolink != 0 {
if extensions&Autolink != 0 {
p.inlineCallback['h'] = maybeAutoLink p.inlineCallback['h'] = maybeAutoLink
p.inlineCallback['m'] = maybeAutoLink p.inlineCallback['m'] = maybeAutoLink
p.inlineCallback['f'] = maybeAutoLink p.inlineCallback['f'] = maybeAutoLink
@ -380,11 +317,90 @@ func Parse(input []byte, opts Options) *Node {
p.inlineCallback['M'] = maybeAutoLink p.inlineCallback['M'] = maybeAutoLink
p.inlineCallback['F'] = maybeAutoLink p.inlineCallback['F'] = maybeAutoLink
} }
if proc.extensions&Footnotes != 0 {
if extensions&Footnotes != 0 {
p.notes = make([]*reference, 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) p.block(input)
// Walk the tree and finish up some of unfinished blocks // Walk the tree and finish up some of unfinished blocks
for p.tip != nil { for p.tip != nil {
@ -402,7 +418,7 @@ func Parse(input []byte, opts Options) *Node {
return p.doc return p.doc
} }
func (p *parser) parseRefsToAST() { func (p *Parser) parseRefsToAST() {
if p.flags&Footnotes == 0 || len(p.notes) == 0 { if p.flags&Footnotes == 0 || len(p.notes) == 0 {
return return
} }
@ -527,7 +543,7 @@ func (r *reference) String() string {
// (in the render struct). // (in the render struct).
// Returns the number of bytes to skip to move past it, // Returns the number of bytes to skip to move past it,
// or zero if the first line is not a reference. // 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 // up to 3 optional leading spaces
if len(data) < 4 { if len(data) < 4 {
return 0 return 0
@ -630,7 +646,7 @@ func isReference(p *parser, data []byte, tabSize int) int {
return lineEnd 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 // link: whitespace-free sequence, optionally between angle brackets
if data[i] == '<' { if data[i] == '<' {
i++ i++
@ -701,13 +717,13 @@ func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffse
return 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 // 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). // 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 // 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 // extracted text that was shifted over one tab. It will need to be rendered at
// the end of the document. // 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 { if i == 0 || len(data) == 0 {
return return
} }

View File

@ -80,7 +80,7 @@ func TestReference_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
var benchResultAnchor string var benchResultAnchor string
func BenchmarkReference(b *testing.B) { func BenchmarkReference(b *testing.B) {
params := TestParams{Options: Options{Extensions: CommonExtensions}} params := TestParams{extensions: CommonExtensions}
files := []string{ files := []string{
"Amps and angle encoding", "Amps and angle encoding",
"Auto links", "Auto links",