├── go.mod ├── block_node.go ├── tex.go ├── block_inline.go ├── inline_renderer.go ├── go.sum ├── block_renderer.go ├── block_tex_renderer.go ├── inline_tex_renderer.go ├── mathjax_test.go ├── mathjax.go ├── README.md ├── block.go ├── inline.go └── tex_renderer.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/litao91/goldmark-mathjax 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/stretchr/testify v1.6.1 7 | github.com/yuin/goldmark v1.2.1 8 | ) 9 | -------------------------------------------------------------------------------- /block_node.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import "github.com/yuin/goldmark/ast" 4 | 5 | type MathBlock struct { 6 | ast.BaseBlock 7 | } 8 | 9 | var KindMathBlock = ast.NewNodeKind("MathBLock") 10 | 11 | func NewMathBlock() *MathBlock { 12 | return &MathBlock{} 13 | } 14 | 15 | func (n *MathBlock) Dump(source []byte, level int) { 16 | m:= map[string]string{} 17 | ast.DumpHelper(n, source, level, m, nil) 18 | } 19 | 20 | func (n *MathBlock) Kind() ast.NodeKind { 21 | return KindMathBlock 22 | } 23 | 24 | func (n *MathBlock) IsRaw() bool { 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /tex.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "github.com/yuin/goldmark" 5 | "github.com/yuin/goldmark/parser" 6 | "github.com/yuin/goldmark/renderer" 7 | "github.com/yuin/goldmark/util" 8 | ) 9 | 10 | type tex struct { 11 | texRenderer *TexRenderer 12 | } 13 | 14 | var Tex = &tex{ 15 | texRenderer: NewDefaultTexRenderer(), 16 | } 17 | 18 | func (e *tex) Extend(m goldmark.Markdown) { 19 | m.Parser().AddOptions(parser.WithBlockParsers( 20 | util.Prioritized(NewMathJaxBlockParser(), 701), 21 | )) 22 | m.Parser().AddOptions(parser.WithInlineParsers( 23 | util.Prioritized(NewInlineMathParser(), 501), 24 | )) 25 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 26 | util.Prioritized(&MathTexBlockRenderer{e.texRenderer}, 501), 27 | util.Prioritized(&InlineTexMathRenderer{ 28 | texRenderer: e.texRenderer, 29 | }, 502), 30 | )) 31 | } 32 | -------------------------------------------------------------------------------- /block_inline.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "github.com/yuin/goldmark/ast" 5 | "github.com/yuin/goldmark/util" 6 | ) 7 | 8 | type InlineMath struct { 9 | ast.BaseInline 10 | } 11 | 12 | func (n *InlineMath) Inline() {} 13 | 14 | func (n *InlineMath) IsBlank(source []byte) bool { 15 | for c := n.FirstChild(); c != nil; c = c.NextSibling() { 16 | text := c.(*ast.Text).Segment 17 | if !util.IsBlank(text.Value(source)) { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | 24 | func (n *InlineMath) Dump(source []byte, level int) { 25 | ast.DumpHelper(n, source, level, nil, nil) 26 | } 27 | 28 | var KindInlineMath = ast.NewNodeKind("InlineMath") 29 | 30 | func (n *InlineMath) Kind() ast.NodeKind { 31 | return KindInlineMath 32 | } 33 | 34 | func NewInlineMath() *InlineMath { 35 | return &InlineMath{ 36 | BaseInline: ast.BaseInline{}, 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /inline_renderer.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/yuin/goldmark/ast" 7 | "github.com/yuin/goldmark/renderer" 8 | "github.com/yuin/goldmark/util" 9 | ) 10 | 11 | type InlineMathRenderer struct { 12 | startDelim string 13 | endDelim string 14 | } 15 | 16 | func (r *InlineMathRenderer) renderInlineMath(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 17 | if entering { 18 | _, _ = w.WriteString(`` + r.startDelim) 19 | for c := n.FirstChild(); c != nil; c = c.NextSibling() { 20 | segment := c.(*ast.Text).Segment 21 | value := segment.Value(source) 22 | if bytes.HasSuffix(value, []byte("\n")) { 23 | w.Write(value[:len(value)-1]) 24 | if c != n.LastChild() { 25 | w.Write([]byte(" ")) 26 | } 27 | } else { 28 | w.Write(value) 29 | } 30 | } 31 | return ast.WalkSkipChildren, nil 32 | } 33 | _, _ = w.WriteString(r.endDelim + ``) 34 | return ast.WalkContinue, nil 35 | } 36 | 37 | func (r *InlineMathRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 38 | reg.Register(KindInlineMath, r.renderInlineMath) 39 | } 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 8 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= 10 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /block_renderer.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | gast "github.com/yuin/goldmark/ast" 5 | "github.com/yuin/goldmark/renderer" 6 | "github.com/yuin/goldmark/util" 7 | ) 8 | 9 | type MathBlockRenderer struct { 10 | startDelim string 11 | endDelim string 12 | } 13 | 14 | func NewMathBlockRenderer(start, end string) renderer.NodeRenderer { 15 | return &MathBlockRenderer{start, end} 16 | } 17 | 18 | func (r *MathBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 19 | reg.Register(KindMathBlock, r.renderMathBlock) 20 | } 21 | 22 | func (r *MathBlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { 23 | l := n.Lines().Len() 24 | for i := 0; i < l; i++ { 25 | line := n.Lines().At(i) 26 | w.Write(line.Value(source)) 27 | } 28 | } 29 | 30 | func (r *MathBlockRenderer) renderMathBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 31 | n := node.(*MathBlock) 32 | if entering { 33 | _, _ = w.WriteString(`
` + r.startDelim) 34 | r.writeLines(w, source, n) 35 | } else { 36 | _, _ = w.WriteString(r.endDelim + `
` + "\n") 37 | } 38 | return gast.WalkContinue, nil 39 | } 40 | -------------------------------------------------------------------------------- /block_tex_renderer.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "encoding/base64" 5 | "bytes" 6 | 7 | gast "github.com/yuin/goldmark/ast" 8 | "github.com/yuin/goldmark/renderer" 9 | "github.com/yuin/goldmark/util" 10 | ) 11 | 12 | type MathTexBlockRenderer struct { 13 | renderer *TexRenderer 14 | } 15 | 16 | func (r *MathTexBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 17 | reg.Register(KindMathBlock, r.renderMathBlock) 18 | } 19 | 20 | func (r *MathTexBlockRenderer) writeLines(w *bytes.Buffer, source []byte, n gast.Node) { 21 | l := n.Lines().Len() 22 | for i := 0; i < l; i++ { 23 | line := n.Lines().At(i) 24 | w.Write(line.Value(source)) 25 | } 26 | } 27 | 28 | func (r *MathTexBlockRenderer) renderMathBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 29 | n := node.(*MathBlock) 30 | if entering { 31 | _, _ = w.WriteString(`foo
`, 27 | }, 28 | { 29 | d: "bold", 30 | in: "**foo**", 31 | out: `foo
`, 32 | }, 33 | { 34 | d: "math inline", 35 | in: "$1+2$", 36 | out: `\(1+2\)
`, 37 | }, 38 | { 39 | d: "math display", 40 | in: "$$\n1+2\n$$", 41 | out: `\[1+2 42 | \]
`, 43 | }, 44 | { 45 | // this input previously triggered a panic in block.go 46 | d: "list-begin", 47 | in: "*foo\n ", 48 | out: "*foo
", 49 | }, 50 | } 51 | 52 | for i, tc := range tests { 53 | t.Run(fmt.Sprintf("%d: %s", i, tc.d), func(t *testing.T) { 54 | out, err := renderMarkdown([]byte(tc.in)) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | assert.Equal(t, tc.out, strings.TrimSpace(string(out))) 59 | }) 60 | } 61 | 62 | } 63 | 64 | func renderMarkdown(src []byte) ([]byte, error) { 65 | md := goldmark.New( 66 | goldmark.WithExtensions(MathJax), 67 | ) 68 | 69 | var buf bytes.Buffer 70 | if err := md.Convert(src, &buf); err != nil { 71 | return nil, err 72 | } 73 | 74 | return buf.Bytes(), nil 75 | } 76 | -------------------------------------------------------------------------------- /mathjax.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "github.com/yuin/goldmark" 5 | "github.com/yuin/goldmark/parser" 6 | "github.com/yuin/goldmark/renderer" 7 | "github.com/yuin/goldmark/util" 8 | ) 9 | 10 | type mathjax struct { 11 | inlineStartDelim string 12 | inlineEndDelim string 13 | blockStartDelim string 14 | blockEndDelim string 15 | } 16 | 17 | type Option interface { 18 | SetOption(e *mathjax) 19 | } 20 | 21 | type withInlineDelim struct { 22 | start string 23 | end string 24 | } 25 | 26 | type withBlockDelim struct { 27 | start string 28 | end string 29 | } 30 | 31 | func WithInlineDelim(start string, end string) Option { 32 | return &withInlineDelim{start, end} 33 | } 34 | 35 | func (o *withInlineDelim) SetOption(e *mathjax) { 36 | e.inlineStartDelim = o.start 37 | e.inlineEndDelim = o.end 38 | } 39 | 40 | func WithBlockDelim(start string, end string) Option { 41 | return &withBlockDelim{start, end} 42 | } 43 | 44 | func (o *withBlockDelim) SetOption(e *mathjax) { 45 | e.blockStartDelim = o.start 46 | e.blockEndDelim = o.end 47 | } 48 | 49 | var MathJax = &mathjax{ 50 | inlineStartDelim: `\(`, 51 | inlineEndDelim: `\)`, 52 | blockStartDelim: `\[`, 53 | blockEndDelim: `\]`, 54 | } 55 | 56 | func NewMathJax(opts ...Option) *mathjax { 57 | r := &mathjax{ 58 | inlineStartDelim: `\(`, 59 | inlineEndDelim: `\)`, 60 | blockStartDelim: `\[`, 61 | blockEndDelim: `\]`, 62 | } 63 | 64 | for _, o := range opts { 65 | o.SetOption(r) 66 | } 67 | return r 68 | } 69 | 70 | func (e *mathjax) Extend(m goldmark.Markdown) { 71 | m.Parser().AddOptions(parser.WithBlockParsers( 72 | util.Prioritized(NewMathJaxBlockParser(), 701), 73 | )) 74 | m.Parser().AddOptions(parser.WithInlineParsers( 75 | util.Prioritized(NewInlineMathParser(), 501), 76 | )) 77 | m.Renderer().AddOptions(renderer.WithNodeRenderers( 78 | util.Prioritized(NewMathBlockRenderer(e.blockStartDelim, e.blockEndDelim), 501), 79 | util.Prioritized(NewInlineMathRenderer(e.inlineStartDelim, e.inlineEndDelim), 502), 80 | )) 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goldmark-mathjax 2 | ========================= 3 | 4 | goldmark-mathjax is an extension for the [goldmark](http://github.com/yuin/goldmark) 5 | that adds both block math and inline math support 6 | 7 | It translate inline math equation quoted by `$` and display math block quoted by `$$` into MathJax compatible format. 8 | hyphen `_` won't break LaTeX render within a math element any more. 9 | 10 | ``` 11 | $$ 12 | \left[ \begin{array}{a} a^l_1 \\ ⋮ \\ a^l_{d_l} \end{array}\right] 13 | = \sigma( 14 | \left[ \begin{matrix} 15 | w^l_{1,1} & ⋯ & w^l_{1,d_{l-1}} \\ 16 | ⋮ & ⋱ & ⋮ \\ 17 | w^l_{d_l,1} & ⋯ & w^l_{d_l,d_{l-1}} \\ 18 | \end{matrix}\right] · 19 | \left[ \begin{array}{x} a^{l-1}_1 \\ ⋮ \\ ⋮ \\ a^{l-1}_{d_{l-1}} \end{array}\right] + 20 | \left[ \begin{array}{b} b^l_1 \\ ⋮ \\ b^l_{d_l} \end{array}\right]) 21 | $$ 22 | ``` 23 | 24 | 25 | Borrow the idea from pandoc and this [blackfriday PR](https://github.com/russross/blackfriday/pull/412/) 26 | 27 | The implementation is heavily inspired by the Fenced Code Block and CodeSpan of goldmark 28 | 29 | Installation 30 | -------------------- 31 | 32 | ``` 33 | go get github.com/litao91/goldmark-mathjax 34 | ``` 35 | 36 | Usage 37 | -------------------- 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "bytes" 44 | "fmt" 45 | 46 | mathjax "github.com/litao91/goldmark-mathjax" 47 | "github.com/yuin/goldmark" 48 | "github.com/yuin/goldmark/parser" 49 | "github.com/yuin/goldmark/renderer/html" 50 | ) 51 | 52 | func main() { 53 | md := goldmark.New( 54 | goldmark.WithExtensions(mathjax.MathJax), 55 | goldmark.WithParserOptions( 56 | parser.WithAutoHeadingID(), 57 | ), 58 | goldmark.WithRendererOptions( 59 | html.WithHardWraps(), 60 | html.WithXHTML(), 61 | ), 62 | ) 63 | 64 | // todo more control on the parsing process 65 | var html bytes.Buffer 66 | mdContent := []byte(` 67 | $$ 68 | \mathbb{E}(X) = \int x d F(x) = \left\{ \begin{aligned} \sum_x x f(x) \; & \text{ if } X \text{ is discrete} 69 | \\ \int x f(x) dx \; & \text{ if } X \text{ is continuous } 70 | \end{aligned} \right. 71 | $$ 72 | 73 | 74 | Inline math $\frac{1}{2}$ 75 | `) 76 | if err := md.Convert(mdContent, &html); err != nil { 77 | fmt.Println(err) 78 | } 79 | fmt.Println(html.String()) 80 | } 81 | ``` 82 | 83 | License 84 | -------------------- 85 | MIT 86 | 87 | -------------------------------------------------------------------------------- /block.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "github.com/yuin/goldmark/ast" 5 | "github.com/yuin/goldmark/parser" 6 | "github.com/yuin/goldmark/text" 7 | "github.com/yuin/goldmark/util" 8 | ) 9 | 10 | type mathJaxBlockParser struct { 11 | } 12 | 13 | var defaultMathJaxBlockParser = &mathJaxBlockParser{} 14 | 15 | type mathBlockData struct { 16 | indent int 17 | } 18 | 19 | var mathBlockInfoKey = parser.NewContextKey() 20 | 21 | func NewMathJaxBlockParser() parser.BlockParser { 22 | return defaultMathJaxBlockParser 23 | } 24 | 25 | func (b *mathJaxBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { 26 | line, _ := reader.PeekLine() 27 | pos := pc.BlockOffset() 28 | if pos == -1 { 29 | return nil, parser.NoChildren 30 | } 31 | if line[pos] != '$' { 32 | return nil, parser.NoChildren 33 | } 34 | i := pos 35 | for ; i < len(line) && line[i] == '$'; i++ { 36 | } 37 | if i-pos < 2 { 38 | return nil, parser.NoChildren 39 | } 40 | pc.Set(mathBlockInfoKey, &mathBlockData{indent: pos}) 41 | node := NewMathBlock() 42 | return node, parser.NoChildren 43 | } 44 | 45 | func (b *mathJaxBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { 46 | line, segment := reader.PeekLine() 47 | data := pc.Get(mathBlockInfoKey).(*mathBlockData) 48 | w, pos := util.IndentWidth(line, 0) 49 | if w < 4 { 50 | i := pos 51 | for ; i < len(line) && line[i] == '$'; i++ { 52 | } 53 | length := i - pos 54 | if length >= 2 && util.IsBlank(line[i:]) { 55 | reader.Advance(segment.Stop - segment.Start - segment.Padding) 56 | return parser.Close 57 | } 58 | } 59 | 60 | pos, padding := util.DedentPosition(line, 0, data.indent) 61 | seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) 62 | node.Lines().Append(seg) 63 | reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) 64 | return parser.Continue | parser.NoChildren 65 | } 66 | 67 | func (b *mathJaxBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { 68 | pc.Set(mathBlockInfoKey, nil) 69 | } 70 | 71 | func (b *mathJaxBlockParser) CanInterruptParagraph() bool { 72 | return true 73 | } 74 | 75 | func (b *mathJaxBlockParser) CanAcceptIndentedLine() bool { 76 | return false 77 | } 78 | 79 | func (b *mathJaxBlockParser) Trigger() []byte { 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /inline.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "github.com/yuin/goldmark/ast" 5 | "github.com/yuin/goldmark/parser" 6 | "github.com/yuin/goldmark/renderer" 7 | "github.com/yuin/goldmark/text" 8 | "github.com/yuin/goldmark/util" 9 | ) 10 | 11 | type inlineMathParser struct { 12 | } 13 | 14 | var defaultInlineMathParser = &inlineMathParser{} 15 | 16 | func NewInlineMathParser() parser.InlineParser { 17 | return defaultInlineMathParser 18 | } 19 | 20 | func (s *inlineMathParser) Trigger() []byte { 21 | return []byte{'$'} 22 | } 23 | 24 | func (s *inlineMathParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 25 | line, startSegment := block.PeekLine() 26 | opener := 0 27 | for ; opener < len(line) && line[opener] == '$'; opener++ { 28 | } 29 | block.Advance(opener) 30 | l, pos := block.Position() 31 | node := NewInlineMath() 32 | for { 33 | line, segment := block.PeekLine() 34 | if line == nil { 35 | block.SetPosition(l, pos) 36 | return ast.NewTextSegment(startSegment.WithStop(startSegment.Start + opener)) 37 | } 38 | for i := 0; i < len(line); i++ { 39 | c := line[i] 40 | if c == '$' { 41 | oldi := i 42 | for ; i < len(line) && line[i] == '$'; i++ { 43 | } 44 | closure := i - oldi 45 | if closure == opener && (i+1 >= len(line) || line[i+1] != '$') { 46 | segment := segment.WithStop(segment.Start + i - closure) 47 | if !segment.IsEmpty() { 48 | node.AppendChild(node, ast.NewRawTextSegment(segment)) 49 | } 50 | block.Advance(i) 51 | goto end 52 | } 53 | } 54 | } 55 | if !util.IsBlank(line) { 56 | node.AppendChild(node, ast.NewRawTextSegment(segment)) 57 | } 58 | block.AdvanceLine() 59 | } 60 | end: 61 | 62 | if !node.IsBlank(block.Source()) { 63 | // trim first halfspace and last halfspace 64 | segment := node.FirstChild().(*ast.Text).Segment 65 | shouldTrimmed := true 66 | if !(!segment.IsEmpty() && block.Source()[segment.Start] == ' ') { 67 | shouldTrimmed = false 68 | } 69 | segment = node.LastChild().(*ast.Text).Segment 70 | if !(!segment.IsEmpty() && block.Source()[segment.Stop-1] == ' ') { 71 | shouldTrimmed = false 72 | } 73 | if shouldTrimmed { 74 | t := node.FirstChild().(*ast.Text) 75 | segment := t.Segment 76 | t.Segment = segment.WithStart(segment.Start + 1) 77 | t = node.LastChild().(*ast.Text) 78 | segment = node.LastChild().(*ast.Text).Segment 79 | t.Segment = segment.WithStop(segment.Stop - 1) 80 | } 81 | 82 | } 83 | return node 84 | } 85 | 86 | func NewInlineMathRenderer(start, end string) renderer.NodeRenderer { 87 | return &InlineMathRenderer{start, end} 88 | } 89 | -------------------------------------------------------------------------------- /tex_renderer.go: -------------------------------------------------------------------------------- 1 | package mathjax 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | const common = ` 14 | \documentclass[preview]{standalone} 15 | \usepackage{amsmath} 16 | \usepackage{amssymb} 17 | \usepackage{stmaryrd} 18 | \begin{document} 19 | %s 20 | \end{document} 21 | ` 22 | 23 | const displayInlineFormula = ` 24 | \documentclass[11pt]{article} 25 | \usepackage[active,tightpage,textmath]{preview} 26 | \usepackage{amsmath} 27 | \usepackage{amssymb} 28 | \usepackage{stmaryrd} 29 | \begin{document} 30 | \(%s\) 31 | \end{document} 32 | ` 33 | 34 | const displayBlockFormula = ` 35 | \documentclass[11pt,preview]{standalone} 36 | \usepackage{amsmath} 37 | \usepackage{amssymb} 38 | \usepackage{stmaryrd} 39 | \begin{document} 40 | \begin{equation*} 41 | %s 42 | \end{equation*} 43 | \end{document} 44 | ` 45 | 46 | const tmpl = ` 47 | \documentclass[11pt]{article} 48 | \usepackage[paperwidth=180in,paperheight=180in]{geometry} 49 | \batchmode 50 | \usepackage[utf8]{inputenc} 51 | \usepackage{amsmath} 52 | \usepackage{amssymb} 53 | \usepackage{stmaryrd} 54 | \usepackage[verbose]{newunicodechar} 55 | \pagestyle{empty} 56 | \setlength{\topskip}{0pt} 57 | \setlength{\parindent}{0pt} 58 | \setlength{\abovedisplayskip}{0pt} 59 | \setlength{\belowdisplayskip}{0pt} 60 | \begin{document} 61 | %s 62 | \end{document} 63 | ` 64 | 65 | const tikz = ` 66 | \documentclass[11pt]{article} 67 | \usepackage{tikz} 68 | \usepackage{lipsum} 69 | \usepackage{paralist,pst-func, pst-plot, pst-math, pstricks-add,pgfplots} 70 | \usetikzlibrary{patterns,matrix,arrows} 71 | \usepackage[active,tightpage]{preview} 72 | \PreviewEnvironment{tikzpicture} 73 | \setlength\PreviewBorder{1pt} 74 | 75 | \begin{document} 76 | %s 77 | \end{document} 78 | ` 79 | 80 | type TexRenderer struct { 81 | texPath string 82 | docTemplate string 83 | inlineFormulaImpl string 84 | commonBlockTmpl string 85 | blockFormulaTmpl string 86 | tikzTmpl string 87 | tmpDir string 88 | } 89 | 90 | func NewDefaultTexRenderer() *TexRenderer { 91 | var wd, _ = os.Getwd() 92 | var texPath = os.Getenv("TEX_PATH") 93 | 94 | var tmpDir = wd + "/tmp/" 95 | 96 | var defaultRenderer = &TexRenderer{ 97 | texPath: texPath, 98 | docTemplate: tmpl, 99 | inlineFormulaImpl: displayInlineFormula, 100 | commonBlockTmpl: common, 101 | blockFormulaTmpl: displayBlockFormula, 102 | tmpDir: tmpDir, 103 | tikzTmpl: tikz, 104 | } 105 | return defaultRenderer 106 | } 107 | 108 | func (r *TexRenderer) RunInline(formula string) []byte { 109 | return r.runRaw(fmt.Sprintf(r.inlineFormulaImpl, formula)) 110 | } 111 | 112 | func (r *TexRenderer) Run(formula string) []byte { 113 | var tmpl string 114 | formula = strings.TrimSpace(formula) 115 | if strings.Contains(formula, `\begin{tikzpicture}`) { 116 | tmpl = r.tikzTmpl 117 | } else if (strings.HasPrefix(formula, `\begin{`)) { 118 | tmpl = r.commonBlockTmpl 119 | } else { 120 | tmpl = r.blockFormulaTmpl 121 | } 122 | return r.runRaw(fmt.Sprintf(tmpl, strings.TrimSpace(formula))) 123 | } 124 | 125 | func (r *TexRenderer) runRaw(formula string) []byte { 126 | f, err := ioutil.TempFile(r.tmpDir, "doc") 127 | if err != nil { 128 | log.Fatalf("%v", err) 129 | } 130 | f.WriteString(formula) 131 | f.Sync() 132 | f.Close() 133 | r.runPdfLatex(f.Name()) 134 | r.runPdf2Svg(f.Name()) 135 | svgf, err := os.Open(f.Name() + ".svg") 136 | if err != nil { 137 | return nil 138 | } 139 | svg, err := ioutil.ReadAll(svgf) 140 | if err != nil { 141 | return nil 142 | } 143 | return svg 144 | } 145 | 146 | func (r *TexRenderer) runDvi2Svg(fname string) { 147 | // fmt.Println([]string{fmt.Sprintf("%sdvisvgm", r.texPath), fmt.Sprintf("%s.dvi", fname), "-o", fmt.Sprintf("%s.svg", fname), "-n", "--exact", "-v0", "--relative", "--zoom=1.2546875"}) 148 | 149 | cmd := exec.Command(fmt.Sprintf("%sdvisvgm", r.texPath), fmt.Sprintf("%s.dvi", fname), "-o", fmt.Sprintf("%s.svg", fname), "-n", "--exact", "-v0", "--relative", "--zoom=1.2546875") 150 | var stdout, stderr bytes.Buffer 151 | cmd.Stdout = &stdout 152 | cmd.Stderr = &stderr 153 | err := cmd.Run() 154 | if err != nil { 155 | fmt.Printf("dvi2svg cmd.Run() failed with %s\n", err) 156 | } 157 | // outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) 158 | // fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr) 159 | } 160 | 161 | func (r *TexRenderer) runLatex(fname string) { 162 | cmd := exec.Command(fmt.Sprintf("%slatex", r.texPath), "-output-directory", r.tmpDir, fname) 163 | var stdout, stderr bytes.Buffer 164 | cmd.Stdout = &stdout 165 | cmd.Stderr = &stderr 166 | err := cmd.Run() 167 | if err != nil { 168 | fmt.Printf("latex cmd.Run() failed with %s\n", err) 169 | } 170 | // outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) 171 | // fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr) 172 | } 173 | 174 | func (r *TexRenderer) runPdfLatex(fname string) { 175 | cmd := exec.Command(fmt.Sprintf("%spdflatex", r.texPath), "-output-directory", r.tmpDir, fname) 176 | var stdout, stderr bytes.Buffer 177 | cmd.Stdout = &stdout 178 | cmd.Stderr = &stderr 179 | err := cmd.Run() 180 | if err != nil { 181 | fmt.Printf("pdflatex %s cmd.Run() failed with %s\n", fname, err) 182 | } 183 | } 184 | 185 | func (r *TexRenderer) runPdf2Svg(fname string) { 186 | cmd := exec.Command("pdf2svg", fmt.Sprintf("%s.pdf", fname), fmt.Sprintf("%s.svg", fname)) 187 | var stdout, stderr bytes.Buffer 188 | cmd.Stdout = &stdout 189 | cmd.Stderr = &stderr 190 | err := cmd.Run() 191 | if err != nil { 192 | fmt.Printf("pdf2svg cmd.Run() failed with %s\n", err) 193 | } 194 | } 195 | --------------------------------------------------------------------------------