diff --git a/d2renderers/d2sketch/sketch.go b/d2renderers/d2sketch/sketch.go index a8b6326b1..ea3eb941c 100644 --- a/d2renderers/d2sketch/sketch.go +++ b/d2renderers/d2sketch/sketch.go @@ -3,13 +3,17 @@ package d2sketch import ( "encoding/json" "fmt" + "strings" _ "embed" "github.com/dop251/goja" "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/svg" + "oss.terrastruct.com/util-go/go2" ) //go:embed fillpattern.svg @@ -81,10 +85,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) { strokeWidth: %d, %s });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) - if _, err := r.run(js); err != nil { - return "", err - } - paths, err := extractPaths(r) + paths, err := computeRoughPaths(r, js) if err != nil { return "", err } @@ -109,10 +110,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) { strokeWidth: %d, %s });`, shape.Width/2, shape.Height/2, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) - if _, err := r.run(js); err != nil { - return "", err - } - paths, err := extractPaths(r) + paths, err := computeRoughPaths(r, js) if err != nil { return "", err } @@ -140,10 +138,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) { strokeWidth: %d, %s });`, path, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) - if _, err := r.run(js); err != nil { - return "", err - } - sketchPaths, err := extractPaths(r) + sketchPaths, err := computeRoughPaths(r, js) if err != nil { return "", err } @@ -180,10 +175,7 @@ func connectionStyle(connection d2target.Connection) string { func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) { roughness := 1.0 js := fmt.Sprintf(`node = rc.path("%s", {roughness: %f, seed: 1});`, path, roughness) - if _, err := r.run(js); err != nil { - return "", err - } - paths, err := extractPaths(r) + paths, err := computeRoughPaths(r, js) if err != nil { return "", err } @@ -197,6 +189,295 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) ( return output, nil } +// TODO cleanup +func Table(r *Runner, shape d2target.Shape) (string, error) { + output := "" + js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { + fill: "%s", + stroke: "%s", + strokeWidth: %d, + %s + });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + paths, err := computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), + ) + } + + box := geo.NewBox( + geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)), + float64(shape.Width), + float64(shape.Height), + ) + rowHeight := box.Height / float64(1+len(shape.SQLTable.Columns)) + headerBox := geo.NewBox(box.TopLeft, box.Width, rowHeight) + + js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { + fill: "%s", + %s + });`, shape.Width, rowHeight, shape.Fill, baseRoughProps) + paths, err = computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + // TODO header fill + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, p, "#0a0f25", + ) + } + + if shape.Label != "" { + tl := label.InsideMiddleLeft.GetPointOnBox( + headerBox, + 20, + float64(shape.LabelWidth), + float64(shape.LabelHeight), + ) + + // TODO header font color + output += fmt.Sprintf(`%s`, + "text", + tl.X, + tl.Y+float64(shape.LabelHeight)*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", + "start", + 4+shape.FontSize, + "white", + ), + svg.EscapeText(shape.Label), + ) + } + + var longestNameWidth int + for _, f := range shape.Columns { + longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth) + } + + rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) + rowBox.TopLeft.Y += headerBox.Height + for _, f := range shape.Columns { + nameTL := label.InsideMiddleLeft.GetPointOnBox( + rowBox, + d2target.NamePadding, + rowBox.Width, + float64(shape.FontSize), + ) + constraintTR := label.InsideMiddleRight.GetPointOnBox( + rowBox, + d2target.TypePadding, + 0, + float64(shape.FontSize), + ) + + // TODO theme based + primaryColor := "rgb(13, 50, 178)" + accentColor := "rgb(74, 111, 243)" + neutralColor := "rgb(103, 108, 126)" + + output += strings.Join([]string{ + fmt.Sprintf(`%s`, + nameTL.X, + nameTL.Y+float64(shape.FontSize)*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), primaryColor), + svg.EscapeText(f.Name.Label), + ), + + // TODO light font + fmt.Sprintf(`%s`, + nameTL.X+float64(longestNameWidth)+2*d2target.NamePadding, + nameTL.Y+float64(shape.FontSize)*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", float64(shape.FontSize), neutralColor), + svg.EscapeText(f.Type.Label), + ), + + fmt.Sprintf(`%s`, + constraintTR.X, + constraintTR.Y+float64(shape.FontSize)*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;letter-spacing:2px;", "end", float64(shape.FontSize), accentColor), + f.ConstraintAbbr(), + ), + }, "\n") + + rowBox.TopLeft.Y += rowHeight + + js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, { + %s + });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps) + paths, err = computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + output += fmt.Sprintf( + ``, + p, "#0a0f25", + ) + } + } + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, shape.Width, shape.Height, + ) + return output, nil +} + +func Class(r *Runner, shape d2target.Shape) (string, error) { + output := "" + js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, { + fill: "%s", + stroke: "%s", + strokeWidth: %d, + %s + });`, shape.Width, shape.Height, shape.Fill, shape.Stroke, shape.StrokeWidth, baseRoughProps) + paths, err := computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, p, shapeStyle(shape), + ) + } + + box := geo.NewBox( + geo.NewPoint(float64(shape.Pos.X), float64(shape.Pos.Y)), + float64(shape.Width), + float64(shape.Height), + ) + + rowHeight := box.Height / float64(2+len(shape.Class.Fields)+len(shape.Class.Methods)) + headerBox := geo.NewBox(box.TopLeft, box.Width, 2*rowHeight) + + js = fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %f, { + fill: "%s", + %s + });`, shape.Width, headerBox.Height, shape.Fill, baseRoughProps) + paths, err = computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + // TODO header fill + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, p, "#0a0f25", + ) + } + + output += fmt.Sprintf( + ``, + shape.Pos.X, shape.Pos.Y, shape.Width, headerBox.Height, + ) + + if shape.Label != "" { + tl := label.InsideMiddleLeft.GetPointOnBox( + headerBox, + 0, + float64(shape.LabelWidth), + float64(shape.LabelHeight), + ) + + // TODO header font color + output += fmt.Sprintf(`%s`, + "text", + tl.X+float64(shape.LabelWidth)/2, + tl.Y+float64(shape.LabelHeight)*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", + "middle", + 4+shape.FontSize, + "white", + ), + svg.EscapeText(shape.Label), + ) + } + + rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) + rowBox.TopLeft.Y += headerBox.Height + for _, f := range shape.Fields { + output += classRow(rowBox, f.VisibilityToken(), f.Name, f.Type, float64(shape.FontSize)) + rowBox.TopLeft.Y += rowHeight + } + + js = fmt.Sprintf(`node = rc.line(%f, %f, %f, %f, { +%s + });`, rowBox.TopLeft.X, rowBox.TopLeft.Y, rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, baseRoughProps) + paths, err = computeRoughPaths(r, js) + if err != nil { + return "", err + } + for _, p := range paths { + output += fmt.Sprintf( + ``, + p, "#0a0f25", + ) + } + + for _, m := range shape.Methods { + output += classRow(rowBox, m.VisibilityToken(), m.Name, m.Return, float64(shape.FontSize)) + rowBox.TopLeft.Y += rowHeight + } + + return output, nil +} + +func classRow(box *geo.Box, prefix, nameText, typeText string, fontSize float64) string { + output := "" + prefixTL := label.InsideMiddleLeft.GetPointOnBox( + box, + d2target.PrefixPadding, + box.Width, + fontSize, + ) + typeTR := label.InsideMiddleRight.GetPointOnBox( + box, + d2target.TypePadding, + 0, + fontSize, + ) + + // TODO theme based + accentColor := "rgb(13, 50, 178)" + + output += strings.Join([]string{ + fmt.Sprintf(`%s`, + prefixTL.X, + prefixTL.Y+fontSize*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, accentColor), + prefix, + ), + + fmt.Sprintf(`%s`, + prefixTL.X+d2target.PrefixWidth, + prefixTL.Y+fontSize*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, "black"), + svg.EscapeText(nameText), + ), + + fmt.Sprintf(`%s`, + typeTR.X, + typeTR.Y+fontSize*3/4, + fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s;", "end", fontSize, accentColor), + svg.EscapeText(typeText), + ), + }, "\n") + return output +} + +func computeRoughPaths(r *Runner, js string) ([]string, error) { + if _, err := r.run(js); err != nil { + return nil, err + } + return extractPaths(r) +} + type attrs struct { D string `json:"d"` } diff --git a/d2renderers/d2sketch/sketch_test.go b/d2renderers/d2sketch/sketch_test.go index cfcb5abdb..87a2e17a2 100644 --- a/d2renderers/d2sketch/sketch_test.go +++ b/d2renderers/d2sketch/sketch_test.go @@ -223,6 +223,58 @@ queue -> package -> step callout -> stored_data -> person diamond -> oval -> circle hexagon -> cloud +`, + }, + { + name: "sql_tables", + script: `users: { + shape: sql_table + id: int + name: string + email: string + password: string + last_login: datetime +} + +products: { + shape: sql_table + id: int + price: decimal + sku: string + name: string +} + +orders: { + shape: sql_table + id: int + user_id: int + product_id: int +} + +shipments: { + shape: sql_table + id: int + order_id: int + tracking_number: string + status: string +} + +users.id <-> orders.user_id +products.id <-> orders.product_id +shipments.order_id <-> orders.id`, + }, + { + name: "class", + script: `manager: BatchManager { + shape: class + -num: int + -timeout: int + -pid + + +getStatus(): Enum + +getJobs(): "Job[]" + +setTimeout(seconds int) +} `, }, } diff --git a/d2renderers/d2sketch/testdata/class/sketch.exp.svg b/d2renderers/d2sketch/testdata/class/sketch.exp.svg new file mode 100644 index 000000000..ed2b21d25 --- /dev/null +++ b/d2renderers/d2sketch/testdata/class/sketch.exp.svg @@ -0,0 +1,55 @@ + + + + + + +BatchManager- +num +int- +timeout +int- +pid ++ +getStatus() +Enum+ +getJobs() +Job[]+ +setTimeout(seconds int) +void + + + \ No newline at end of file diff --git a/d2renderers/d2sketch/testdata/sql_tables/sketch.exp.svg b/d2renderers/d2sketch/testdata/sql_tables/sketch.exp.svg new file mode 100644 index 000000000..5509ac7ab --- /dev/null +++ b/d2renderers/d2sketch/testdata/sql_tables/sketch.exp.svg @@ -0,0 +1,75 @@ + + + + + + +usersid +int +name +string +email +string +password +string +last_login +datetime +productsid +int +price +decimal +sku +string +name +string +ordersid +int +user_id +int +product_id +int +shipmentsid +int +order_id +int +tracking_number +string +status +string + + + + \ No newline at end of file diff --git a/d2renderers/d2svg/class.go b/d2renderers/d2svg/class.go index af71ac1af..df30a8381 100644 --- a/d2renderers/d2svg/class.go +++ b/d2renderers/d2svg/class.go @@ -8,6 +8,7 @@ import ( "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" + "oss.terrastruct.com/d2/lib/svg" ) func classHeader(box *geo.Box, text string, textWidth, textHeight, fontSize float64) string { @@ -32,30 +33,24 @@ func classHeader(box *geo.Box, text string, textWidth, textHeight, fontSize floa 4+fontSize, "white", ), - escapeText(text), + svg.EscapeText(text), ) } return str } -const ( - prefixPadding = 10 - prefixWidth = 20 - typePadding = 20 -) - func classRow(box *geo.Box, prefix, nameText, typeText string, fontSize float64) string { // Row is made up of prefix, name, and type // e.g. | + firstName string | prefixTL := label.InsideMiddleLeft.GetPointOnBox( box, - prefixPadding, + d2target.PrefixPadding, box.Width, fontSize, ) typeTR := label.InsideMiddleRight.GetPointOnBox( box, - typePadding, + d2target.TypePadding, 0, fontSize, ) @@ -70,32 +65,21 @@ func classRow(box *geo.Box, prefix, nameText, typeText string, fontSize float64) ), fmt.Sprintf(`%s`, - prefixTL.X+prefixWidth, + prefixTL.X+d2target.PrefixWidth, prefixTL.Y+fontSize*3/4, fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, "black"), - escapeText(nameText), + svg.EscapeText(nameText), ), fmt.Sprintf(`%s`, typeTR.X, typeTR.Y+fontSize*3/4, fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "end", fontSize, accentColor), - escapeText(typeText), + svg.EscapeText(typeText), ), }, "\n") } -func visibilityToken(visibility string) string { - switch visibility { - case "protected": - return "#" - case "private": - return "-" - default: - return "+" - } -} - func drawClass(writer io.Writer, targetShape d2target.Shape) { fmt.Fprintf(writer, ``, targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) @@ -114,9 +98,9 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) { rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) rowBox.TopLeft.Y += headerBox.Height - for _, f := range targetShape.Class.Fields { + for _, f := range targetShape.Fields { fmt.Fprint(writer, - classRow(rowBox, visibilityToken(f.Visibility), f.Name, f.Type, float64(targetShape.FontSize)), + classRow(rowBox, f.VisibilityToken(), f.Name, f.Type, float64(targetShape.FontSize)), ) rowBox.TopLeft.Y += rowHeight } @@ -126,9 +110,9 @@ func drawClass(writer io.Writer, targetShape d2target.Shape) { rowBox.TopLeft.X+rowBox.Width, rowBox.TopLeft.Y, fmt.Sprintf("stroke-width:1;stroke:%v", targetShape.Stroke)) - for _, m := range targetShape.Class.Methods { + for _, m := range targetShape.Methods { fmt.Fprint(writer, - classRow(rowBox, visibilityToken(m.Visibility), m.Name, m.Return, float64(targetShape.FontSize)), + classRow(rowBox, m.VisibilityToken(), m.Name, m.Return, float64(targetShape.FontSize)), ) rowBox.TopLeft.Y += rowHeight } diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 7bd081068..46ae19a2d 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -5,7 +5,6 @@ package d2svg import ( "bytes" _ "embed" - "encoding/xml" "errors" "fmt" "hash/fnv" @@ -357,7 +356,7 @@ func makeLabelMask(labelTL *geo.Point, width, height int) string { } func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, _ error) { - fmt.Fprintf(writer, ``, escapeText(connection.ID)) + fmt.Fprintf(writer, ``, svg.EscapeText(connection.ID)) var markerStart string if connection.SrcArrow != d2target.NoArrowhead { id := arrowheadMarkerID(false, connection) @@ -536,7 +535,7 @@ func render3dRect(targetShape d2target.Shape) string { strings.Join(borderSegments, " "), borderStyle) // create mask from border stroke, to cut away from the shape fills - maskID := fmt.Sprintf("border-mask-%v", escapeText(targetShape.ID)) + maskID := fmt.Sprintf("border-mask-%v", svg.EscapeText(targetShape.ID)) borderMask := strings.Join([]string{ fmt.Sprintf(``, maskID, targetShape.Pos.X, targetShape.Pos.Y-threeDeeOffset, targetShape.Width+threeDeeOffset, targetShape.Height+threeDeeOffset, @@ -583,7 +582,7 @@ func render3dRect(targetShape d2target.Shape) string { } func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2sketch.Runner) (labelMask string, err error) { - fmt.Fprintf(writer, ``, escapeText(targetShape.ID)) + fmt.Fprintf(writer, ``, svg.EscapeText(targetShape.ID)) tl := geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)) width := float64(targetShape.Width) height := float64(targetShape.Height) @@ -618,11 +617,27 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske switch targetShape.Type { case d2target.ShapeClass: - drawClass(writer, targetShape) + if sketchRunner != nil { + out, err := d2sketch.Class(sketchRunner, targetShape) + if err != nil { + return "", err + } + fmt.Fprintf(writer, out) + } else { + drawClass(writer, targetShape) + } fmt.Fprintf(writer, ``) return labelMask, nil case d2target.ShapeSQLTable: - drawTable(writer, targetShape) + if sketchRunner != nil { + out, err := d2sketch.Table(sketchRunner, targetShape) + if err != nil { + return "", err + } + fmt.Fprintf(writer, out) + } else { + drawTable(writer, targetShape) + } fmt.Fprintf(writer, ``) return labelMask, nil case d2target.ShapeOval: @@ -823,15 +838,9 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske return labelMask, nil } -func escapeText(text string) string { - buf := new(bytes.Buffer) - _ = xml.EscapeText(buf, []byte(text)) - return buf.String() -} - func renderText(text string, x, height float64) string { if !strings.Contains(text, "\n") { - return escapeText(text) + return svg.EscapeText(text) } rendered := []string{} lines := strings.Split(text, "\n") @@ -840,7 +849,7 @@ func renderText(text string, x, height float64) string { if i == 0 { dy = 0 } - escaped := escapeText(line) + escaped := svg.EscapeText(line) if escaped == "" { // if there are multiple newlines in a row we still need text for the tspan to render escaped = " " diff --git a/d2renderers/d2svg/table.go b/d2renderers/d2svg/table.go index 9407c0b82..5b4abaed9 100644 --- a/d2renderers/d2svg/table.go +++ b/d2renderers/d2svg/table.go @@ -8,6 +8,7 @@ import ( "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" + "oss.terrastruct.com/d2/lib/svg" "oss.terrastruct.com/util-go/go2" ) @@ -32,7 +33,7 @@ func tableHeader(box *geo.Box, text string, textWidth, textHeight, fontSize floa 4+fontSize, "white", ), - escapeText(text), + svg.EscapeText(text), ) } return str @@ -64,7 +65,7 @@ func tableRow(box *geo.Box, nameText, typeText, constraintText string, fontSize, nameTL.X, nameTL.Y+fontSize*3/4, fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, primaryColor), - escapeText(nameText), + svg.EscapeText(nameText), ), // TODO light font @@ -72,7 +73,7 @@ func tableRow(box *geo.Box, nameText, typeText, constraintText string, fontSize, nameTL.X+longestNameWidth+2*d2target.NamePadding, nameTL.Y+fontSize*3/4, fmt.Sprintf("text-anchor:%s;font-size:%vpx;fill:%s", "start", fontSize, neutralColor), - escapeText(typeText), + svg.EscapeText(typeText), ), fmt.Sprintf(`%s`, @@ -84,19 +85,6 @@ func tableRow(box *geo.Box, nameText, typeText, constraintText string, fontSize, }, "\n") } -func constraintAbbr(constraint string) string { - switch constraint { - case "primary_key": - return "PK" - case "foreign_key": - return "FK" - case "unique": - return "UNQ" - default: - return "" - } -} - func drawTable(writer io.Writer, targetShape d2target.Shape) { fmt.Fprintf(writer, ``, targetShape.Pos.X, targetShape.Pos.Y, targetShape.Width, targetShape.Height, shapeStyle(targetShape)) @@ -114,15 +102,15 @@ func drawTable(writer io.Writer, targetShape d2target.Shape) { ) var longestNameWidth int - for _, f := range targetShape.SQLTable.Columns { + for _, f := range targetShape.Columns { longestNameWidth = go2.Max(longestNameWidth, f.Name.LabelWidth) } rowBox := geo.NewBox(box.TopLeft.Copy(), box.Width, rowHeight) rowBox.TopLeft.Y += headerBox.Height - for _, f := range targetShape.SQLTable.Columns { + for _, f := range targetShape.Columns { fmt.Fprint(writer, - tableRow(rowBox, f.Name.Label, f.Type.Label, constraintAbbr(f.Constraint), float64(targetShape.FontSize), float64(longestNameWidth)), + tableRow(rowBox, f.Name.Label, f.Type.Label, f.ConstraintAbbr(), float64(targetShape.FontSize), float64(longestNameWidth)), ) rowBox.TopLeft.Y += rowHeight fmt.Fprintf(writer, ``, diff --git a/d2target/class.go b/d2target/class.go index 8a0a7e71e..f62a3ea43 100644 --- a/d2target/class.go +++ b/d2target/class.go @@ -6,6 +6,11 @@ import ( "oss.terrastruct.com/d2/d2renderers/d2fonts" ) +const ( + PrefixPadding = 10 + PrefixWidth = 20 +) + type Class struct { Fields []ClassField `json:"fields"` Methods []ClassMethod `json:"methods"` @@ -27,6 +32,17 @@ func (cf ClassField) Text() *MText { } } +func (cf ClassField) VisibilityToken() string { + switch cf.Visibility { + case "protected": + return "#" + case "private": + return "-" + default: + return "+" + } +} + type ClassMethod struct { Name string `json:"name"` Return string `json:"return"` @@ -42,3 +58,14 @@ func (cm ClassMethod) Text() *MText { Shape: "class", } } + +func (cm ClassMethod) VisibilityToken() string { + switch cm.Visibility { + case "protected": + return "#" + case "private": + return "-" + default: + return "+" + } +} diff --git a/d2target/sqltable.go b/d2target/sqltable.go index 8642e77a0..ad6b6f61b 100644 --- a/d2target/sqltable.go +++ b/d2target/sqltable.go @@ -31,3 +31,16 @@ func (c SQLColumn) Text() *MText { Shape: "sql_table", } } + +func (c SQLColumn) ConstraintAbbr() string { + switch c.Constraint { + case "primary_key": + return "PK" + case "foreign_key": + return "FK" + case "unique": + return "UNQ" + default: + return "" + } +} diff --git a/lib/svg/text.go b/lib/svg/text.go new file mode 100644 index 000000000..a729d5675 --- /dev/null +++ b/lib/svg/text.go @@ -0,0 +1,12 @@ +package svg + +import ( + "bytes" + "encoding/xml" +) + +func EscapeText(text string) string { + buf := new(bytes.Buffer) + _ = xml.EscapeText(buf, []byte(text)) + return buf.String() +}