diff --git a/.gitattributes b/.gitattributes
index fe07df655..e5c4c1ca0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -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
diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 10ba72b57..e669b635f 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -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)
diff --git a/d2compiler/compile.go b/d2compiler/compile.go
index 0db0d62d4..4e1face77 100644
--- a/d2compiler/compile.go
+++ b/d2compiler/compile.go
@@ -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
@@ -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
diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go
index 5687608f1..1fe88fba4 100644
--- a/d2graph/d2graph.go
+++ b/d2graph/d2graph.go
@@ -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"
@@ -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 {
diff --git a/d2renderers/d2latex/latex.go b/d2renderers/d2latex/latex.go
new file mode 100644
index 000000000..d9a68b51b
--- /dev/null
+++ b/d2renderers/d2latex/latex.go
@@ -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
+//
\ No newline at end of file
diff --git a/e2etests/testdata/stable/latex/elk/board.exp.json b/e2etests/testdata/stable/latex/elk/board.exp.json
new file mode 100644
index 000000000..e97a6e2ab
--- /dev/null
+++ b/e2etests/testdata/stable/latex/elk/board.exp.json
@@ -0,0 +1,484 @@
+{
+ "name": "",
+ "shapes": [
+ {
+ "id": "a",
+ "type": "text",
+ "pos": {
+ "x": 291,
+ "y": 196
+ },
+ "width": 382,
+ "height": 101,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "\\\\Huge{\\\\frac{\\\\alpha g^2}{\\\\omega^5} e^{[ -0.74\\\\bigl\\\\{\\\\frac{\\\\omega U_\\\\omega 19.5}{g}\\\\bigr\\\\}^{\\\\!-4}\\\\,]}}",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "latex",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 382,
+ "labelHeight": 101
+ },
+ {
+ "id": "b",
+ "type": "text",
+ "pos": {
+ "x": 450,
+ "y": 158
+ },
+ "width": 65,
+ "height": 18,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "e = mc^2",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "latex",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 65,
+ "labelHeight": 18
+ },
+ {
+ "id": "z",
+ "type": "text",
+ "pos": {
+ "x": 12,
+ "y": 150
+ },
+ "width": 179,
+ "height": 51,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "gibberish\\\\; math:\\\\sum_{i=0}^\\\\infty i^2",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "latex",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 179,
+ "labelHeight": 51
+ },
+ {
+ "id": "c",
+ "type": "",
+ "pos": {
+ "x": 773,
+ "y": 104
+ },
+ "width": 214,
+ "height": 126,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "mixed together",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 114,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER"
+ },
+ {
+ "id": "sugar",
+ "type": "",
+ "pos": {
+ "x": 527,
+ "y": 12
+ },
+ "width": 146,
+ "height": 126,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "sugar",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 46,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER"
+ },
+ {
+ "id": "solution",
+ "type": "",
+ "pos": {
+ "x": 1231,
+ "y": 104
+ },
+ "width": 164,
+ "height": 126,
+ "level": 1,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#F7F8FE",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "solution",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 64,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_MIDDLE_CENTER"
+ }
+ ],
+ "connections": [
+ {
+ "id": "(z -> a)[0]",
+ "src": "z",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "a",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 191,
+ "y": 184
+ },
+ {
+ "x": 241,
+ "y": 184
+ },
+ {
+ "x": 241,
+ "y": 246.5
+ },
+ {
+ "x": 291,
+ "y": 246.5
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ },
+ {
+ "id": "(z -> b)[0]",
+ "src": "z",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "b",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 191,
+ "y": 167
+ },
+ {
+ "x": 449.5,
+ "y": 167
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ },
+ {
+ "id": "(a -> c)[0]",
+ "src": "a",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 673,
+ "y": 246.5
+ },
+ {
+ "x": 723,
+ "y": 246.5
+ },
+ {
+ "x": 723,
+ "y": 198.5
+ },
+ {
+ "x": 773,
+ "y": 198.5
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ },
+ {
+ "id": "(b -> c)[0]",
+ "src": "b",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 514.5,
+ "y": 167
+ },
+ {
+ "x": 773,
+ "y": 167
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ },
+ {
+ "id": "(sugar -> c)[0]",
+ "src": "sugar",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "c",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 673,
+ "y": 75
+ },
+ {
+ "x": 723,
+ "y": 75
+ },
+ {
+ "x": 723,
+ "y": 135.5
+ },
+ {
+ "x": 773,
+ "y": 135.5
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ },
+ {
+ "id": "(c -> solution)[0]",
+ "src": "c",
+ "srcArrow": "none",
+ "srcLabel": "",
+ "dst": "solution",
+ "dstArrow": "triangle",
+ "dstLabel": "",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "#0D32B2",
+ "label": "we get",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#676C7E",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "labelPercentage": 0,
+ "route": [
+ {
+ "x": 987,
+ "y": 167
+ },
+ {
+ "x": 1231,
+ "y": 167
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/latex/elk/sketch.exp.svg b/e2etests/testdata/stable/latex/elk/sketch.exp.svg
new file mode 100644
index 000000000..4bc0231e8
--- /dev/null
+++ b/e2etests/testdata/stable/latex/elk/sketch.exp.svg
@@ -0,0 +1,790 @@
+
+mixed togethersugarsolution
+
+
+we get
\ No newline at end of file