2022-11-27 01:54:41PM

This commit is contained in:
Alexander Wang 2022-11-27 13:54:41 -08:00
parent 2fee03e29d
commit 72dcdf9cc4
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
9 changed files with 1985 additions and 186 deletions

View file

@ -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

View file

@ -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 {

View file

@ -3,6 +3,9 @@ package d2latex
import (
_ "embed"
"fmt"
"math"
"regexp"
"strconv"
v8 "rogchap.com/v8go"
)
@ -16,7 +19,9 @@ var setupJS string
//go:embed mathjax.js
var mathjaxJS string
func SVG(s string) (string, error) {
var svgRe = regexp.MustCompile(`<svg[^>]+width="([0-9\.]+)ex" height="([0-9\.]+)ex"[^>]+>`)
func Render(s string) (string, error) {
v8ctx := v8.NewContext()
if _, err := v8ctx.RunScript(polyfillsJS, "polyfills.js"); err != nil {
@ -41,3 +46,29 @@ func SVG(s string) (string, error) {
return val.String(), nil
}
func Measure(s string) (width, height int, _ error) {
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 * 8)), int(math.Ceil(hf * 8)), nil
}

View file

@ -18,7 +18,6 @@ import (
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/davecgh/go-spew/spew"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2latex"
@ -662,72 +661,68 @@ func drawShape(writer io.Writer, targetShape d2target.Shape) error {
switch targetShape.Type {
case d2target.ShapeCode:
if targetShape.Language == "latex" {
spew.Dump(targetShape.Label)
render, err := d2latex.SVG(targetShape.Label)
if err != nil {
spew.Dump(err)
return err
}
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
// render = strings.Replace(render, "svg", "g", -1)
spew.Dump(render)
fmt.Fprintf(writer, render)
fmt.Fprintf(writer, "</g>")
} else {
lexer := lexers.Get(targetShape.Language)
if lexer == nil {
return fmt.Errorf("code snippet lexer for %s not found", targetShape.Language)
}
style := styles.Get("github")
if style == nil {
return errors.New(`code snippet style "github" not found`)
}
formatter := formatters.Get("svg")
if formatter == nil {
return errors.New(`code snippet formatter "svg" not found`)
}
iterator, err := lexer.Tokenise(nil, targetShape.Label)
if err != nil {
return err
}
svgStyles := styleToSVG(style)
containerStyle := fmt.Sprintf(`stroke: %s;fill:%s`, targetShape.Stroke, style.Get(chroma.Background).Background.String())
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
fmt.Fprintf(writer, `<rect class="shape" width="%d" height="%d" style="%s" />`,
targetShape.Width, targetShape.Height, containerStyle)
// Padding
fmt.Fprintf(writer, `<g transform="translate(6 6)">`)
for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
// TODO mono font looks better with 1.2 em (use px equivalent), but textmeasure needs to account for it. Not obvious how that should be done
fmt.Fprintf(writer, "<text class=\"text-mono\" x=\"0\" y=\"%fem\" xml:space=\"preserve\">", 1*float64(index+1))
for _, token := range tokens {
text := svgEscaper.Replace(token.String())
attr := styleAttr(svgStyles, token.Type)
if attr != "" {
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
}
fmt.Fprint(writer, text)
}
fmt.Fprint(writer, "</text>")
}
fmt.Fprintf(writer, "</g></g>")
lexer := lexers.Get(targetShape.Language)
if lexer == nil {
return fmt.Errorf("code snippet lexer for %s not found", targetShape.Language)
}
case d2target.ShapeText:
render, err := textmeasure.RenderMarkdown(targetShape.Label)
style := styles.Get("github")
if style == nil {
return errors.New(`code snippet style "github" not found`)
}
formatter := formatters.Get("svg")
if formatter == nil {
return errors.New(`code snippet formatter "svg" not found`)
}
iterator, err := lexer.Tokenise(nil, 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>`)
svgStyles := styleToSVG(style)
containerStyle := fmt.Sprintf(`stroke: %s;fill:%s`, targetShape.Stroke, style.Get(chroma.Background).Background.String())
fmt.Fprintf(writer, `<g transform="translate(%f %f)" style="opacity:%f">`, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity)
fmt.Fprintf(writer, `<rect class="shape" width="%d" height="%d" style="%s" />`,
targetShape.Width, targetShape.Height, containerStyle)
// Padding
fmt.Fprintf(writer, `<g transform="translate(6 6)">`)
for index, tokens := range chroma.SplitTokensIntoLines(iterator.Tokens()) {
// TODO mono font looks better with 1.2 em (use px equivalent), but textmeasure needs to account for it. Not obvious how that should be done
fmt.Fprintf(writer, "<text class=\"text-mono\" x=\"0\" y=\"%fem\" xml:space=\"preserve\">", 1*float64(index+1))
for _, token := range tokens {
text := svgEscaper.Replace(token.String())
attr := styleAttr(svgStyles, token.Type)
if attr != "" {
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
}
fmt.Fprint(writer, text)
}
fmt.Fprint(writer, "</text>")
}
fmt.Fprintf(writer, "</g></g>")
case d2target.ShapeText:
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>`)
}
default:
fontColor := "black"
if targetShape.Color != "" {

View file

@ -3,20 +3,20 @@
"shapes": [
{
"id": "a",
"type": "code",
"type": "text",
"pos": {
"x": 0,
"y": 44
"y": 194
},
"width": 1022,
"height": 38,
"width": 154,
"height": 41,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0A0F25",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
@ -35,25 +35,25 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 1022,
"labelHeight": 38
"labelWidth": 154,
"labelHeight": 41
},
{
"id": "b",
"type": "code",
"type": "text",
"pos": {
"x": 1082,
"y": 44
"x": 214,
"y": 205
},
"width": 93,
"height": 38,
"width": 65,
"height": 18,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0A0F25",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
@ -72,15 +72,52 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 93,
"labelHeight": 38
"labelWidth": 65,
"labelHeight": 18
},
{
"id": "z",
"type": "text",
"pos": {
"x": 72,
"y": 0
},
"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": 1022,
"y": 226
"x": 140,
"y": 377
},
"width": 214,
"height": 126,
@ -117,8 +154,8 @@
"id": "sugar",
"type": "",
"pos": {
"x": 1235,
"y": 0
"x": 339,
"y": 151
},
"width": 146,
"height": 126,
@ -155,8 +192,8 @@
"id": "solution",
"type": "",
"pos": {
"x": 1047,
"y": 452
"x": 165,
"y": 603
},
"width": 164,
"height": 126,
@ -191,6 +228,100 @@
}
],
"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": 133.12582781456953,
"y": 51
},
{
"x": 88.2251655629139,
"y": 91
},
{
"x": 77,
"y": 119.5
},
{
"x": 77,
"y": 193.5
}
],
"isCurve": true,
"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": 190.37417218543047,
"y": 51
},
{
"x": 235.2748344370861,
"y": 91
},
{
"x": 246.5,
"y": 121.8
},
{
"x": 246.5,
"y": 205
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null
},
{
"id": "(a -> c)[0]",
"src": "a",
@ -217,20 +348,20 @@
"labelPercentage": 0,
"route": [
{
"x": 511,
"y": 82
"x": 77,
"y": 235.5
},
{
"x": 511,
"y": 157.2
"x": 77,
"y": 308.7
},
{
"x": 613.1,
"y": 194.6838866396761
"x": 92,
"y": 337
},
{
"x": 1021.5,
"y": 269.41943319838055
"x": 152,
"y": 377
}
],
"isCurve": true,
@ -264,20 +395,20 @@
"labelPercentage": 0,
"route": [
{
"x": 1128.5,
"y": 82
"x": 246.5,
"y": 223
},
{
"x": 1128.5,
"y": 157.2
"x": 246.5,
"y": 306.2
},
{
"x": 1128.5,
"y": 186
"x": 246.5,
"y": 337
},
{
"x": 1128.5,
"y": 226
"x": 246.5,
"y": 377
}
],
"isCurve": true,
@ -311,20 +442,20 @@
"labelPercentage": 0,
"route": [
{
"x": 1308,
"y": 126
"x": 412,
"y": 277
},
{
"x": 1308,
"y": 166
"x": 412,
"y": 317
},
{
"x": 1292.2,
"y": 186
"x": 397.4,
"y": 337
},
{
"x": 1229,
"y": 226
"x": 339,
"y": 377
}
],
"isCurve": true,
@ -358,20 +489,20 @@
"labelPercentage": 0,
"route": [
{
"x": 1128.5,
"y": 352
"x": 246.5,
"y": 503
},
{
"x": 1128.5,
"y": 392
"x": 246.5,
"y": 543
},
{
"x": 1128.5,
"y": 412
"x": 246.5,
"y": 563
},
{
"x": 1128.5,
"y": 452
"x": 246.5,
"y": 603
}
],
"isCurve": true,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View file

@ -3,20 +3,20 @@
"shapes": [
{
"id": "a",
"type": "code",
"type": "text",
"pos": {
"x": 12,
"y": 216
"x": 291,
"y": 196
},
"width": 1022,
"height": 38,
"width": 154,
"height": 41,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0A0F25",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
@ -35,25 +35,25 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 1022,
"labelHeight": 38
"labelWidth": 154,
"labelHeight": 41
},
{
"id": "b",
"type": "code",
"type": "text",
"pos": {
"x": 941,
"x": 336,
"y": 158
},
"width": 93,
"height": 38,
"width": 65,
"height": 18,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0A0F25",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
@ -72,15 +72,52 @@
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 93,
"labelHeight": 38
"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": 1134,
"y": 114
"x": 545,
"y": 104
},
"width": 214,
"height": 126,
@ -117,7 +154,7 @@
"id": "sugar",
"type": "",
"pos": {
"x": 888,
"x": 299,
"y": 12
},
"width": 146,
@ -155,8 +192,8 @@
"id": "solution",
"type": "",
"pos": {
"x": 1592,
"y": 114
"x": 1003,
"y": 104
},
"width": 164,
"height": 126,
@ -191,6 +228,90 @@
}
],
"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": 216.5
},
{
"x": 291,
"y": 216.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": 335.5,
"y": 167
}
],
"animated": false,
"tooltip": "",
"icon": null
},
{
"id": "(a -> c)[0]",
"src": "a",
@ -217,20 +338,20 @@
"labelPercentage": 0,
"route": [
{
"x": 1034,
"y": 235
"x": 445,
"y": 216.5
},
{
"x": 1084,
"y": 235
"x": 495,
"y": 216.5
},
{
"x": 1084,
"y": 208.5
"x": 495,
"y": 198.5
},
{
"x": 1134,
"y": 208.5
"x": 545,
"y": 198.5
}
],
"animated": false,
@ -263,12 +384,12 @@
"labelPercentage": 0,
"route": [
{
"x": 1034,
"y": 177
"x": 400.5,
"y": 167
},
{
"x": 1134,
"y": 177
"x": 545,
"y": 167
}
],
"animated": false,
@ -301,20 +422,20 @@
"labelPercentage": 0,
"route": [
{
"x": 1034,
"x": 445,
"y": 75
},
{
"x": 1084,
"x": 495,
"y": 75
},
{
"x": 1084,
"y": 145.5
"x": 495,
"y": 135.5
},
{
"x": 1134,
"y": 145.5
"x": 545,
"y": 135.5
}
],
"animated": false,
@ -347,12 +468,12 @@
"labelPercentage": 0,
"route": [
{
"x": 1348,
"y": 177
"x": 759,
"y": 167
},
{
"x": 1592,
"y": 177
"x": 1003,
"y": 167
}
],
"animated": false,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 508 KiB

View file

@ -25,14 +25,13 @@ b: |latex
e = mc^2
|
complex: |latex
f(x) = \\begin{dcases*}
x & when $x$ is even\\\
-x & when $x$ is odd
\\end{dcases*}
z: |latex
gibberish\\; math:\\sum_{i=0}^\\infty i^2
|
complex -> c
z -> a
z -> b
a -> c
b -> c
sugar -> c