From 72dcdf9cc4ec04e7d6b1f61f0ae27ebc9f67ca3c Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 27 Nov 2022 13:54:41 -0800 Subject: [PATCH] 2022-11-27 01:54:41PM --- d2compiler/compile.go | 15 +- d2graph/d2graph.go | 17 +- d2renderers/d2latex/latex.go | 33 +- d2renderers/d2svg/d2svg.go | 121 ++- .../testdata/todo/latex/dagre/board.exp.json | 237 ++++-- .../testdata/todo/latex/dagre/sketch.exp.svg | 766 +++++++++++++++++- .../testdata/todo/latex/elk/board.exp.json | 205 ++++- .../testdata/todo/latex/elk/sketch.exp.svg | 766 +++++++++++++++++- e2etests/todo_test.go | 11 +- 9 files changed, 1985 insertions(+), 186 deletions(-) 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 index 224d14dfd..0013e1b8b 100644 --- a/d2renderers/d2latex/latex.go +++ b/d2renderers/d2latex/latex.go @@ -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(`]+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 +} diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 3e9374098..89ee90444 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -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, ``, 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, "") - } 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, ``, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity) - fmt.Fprintf(writer, ``, - targetShape.Width, targetShape.Height, containerStyle) - // Padding - fmt.Fprintf(writer, ``) - - 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, "", 1*float64(index+1)) - for _, token := range tokens { - text := svgEscaper.Replace(token.String()) - attr := styleAttr(svgStyles, token.Type) - if attr != "" { - text = fmt.Sprintf("%s", attr, text) - } - fmt.Fprint(writer, text) - } - fmt.Fprint(writer, "") - } - fmt.Fprintf(writer, "") + 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, ``, - 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, "
", "
") - fmt.Fprintf(writer, `
%v
`, render) - fmt.Fprint(writer, `
`) + + svgStyles := styleToSVG(style) + containerStyle := fmt.Sprintf(`stroke: %s;fill:%s`, targetShape.Stroke, style.Get(chroma.Background).Background.String()) + + fmt.Fprintf(writer, ``, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity) + fmt.Fprintf(writer, ``, + targetShape.Width, targetShape.Height, containerStyle) + // Padding + fmt.Fprintf(writer, ``) + + 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, "", 1*float64(index+1)) + for _, token := range tokens { + text := svgEscaper.Replace(token.String()) + attr := styleAttr(svgStyles, token.Type) + if attr != "" { + text = fmt.Sprintf("%s", attr, text) + } + fmt.Fprint(writer, text) + } + fmt.Fprint(writer, "") + } + fmt.Fprintf(writer, "") + case d2target.ShapeText: + if targetShape.Language == "latex" { + render, err := d2latex.Render(targetShape.Label) + if err != nil { + return err + } + fmt.Fprintf(writer, ``, box.TopLeft.X, box.TopLeft.Y, targetShape.Opacity) + fmt.Fprintf(writer, render) + fmt.Fprintf(writer, "") + } else { + render, err := textmeasure.RenderMarkdown(targetShape.Label) + if err != nil { + return err + } + fmt.Fprintf(writer, ``, + 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, "
", "
") + fmt.Fprintf(writer, `
%v
`, render) + fmt.Fprint(writer, `
`) + } default: fontColor := "black" if targetShape.Color != "" { diff --git a/e2etests/testdata/todo/latex/dagre/board.exp.json b/e2etests/testdata/todo/latex/dagre/board.exp.json index 96c82cce5..ffda2038b 100644 --- a/e2etests/testdata/todo/latex/dagre/board.exp.json +++ b/e2etests/testdata/todo/latex/dagre/board.exp.json @@ -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, diff --git a/e2etests/testdata/todo/latex/dagre/sketch.exp.svg b/e2etests/testdata/todo/latex/dagre/sketch.exp.svg index c086185bf..b1d90464b 100644 --- a/e2etests/testdata/todo/latex/dagre/sketch.exp.svg +++ b/e2etests/testdata/todo/latex/dagre/sketch.exp.svg @@ -2,7 +2,7 @@