diff --git a/d2ast/keywords.go b/d2ast/keywords.go
index 7ad1fae65..65f67b775 100644
--- a/d2ast/keywords.go
+++ b/d2ast/keywords.go
@@ -8,7 +8,6 @@ var ReservedKeywords map[string]struct{}
// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
- "desc": {},
"shape": {},
"icon": {},
"constraint": {},
@@ -31,17 +30,17 @@ var SimpleReservedKeywords = map[string]struct{}{
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
var ReservedKeywordHolders = map[string]struct{}{
- "style": {},
- "source-arrowhead": {},
- "target-arrowhead": {},
+ "style": {},
}
// CompositeReservedKeywords are reserved keywords that can hold composites
var CompositeReservedKeywords = map[string]struct{}{
- "classes": {},
- "constraint": {},
- "label": {},
- "icon": {},
+ "source-arrowhead": {},
+ "target-arrowhead": {},
+ "classes": {},
+ "constraint": {},
+ "label": {},
+ "icon": {},
}
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
diff --git a/d2lsp/completion.go b/d2lsp/completion.go
new file mode 100644
index 000000000..cbdd2706c
--- /dev/null
+++ b/d2lsp/completion.go
@@ -0,0 +1,500 @@
+// Completion implements lsp autocomplete features
+// Currently handles:
+// - Complete dot and inside maps for reserved keyword holders (style, labels, etc)
+// - Complete discrete values for keywords like shape
+// - Complete suggestions for formats for keywords like opacity
+package d2lsp
+
+import (
+ "strings"
+ "unicode"
+
+ "oss.terrastruct.com/d2/d2ast"
+ "oss.terrastruct.com/d2/d2parser"
+ "oss.terrastruct.com/d2/d2target"
+)
+
+type CompletionKind int
+
+const (
+ KeywordCompletion CompletionKind = iota
+ StyleCompletion
+ ShapeCompletion
+)
+
+type CompletionItem struct {
+ Label string
+ Kind CompletionKind
+ Detail string
+ InsertText string
+}
+
+func GetCompletionItems(text string, line, column int) ([]CompletionItem, error) {
+ ast, err := d2parser.Parse("", strings.NewReader(text), nil)
+ if err != nil {
+ ast, _ = d2parser.Parse("", strings.NewReader(getTextUntilPosition(text, line, column)), nil)
+ }
+
+ keyword := getKeywordContext(text, ast, line, column)
+ switch keyword {
+ case "style", "style.":
+ return getStyleCompletions(), nil
+ case "shape", "shape:":
+ return getShapeCompletions(), nil
+ case "shadow", "3d", "multiple", "animated", "bold", "italic", "underline", "filled", "double-border",
+ "shadow:", "3d:", "multiple:", "animated:", "bold:", "italic:", "underline:", "filled:", "double-border:":
+ return getBooleanCompletions(), nil
+ case "fill-pattern", "fill-pattern:":
+ return getFillPatternCompletions(), nil
+ case "text-transform", "text-transform:":
+ return getTextTransformCompletions(), nil
+ case "opacity", "stroke-width", "stroke-dash", "border-radius", "font-size",
+ "stroke", "fill", "font-color":
+ return getValueCompletions(keyword), nil
+ case "opacity:", "stroke-width:", "stroke-dash:", "border-radius:", "font-size:",
+ "stroke:", "fill:", "font-color:":
+ return getValueCompletions(keyword[:len(keyword)-1]), nil
+ case "width", "height", "top", "left":
+ return getValueCompletions(keyword), nil
+ case "width:", "height:", "top:", "left:":
+ return getValueCompletions(keyword[:len(keyword)-1]), nil
+ case "source-arrowhead", "target-arrowhead":
+ return getArrowheadCompletions(), nil
+ case "source-arrowhead.shape:", "target-arrowhead.shape:":
+ return getArrowheadShapeCompletions(), nil
+ case "label", "label.":
+ return getLabelCompletions(), nil
+ case "icon", "icon:":
+ return getIconCompletions(), nil
+ case "icon.":
+ return getLabelCompletions(), nil
+ case "near", "near:":
+ return getNearCompletions(), nil
+ case "tooltip:", "tooltip":
+ return getTooltipCompletions(), nil
+ case "direction:", "direction":
+ return getDirectionCompletions(), nil
+ default:
+ return nil, nil
+ }
+}
+
+func getTextUntilPosition(text string, line, column int) string {
+ lines := strings.Split(text, "\n")
+ if line >= len(lines) {
+ return text
+ }
+
+ result := strings.Join(lines[:line], "\n")
+ if len(result) > 0 {
+ result += "\n"
+ }
+ if column > len(lines[line]) {
+ result += lines[line]
+ } else {
+ result += lines[line][:column]
+ }
+ return result
+}
+
+func getKeywordContext(text string, m *d2ast.Map, line, column int) string {
+ if m == nil {
+ return ""
+ }
+ lines := strings.Split(text, "\n")
+
+ for _, n := range m.Nodes {
+ if n.MapKey == nil {
+ continue
+ }
+
+ var firstPart, lastPart string
+ var key *d2ast.KeyPath
+ if len(n.MapKey.Edges) > 0 {
+ key = n.MapKey.EdgeKey
+ } else {
+ key = n.MapKey.Key
+ }
+ if key != nil && len(key.Path) > 0 {
+ firstKey := key.Path[0].Unbox()
+ if !firstKey.IsUnquoted() {
+ continue
+ }
+ firstPart = firstKey.ScalarString()
+
+ pathLen := len(key.Path)
+ if pathLen > 1 {
+ lastKey := key.Path[pathLen-1].Unbox()
+ if lastKey.IsUnquoted() {
+ lastPart = lastKey.ScalarString()
+ _, isHolderLast := d2ast.ReservedKeywordHolders[lastPart]
+ if !isHolderLast {
+ _, isHolderLast = d2ast.CompositeReservedKeywords[lastPart]
+ }
+ keyRange := n.MapKey.Range
+ lineText := lines[keyRange.End.Line]
+ if isHolderLast && isAfterDot(lineText, column) {
+ return lastPart + "."
+ }
+ }
+ }
+ }
+ if _, isBoard := d2ast.BoardKeywords[firstPart]; isBoard {
+ firstPart = ""
+ }
+
+ _, isHolder := d2ast.ReservedKeywordHolders[firstPart]
+ if !isHolder {
+ _, isHolder = d2ast.CompositeReservedKeywords[firstPart]
+ }
+
+ // Check nested map
+ if n.MapKey.Value.Map != nil && isPositionInMap(line, column, n.MapKey.Value.Map) {
+ if nested := getKeywordContext(text, n.MapKey.Value.Map, line, column); nested != "" {
+ if isHolder {
+ // If we got a direct key completion from inside a holder's map,
+ // prefix it with the holder's name
+ if strings.HasSuffix(nested, ":") && !strings.Contains(nested, ".") {
+ return firstPart + "." + strings.TrimSuffix(nested, ":") + ":"
+ }
+ }
+ return nested
+ }
+ return firstPart
+ }
+
+ keyRange := n.MapKey.Range
+ if line != keyRange.End.Line {
+ continue
+ }
+
+ // 1) Skip if cursor is well above/below this key
+ if line < keyRange.Start.Line || line > keyRange.End.Line {
+ continue
+ }
+
+ // 2) If on the start line, skip if before the key
+ if line == keyRange.Start.Line && column < keyRange.Start.Column {
+ continue
+ }
+
+ // 3) If on the end line, allow up to keyRange.End.Column + 1
+ if line == keyRange.End.Line && column > keyRange.End.Column+1 {
+ continue
+ }
+
+ lineText := lines[keyRange.End.Line]
+
+ if isAfterColon(lineText, column) {
+ if key != nil && len(key.Path) > 1 {
+ if isHolder && (firstPart == "source-arrowhead" || firstPart == "target-arrowhead") {
+ return firstPart + "." + lastPart + ":"
+ }
+
+ _, isHolder := d2ast.ReservedKeywordHolders[lastPart]
+ if !isHolder {
+ return lastPart
+ }
+ }
+ return firstPart + ":"
+ }
+
+ if isAfterDot(lineText, column) && isHolder {
+ return firstPart
+ }
+ }
+
+ return ""
+}
+
+func isAfterDot(text string, pos int) bool {
+ return pos > 0 && pos <= len(text) && text[pos-1] == '.'
+}
+
+func isAfterColon(text string, pos int) bool {
+ if pos < 1 || pos > len(text) {
+ return false
+ }
+ i := pos - 1
+ for i >= 0 && unicode.IsSpace(rune(text[i])) {
+ i--
+ }
+ return i >= 0 && text[i] == ':'
+}
+
+func isPositionInMap(line, column int, m *d2ast.Map) bool {
+ if m == nil {
+ return false
+ }
+
+ mapRange := m.Range
+ if line < mapRange.Start.Line || line > mapRange.End.Line {
+ return false
+ }
+
+ if line == mapRange.Start.Line && column < mapRange.Start.Column {
+ return false
+ }
+ if line == mapRange.End.Line && column > mapRange.End.Column {
+ return false
+ }
+ return true
+}
+
+func getShapeCompletions() []CompletionItem {
+ items := make([]CompletionItem, 0, len(d2target.Shapes))
+ for _, shape := range d2target.Shapes {
+ item := CompletionItem{
+ Label: shape,
+ Kind: ShapeCompletion,
+ Detail: "shape",
+ InsertText: shape,
+ }
+ items = append(items, item)
+ }
+ return items
+}
+
+func getValueCompletions(property string) []CompletionItem {
+ switch property {
+ case "opacity":
+ return []CompletionItem{{
+ Label: "(number between 0.0 and 1.0)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 0.4",
+ InsertText: "",
+ }}
+ case "stroke-width":
+ return []CompletionItem{{
+ Label: "(number between 0 and 15)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 2",
+ InsertText: "",
+ }}
+ case "font-size":
+ return []CompletionItem{{
+ Label: "(number between 8 and 100)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 14",
+ InsertText: "",
+ }}
+ case "stroke-dash":
+ return []CompletionItem{{
+ Label: "(number between 0 and 10)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 5",
+ InsertText: "",
+ }}
+ case "border-radius":
+ return []CompletionItem{{
+ Label: "(number greater than or equal to 0)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 4",
+ InsertText: "",
+ }}
+ case "font-color", "stroke", "fill":
+ return []CompletionItem{{
+ Label: "(color name or hex code)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. blue, #ff0000",
+ InsertText: "",
+ }}
+ case "width", "height", "top", "left":
+ return []CompletionItem{{
+ Label: "(pixels)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. 400",
+ InsertText: "",
+ }}
+ }
+ return nil
+}
+
+func getStyleCompletions() []CompletionItem {
+ items := make([]CompletionItem, 0, len(d2ast.StyleKeywords))
+ for keyword := range d2ast.StyleKeywords {
+ item := CompletionItem{
+ Label: keyword,
+ Kind: StyleCompletion,
+ Detail: "style property",
+ InsertText: keyword + ": ",
+ }
+ items = append(items, item)
+ }
+ return items
+}
+
+func getBooleanCompletions() []CompletionItem {
+ return []CompletionItem{
+ {
+ Label: "true",
+ Kind: KeywordCompletion,
+ Detail: "boolean",
+ InsertText: "true",
+ },
+ {
+ Label: "false",
+ Kind: KeywordCompletion,
+ Detail: "boolean",
+ InsertText: "false",
+ },
+ }
+}
+
+func getFillPatternCompletions() []CompletionItem {
+ items := make([]CompletionItem, 0, len(d2ast.FillPatterns))
+ for _, pattern := range d2ast.FillPatterns {
+ item := CompletionItem{
+ Label: pattern,
+ Kind: KeywordCompletion,
+ Detail: "fill pattern",
+ InsertText: pattern,
+ }
+ items = append(items, item)
+ }
+ return items
+}
+
+func getTextTransformCompletions() []CompletionItem {
+ items := make([]CompletionItem, 0, len(d2ast.TextTransforms))
+ for _, transform := range d2ast.TextTransforms {
+ item := CompletionItem{
+ Label: transform,
+ Kind: KeywordCompletion,
+ Detail: "text transform",
+ InsertText: transform,
+ }
+ items = append(items, item)
+ }
+ return items
+}
+
+func isOnEmptyLine(text string, line int) bool {
+ lines := strings.Split(text, "\n")
+ if line >= len(lines) {
+ return true
+ }
+
+ return strings.TrimSpace(lines[line]) == ""
+}
+
+func getLabelCompletions() []CompletionItem {
+ return []CompletionItem{{
+ Label: "near",
+ Kind: StyleCompletion,
+ Detail: "label position",
+ InsertText: "near: ",
+ }}
+}
+
+func getNearCompletions() []CompletionItem {
+ items := make([]CompletionItem, 0, len(d2ast.LabelPositionsArray)+1)
+
+ items = append(items, CompletionItem{
+ Label: "(object ID)",
+ Kind: KeywordCompletion,
+ Detail: "e.g. container.inner_shape",
+ InsertText: "",
+ })
+
+ for _, pos := range d2ast.LabelPositionsArray {
+ item := CompletionItem{
+ Label: pos,
+ Kind: KeywordCompletion,
+ Detail: "label position",
+ InsertText: pos,
+ }
+ items = append(items, item)
+ }
+ return items
+}
+
+func getTooltipCompletions() []CompletionItem {
+ return []CompletionItem{
+ {
+ Label: "(markdown)",
+ Kind: KeywordCompletion,
+ Detail: "markdown formatted text",
+ InsertText: "|md\n # Tooltip\n Hello world\n|",
+ },
+ }
+}
+
+func getIconCompletions() []CompletionItem {
+ return []CompletionItem{
+ {
+ Label: "(URL, e.g. https://icons.terrastruct.com/xyz.svg)",
+ Kind: KeywordCompletion,
+ Detail: "icon URL",
+ InsertText: "https://icons.terrastruct.com/essentials%2F073-add.svg",
+ },
+ }
+}
+
+func getDirectionCompletions() []CompletionItem {
+ directions := []string{"up", "down", "right", "left"}
+ items := make([]CompletionItem, len(directions))
+ for i, dir := range directions {
+ items[i] = CompletionItem{
+ Label: dir,
+ Kind: KeywordCompletion,
+ Detail: "direction",
+ InsertText: dir,
+ }
+ }
+ return items
+}
+
+func getArrowheadShapeCompletions() []CompletionItem {
+ arrowheads := []string{
+ "triangle",
+ "arrow",
+ "diamond",
+ "circle",
+ "cf-one", "cf-one-required",
+ "cf-many", "cf-many-required",
+ }
+
+ items := make([]CompletionItem, len(arrowheads))
+ details := map[string]string{
+ "triangle": "default",
+ "arrow": "like triangle but pointier",
+ "cf-one": "crows foot one",
+ "cf-one-required": "crows foot one (required)",
+ "cf-many": "crows foot many",
+ "cf-many-required": "crows foot many (required)",
+ }
+
+ for i, shape := range arrowheads {
+ detail := details[shape]
+ if detail == "" {
+ detail = "arrowhead shape"
+ }
+ items[i] = CompletionItem{
+ Label: shape,
+ Kind: ShapeCompletion,
+ Detail: detail,
+ InsertText: shape,
+ }
+ }
+ return items
+}
+
+func getArrowheadCompletions() []CompletionItem {
+ completions := []string{
+ "shape",
+ "label",
+ "style.filled",
+ }
+
+ items := make([]CompletionItem, len(completions))
+
+ for i, shape := range completions {
+ items[i] = CompletionItem{
+ Label: shape,
+ Kind: ShapeCompletion,
+ InsertText: shape,
+ }
+ }
+ return items
+}
diff --git a/d2lsp/completion_test.go b/d2lsp/completion_test.go
new file mode 100644
index 000000000..9937ac358
--- /dev/null
+++ b/d2lsp/completion_test.go
@@ -0,0 +1,421 @@
+package d2lsp
+
+import (
+ "testing"
+)
+
+func TestGetCompletionItems(t *testing.T) {
+ tests := []struct {
+ name string
+ text string
+ line int
+ column int
+ want []CompletionItem
+ wantErr bool
+ }{
+ {
+ name: "style dot suggestions",
+ text: "a.style.",
+ line: 0,
+ column: 8,
+ want: getStyleCompletions(),
+ },
+ {
+ name: "style map suggestions",
+ text: `a: {
+ style.
+}
+`,
+ line: 1,
+ column: 8,
+ want: getStyleCompletions(),
+ },
+ {
+ name: "3d style map suggestions",
+ text: `a.style: {
+ 3d:
+}
+`,
+ line: 1,
+ column: 5,
+ want: getBooleanCompletions(),
+ },
+ {
+ name: "fill pattern style map suggestions",
+ text: `a.style: {
+ fill-pattern:
+}
+`,
+ line: 1,
+ column: 15,
+ want: getFillPatternCompletions(),
+ },
+ {
+ name: "opacity style map suggestions",
+ text: `a.style: {
+ opacity:
+}
+`,
+ line: 1,
+ column: 10,
+ want: getValueCompletions("opacity"),
+ },
+ {
+ name: "width dot",
+ text: `a.width:`,
+ line: 0,
+ column: 8,
+ want: getValueCompletions("width"),
+ },
+ {
+ name: "layer shape",
+ text: `a
+
+layers: {
+ hey: {
+ go: {
+ shape:
+ }
+ }
+}
+`,
+ line: 5,
+ column: 12,
+ want: getShapeCompletions(),
+ },
+ {
+ name: "stroke width value",
+ text: `a.style.stroke-width: 1`,
+ line: 0,
+ column: 23,
+ want: nil,
+ },
+ {
+ name: "no style suggestions",
+ text: `a.style:
+`,
+ line: 0,
+ column: 8,
+ want: nil,
+ },
+ {
+ name: "style property suggestions",
+ text: "a -> b: { style. }",
+ line: 0,
+ column: 16,
+ want: getStyleCompletions(),
+ },
+ {
+ name: "style.opacity value hint",
+ text: "a -> b: { style.opacity: }",
+ line: 0,
+ column: 24,
+ want: getValueCompletions("opacity"),
+ },
+ {
+ name: "fill pattern completions",
+ text: "a -> b: { style.fill-pattern: }",
+ line: 0,
+ column: 29,
+ want: getFillPatternCompletions(),
+ },
+ {
+ name: "text transform completions",
+ text: "a -> b: { style.text-transform: }",
+ line: 0,
+ column: 31,
+ want: getTextTransformCompletions(),
+ },
+ {
+ name: "boolean property completions",
+ text: "a -> b: { style.shadow: }",
+ line: 0,
+ column: 23,
+ want: getBooleanCompletions(),
+ },
+ {
+ name: "near position completions",
+ text: "a -> b: { label.near: }",
+ line: 0,
+ column: 21,
+ want: getNearCompletions(),
+ },
+ {
+ name: "direction completions",
+ text: "a -> b: { direction: }",
+ line: 0,
+ column: 20,
+ want: getDirectionCompletions(),
+ },
+ {
+ name: "icon url completions",
+ text: "a -> b: { icon: }",
+ line: 0,
+ column: 15,
+ want: getIconCompletions(),
+ },
+ {
+ name: "icon dot url completions",
+ text: "a.icon:",
+ line: 0,
+ column: 7,
+ want: getIconCompletions(),
+ },
+ {
+ name: "icon near completions",
+ text: "a -> b: { icon.near: }",
+ line: 0,
+ column: 20,
+ want: getNearCompletions(),
+ },
+ {
+ name: "icon map",
+ text: `a.icon: {
+ # here
+}`,
+ line: 1,
+ column: 2,
+ want: nil,
+ },
+ {
+ name: "icon flat dot",
+ text: `a.icon.`,
+ line: 0,
+ column: 7,
+ want: getLabelCompletions(),
+ },
+ {
+ name: "label flat dot",
+ text: `a.label.`,
+ line: 0,
+ column: 8,
+ want: getLabelCompletions(),
+ },
+ {
+ name: "arrowhead completions - dot syntax",
+ text: "a -> b: { source-arrowhead. }",
+ line: 0,
+ column: 27,
+ want: getArrowheadCompletions(),
+ },
+ {
+ name: "arrowhead completions - colon syntax",
+ text: "a -> b: { source-arrowhead: }",
+ line: 0,
+ column: 27,
+ want: nil,
+ },
+ {
+ name: "arrowhead completions - map syntax",
+ text: `a -> b: {
+ source-arrowhead: {
+ # here
+ }
+}`,
+ line: 2,
+ column: 4,
+ want: getArrowheadCompletions(),
+ },
+ {
+ name: "arrowhead shape completions - flat dot syntax",
+ text: "(a -> b)[0].source-arrowhead.shape:",
+ line: 0,
+ column: 35,
+ want: getArrowheadShapeCompletions(),
+ },
+ {
+ name: "arrowhead shape completions - dot syntax",
+ text: "a -> b: { source-arrowhead.shape: }",
+ line: 0,
+ column: 33,
+ want: getArrowheadShapeCompletions(),
+ },
+ {
+ name: "arrowhead shape completions - map syntax",
+ text: "a -> b: { source-arrowhead: { shape: } }",
+ line: 0,
+ column: 36,
+ want: getArrowheadShapeCompletions(),
+ },
+ {
+ name: "width value hint",
+ text: "a -> b: { width: }",
+ line: 0,
+ column: 16,
+ want: getValueCompletions("width"),
+ },
+ {
+ name: "height value hint",
+ text: "a -> b: { height: }",
+ line: 0,
+ column: 17,
+ want: getValueCompletions("height"),
+ },
+ {
+ name: "tooltip markdown template",
+ text: "a -> b: { tooltip: }",
+ line: 0,
+ column: 18,
+ want: getTooltipCompletions(),
+ },
+ {
+ name: "tooltip dot markdown template",
+ text: "a.tooltip:",
+ line: 0,
+ column: 10,
+ want: getTooltipCompletions(),
+ },
+ {
+ name: "shape dot suggestions",
+ text: "a.shape:",
+ line: 0,
+ column: 8,
+ want: getShapeCompletions(),
+ },
+ {
+ name: "shape suggestions",
+ text: "a -> b: { shape: }",
+ line: 0,
+ column: 16,
+ want: getShapeCompletions(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := GetCompletionItems(tt.text, tt.line, tt.column)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetCompletionItems() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ if len(got) != len(tt.want) {
+ t.Errorf("GetCompletionItems() got %d completions, want %d", len(got), len(tt.want))
+ return
+ }
+
+ // Create maps for easy comparison
+ gotMap := make(map[string]CompletionItem)
+ wantMap := make(map[string]CompletionItem)
+ for _, item := range got {
+ gotMap[item.Label] = item
+ }
+ for _, item := range tt.want {
+ wantMap[item.Label] = item
+ }
+
+ // Check that each completion exists and has correct properties
+ for label, wantItem := range wantMap {
+ gotItem, exists := gotMap[label]
+ if !exists {
+ t.Errorf("missing completion for %q", label)
+ continue
+ }
+ if gotItem.Kind != wantItem.Kind {
+ t.Errorf("completion %q Kind = %v, want %v", label, gotItem.Kind, wantItem.Kind)
+ }
+ if gotItem.Detail != wantItem.Detail {
+ t.Errorf("completion %q Detail = %v, want %v", label, gotItem.Detail, wantItem.Detail)
+ }
+ if gotItem.InsertText != wantItem.InsertText {
+ t.Errorf("completion %q InsertText = %v, want %v", label, gotItem.InsertText, wantItem.InsertText)
+ }
+ }
+ })
+ }
+}
+
+// Helper function to compare CompletionItem slices
+func equalCompletions(a, b []CompletionItem) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i].Label != b[i].Label ||
+ a[i].Kind != b[i].Kind ||
+ a[i].Detail != b[i].Detail ||
+ a[i].InsertText != b[i].InsertText {
+ return false
+ }
+ }
+ return true
+}
+
+func TestGetArrowheadShapeCompletions(t *testing.T) {
+ got := getArrowheadShapeCompletions()
+
+ expectedLabels := []string{
+ "triangle", "arrow", "diamond", "circle",
+ "cf-one", "cf-one-required",
+ "cf-many", "cf-many-required",
+ }
+
+ if len(got) != len(expectedLabels) {
+ t.Errorf("getArrowheadShapeCompletions() returned %d items, want %d", len(got), len(expectedLabels))
+ return
+ }
+
+ for i, label := range expectedLabels {
+ if got[i].Label != label {
+ t.Errorf("completion[%d].Label = %v, want %v", i, got[i].Label, label)
+ }
+ if got[i].Kind != ShapeCompletion {
+ t.Errorf("completion[%d].Kind = %v, want ShapeCompletion", i, got[i].Kind)
+ }
+ if got[i].InsertText != label {
+ t.Errorf("completion[%d].InsertText = %v, want %v", i, got[i].InsertText, label)
+ }
+ }
+}
+
+func TestGetValueCompletions(t *testing.T) {
+ tests := []struct {
+ property string
+ wantLabel string
+ wantDetail string
+ }{
+ {
+ property: "opacity",
+ wantLabel: "(number between 0.0 and 1.0)",
+ wantDetail: "e.g. 0.4",
+ },
+ {
+ property: "stroke-width",
+ wantLabel: "(number between 0 and 15)",
+ wantDetail: "e.g. 2",
+ },
+ {
+ property: "font-size",
+ wantLabel: "(number between 8 and 100)",
+ wantDetail: "e.g. 14",
+ },
+ {
+ property: "width",
+ wantLabel: "(pixels)",
+ wantDetail: "e.g. 400",
+ },
+ {
+ property: "stroke",
+ wantLabel: "(color name or hex code)",
+ wantDetail: "e.g. blue, #ff0000",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.property, func(t *testing.T) {
+ got := getValueCompletions(tt.property)
+ if len(got) != 1 {
+ t.Fatalf("getValueCompletions(%s) returned %d items, want 1", tt.property, len(got))
+ }
+ if got[0].Label != tt.wantLabel {
+ t.Errorf("completion.Label = %v, want %v", got[0].Label, tt.wantLabel)
+ }
+ if got[0].Detail != tt.wantDetail {
+ t.Errorf("completion.Detail = %v, want %v", got[0].Detail, tt.wantDetail)
+ }
+ if got[0].InsertText != "" {
+ t.Errorf("completion.InsertText = %v, want empty string", got[0].InsertText)
+ }
+ })
+ }
+}
diff --git a/e2etests/testdata/stable/complex-layers/dagre/board.exp.json b/e2etests/testdata/stable/complex-layers/dagre/board.exp.json
index 21f84ea29..ae57a8c07 100644
--- a/e2etests/testdata/stable/complex-layers/dagre/board.exp.json
+++ b/e2etests/testdata/stable/complex-layers/dagre/board.exp.json
@@ -4,12 +4,54 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "window",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
+ "width": 261,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Multi-layer diagram of a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 216,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "window",
+ "type": "rectangle",
+ "pos": {
+ "x": 321,
+ "y": 0
+ },
"width": 103,
"height": 66,
"opacity": 1,
@@ -49,7 +91,7 @@
"id": "roof",
"type": "rectangle",
"pos": {
- "x": 163,
+ "x": 484,
"y": 0
},
"width": 75,
@@ -91,7 +133,7 @@
"id": "garage",
"type": "rectangle",
"pos": {
- "x": 298,
+ "x": 619,
"y": 0
},
"width": 94,
@@ -616,9 +658,52 @@
},
{
"name": "repair",
- "isFolderOnly": true,
+ "isFolderOnly": false,
"fontFamily": "SourceSansPro",
- "shapes": [],
+ "shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
"connections": [],
"root": {
"id": "",
@@ -667,11 +752,53 @@
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 50
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
{
"id": "find contractors",
"type": "rectangle",
"pos": {
- "x": 10,
+ "x": 250,
"y": 20
},
"width": 341,
@@ -713,7 +840,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 40,
+ "x": 280,
"y": 50
},
"width": 110,
@@ -755,7 +882,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 210,
+ "x": 450,
"y": 50
},
"width": 111,
@@ -842,11 +969,53 @@
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 50
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
{
"id": "find contractors",
"type": "rectangle",
"pos": {
- "x": 10,
+ "x": 250,
"y": 20
},
"width": 341,
@@ -888,7 +1057,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 40,
+ "x": 280,
"y": 50
},
"width": 110,
@@ -930,7 +1099,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 210,
+ "x": 450,
"y": 50
},
"width": 111,
@@ -972,7 +1141,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 196,
+ "x": 436,
"y": 266
},
"width": 140,
@@ -1038,19 +1207,19 @@
"link": "",
"route": [
{
- "x": 265.5,
+ "x": 505.5,
"y": 146
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 202
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 226
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 266
}
],
@@ -1108,11 +1277,53 @@
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 50
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
{
"id": "find contractors",
"type": "rectangle",
"pos": {
- "x": 10,
+ "x": 250,
"y": 20
},
"width": 341,
@@ -1154,7 +1365,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 40,
+ "x": 280,
"y": 50
},
"width": 110,
@@ -1196,7 +1407,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 210,
+ "x": 450,
"y": 50
},
"width": 111,
@@ -1238,7 +1449,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 196,
+ "x": 436,
"y": 266
},
"width": 140,
@@ -1280,7 +1491,7 @@
"id": "obtain quotes",
"type": "rectangle",
"pos": {
- "x": 401,
+ "x": 641,
"y": 50
},
"width": 143,
@@ -1322,7 +1533,7 @@
"id": "negotiate",
"type": "rectangle",
"pos": {
- "x": 417,
+ "x": 657,
"y": 266
},
"width": 112,
@@ -1388,19 +1599,19 @@
"link": "",
"route": [
{
- "x": 265.5,
+ "x": 505.5,
"y": 146
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 202
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 226
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 266
}
],
@@ -1436,19 +1647,19 @@
"link": "",
"route": [
{
- "x": 472.5,
+ "x": 712.5,
"y": 116
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 156
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 226
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 266
}
],
@@ -1506,11 +1717,53 @@
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 50
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
{
"id": "find contractors",
"type": "rectangle",
"pos": {
- "x": 10,
+ "x": 250,
"y": 20
},
"width": 341,
@@ -1552,7 +1805,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 40,
+ "x": 280,
"y": 50
},
"width": 110,
@@ -1594,7 +1847,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 210,
+ "x": 450,
"y": 50
},
"width": 111,
@@ -1636,7 +1889,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 196,
+ "x": 436,
"y": 266
},
"width": 140,
@@ -1678,7 +1931,7 @@
"id": "obtain quotes",
"type": "rectangle",
"pos": {
- "x": 401,
+ "x": 641,
"y": 50
},
"width": 143,
@@ -1720,7 +1973,7 @@
"id": "negotiate",
"type": "rectangle",
"pos": {
- "x": 417,
+ "x": 657,
"y": 266
},
"width": 112,
@@ -1762,7 +2015,7 @@
"id": "book the best bid",
"type": "rectangle",
"pos": {
- "x": 389,
+ "x": 629,
"y": 432
},
"width": 167,
@@ -1828,19 +2081,19 @@
"link": "",
"route": [
{
- "x": 265.5,
+ "x": 505.5,
"y": 146
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 202
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 226
},
{
- "x": 265.5,
+ "x": 505.5,
"y": 266
}
],
@@ -1876,19 +2129,19 @@
"link": "",
"route": [
{
- "x": 472.5,
+ "x": 712.5,
"y": 116
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 156
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 226
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 266
}
],
@@ -1924,19 +2177,19 @@
"link": "",
"route": [
{
- "x": 472.5,
+ "x": 712.5,
"y": 332
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 372
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 392
},
{
- "x": 472.5,
+ "x": 712.5,
"y": 432
}
],
@@ -1999,12 +2252,54 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "window",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
+ "width": 261,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Multi-layer diagram of a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 216,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "window",
+ "type": "rectangle",
+ "pos": {
+ "x": 321,
+ "y": 0
+ },
"width": 103,
"height": 66,
"opacity": 1,
@@ -2044,7 +2339,7 @@
"id": "roof",
"type": "rectangle",
"pos": {
- "x": 163,
+ "x": 484,
"y": 0
},
"width": 75,
@@ -2086,7 +2381,7 @@
"id": "garage",
"type": "rectangle",
"pos": {
- "x": 298,
+ "x": 619,
"y": 0
},
"width": 94,
@@ -2128,7 +2423,7 @@
"id": "water",
"type": "rectangle",
"pos": {
- "x": 452,
+ "x": 773,
"y": 0
},
"width": 88,
@@ -2170,7 +2465,7 @@
"id": "rain",
"type": "rectangle",
"pos": {
- "x": 600,
+ "x": 921,
"y": 0
},
"width": 73,
@@ -2212,7 +2507,7 @@
"id": "thunder",
"type": "rectangle",
"pos": {
- "x": 733,
+ "x": 1054,
"y": 0
},
"width": 103,
diff --git a/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg
index d692ba458..a657284fd 100644
--- a/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/complex-layers/dagre/sketch.exp.svg
@@ -1,17 +1,17 @@
-
\ No newline at end of file
diff --git a/e2etests/testdata/stable/complex-layers/elk/board.exp.json b/e2etests/testdata/stable/complex-layers/elk/board.exp.json
index d8da04f90..59583bc63 100644
--- a/e2etests/testdata/stable/complex-layers/elk/board.exp.json
+++ b/e2etests/testdata/stable/complex-layers/elk/board.exp.json
@@ -4,12 +4,54 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "window",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
"y": 12
},
+ "width": 261,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Multi-layer diagram of a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 216,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "window",
+ "type": "rectangle",
+ "pos": {
+ "x": 293,
+ "y": 12
+ },
"width": 103,
"height": 66,
"opacity": 1,
@@ -49,7 +91,7 @@
"id": "roof",
"type": "rectangle",
"pos": {
- "x": 135,
+ "x": 416,
"y": 12
},
"width": 75,
@@ -91,7 +133,7 @@
"id": "garage",
"type": "rectangle",
"pos": {
- "x": 230,
+ "x": 511,
"y": 12
},
"width": 94,
@@ -616,9 +658,52 @@
},
{
"name": "repair",
- "isFolderOnly": true,
+ "isFolderOnly": false,
"fontFamily": "SourceSansPro",
- "shapes": [],
+ "shapes": [
+ {
+ "id": "desc",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
"connections": [],
"root": {
"id": "",
@@ -668,10 +753,52 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "find contractors",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
+ "y": 62
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "find contractors",
+ "type": "rectangle",
+ "pos": {
+ "x": 232,
"y": 12
},
"width": 341,
@@ -713,7 +840,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 62,
+ "x": 282,
"y": 62
},
"width": 110,
@@ -755,7 +882,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 192,
+ "x": 412,
"y": 62
},
"width": 111,
@@ -843,10 +970,52 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "find contractors",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
+ "y": 62
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "find contractors",
+ "type": "rectangle",
+ "pos": {
+ "x": 232,
"y": 12
},
"width": 341,
@@ -888,7 +1057,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 62,
+ "x": 282,
"y": 62
},
"width": 110,
@@ -930,7 +1099,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 192,
+ "x": 412,
"y": 62
},
"width": 111,
@@ -972,7 +1141,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 112,
+ "x": 332,
"y": 248
},
"width": 140,
@@ -1038,11 +1207,11 @@
"link": "",
"route": [
{
- "x": 182.5,
+ "x": 402.5,
"y": 178
},
{
- "x": 182.5,
+ "x": 402.5,
"y": 248
}
],
@@ -1100,10 +1269,52 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "find contractors",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
+ "y": 62
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "find contractors",
+ "type": "rectangle",
+ "pos": {
+ "x": 232,
"y": 12
},
"width": 341,
@@ -1145,7 +1356,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 62,
+ "x": 282,
"y": 62
},
"width": 110,
@@ -1187,7 +1398,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 192,
+ "x": 412,
"y": 62
},
"width": 111,
@@ -1229,7 +1440,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 112,
+ "x": 332,
"y": 248
},
"width": 140,
@@ -1271,7 +1482,7 @@
"id": "obtain quotes",
"type": "rectangle",
"pos": {
- "x": 373,
+ "x": 593,
"y": 112
},
"width": 143,
@@ -1313,7 +1524,7 @@
"id": "negotiate",
"type": "rectangle",
"pos": {
- "x": 388,
+ "x": 608,
"y": 248
},
"width": 112,
@@ -1379,11 +1590,11 @@
"link": "",
"route": [
{
- "x": 182.5,
+ "x": 402.5,
"y": 178
},
{
- "x": 182.5,
+ "x": 402.5,
"y": 248
}
],
@@ -1418,11 +1629,11 @@
"link": "",
"route": [
{
- "x": 444.5,
+ "x": 664.5,
"y": 178
},
{
- "x": 444.5,
+ "x": 664.5,
"y": 248
}
],
@@ -1480,10 +1691,52 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "find contractors",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
+ "y": 62
+ },
+ "width": 200,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "How to repair a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 155,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "find contractors",
+ "type": "rectangle",
+ "pos": {
+ "x": 232,
"y": 12
},
"width": 341,
@@ -1525,7 +1778,7 @@
"id": "find contractors.craigslist",
"type": "rectangle",
"pos": {
- "x": 62,
+ "x": 282,
"y": 62
},
"width": 110,
@@ -1567,7 +1820,7 @@
"id": "find contractors.facebook",
"type": "rectangle",
"pos": {
- "x": 192,
+ "x": 412,
"y": 62
},
"width": 111,
@@ -1609,7 +1862,7 @@
"id": "solicit quotes",
"type": "rectangle",
"pos": {
- "x": 112,
+ "x": 332,
"y": 248
},
"width": 140,
@@ -1651,7 +1904,7 @@
"id": "obtain quotes",
"type": "rectangle",
"pos": {
- "x": 373,
+ "x": 593,
"y": 112
},
"width": 143,
@@ -1693,7 +1946,7 @@
"id": "negotiate",
"type": "rectangle",
"pos": {
- "x": 388,
+ "x": 608,
"y": 248
},
"width": 112,
@@ -1735,7 +1988,7 @@
"id": "book the best bid",
"type": "rectangle",
"pos": {
- "x": 361,
+ "x": 581,
"y": 384
},
"width": 167,
@@ -1801,11 +2054,11 @@
"link": "",
"route": [
{
- "x": 182.5,
+ "x": 402.5,
"y": 178
},
{
- "x": 182.5,
+ "x": 402.5,
"y": 248
}
],
@@ -1840,11 +2093,11 @@
"link": "",
"route": [
{
- "x": 444.5,
+ "x": 664.5,
"y": 178
},
{
- "x": 444.5,
+ "x": 664.5,
"y": 248
}
],
@@ -1879,11 +2132,11 @@
"link": "",
"route": [
{
- "x": 444.5,
+ "x": 664.5,
"y": 314
},
{
- "x": 444.5,
+ "x": 664.5,
"y": 384
}
],
@@ -1945,12 +2198,54 @@
"fontFamily": "SourceSansPro",
"shapes": [
{
- "id": "window",
+ "id": "desc",
"type": "rectangle",
"pos": {
"x": 12,
"y": 12
},
+ "width": 261,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B6",
+ "stroke": "B1",
+ "animated": false,
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Multi-layer diagram of a home.",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 216,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "window",
+ "type": "rectangle",
+ "pos": {
+ "x": 293,
+ "y": 12
+ },
"width": 103,
"height": 66,
"opacity": 1,
@@ -1990,7 +2285,7 @@
"id": "roof",
"type": "rectangle",
"pos": {
- "x": 135,
+ "x": 416,
"y": 12
},
"width": 75,
@@ -2032,7 +2327,7 @@
"id": "garage",
"type": "rectangle",
"pos": {
- "x": 230,
+ "x": 511,
"y": 12
},
"width": 94,
@@ -2074,7 +2369,7 @@
"id": "water",
"type": "rectangle",
"pos": {
- "x": 344,
+ "x": 625,
"y": 12
},
"width": 88,
@@ -2116,7 +2411,7 @@
"id": "rain",
"type": "rectangle",
"pos": {
- "x": 452,
+ "x": 733,
"y": 12
},
"width": 73,
@@ -2158,7 +2453,7 @@
"id": "thunder",
"type": "rectangle",
"pos": {
- "x": 545,
+ "x": 826,
"y": 12
},
"width": 103,
diff --git a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg
index 0fb3f8fad..4f3a0e495 100644
--- a/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/complex-layers/elk/sketch.exp.svg
@@ -1,17 +1,17 @@
-windowroofgarage
-
-
-
-
-blindsglass
+ 90.000000%, 100% {
+ opacity: 0;
+ }
+}@keyframes d2Transition-d2-469555219-9 {
+ 0%, 89.990000% {
+ opacity: 0;
+ }
+ 90.000000%, 100.000000% {
+ opacity: 1;
+ }
+}]]>Multi-layer diagram of a home.windowroofgarage
+
+
+
+
+
+blindsglass
-shinglesstarlinkutility hookup
+shinglesstarlinkutility hookup
-toolsvehicles
+toolsvehicles
-find contractorscraigslistfacebook
-
-
-
-
-find contractorssolicit quotescraigslistfacebook
-
-
-
-
-
-find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook
-
-
-
-
-
-
-
-find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook
-
-
-
-
-
-
-
-
-windowroofgaragewaterrainthunder
-
-
-
-
-
-
-
+How to repair a home.
+
+
+How to repair a home.find contractorscraigslistfacebook
+
+
+
+
+
+How to repair a home.find contractorssolicit quotescraigslistfacebook
+
+
+
+
+
+
+How to repair a home.find contractorssolicit quotesobtain quotesnegotiatecraigslistfacebook
+
+
+
+
+
+
+
+
+How to repair a home.find contractorssolicit quotesobtain quotesnegotiatebook the best bidcraigslistfacebook
+
+
+
+
+
+
+
+
+
+Multi-layer diagram of a home.windowroofgaragewaterrainthunder
+
+
+
+
+
+
+
+
\ No newline at end of file