From 4e5da679f1a83b9cafd86daa94e646142482ef72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E8=88=AA?= Date: Sat, 4 Nov 2017 16:34:49 +0800 Subject: [PATCH 1/2] add mathjax support --- .gitignore | 2 ++ block.go | 51 ++++++++++++++++++++++++++++++++++++++++---------- block_test.go | 10 ++++++++++ html.go | 16 ++++++++++++++++ inline.go | 41 ++++++++++++++++++++++++++++++++-------- inline_test.go | 8 ++++++++ markdown.go | 18 +++++++++++------- node.go | 6 +++++- 8 files changed, 126 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 75623dcc..f3357d78 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ _obj _test* markdown tags + +.idea/ \ No newline at end of file diff --git a/block.go b/block.go index d7da33f2..8a5d0f16 100644 --- a/block.go +++ b/block.go @@ -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 diff --git a/block_test.go b/block_test.go index 0a2a4d84..5c090542 100644 --- a/block_test.go +++ b/block_test.go @@ -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$$", + "

\\(y=a+b\\)$

\n", + "$$y_2=a_3+b_4$$", + "

\\[y_2=a_3+b_4\\]

", + } + doTestsBlock(t, tests, CommonExtensions) +} + func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) { var tests = []string{ "% Some title\n" + diff --git a/html.go b/html.go index 25fb185e..48283d02 100644 --- a/html.go +++ b/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("") h6Tag = []byte("") + mathTag = []byte(`\(`) + mathCloseTag = []byte(`\)`) + blockMathTag = []byte(`

\[`) + blockMathCloseTag = []byte(`\]

`) footnotesDivBytes = []byte("\n
\n\n") footnotesCloseDivBytes = []byte("\n
\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,``) + } io.WriteString(w, "\n") io.WriteString(w, "\n\n") } diff --git a/inline.go b/inline.go index 3d633106..4f671cd9 100644 --- a/inline.go +++ b/inline.go @@ -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,12 +227,12 @@ 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 @@ -240,7 +240,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) { } 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 diff --git a/inline_test.go b/inline_test.go index 966dc22f..7ee084a6 100644 --- a/inline_test.go +++ b/inline_test.go @@ -1165,6 +1165,14 @@ func TestSkipHTML(t *testing.T) { }, TestParams{HTMLFlags: SkipHTML}) } +func TestInlineMath(t *testing.T) { + doTestsParam(t, []string{ + "$a_b$", + `

\(a_b\)

+`, + }, TestParams{HTMLFlags: SkipHTML, extensions: CommonExtensions}) +} + func BenchmarkSmartDoubleQuotes(b *testing.B) { params := TestParams{HTMLFlags: Smartypants} params.extensions |= Autolink | Strikethrough diff --git a/markdown.go b/markdown.go index ff61cb05..f1e987f7 100644 --- a/markdown.go +++ b/markdown.go @@ -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] } diff --git a/node.go b/node.go index 51b9e8c1..e1364acf 100644 --- a/node.go +++ b/node.go @@ -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 { From 71fb3458a0c3c06bd3a1f2847a8afd3c2b68009a Mon Sep 17 00:00:00 2001 From: Vonng Date: Wed, 29 Nov 2017 11:35:02 +0800 Subject: [PATCH 2/2] add mathjax readme --- .gitignore | 4 +--- README.md | 36 +++++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index f3357d78..d59da84e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ _obj _test* markdown -tags - -.idea/ \ No newline at end of file +tags \ No newline at end of file diff --git a/README.md b/README.md index 8ee3aa5a..055f397d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -237,6 +237,24 @@ implements the following extensions: becomes `45`, which renders as 45. +* **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 --------------- @@ -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"