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

render: Latex support #229

Merged
merged 18 commits into from
Nov 28, 2022
Merged
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: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
d2layouts/d2dagrelayout/dagre.js linguist-vendored
d2layouts/d2elklayout/elk.js linguist-vendored
d2renderers/d2svg/github-markdown.css linguist-vendored
d2renderers/d2latex/mathjax.js linguist-vendored
d2renderers/d2latex/polyfills.js linguist-vendored
2 changes: 2 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#### Features 🚀

- Latex is now supported. See [docs](https://d2lang.com/tour/text) for more.
[#229](https://github.com/terrastruct/d2/pull/229)
- Arrowhead labels are now supported. [#182](https://github.com/terrastruct/d2/pull/182)
- `stroke-dash` on shapes is now supported. [#188](https://github.com/terrastruct/d2/issues/188)
- `font-color` is now supported on shapes and connections. [#215](https://github.com/terrastruct/d2/pull/215)
Expand Down
15 changes: 8 additions & 7 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func (c *compiler) applyScalar(attrs *d2graph.Attributes, reserved string, box d
if ok {
attrs.Language = fullTag
}
if attrs.Language == "markdown" {
if attrs.Language == "markdown" || attrs.Language == "latex" {
attrs.Shape.Value = d2target.ShapeText
} else {
attrs.Shape.Value = d2target.ShapeCode
Expand Down Expand Up @@ -548,12 +548,13 @@ func (c *compiler) compileFlatKey(k *d2ast.KeyPath) ([]string, string, bool) {

// TODO add more, e.g. C, bash
var ShortToFullLanguageAliases = map[string]string{
"md": "markdown",
"js": "javascript",
"go": "golang",
"py": "python",
"rb": "ruby",
"ts": "typescript",
"md": "markdown",
"tex": "latex",
"js": "javascript",
"go": "golang",
"py": "python",
"rb": "ruby",
"ts": "typescript",
}
var FullToShortLanguageAliases map[string]string

Expand Down
17 changes: 13 additions & 4 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
Expand Down Expand Up @@ -833,10 +834,18 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler
var dims *d2target.TextDimensions
var innerLabelPadding = 5
if obj.Attributes.Shape.Value == d2target.ShapeText {
var err error
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text())
if err != nil {
return err
if obj.Attributes.Language == "latex" {
width, height, err := d2latex.Measure(obj.Text().Text)
if err != nil {
return err
}
dims = d2target.NewTextDimensions(width, height)
} else {
var err error
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text())
if err != nil {
return err
}
}
innerLabelPadding = 0
} else {
Expand Down
83 changes: 83 additions & 0 deletions d2renderers/d2latex/latex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build cgo

package d2latex

import (
_ "embed"
"fmt"
"math"
"regexp"
"strconv"

"oss.terrastruct.com/xdefer"
v8 "rogchap.com/v8go"
)

var pxPerEx = 8

//go:embed polyfills.js
var polyfillsJS string

//go:embed setup.js
var setupJS string

//go:embed mathjax.js
var mathjaxJS string

// Matches this
// <svg style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="563" height="326" viewBox="-100 -100 563 326"><style type="text/css">
var svgRe = regexp.MustCompile(`<svg[^>]+width="([0-9\.]+)ex" height="([0-9\.]+)ex"[^>]+>`)

func Render(s string) (_ string, err error) {
defer xdefer.Errorf(&err, "latex failed to parse")
v8ctx := v8.NewContext()

if _, err := v8ctx.RunScript(polyfillsJS, "polyfills.js"); err != nil {
return "", err
}

if _, err := v8ctx.RunScript(mathjaxJS, "mathjax.js"); err != nil {
return "", err
}

if _, err := v8ctx.RunScript(setupJS, "setup.js"); err != nil {
return "", err
}

val, err := v8ctx.RunScript(fmt.Sprintf(`adaptor.innerHTML(html.convert("%s", {
em: %d,
ex: %d,
}))`, s, pxPerEx*2, pxPerEx), "value.js")
if err != nil {
return "", err
}

return val.String(), nil
}

func Measure(s string) (width, height int, err error) {
defer xdefer.Errorf(&err, "latex failed to parse")
svg, err := Render(s)
if err != nil {
return 0, 0, err
}

dims := svgRe.FindAllStringSubmatch(svg, -1)
if len(dims) != 1 || len(dims[0]) != 3 {
return 0, 0, fmt.Errorf("svg parsing failed for latex: %v", svg)
}

wEx := dims[0][1]
hEx := dims[0][2]

wf, err := strconv.ParseFloat(wEx, 64)
if err != nil {
return 0, 0, fmt.Errorf("svg parsing failed for latex: %v", svg)
}
hf, err := strconv.ParseFloat(hEx, 64)
if err != nil {
return 0, 0, fmt.Errorf("svg parsing failed for latex: %v", svg)
}

return int(math.Ceil(wf * float64(pxPerEx))), int(math.Ceil(hf * float64(pxPerEx))), nil
}
13 changes: 13 additions & 0 deletions d2renderers/d2latex/latex_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !cgo

package d2latex

import "errors"

func Render(s string) (string, error) {
return "", errors.New("not found in build")
}

func Measure(s string) (width, height int, _ error) {
return 0, 0, errors.New("not found in build")
}
30 changes: 30 additions & 0 deletions d2renderers/d2latex/latex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package d2latex

import (
"encoding/xml"
"testing"
)

func TestRender(t *testing.T) {
txts := []string{
`a + b = c`,
`\\frac{1}{2}`,
}
for _, txt := range txts {
svg, err := Render(txt)
if err != nil {
t.Fatal(err)
}
var xmlParsed interface{}
if err := xml.Unmarshal([]byte(svg), &xmlParsed); err != nil {
t.Fatalf("invalid SVG: %v", err)
}
}
}

func TestRenderError(t *testing.T) {
_, err := Render(`\frac{1}{2}`)
if err == nil {
t.Fatal("expected to error on invalid latex syntax")
}
}
1 change: 1 addition & 0 deletions d2renderers/d2latex/mathjax.js

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions d2renderers/d2latex/polyfills.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions d2renderers/d2latex/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const adaptor = MathJax._.adaptors.liteAdaptor.liteAdaptor();
MathJax._.handlers.html_ts.RegisterHTMLHandler(adaptor)
const html = MathJax._.mathjax.mathjax.document('', {
InputJax: new MathJax._.input.tex_ts.TeX(),
OutputJax: new MathJax._.output.svg_ts.SVG(),
});
31 changes: 21 additions & 10 deletions d2renderers/d2svg/d2svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/alecthomas/chroma/styles"

"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/color"
Expand Down Expand Up @@ -701,17 +702,27 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) error {
}
fmt.Fprintf(writer, "</g></g>")
case d2target.ShapeText:
render, err := textmeasure.RenderMarkdown(targetShape.Label)
if err != nil {
return err
if targetShape.Language == "latex" {
render, err := d2latex.Render(targetShape.Label)
if err != nil {
return err
}
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
fmt.Fprintf(writer, render)
fmt.Fprintf(writer, "</g>")
} else {
render, err := textmeasure.RenderMarkdown(targetShape.Label)
if err != nil {
return err
}
fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
box.TopLeft.X, box.TopLeft.Y, targetShape.Width, targetShape.Height,
)
// we need the self closing form in this svg/xhtml context
render = strings.ReplaceAll(render, "<hr>", "<hr />")
fmt.Fprintf(writer, `<div xmlns="http://www.w3.org/1999/xhtml" class="md">%v</div>`, render)
fmt.Fprint(writer, `</foreignObject></g>`)
}
fmt.Fprintf(writer, `<g><foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" x="%f" y="%f" width="%d" height="%d">`,
box.TopLeft.X, box.TopLeft.Y, targetShape.Width, targetShape.Height,
)
// we need the self closing form in this svg/xhtml context
render = strings.ReplaceAll(render, "<hr>", "<hr />")
fmt.Fprintf(writer, `<div xmlns="http://www.w3.org/1999/xhtml" class="md">%v</div>`, render)
fmt.Fprint(writer, `</foreignObject></g>`)
default:
fontColor := "black"
if targetShape.Color != "" {
Expand Down
25 changes: 25 additions & 0 deletions e2etests/stable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,31 @@ beta: {
alpha -> beta: gamma {
style.font-color: green
}
`,
},
{
name: "latex",
script: `a: |latex
\\Huge{\\frac{\\alpha g^2}{\\omega^5} e^{[ -0.74\\bigl\\{\\frac{\\omega U_\\omega 19.5}{g}\\bigr\\}^{\\!-4}\\,]}}
|

b: |latex
e = mc^2
|

z: |latex
gibberish\\; math:\\sum_{i=0}^\\infty i^2
|

z -> a
z -> b

a -> c
b -> c
sugar -> c
c: mixed together

c -> solution: we get
`,
},
}
Expand Down
Loading