From 11635eb403ff09dbc3a6b5a007ab5ab09151c229 Mon Sep 17 00:00:00 2001 From: Nathan Glenn Date: Sat, 28 Apr 2018 12:25:19 +0200 Subject: [PATCH] Accept info strings in code fences (#448) * Accept info strings in code fences According to the common mark standard, code fence info strings can be anything, not just single words. Update the tests and parser accordingly. The formatter already expected an info string with a language and HTML classes, so this does not need to change. Update the LaTeX formatter to take the first word of the info string as the language. Fixes #410 (in v1). * Don't output whole info string as code classes This follows the common mark specification. * run go fmt --- block.go | 39 +++++++++++++++++++------------------ block_test.go | 54 ++++++++++++++++++++++++++++++++++++--------------- html.go | 28 ++++++++------------------ latex.go | 8 +++++--- markdown.go | 14 +++++++++++-- 5 files changed, 83 insertions(+), 60 deletions(-) diff --git a/block.go b/block.go index 7fc731d..929638a 100644 --- a/block.go +++ b/block.go @@ -15,6 +15,7 @@ package blackfriday import ( "bytes" + "strings" "unicode" ) @@ -92,7 +93,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { // fenced code block: // - // ``` go + // ``` go info string here // func fact(n int) int { // if n <= 1 { // return n @@ -562,7 +563,7 @@ func (*parser) isHRule(data []byte) bool { // and returns the end index if so, or 0 otherwise. It also returns the marker found. // If syntax is not nil, it gets set to the syntax specified in the fence line. // A final newline is mandatory to recognize the fence line, unless newlineOptional is true. -func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) { +func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) { i, size := 0, 0 // skip up to three spaces @@ -598,9 +599,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional } // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { @@ -610,14 +611,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional return 0, "" } - syntaxStart := i + infoStart := i if data[i] == '{' { i++ - syntaxStart++ + infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ + infoLength++ i++ } @@ -627,24 +628,24 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional // strip all whitespace at the beginning and the end // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- } - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- } i++ } else { - for i < len(data) && !isspace(data[i]) { - syn++ + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ i++ } } - *syntax = string(data[syntaxStart : syntaxStart+syn]) + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } i = skipChar(data, i, ' ') @@ -662,8 +663,8 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional // 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(out *bytes.Buffer, data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "", false) + var infoString string + beg, marker := isFenceLine(data, &infoString, "", false) if beg == 0 || beg >= len(data) { return 0 } @@ -697,7 +698,7 @@ func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) } if doRender { - p.r.BlockCode(out, work.Bytes(), syntax) + p.r.BlockCode(out, work.Bytes(), infoString) } return beg diff --git a/block_test.go b/block_test.go index 89d5775..b9d6653 100644 --- a/block_test.go +++ b/block_test.go @@ -1053,6 +1053,9 @@ func TestFencedCodeBlock(t *testing.T) { "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n", "
func foo() bool {\n\treturn true;\n}\n
\n", + "``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n", + "
func foo() bool {\n\treturn true;\n}\n
\n", + "``` c\n/* special & char < > \" escaping */\n```\n", "
/* special & char < > " escaping */\n
\n", @@ -1511,6 +1514,9 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n", "
func foo() bool {\n\treturn true;\n}\n
\n", + "``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n", + "
func foo() bool {\n\treturn true;\n}\n
\n", + "``` c\n/* special & char < > \" escaping */\n```\n", "
/* special & char < > " escaping */\n
\n", @@ -1646,11 +1652,11 @@ func TestCDATA(t *testing.T) { func TestIsFenceLine(t *testing.T) { tests := []struct { data []byte - syntaxRequested bool + infoRequested bool newlineOptional bool wantEnd int wantMarker string - wantSyntax string + wantInfo string }{ { data: []byte("```"), @@ -1662,10 +1668,10 @@ func TestIsFenceLine(t *testing.T) { wantMarker: "```", }, { - data: []byte("```\nstuff here\n"), - syntaxRequested: true, - wantEnd: 4, - wantMarker: "```", + data: []byte("```\nstuff here\n"), + infoRequested: true, + wantEnd: 4, + wantMarker: "```", }, { data: []byte("stuff here\n```\n"), @@ -1679,36 +1685,52 @@ func TestIsFenceLine(t *testing.T) { }, { data: []byte("```"), - syntaxRequested: true, + infoRequested: true, newlineOptional: true, wantEnd: 3, wantMarker: "```", }, { data: []byte("``` go"), - syntaxRequested: true, + infoRequested: true, newlineOptional: true, wantEnd: 6, wantMarker: "```", - wantSyntax: "go", + wantInfo: "go", + }, + { + data: []byte("``` go foo bar"), + infoRequested: true, + newlineOptional: true, + wantEnd: 14, + wantMarker: "```", + wantInfo: "go foo bar", + }, + { + data: []byte("``` go foo bar "), + infoRequested: true, + newlineOptional: true, + wantEnd: 16, + wantMarker: "```", + wantInfo: "go foo bar", }, } for _, test := range tests { - var syntax *string - if test.syntaxRequested { - syntax = new(string) + var info *string + if test.infoRequested { + info = new(string) } - end, marker := isFenceLine(test.data, syntax, "```", test.newlineOptional) + end, marker := isFenceLine(test.data, info, "```", test.newlineOptional) if got, want := end, test.wantEnd; got != want { t.Errorf("got end %v, want %v", got, want) } if got, want := marker, test.wantMarker; got != want { t.Errorf("got marker %q, want %q", got, want) } - if test.syntaxRequested { - if got, want := *syntax, test.wantSyntax; got != want { - t.Errorf("got syntax %q, want %q", got, want) + if test.infoRequested { + if got, want := *info, test.wantInfo; got != want { + t.Errorf("got info %q, want %q", got, want) } } } diff --git a/html.go b/html.go index c917c7d..e0a6c69 100644 --- a/html.go +++ b/html.go @@ -255,33 +255,21 @@ func (options *Html) HRule(out *bytes.Buffer) { out.WriteByte('\n') } -func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) { +func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) { doubleSpace(out) - // parse out the language names/classes - count := 0 - for _, elt := range strings.Fields(lang) { - if elt[0] == '.' { - elt = elt[1:] - } - if len(elt) == 0 { - continue - } - if count == 0 { - out.WriteString("
")
 	} else {
+		out.WriteString("
")
 	}
-
 	attrEscape(out, text)
 	out.WriteString("
\n") } diff --git a/latex.go b/latex.go index 70705aa..3d30d09 100644 --- a/latex.go +++ b/latex.go @@ -17,6 +17,7 @@ package blackfriday import ( "bytes" + "strings" ) // Latex is a type that implements the Renderer interface for LaTeX output. @@ -39,16 +40,17 @@ func (options *Latex) GetFlags() int { } // render code chunks using verbatim, or listings if we have a language -func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) { - if lang == "" { +func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) { + if info == "" { out.WriteString("\n\\begin{verbatim}\n") } else { + lang := strings.Fields(info)[0] out.WriteString("\n\\begin{lstlisting}[language=") out.WriteString(lang) out.WriteString("]\n") } out.Write(text) - if lang == "" { + if info == "" { out.WriteString("\n\\end{verbatim}\n") } else { out.WriteString("\n\\end{lstlisting}\n") diff --git a/markdown.go b/markdown.go index 1722a73..41595d6 100644 --- a/markdown.go +++ b/markdown.go @@ -159,7 +159,7 @@ var blockTags = map[string]struct{}{ // Currently Html and Latex implementations are provided type Renderer interface { // block-level callbacks - BlockCode(out *bytes.Buffer, text []byte, lang string) + BlockCode(out *bytes.Buffer, text []byte, infoString string) BlockQuote(out *bytes.Buffer, text []byte) BlockHtml(out *bytes.Buffer, text []byte) Header(out *bytes.Buffer, text func() bool, level int, id string) @@ -804,7 +804,17 @@ func ispunct(c byte) bool { // Test if a character is a whitespace character. func isspace(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical whitespace character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' } // Test if a character is letter.