Permalink
Browse files

add mathjax support

  • Loading branch information...
墨航
墨航 committed Nov 4, 2017
1 parent 187c33f commit 4e5da679f1a83b9cafd86daa94e646142482ef72
Showing with 126 additions and 26 deletions.
  1. +2 −0 .gitignore
  2. +41 −10 block.go
  3. +10 −0 block_test.go
  4. +16 −0 html.go
  5. +33 −8 inline.go
  6. +8 −0 inline_test.go
  7. +11 −7 markdown.go
  8. +5 −1 node.go
@@ -6,3 +6,5 @@ _obj
_test*
markdown
tags

.idea/
@@ -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):]
@@ -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] == ' ' {
@@ -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 {
@@ -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, ' ')
@@ -1277,7 +1285,7 @@ gatherlines:
}
}

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

// evaluate how this line fits in
switch {
@@ -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
@@ -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?
@@ -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
@@ -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
}
@@ -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
@@ -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" +
16 html.go
@@ -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 (
@@ -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")
@@ -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())
}
@@ -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")
}
@@ -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
@@ -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
}
@@ -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
@@ -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

@@ -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
)
@@ -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
@@ -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
@@ -41,19 +41,20 @@ const (
HardLineBreak // Translate newlines into line breaks
TabSizeEight // Expand tabs to eight spaces instead of four
Footnotes // Pandoc-style footnotes
NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, math, ordered list, unordered list) block
HeadingIDs // specify heading IDs with {#id}
Titleblock // Titleblock ala pandoc
AutoHeadingIDs // Create the heading ID from the text
BackslashLineBreak // Translate trailing backslashes into line breaks
DefinitionLists // Render definition lists
MathJaxSupport // Render with MathJax compatible

CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants |
SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes

CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode |
Autolink | Strikethrough | SpaceHeadings | HeadingIDs |
BackslashLineBreak | DefinitionLists
BackslashLineBreak | DefinitionLists | NoEmptyLineBeforeBlock | MathJaxSupport
)

// ListType contains bitwise or'ed flags for list and list item objects.
@@ -63,12 +64,12 @@ type ListType int
// Multiple flag values may be ORed together.
// These are mostly of interest if you are writing a new output format.
const (
ListTypeOrdered ListType = 1 << iota
ListTypeOrdered ListType = 1 << iota
ListTypeDefinition
ListTypeTerm

ListItemContainsBlock
ListItemBeginningOfList // TODO: figure out if this is of any use now
ListItemBeginningOfList // TODO: figure out if this is of any use now
ListItemEndOfList
)

@@ -79,7 +80,7 @@ type CellAlignFlags int
// Only a single one of these values will be used; they are not ORed together.
// These are mostly of interest if you are writing a new output format.
const (
TableAlignmentLeft CellAlignFlags = 1 << iota
TableAlignmentLeft CellAlignFlags = 1 << iota
TableAlignmentRight
TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight)
)
@@ -305,6 +306,9 @@ func New(opts ...Option) *Markdown {
p.inlineCallback['M'] = maybeAutoLink
p.inlineCallback['F'] = maybeAutoLink
}
if p.extensions&MathJaxSupport != 0 {
p.inlineCallback['$'] = math
}
if p.extensions&Footnotes != 0 {
p.notes = make([]*reference, 0)
}
@@ -779,7 +783,7 @@ gatherLines:
}

// get rid of that first tab, write to buffer
raw.Write(data[blockEnd+n : i])
raw.Write(data[blockEnd+n: i])
hasBlock = true

blockEnd = i
@@ -936,5 +940,5 @@ func slugify(in []byte) []byte {
break
}
}
return out[a : b+1]
return out[a: b+1]
}
@@ -12,7 +12,7 @@ type NodeType int

// Constants for identifying different types of nodes. See NodeType.
const (
Document NodeType = iota
Document NodeType = iota
BlockQuote
List
Item
@@ -36,6 +36,8 @@ const (
TableHead
TableBody
TableRow
Math
MathBlock
)

var nodeTypeNames = []string{
@@ -63,6 +65,8 @@ var nodeTypeNames = []string{
TableHead: "TableHead",
TableBody: "TableBody",
TableRow: "TableRow",
Math: "Math",
MathBlock: "MathBlock",
}

func (t NodeType) String() string {

0 comments on commit 4e5da67

Please sign in to comment.