Skip to content

Commit

Permalink
add mathjax support
Browse files Browse the repository at this point in the history
  • Loading branch information
墨航 committed Nov 4, 2017
1 parent 187c33f commit 4e5da67
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -6,3 +6,5 @@ _obj
_test* _test*
markdown markdown
tags tags

.idea/
51 changes: 41 additions & 10 deletions block.go
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 // anything else must look like a normal paragraph
// note: this finds underlined headings, too // note: this finds underlined headings, too
data = data[p.paragraph(data):] 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 // extract heading id iff found
if j < end && k < end { if j < end && k < end {
id = string(data[j+2 : k]) id = string(data[j+2: k])
end = j end = j
skip = k + 1 skip = k + 1
for end > 0 && data[end-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 { if size < 3 {
return 0, "" 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 this is the end marker, it must match the beginning marker
if oldmarker != "" && marker != oldmarker { 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, ' ') 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 // evaluate how this line fits in
switch { switch {
Expand All @@ -1301,7 +1309,7 @@ gatherlines:
sublist = raw.Len() sublist = raw.Len()
} }


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


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


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


line = i line = i
} }
Expand Down Expand Up @@ -1408,6 +1416,29 @@ func (p *Markdown) renderParagraph(data []byte) {
p.addBlock(Paragraph, data[beg:end]) 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 { func (p *Markdown) paragraph(data []byte) int {
// prev: index of 1st char of previous line // prev: index of 1st char of previous line
// line: index of 1st char of current line // line: index of 1st char of current line
Expand Down
10 changes: 10 additions & 0 deletions block_test.go
Expand Up @@ -1458,6 +1458,16 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
doTestsBlock(t, tests, FencedCode|NoEmptyLineBeforeBlock) 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) { func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) {
var tests = []string{ var tests = []string{
"% Some title\n" + "% Some title\n" +
Expand Down
16 changes: 16 additions & 0 deletions html.go
Expand Up @@ -46,6 +46,7 @@ const (
SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
TOC // Generate a table of contents TOC // Generate a table of contents
MathJaxFromCDN // Import MathJax js from CDN
) )


var ( var (
Expand Down Expand Up @@ -454,6 +455,10 @@ var (
h5CloseTag = []byte("</h5>") h5CloseTag = []byte("</h5>")
h6Tag = []byte("<h6") h6Tag = []byte("<h6")
h6CloseTag = []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") footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
footnotesCloseDivBytes = []byte("\n</div>\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.out(w, trCloseTag)
r.cr(w) 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: default:
panic("Unknown node type " + node.Type.String()) 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, ending)
io.WriteString(w, ">\n") 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, "</head>\n")
io.WriteString(w, "<body>\n\n") io.WriteString(w, "<body>\n\n")
} }
Expand Down
41 changes: 33 additions & 8 deletions inline.go
Expand Up @@ -187,7 +187,7 @@ func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) {
type linkType int type linkType int


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


i++ i++


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


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


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


// These are the possible flag values for the autolink renderer. // These are the possible flag values for the autolink renderer.
const ( const (
notAutolink autolinkType = iota notAutolink autolinkType = iota
normalAutolink normalAutolink
emailAutolink emailAutolink
) )
Expand Down Expand Up @@ -1203,6 +1203,31 @@ func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *N
return 0, nil 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 { func text(s []byte) *Node {
node := NewNode(Text) node := NewNode(Text)
node.Literal = s node.Literal = s
Expand Down
8 changes: 8 additions & 0 deletions inline_test.go
Expand Up @@ -1165,6 +1165,14 @@ func TestSkipHTML(t *testing.T) {
}, TestParams{HTMLFlags: SkipHTML}) }, 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) { func BenchmarkSmartDoubleQuotes(b *testing.B) {
params := TestParams{HTMLFlags: Smartypants} params := TestParams{HTMLFlags: Smartypants}
params.extensions |= Autolink | Strikethrough params.extensions |= Autolink | Strikethrough
Expand Down
18 changes: 11 additions & 7 deletions markdown.go
Expand Up @@ -41,19 +41,20 @@ const (
HardLineBreak // Translate newlines into line breaks HardLineBreak // Translate newlines into line breaks
TabSizeEight // Expand tabs to eight spaces instead of four TabSizeEight // Expand tabs to eight spaces instead of four
Footnotes // Pandoc-style footnotes 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} HeadingIDs // specify heading IDs with {#id}
Titleblock // Titleblock ala pandoc Titleblock // Titleblock ala pandoc
AutoHeadingIDs // Create the heading ID from the text AutoHeadingIDs // Create the heading ID from the text
BackslashLineBreak // Translate trailing backslashes into line breaks BackslashLineBreak // Translate trailing backslashes into line breaks
DefinitionLists // Render definition lists DefinitionLists // Render definition lists
MathJaxSupport // Render with MathJax compatible


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


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


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


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


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


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


blockEnd = i blockEnd = i
Expand Down Expand Up @@ -936,5 +940,5 @@ func slugify(in []byte) []byte {
break break
} }
} }
return out[a : b+1] return out[a: b+1]
} }
6 changes: 5 additions & 1 deletion node.go
Expand Up @@ -12,7 +12,7 @@ type NodeType int


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


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


func (t NodeType) String() string { func (t NodeType) String() string {
Expand Down

0 comments on commit 4e5da67

Please sign in to comment.