diff --git a/d2ast/d2ast.go b/d2ast/d2ast.go
index b0e9386e9..ca81d95f8 100644
--- a/d2ast/d2ast.go
+++ b/d2ast/d2ast.go
@@ -56,6 +56,7 @@ var _ Node = &Comment{}
var _ Node = &BlockComment{}
var _ Node = &Null{}
+var _ Node = &Suspension{}
var _ Node = &Boolean{}
var _ Node = &Number{}
var _ Node = &UnquotedString{}
@@ -329,6 +330,7 @@ type Scalar interface {
// See String for rest.
var _ Scalar = &Null{}
+var _ Scalar = &Suspension{}
var _ Scalar = &Boolean{}
var _ Scalar = &Number{}
@@ -349,6 +351,7 @@ var _ String = &BlockString{}
func (c *Comment) node() {}
func (c *BlockComment) node() {}
func (n *Null) node() {}
+func (n *Suspension) node() {}
func (b *Boolean) node() {}
func (n *Number) node() {}
func (s *UnquotedString) node() {}
@@ -367,6 +370,7 @@ func (i *EdgeIndex) node() {}
func (c *Comment) Type() string { return "comment" }
func (c *BlockComment) Type() string { return "block comment" }
func (n *Null) Type() string { return "null" }
+func (n *Suspension) Type() string { return "suspension" }
func (b *Boolean) Type() string { return "boolean" }
func (n *Number) Type() string { return "number" }
func (s *UnquotedString) Type() string { return "unquoted string" }
@@ -385,6 +389,7 @@ func (i *EdgeIndex) Type() string { return "edge index" }
func (c *Comment) GetRange() Range { return c.Range }
func (c *BlockComment) GetRange() Range { return c.Range }
func (n *Null) GetRange() Range { return n.Range }
+func (n *Suspension) GetRange() Range { return n.Range }
func (b *Boolean) GetRange() Range { return b.Range }
func (n *Number) GetRange() Range { return n.Range }
func (s *UnquotedString) GetRange() Range { return s.Range }
@@ -409,6 +414,7 @@ func (i *Import) mapNode() {}
func (c *Comment) arrayNode() {}
func (c *BlockComment) arrayNode() {}
func (n *Null) arrayNode() {}
+func (n *Suspension) arrayNode() {}
func (b *Boolean) arrayNode() {}
func (n *Number) arrayNode() {}
func (s *UnquotedString) arrayNode() {}
@@ -421,6 +427,7 @@ func (a *Array) arrayNode() {}
func (m *Map) arrayNode() {}
func (n *Null) value() {}
+func (n *Suspension) value() {}
func (b *Boolean) value() {}
func (n *Number) value() {}
func (s *UnquotedString) value() {}
@@ -432,6 +439,7 @@ func (m *Map) value() {}
func (i *Import) value() {}
func (n *Null) scalar() {}
+func (n *Suspension) scalar() {}
func (b *Boolean) scalar() {}
func (n *Number) scalar() {}
func (s *UnquotedString) scalar() {}
@@ -442,6 +450,7 @@ func (s *BlockString) scalar() {}
func (c *Comment) Children() []Node { return nil }
func (c *BlockComment) Children() []Node { return nil }
func (n *Null) Children() []Node { return nil }
+func (n *Suspension) Children() []Node { return nil }
func (b *Boolean) Children() []Node { return nil }
func (n *Number) Children() []Node { return nil }
func (s *SingleQuotedString) Children() []Node { return nil }
@@ -573,9 +582,10 @@ func Walk(node Node, fn func(Node) bool) {
}
// TODO: mistake, move into parse.go
-func (n *Null) ScalarString() string { return "" }
-func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
-func (n *Number) ScalarString() string { return n.Raw }
+func (n *Null) ScalarString() string { return "" }
+func (n *Suspension) ScalarString() string { return "" }
+func (b *Boolean) ScalarString() string { return strconv.FormatBool(b.Value) }
+func (n *Number) ScalarString() string { return n.Raw }
func (s *UnquotedString) ScalarString() string {
if len(s.Value) == 0 {
return ""
@@ -631,6 +641,11 @@ type Null struct {
Range Range `json:"range"`
}
+type Suspension struct {
+ Range Range `json:"range"`
+ Value bool `json:"value"`
+}
+
type Boolean struct {
Range Range `json:"range"`
Value bool `json:"value"`
@@ -1369,6 +1384,7 @@ func (ab ArrayNodeBox) Unbox() ArrayNode {
// ValueBox is used to box Value for JSON persistence.
type ValueBox struct {
Null *Null `json:"null,omitempty"`
+ Suspension *Suspension `json:"suspension,omitempty"`
Boolean *Boolean `json:"boolean,omitempty"`
Number *Number `json:"number,omitempty"`
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
@@ -1384,6 +1400,8 @@ func (vb ValueBox) Unbox() Value {
switch {
case vb.Null != nil:
return vb.Null
+ case vb.Suspension != nil:
+ return vb.Suspension
case vb.Boolean != nil:
return vb.Boolean
case vb.Number != nil:
@@ -1412,6 +1430,8 @@ func MakeValueBox(v Value) ValueBox {
switch v := v.(type) {
case *Null:
vb.Null = v
+ case *Suspension:
+ vb.Suspension = v
case *Boolean:
vb.Boolean = v
case *Number:
@@ -1437,6 +1457,7 @@ func MakeValueBox(v Value) ValueBox {
func (vb ValueBox) ScalarBox() ScalarBox {
var sb ScalarBox
sb.Null = vb.Null
+ sb.Suspension = vb.Suspension
sb.Boolean = vb.Boolean
sb.Number = vb.Number
sb.UnquotedString = vb.UnquotedString
@@ -1459,6 +1480,7 @@ func (vb ValueBox) StringBox() *StringBox {
// TODO: implement ScalarString()
type ScalarBox struct {
Null *Null `json:"null,omitempty"`
+ Suspension *Suspension `json:"suspension,omitempty"`
Boolean *Boolean `json:"boolean,omitempty"`
Number *Number `json:"number,omitempty"`
UnquotedString *UnquotedString `json:"unquoted_string,omitempty"`
@@ -1471,6 +1493,10 @@ func (sb ScalarBox) Unbox() Scalar {
switch {
case sb.Null != nil:
return sb.Null
+ case sb.Null != nil:
+ return sb.Null
+ case sb.Suspension != nil:
+ return sb.Suspension
case sb.Boolean != nil:
return sb.Boolean
case sb.Number != nil:
@@ -1559,7 +1585,7 @@ func RawString(s string, inKey bool) String {
return &SingleQuotedString{Value: s}
}
}
- } else if s == "null" || strings.ContainsAny(s, UnquotedValueSpecials) {
+ } else if s == "null" || s == "suspend" || s == "restore" || strings.ContainsAny(s, UnquotedValueSpecials) {
if !strings.ContainsRune(s, '"') && !strings.ContainsRune(s, '$') {
return FlatDoubleQuotedString(s)
}
diff --git a/d2ir/compile.go b/d2ir/compile.go
index 668bc9190..5d6e224c5 100644
--- a/d2ir/compile.go
+++ b/d2ir/compile.go
@@ -81,6 +81,7 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
c.compileMap(m, ast, ast)
c.compileSubstitutions(m, nil)
c.overlayClasses(m)
+ m.removeSuspendedFields()
if !c.err.Empty() {
return nil, nil, c.err
}
@@ -866,6 +867,15 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
}
}
+ if len(refctx.Key.Edges) == 0 && (refctx.Key.Primary.Suspension != nil || refctx.Key.Value.Suspension != nil) {
+ if refctx.Key.Primary.Suspension != nil {
+ f.suspended = refctx.Key.Primary.Suspension.Value
+ } else {
+ f.suspended = refctx.Key.Value.Suspension.Value
+ }
+ return
+ }
+
if refctx.Key.Primary.Unbox() != nil {
if c.ignoreLazyGlob(f) {
return
@@ -1164,6 +1174,16 @@ func (c *compiler) _compileEdges(refctx *RefContext) {
refctx.ScopeMap.DeleteEdge(e.ID)
continue
}
+
+ if refctx.Key.Primary.Suspension != nil || refctx.Key.Value.Suspension != nil {
+ if refctx.Key.Primary.Suspension != nil {
+ e.suspended = refctx.Key.Primary.Suspension.Value
+ } else {
+ e.suspended = refctx.Key.Value.Suspension.Value
+ }
+ continue
+ }
+
e.References = append(e.References, &EdgeReference{
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
@@ -1289,3 +1309,36 @@ func (c *compiler) compileArray(dst *Array, a *d2ast.Array, scopeAST *d2ast.Map)
dst.Values = append(dst.Values, irv)
}
}
+
+func (m *Map) removeSuspendedFields() {
+ if m == nil {
+ return
+ }
+
+ for _, f := range m.Fields {
+ if f.Map() != nil {
+ f.Map().removeSuspendedFields()
+ }
+ }
+
+ for i := len(m.Fields) - 1; i >= 0; i-- {
+ _, isReserved := d2ast.ReservedKeywords[m.Fields[i].Name.ScalarString()]
+ if isReserved {
+ continue
+ }
+ if m.Fields[i].suspended {
+ m.DeleteField(m.Fields[i].Name.ScalarString())
+ }
+ }
+
+ for _, e := range m.Edges {
+ if e.Map() != nil {
+ e.Map().removeSuspendedFields()
+ }
+ }
+ for i := len(m.Edges) - 1; i >= 0; i-- {
+ if m.Edges[i].suspended {
+ m.DeleteEdge(m.Edges[i].ID)
+ }
+ }
+}
diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go
index d5d014515..4ea360b92 100644
--- a/d2ir/d2ir.go
+++ b/d2ir/d2ir.go
@@ -318,6 +318,7 @@ type Field struct {
// *Map.
parent Node
importAST d2ast.Node
+ suspended bool
Name d2ast.String `json:"name"`
@@ -488,6 +489,7 @@ type Edge struct {
// *Map
parent Node
importAST d2ast.Node
+ suspended bool
ID *EdgeID `json:"edge_id"`
diff --git a/d2parser/parse.go b/d2parser/parse.go
index 3eb500484..2f38652ce 100644
--- a/d2parser/parse.go
+++ b/d2parser/parse.go
@@ -1668,6 +1668,20 @@ func (p *parser) parseValue() d2ast.ValueBox {
}
return box
}
+ if strings.EqualFold(s.ScalarString(), "suspend") {
+ box.Suspension = &d2ast.Suspension{
+ Range: s.Range,
+ Value: true,
+ }
+ return box
+ }
+ if strings.EqualFold(s.ScalarString(), "restore") {
+ box.Suspension = &d2ast.Suspension{
+ Range: s.Range,
+ Value: false,
+ }
+ return box
+ }
if strings.EqualFold(s.ScalarString(), "true") {
box.Boolean = &d2ast.Boolean{
diff --git a/e2etests/testdata/txtar/model-view/dagre/board.exp.json b/e2etests/testdata/txtar/model-view/dagre/board.exp.json
new file mode 100644
index 000000000..901a987f6
--- /dev/null
+++ b/e2etests/testdata/txtar/model-view/dagre/board.exp.json
@@ -0,0 +1,322 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "user",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 50
+ },
+ "width": 77,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "blue",
+ "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": "user",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 32,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "softwareSystem",
+ "type": "rectangle",
+ "pos": {
+ "x": 127,
+ "y": 20
+ },
+ "width": 331,
+ "height": 126,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "softwareSystem",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 188,
+ "labelHeight": 36,
+ "labelPosition": "OUTSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "softwareSystem.serviceA",
+ "type": "rectangle",
+ "classes": [
+ "ok"
+ ],
+ "pos": {
+ "x": 157,
+ "y": 50
+ },
+ "width": 106,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "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": "serviceA",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 61,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "softwareSystem.serviceC",
+ "type": "rectangle",
+ "classes": [
+ "ok"
+ ],
+ "pos": {
+ "x": 323,
+ "y": 50
+ },
+ "width": 105,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "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": "serviceC",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "externalSystem",
+ "type": "rectangle",
+ "pos": {
+ "x": 297,
+ "y": 266
+ },
+ "width": 157,
+ "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": "externalSystem",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 112,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(softwareSystem -> externalSystem)[0]",
+ "src": "softwareSystem",
+ "srcArrow": "none",
+ "dst": "externalSystem",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 375.5,
+ "y": 146
+ },
+ {
+ "x": 375.5,
+ "y": 202
+ },
+ {
+ "x": 375.5,
+ "y": 226
+ },
+ {
+ "x": 375.5,
+ "y": 266
+ }
+ ],
+ "isCurve": true,
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/model-view/dagre/sketch.exp.svg b/e2etests/testdata/txtar/model-view/dagre/sketch.exp.svg
new file mode 100644
index 000000000..8773ebf93
--- /dev/null
+++ b/e2etests/testdata/txtar/model-view/dagre/sketch.exp.svg
@@ -0,0 +1,106 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/txtar/model-view/elk/board.exp.json b/e2etests/testdata/txtar/model-view/elk/board.exp.json
new file mode 100644
index 000000000..7a2d1c3a2
--- /dev/null
+++ b/e2etests/testdata/txtar/model-view/elk/board.exp.json
@@ -0,0 +1,313 @@
+{
+ "name": "",
+ "config": {
+ "sketch": false,
+ "themeID": 0,
+ "darkThemeID": null,
+ "pad": null,
+ "center": null,
+ "layoutEngine": null
+ },
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "user",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 62
+ },
+ "width": 77,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "blue",
+ "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": "user",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 32,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "softwareSystem",
+ "type": "rectangle",
+ "pos": {
+ "x": 109,
+ "y": 12
+ },
+ "width": 331,
+ "height": 166,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "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": "softwareSystem",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 188,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "softwareSystem.serviceA",
+ "type": "rectangle",
+ "classes": [
+ "ok"
+ ],
+ "pos": {
+ "x": 159,
+ "y": 62
+ },
+ "width": 106,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "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": "serviceA",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 61,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "softwareSystem.serviceC",
+ "type": "rectangle",
+ "classes": [
+ "ok"
+ ],
+ "pos": {
+ "x": 285,
+ "y": 62
+ },
+ "width": 105,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "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": "serviceC",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 60,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "externalSystem",
+ "type": "rectangle",
+ "pos": {
+ "x": 196,
+ "y": 248
+ },
+ "width": 157,
+ "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": "externalSystem",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 112,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 1
+ }
+ ],
+ "connections": [
+ {
+ "id": "(softwareSystem -> externalSystem)[0]",
+ "src": "softwareSystem",
+ "srcArrow": "none",
+ "dst": "externalSystem",
+ "dstArrow": "triangle",
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "stroke": "B1",
+ "borderRadius": 10,
+ "label": "",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N2",
+ "italic": true,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "labelPosition": "",
+ "labelPercentage": 0,
+ "link": "",
+ "route": [
+ {
+ "x": 274.5,
+ "y": 178
+ },
+ {
+ "x": 274.5,
+ "y": 248
+ }
+ ],
+ "animated": false,
+ "tooltip": "",
+ "icon": null,
+ "zIndex": 0
+ }
+ ],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "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": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/txtar/model-view/elk/sketch.exp.svg b/e2etests/testdata/txtar/model-view/elk/sketch.exp.svg
new file mode 100644
index 000000000..890eccd04
--- /dev/null
+++ b/e2etests/testdata/txtar/model-view/elk/sketch.exp.svg
@@ -0,0 +1,106 @@
+usersoftwareSystemexternalSystemserviceAserviceC
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt
index fcc6a7619..c1040bec6 100644
--- a/e2etests/txtar.txt
+++ b/e2etests/txtar.txt
@@ -775,3 +775,33 @@ a -> b: hello {
b -> c: {
icon: https://icons.terrastruct.com/essentials%2F213-alarm.svg
}
+
+-- model-view --
+# Models
+user.style.fill: blue
+softwareSystem: {
+ serviceA.class: ok
+ serviceB
+ serviceC.class: ok
+ serviceD
+
+ serviceA -> serviceB
+ serviceA -> serviceD
+ serviceC -> serviceB
+}
+externalSystem
+user -> softwareSystem
+softwareSystem -> externalSystem
+
+# Clear models
+**: suspend
+(** -> **)[*]: suspend
+
+# Include all top-level objects
+*: restore
+# Include all objects with a certain class
+**: restore {
+ &class: ok
+}
+# Include all connections/objects connected to an object
+(** -> externalSystem)[*]: restore