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
pull/465/head
Nathan Glenn 2018-04-28 12:25:19 +02:00 committed by Vytautas Šaltenis
parent 16ac584625
commit 11635eb403
5 changed files with 83 additions and 60 deletions

View File

@ -15,6 +15,7 @@ package blackfriday
import ( import (
"bytes" "bytes"
"strings"
"unicode" "unicode"
) )
@ -92,7 +93,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) {
// fenced code block: // fenced code block:
// //
// ``` go // ``` go info string here
// func fact(n int) int { // func fact(n int) int {
// if n <= 1 { // if n <= 1 {
// return n // 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. // 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. // 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. // 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 i, size := 0, 0
// skip up to three spaces // 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 // 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. // into one, always get the info string, and discard it if the caller doesn't care.
if syntax != nil { if info != nil {
syn := 0 infoLength := 0
i = skipChar(data, i, ' ') i = skipChar(data, i, ' ')
if i >= len(data) { if i >= len(data) {
@ -610,14 +611,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
return 0, "" return 0, ""
} }
syntaxStart := i infoStart := i
if data[i] == '{' { if data[i] == '{' {
i++ i++
syntaxStart++ infoStart++
for i < len(data) && data[i] != '}' && data[i] != '\n' { for i < len(data) && data[i] != '}' && data[i] != '\n' {
syn++ infoLength++
i++ i++
} }
@ -627,24 +628,24 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
// strip all whitespace at the beginning and the end // strip all whitespace at the beginning and the end
// of the {} block // of the {} block
for syn > 0 && isspace(data[syntaxStart]) { for infoLength > 0 && isspace(data[infoStart]) {
syntaxStart++ infoStart++
syn-- infoLength--
} }
for syn > 0 && isspace(data[syntaxStart+syn-1]) { for infoLength > 0 && isspace(data[infoStart+infoLength-1]) {
syn-- infoLength--
} }
i++ i++
} else { } else {
for i < len(data) && !isspace(data[i]) { for i < len(data) && !isverticalspace(data[i]) {
syn++ infoLength++
i++ i++
} }
} }
*syntax = string(data[syntaxStart : syntaxStart+syn]) *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength]))
} }
i = skipChar(data, i, ' ') 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. // 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(out *bytes.Buffer, data []byte, doRender bool) int { func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int {
var syntax string var infoString string
beg, marker := isFenceLine(data, &syntax, "", false) beg, marker := isFenceLine(data, &infoString, "", false)
if beg == 0 || beg >= len(data) { if beg == 0 || beg >= len(data) {
return 0 return 0
} }
@ -697,7 +698,7 @@ func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool)
} }
if doRender { if doRender {
p.r.BlockCode(out, work.Bytes(), syntax) p.r.BlockCode(out, work.Bytes(), infoString)
} }
return beg return beg

View File

