Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Math support (MathJaX) to blackfriday.v2 #412

Open
wants to merge 2 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
_obj
_test*
markdown
tags
tags
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ implements the following extensions:
and supply a language (to make syntax highlighting simple). Just
mark it like this:

```go
```go
func getTrue() bool {
return true
}
```
```

You can use 3 or more backticks to mark the beginning of the
block, and the same number to mark the end of the block.
Expand All @@ -209,7 +209,7 @@ implements the following extensions:
end of the document. A footnote looks like this:

This is a footnote.[^1]

[^1]: the footnote text.

* **Autolinking**. Blackfriday can find URLs that have not been
Expand Down Expand Up @@ -237,6 +237,24 @@ implements the following extensions:
becomes `<sup>4</sup>&frasl;<sub>5</sub>`, which renders as
<sup>4</sup>&frasl;<sub>5</sub>.

* **MathJaX Support** is an additional feature which is supported by
many markdown editor. It translate inline math equation quoted by `$`
and display math block quoted by `$$` into MathJax compatible format.
hyphen `_` won't break LaTeX render within a math element any more.
```
$$
\left[ \begin{array}{a} a^l_1 \\ ⋮ \\ a^l_{d_l} \end{array}\right]
= \sigma(
\left[ \begin{matrix}
w^l_{1,1} & ⋯ & w^l_{1,d_{l-1}} \\
⋮ & ⋱ & ⋮ \\
w^l_{d_l,1} & ⋯ & w^l_{d_l,d_{l-1}} \\
\end{matrix}\right] ·
\left[ \begin{array}{x} a^{l-1}_1 \\ ⋮ \\ ⋮ \\ a^{l-1}_{d_{l-1}} \end{array}\right] +
\left[ \begin{array}{b} b^l_1 \\ ⋮ \\ b^l_{d_l} \end{array}\right])
$$
```


Other renderers
---------------
Expand Down Expand Up @@ -275,9 +293,9 @@ License
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)


[1]: https://daringfireball.net/projects/markdown/ "Markdown"
[2]: https://golang.org/ "Go Language"
[3]: https://github.com/vmg/sundown "Sundown"
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
[6]: https://labix.org/gopkg.in "gopkg.in"
[1]: https://daringfireball.net/projects/markdown/ "Markdown"
[2]: https://golang.org/ "Go Language"
[3]: https://github.com/vmg/sundown "Sundown"
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
[6]: https://labix.org/gopkg.in "gopkg.in"
51 changes: 41 additions & 10 deletions block.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ func (p *Markdown) block(data []byte) {
}
}

// handle math block
if p.extensions&MathJaxSupport != 0 {
if i := p.blockMath(data); i > 0 {
data = data[i:]
continue
}
}

// anything else must look like a normal paragraph
// note: this finds underlined headings, too
data = data[p.paragraph(data):]
Expand Down Expand Up @@ -239,7 +247,7 @@ func (p *Markdown) prefixHeading(data []byte) int {
}
// extract heading id iff found
if j < end && k < end {
id = string(data[j+2 : k])
id = string(data[j+2: k])
end = j
skip = k + 1
for end > 0 && data[end-1] == ' ' {
Expand Down Expand Up @@ -597,7 +605,7 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
if size < 3 {
return 0, ""
}
marker = string(data[i-size : i])
marker = string(data[i-size: i])

// if this is the end marker, it must match the beginning marker
if oldmarker != "" && marker != oldmarker {
Expand Down Expand Up @@ -651,7 +659,7 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker
}
}

*syntax = string(data[syntaxStart : syntaxStart+syn])
*syntax = string(data[syntaxStart: syntaxStart+syn])
}

i = skipChar(data, i, ' ')
Expand Down Expand Up @@ -1277,7 +1285,7 @@ gatherlines:
}
}

chunk := data[line+indentIndex : i]
chunk := data[line+indentIndex: i]

// evaluate how this line fits in
switch {
Expand All @@ -1301,7 +1309,7 @@ gatherlines:
sublist = raw.Len()
}

// is this a nested prefix heading?
// is this a nested prefix heading?
case p.isPrefixHeading(chunk):
// if the heading is not indented, it is not nested in the list
// and thus ends the list
Expand All @@ -1311,9 +1319,9 @@ gatherlines:
}
*flags |= ListItemContainsBlock

// anything following an empty line is only part
// of this item if it is indented 4 spaces
// (regardless of the indentation of the beginning of the item)
// anything following an empty line is only part
// of this item if it is indented 4 spaces
// (regardless of the indentation of the beginning of the item)
case containsBlankLine && indent < 4:
if *flags&ListTypeDefinition != 0 && i < len(data)-1 {
// is the next item still a part of this list?
Expand All @@ -1332,7 +1340,7 @@ gatherlines:
}
break gatherlines

// a blank line means this should be parsed as a block
// a blank line means this should be parsed as a block
case containsBlankLine:
raw.WriteByte('\n')
*flags |= ListItemContainsBlock
Expand All @@ -1346,7 +1354,7 @@ gatherlines:
}

// add the line into the working buffer without prefix
raw.Write(data[line+indentIndex : i])
raw.Write(data[line+indentIndex: i])

line = i
}
Expand Down Expand Up @@ -1408,6 +1416,29 @@ func (p *Markdown) renderParagraph(data []byte) {
p.addBlock(Paragraph, data[beg:end])
}

