Skip to content

Commit

Permalink
Accept info strings in code fences (russross#448)
Browse files Browse the repository at this point in the history
* 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 russross#410 (in v1).

* Don't output whole info string as code classes

This follows the common mark specification.

* run go fmt
  • Loading branch information
garfieldnate authored and willdollman committed Feb 27, 2021
1 parent 793b86e commit 92a95a8
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 60 deletions.
39 changes: 20 additions & 19 deletions block.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package blackfriday

import (
"bytes"
"strings"
"unicode"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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++
}

Expand All @@ -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, ' ')
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
54 changes: 38 additions & 16 deletions block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,9 @@ func TestFencedCodeBlock(t *testing.T) {
"``` 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",

"``` 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",
"<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\n",

Expand Down Expand Up @@ -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",
"<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",
"<pre><code class=\"language-c\">/* special &amp; char &lt; &gt; &quot; escaping */\n</code></pre>\n",

Expand Down Expand Up @@ -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("```"),
Expand All @@ -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"),
Expand All @@ -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)
}
}
}
Expand Down
28 changes: 8 additions & 20 deletions html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("<pre><code class=\"language-")
} else {
out.WriteByte(' ')
}
attrEscape(out, []byte(elt))
count++
endOfLang := strings.IndexAny(info, "\t ")
if endOfLang < 0 {
endOfLang = len(info)
}

if count == 0 {
lang := info[:endOfLang]
if len(lang) == 0 || lang == "." {
out.WriteString("<pre><code>")
} else {
out.WriteString("<pre><code class=\"language-")
attrEscape(out, []byte(lang))
out.WriteString("\">")
}

attrEscape(out, text)
out.WriteString("</code></pre>\n")
}
Expand Down
8 changes: 5 additions & 3 deletions latex.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package blackfriday

import (
"bytes"
"strings"
)

// Latex is a type that implements the Renderer interface for LaTeX output.
Expand All @@ -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")
Expand Down
14 changes: 12 additions & 2 deletions markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 92a95a8

Please sign in to comment.