@ -1053,6 +1053,9 @@ func TestFencedCodeBlock(t *testing.T) {
"``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n", "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n", "<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
"``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n",
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
"``` c\n/* special & char < > \" escaping */\n```\n", "``` c\n/* special & char < > \" escaping */\n```\n",
"<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\n", "<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\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", "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n", "<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
"``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n",
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
"``` c\n/* special & char < > \" escaping */\n```\n", "``` c\n/* special & char < > \" escaping */\n```\n",
"<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\n", "<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\n",
@ -1646,11 +1652,11 @@ func TestCDATA(t *testing.T) {
func TestIsFenceLine(t *testing.T) { func TestIsFenceLine(t *testing.T) {
tests := []struct { tests := []struct {
data []byte data []byte
syntaxRequested bool infoRequested bool
newlineOptional bool newlineOptional bool
wantEnd int wantEnd int
wantMarker string wantMarker string
wantSyntax string wantInfo string
}{ }{
{ {
data: []byte("```"), data: []byte("```"),
@ -1662,10 +1668,10 @@ func TestIsFenceLine(t *testing.T) {
wantMarker: "```", wantMarker: "```",
}, },
{ {
data: []byte("```\nstuff here\n"), data: []byte("```\nstuff here\n"),
syntaxRequested: true, infoRequested: true,
wantEnd: 4, wantEnd: 4,
wantMarker: "```", wantMarker: "```",
}, },
{ {
data: []byte("stuff here\n```\n"), data: []byte("stuff here\n```\n"),
@ -1679,36 +1685,52 @@ func TestIsFenceLine(t *testing.T) {
}, },
{ {
data: []byte("```"), data: []byte("```"),
syntaxRequested: true, infoRequested: true,
newlineOptional: true, newlineOptional: true,
wantEnd: 3, wantEnd: 3,
wantMarker: "```", wantMarker: "```",
}, },
{ {
data: []byte("``` go"), data: []byte("``` go"),
syntaxRequested: true, infoRequested: true,
newlineOptional: true, newlineOptional: true,
wantEnd: 6, wantEnd: 6,
wantMarker: "```", 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 { for _, test := range tests {
var syntax *string var info *string
if test.syntaxRequested { if test.infoRequested {
syntax = new(string) 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 { if got, want := end, test.wantEnd; got != want {
t.Errorf("got end %v, want %v", got, want) t.Errorf("got end %v, want %v", got, want)
} }
if got, want := marker, test.wantMarker; got != want { if got, want := marker, test.wantMarker; got != want {
t.Errorf("got marker %q, want %q", got, want) t.Errorf("got marker %q, want %q", got, want)
} }
if test.syntaxRequested { if test.infoRequested {
if got, want := *syntax, test.wantSyntax; got != want { if got, want := *info, test.wantInfo; got != want {
t.Errorf("got syntax %q, want %q", got, want) t.Errorf("got info %q, want %q", got, want)
} }
} }
} }

28
html.go
View File

@ -255,33 +255,21 @@ func (options *Html) HRule(out *bytes.Buffer) {
out.WriteByte('\n') 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) doubleSpace(out)
// parse out the language names/classes endOfLang := strings.IndexAny(info, "\t ")
count := 0 if endOfLang < 0 {
for _, elt := range strings.Fields(lang) { endOfLang = len(info)
if elt[0] == '.' {
elt = elt[1:]
}
if len(elt) == 0 {
continue
}
if count == 0 {
out.WriteString("<pre><code class=\"language-")
} else {
out.WriteByte(' ')
}
attrEscape(out, []byte(elt))
count++
} }
lang := info[:endOfLang]
if count == 0 { if len(lang) == 0 || lang == "." {
out.WriteString("<pre><code>") out.WriteString("<pre><code>")
} else { } else {
out.WriteString("<pre><code class=\"language-")
attrEscape(out, []byte(lang))
out.WriteString("\">") out.WriteString("\">")
} }
attrEscape(out, text) attrEscape(out, text)
out.WriteString("</code></pre>\n") out.WriteString("</code></pre>\n")
} }

View File

@ -17,6 +17,7 @@ package blackfriday
import ( import (
"bytes" "bytes"
"strings"
) )
// Latex is a type that implements the Renderer interface for LaTeX output. // 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 // render code chunks using verbatim, or listings if we have a language
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) { func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) {
if lang == "" { if info == "" {
out.WriteString("\n\\begin{verbatim}\n") out.WriteString("\n\\begin{verbatim}\n")
} else { } else {
lang := strings.Fields(info)[0]
out.WriteString("\n\\begin{lstlisting}[language=") out.WriteString("\n\\begin{lstlisting}[language=")
out.WriteString(lang) out.WriteString(lang)
out.WriteString("]\n") out.WriteString("]\n")
} }
out.Write(text) out.Write(text)
if lang == "" { if info == "" {
out.WriteString("\n\\end{verbatim}\n") out.WriteString("\n\\end{verbatim}\n")
} else { } else {
out.WriteString("\n\\end{lstlisting}\n") out.WriteString("\n\\end{lstlisting}\n")

View File

@ -159,7 +159,7 @@ var blockTags = map[string]struct{}{
// Currently Html and Latex implementations are provided // Currently Html and Latex implementations are provided
type Renderer interface { type Renderer interface {
// block-level callbacks // 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) BlockQuote(out *bytes.Buffer, text []byte)
BlockHtml(out *bytes.Buffer, text []byte) BlockHtml(out *bytes.Buffer, text []byte)
Header(out *bytes.Buffer, text func() bool, level int, id string) 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. // Test if a character is a whitespace character.
func isspace(c byte) bool { 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. // Test if a character is letter.