// blockMath handle block surround with $$
func (p *Markdown) blockMath(data []byte) int {
if len(data) <= 4 || data[0] != '$' || data[1] != '$' || data[2] == '$' {
return 0
}

// find next $$
var end int
for end = 2; end+1 < len(data) && (data[end] != '$' || data[end+1] != '$'); end++ {
}

// $$ not match
if end+1 == len(data) {
return 0
}

// render the display math
container := p.addChild(MathBlock, 0)
container.Literal = data[2:end]

return end + 2
}

func (p *Markdown) paragraph(data []byte) int {
// prev: index of 1st char of previous line
// line: index of 1st char of current line
Expand Down
10 changes: 10 additions & 0 deletions block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,16 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
doTestsBlock(t, tests, FencedCode|NoEmptyLineBeforeBlock)
}

func TestMathBlock(t *testing.T) {
var tests = []string{
"$y=a+b$$",
"<p><span class=\"math inline\">\\(y=a+b\\)</span>$</p>\n",
"$$y_2=a_3+b_4$$",
"<p><span class=\"math display\">\\[y_2=a_3+b_4\\]</span></p>",
}
doTestsBlock(t, tests, CommonExtensions)
}

func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) {
var tests = []string{
"% Some title\n" +
Expand Down
16 changes: 16 additions & 0 deletions html.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const (
SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
TOC // Generate a table of contents
MathJaxFromCDN // Import MathJax js from CDN
)

var (
Expand Down Expand Up @@ -454,6 +455,10 @@ var (
h5CloseTag = []byte("</h5>")
h6Tag = []byte("<h6")
h6CloseTag = []byte("</h6>")
mathTag = []byte(`<span class="math inline">\(`)
mathCloseTag = []byte(`\)</span>`)
blockMathTag = []byte(`<p><span class="math display">\[`)
blockMathCloseTag = []byte(`\]</span></p>`)

footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
footnotesCloseDivBytes = []byte("\n</div>\n")
Expand Down Expand Up @@ -815,6 +820,14 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
r.out(w, trCloseTag)
r.cr(w)
}
case Math:
r.out(w, mathTag)
escapeHTML(w, node.Literal)
r.out(w, mathCloseTag)
case MathBlock:
r.out(w, blockMathTag)
escapeHTML(w, node.Literal)
r.out(w, blockMathCloseTag)
default:
panic("Unknown node type " + node.Type.String())
}
Expand Down Expand Up @@ -881,6 +894,9 @@ func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
io.WriteString(w, ending)
io.WriteString(w, ">\n")
}
if r.Flags&MathJaxFromCDN != 0 {
io.WriteString(w,`<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML-full""></script>`)
}
io.WriteString(w, "</head>\n")
io.WriteString(w, "<body>\n\n")
}
Expand Down
41 changes: 33 additions & 8 deletions inline.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) {
type linkType int

const (
linkNormal linkType = iota
linkNormal linkType = iota
linkImg
linkDeferredFootnote
linkInlineFootnote
Expand Down Expand Up @@ -227,20 +227,20 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) {
// an exclamation point)
case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^':
t = linkDeferredFootnote
// ![alt] == image
// ![alt] == image
case offset >= 0 && data[offset] == '!':
t = linkImg
offset++
// ^[text] == inline footnote
// [^refId] == deferred footnote
// ^[text] == inline footnote
// [^refId] == deferred footnote
case p.extensions&Footnotes != 0:
if offset >= 0 && data[offset] == '^' {
t = linkInlineFootnote
offset++
} else if len(data)-1 > offset && data[offset+1] == '^' {
t = linkDeferredFootnote
}
// [text] == regular link
// [text] == regular link
default:
t = linkNormal
}
Expand Down Expand Up @@ -385,7 +385,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) {

i++

// reference style link
// reference style link
case isReferenceStyleLink(data, i, t):
var id []byte
altContentConsidered := false
Expand Down Expand Up @@ -438,7 +438,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) {
}
i++

// shortcut reference style link or reference or inline footnote
// shortcut reference style link or reference or inline footnote
default:
var id []byte

Expand Down Expand Up @@ -607,7 +607,7 @@ type autolinkType int

// These are the possible flag values for the autolink renderer.
const (
notAutolink autolinkType = iota
notAutolink autolinkType = iota
normalAutolink
emailAutolink
)
Expand Down Expand Up @@ -1203,6 +1203,31 @@ func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *N
return 0, nil
}

// math handle inline math wrapped with '$'
func math(p *Markdown, data []byte, offset int) (int, *Node) {
data = data[offset:]

// too short, or block math
if len(data) <= 2 || data[1] == '$' {
return 0, nil
}

// find next '$'
var end int
for end = 1; end < len(data) && data[end] != '$'; end++ {
}

// $ not match
if end == len(data) {
return 0, nil
}

// create inline math node
math := NewNode(Math)
math.Literal = data[1:end]
return end + 1, math
}

func text(s []byte) *Node {
node := NewNode(Text)
node.Literal = s
Expand Down
8 changes: 8 additions & 0 deletions inline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,14 @@ func TestSkipHTML(t *testing.T) {
}, TestParams{HTMLFlags: SkipHTML})
}

func TestInlineMath(t *testing.T) {
doTestsParam(t, []string{
"$a_b$",
`<p><span class="math inline">\(a_b\)</span></p>
`,
}, TestParams{HTMLFlags: SkipHTML, extensions: CommonExtensions})
}

func BenchmarkSmartDoubleQuotes(b *testing.B) {
params := TestParams{HTMLFlags: Smartypants}
params.extensions |= Autolink | Strikethrough
Expand Down
Loading