From 7ea3aa2737a0bf4092a537a64ef930d844ab6d74 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 24 Nov 2024 10:45:06 -0800 Subject: [PATCH] apply to edges --- d2ast/d2ast.go | 16 +- d2compiler/compile.go | 65 ++-- d2ir/compile.go | 40 +-- d2ir/d2ir.go | 130 ++++---- d2ir/merge.go | 4 +- .../sql-table-reserved/dagre/board.exp.json | 305 ++++++++++++++++++ .../sql-table-reserved/dagre/sketch.exp.svg | 102 ++++++ .../sql-table-reserved/elk/board.exp.json | 300 +++++++++++++++++ .../sql-table-reserved/elk/sketch.exp.svg | 102 ++++++ e2etests/txtar.txt | 14 + 10 files changed, 957 insertions(+), 121 deletions(-) create mode 100644 e2etests/testdata/txtar/sql-table-reserved/dagre/board.exp.json create mode 100644 e2etests/testdata/txtar/sql-table-reserved/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/txtar/sql-table-reserved/elk/board.exp.json create mode 100644 e2etests/testdata/txtar/sql-table-reserved/elk/sketch.exp.svg diff --git a/d2ast/d2ast.go b/d2ast/d2ast.go index 446a8975b..464faf196 100644 --- a/d2ast/d2ast.go +++ b/d2ast/d2ast.go @@ -1065,9 +1065,17 @@ func MakeKeyPath(a []string) *KeyPath { return kp } -func (kp *KeyPath) IDA() (ida []string) { +func MakeKeyPathString(a []String) *KeyPath { + kp := &KeyPath{} + for _, el := range a { + kp.Path = append(kp.Path, MakeValueBox(RawString(el.ScalarString(), true)).StringBox()) + } + return kp +} + +func (kp *KeyPath) IDA() (ida []String) { for _, el := range kp.Path { - ida = append(ida, el.Unbox().ScalarString()) + ida = append(ida, el.Unbox()) } return ida } @@ -1578,9 +1586,9 @@ func (s *Substitution) IDA() (ida []string) { return ida } -func (i *Import) IDA() (ida []string) { +func (i *Import) IDA() (ida []String) { for _, el := range i.Path[1:] { - ida = append(ida, el.Unbox().ScalarString()) + ida = append(ida, el.Unbox()) } return ida } diff --git a/d2compiler/compile.go b/d2compiler/compile.go index c7573e21e..221bb9d67 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -110,7 +110,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph { } func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) { - boards := ir.GetField(fieldName) + boards := ir.GetField(d2ast.FlatUnquotedString(fieldName)) if boards.Map() == nil { return } @@ -224,7 +224,7 @@ func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { } func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) { - class := m.GetField("class") + class := m.GetField(d2ast.FlatUnquotedString("class")) if class != nil { var classNames []string if class.Primary() != nil { @@ -265,7 +265,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) { } } } - shape := m.GetField("shape") + shape := m.GetField(d2ast.FlatUnquotedString("shape")) if shape != nil { if shape.Composite != nil { c.errorf(shape.LastPrimaryKey(), "reserved field shape does not accept composite") @@ -388,7 +388,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) { IsVar: d2ir.IsVar(fr.Context_.ScopeMap), } if fr.Context_.ScopeMap != nil && !d2ir.IsVar(fr.Context_.ScopeMap) { - scopeObjIDA := d2graphIDA(d2ir.BoardIDA(fr.Context_.ScopeMap)) + scopeObjIDA := d2ir.BoardIDA(fr.Context_.ScopeMap) r.ScopeObj = obj.Graph.Root.EnsureChild(scopeObjIDA) } obj.References = append(obj.References, r) @@ -766,7 +766,7 @@ func compileStyleFieldInit(attrs *d2graph.Attributes, f *d2ir.Field) { } func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) { - edge, err := obj.Connect(d2graphIDA(e.ID.SrcPath), d2graphIDA(e.ID.DstPath), e.ID.SrcArrow, e.ID.DstArrow, "") + edge, err := obj.Connect(e.ID.SrcPath, e.ID.DstPath, e.ID.SrcArrow, e.ID.DstArrow, "") if err != nil { c.errorf(e.References[0].AST(), err.Error()) return @@ -790,7 +790,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) { ScopeObj: obj, } if er.Context_.ScopeMap != nil && !d2ir.IsVar(er.Context_.ScopeMap) { - scopeObjIDA := d2graphIDA(d2ir.BoardIDA(er.Context_.ScopeMap)) + scopeObjIDA := d2ir.BoardIDA(er.Context_.ScopeMap) r.ScopeObj = edge.Src.Graph.Root.EnsureChild(scopeObjIDA) } edge.References = append(edge.References, r) @@ -798,7 +798,7 @@ func (c *compiler) compileEdge(obj *d2graph.Object, e *d2ir.Edge) { } func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) { - class := m.GetField("class") + class := m.GetField(d2ast.FlatUnquotedString("class")) if class != nil { var classNames []string if class.Primary() != nil { @@ -1230,7 +1230,11 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) { continue } - if slices.Equal(linkKey.IDA(), obj.Graph.IDA()) { + formattedIDA := []string{} + for _, id := range linkKey.IDA() { + formattedIDA = append(formattedIDA, id.ScalarString()) + } + if slices.Equal(formattedIDA, obj.Graph.IDA()) { obj.Link = nil continue } @@ -1246,34 +1250,34 @@ func (c *compiler) validateBoardLinks(g *d2graph.Graph) { } } -func hasBoard(root *d2graph.Graph, ida []string) bool { +func hasBoard(root *d2graph.Graph, ida []d2ast.String) bool { if len(ida) == 0 { return true } - if ida[0] == "root" { + if ida[0].ScalarString() == "root" && ida[0].IsUnquoted() { return hasBoard(root, ida[1:]) } id := ida[0] if len(ida) == 1 { - return root.Name == id + return root.Name == id.ScalarString() } nextID := ida[1] - switch id { + switch id.ScalarString() { case "layers": for _, b := range root.Layers { - if b.Name == nextID { + if b.Name == nextID.ScalarString() { return hasBoard(b, ida[2:]) } } case "scenarios": for _, b := range root.Scenarios { - if b.Name == nextID { + if b.Name == nextID.ScalarString() { return hasBoard(b, ida[2:]) } } case "steps": for _, b := range root.Steps { - if b.Name == nextID { + if b.Name == nextID.ScalarString() { return hasBoard(b, ida[2:]) } } @@ -1288,13 +1292,6 @@ func init() { } } -func d2graphIDA(irIDA []string) (ida []d2ast.String) { - for _, el := range irIDA { - ida = append(ida, d2ast.RawString(el, true)) - } - return ida -} - // Unused for now until shape: edge_group func (c *compiler) preprocessSeqDiagrams(m *d2ir.Map) { for _, f := range m.Fields { @@ -1350,8 +1347,8 @@ func (c *compiler) preprocessEdgeGroup(seqDiagram, m *d2ir.Map) { f := srcParent.GetField(el) if !isEdgeGroup(f) { for j := 0; j < i+1; j++ { - e.ID.SrcPath = append([]string{"_"}, e.ID.SrcPath...) - e.ID.DstPath = append([]string{"_"}, e.ID.DstPath...) + e.ID.SrcPath = append([]d2ast.String{d2ast.FlatUnquotedString("_")}, e.ID.SrcPath...) + e.ID.DstPath = append([]d2ast.String{d2ast.FlatUnquotedString("_")}, e.ID.DstPath...) } break } @@ -1361,7 +1358,7 @@ func (c *compiler) preprocessEdgeGroup(seqDiagram, m *d2ir.Map) { } func hoistActor(seqDiagram *d2ir.Map, f *d2ir.Field) { - f2 := seqDiagram.GetField(f.Name.ScalarString()) + f2 := seqDiagram.GetField(f.Name) if f2 == nil { seqDiagram.Fields = append(seqDiagram.Fields, f.Copy(seqDiagram).(*d2ir.Field)) } else { @@ -1420,7 +1417,7 @@ func parentSeqDiagram(n d2ir.Node) *d2ir.Map { } func compileConfig(ir *d2ir.Map) (*d2target.Config, error) { - f := ir.GetField("vars", "d2-config") + f := ir.GetField(d2ast.FlatUnquotedString("vars"), d2ast.FlatUnquotedString("d2-config")) if f == nil || f.Map() == nil { return nil, nil } @@ -1429,36 +1426,36 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) { config := &d2target.Config{} - f = configMap.GetField("sketch") + f = configMap.GetField(d2ast.FlatUnquotedString("sketch")) if f != nil { val, _ := strconv.ParseBool(f.Primary().Value.ScalarString()) config.Sketch = &val } - f = configMap.GetField("theme-id") + f = configMap.GetField(d2ast.FlatUnquotedString("theme-id")) if f != nil { val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) config.ThemeID = go2.Pointer(int64(val)) } - f = configMap.GetField("dark-theme-id") + f = configMap.GetField(d2ast.FlatUnquotedString("dark-theme-id")) if f != nil { val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) config.DarkThemeID = go2.Pointer(int64(val)) } - f = configMap.GetField("pad") + f = configMap.GetField(d2ast.FlatUnquotedString("pad")) if f != nil { val, _ := strconv.Atoi(f.Primary().Value.ScalarString()) config.Pad = go2.Pointer(int64(val)) } - f = configMap.GetField("layout-engine") + f = configMap.GetField(d2ast.FlatUnquotedString("layout-engine")) if f != nil { config.LayoutEngine = go2.Pointer(f.Primary().Value.ScalarString()) } - f = configMap.GetField("theme-overrides") + f = configMap.GetField(d2ast.FlatUnquotedString("theme-overrides")) if f != nil { overrides, err := compileThemeOverrides(f.Map()) if err != nil { @@ -1466,7 +1463,7 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) { } config.ThemeOverrides = overrides } - f = configMap.GetField("dark-theme-overrides") + f = configMap.GetField(d2ast.FlatUnquotedString("dark-theme-overrides")) if f != nil { overrides, err := compileThemeOverrides(f.Map()) if err != nil { @@ -1474,7 +1471,7 @@ func compileConfig(ir *d2ir.Map) (*d2target.Config, error) { } config.DarkThemeOverrides = overrides } - f = configMap.GetField("data") + f = configMap.GetField(d2ast.FlatUnquotedString("data")) if f != nil && f.Map() != nil { config.Data = make(map[string]interface{}) for _, f := range f.Map().Fields { diff --git a/d2ir/compile.go b/d2ir/compile.go index 07e882a15..a112a99e3 100644 --- a/d2ir/compile.go +++ b/d2ir/compile.go @@ -88,12 +88,12 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) { } func (c *compiler) overlayClasses(m *Map) { - classes := m.GetField("classes") + classes := m.GetField(d2ast.FlatUnquotedString("classes")) if classes == nil || classes.Map() == nil { return } - layersField := m.GetField("layers") + layersField := m.GetField(d2ast.FlatUnquotedString("layers")) if layersField == nil { return } @@ -108,7 +108,7 @@ func (c *compiler) overlayClasses(m *Map) { continue } l := lf.Map() - lClasses := l.GetField("classes") + lClasses := l.GetField(d2ast.FlatUnquotedString("classes")) if lClasses == nil { lClasses = classes.Copy(l).(*Field) @@ -153,7 +153,7 @@ func (c *compiler) compileSubstitutions(m *Map, varsStack []*Map) { } else if f.Map() != nil { if f.Name != nil && f.Name.ScalarString() == "vars" && f.Name.IsUnquoted() { c.compileSubstitutions(f.Map(), varsStack) - c.validateConfigs(f.Map().GetField("d2-config")) + c.validateConfigs(f.Map().GetField(d2ast.FlatUnquotedString("d2-config"))) } else { c.compileSubstitutions(f.Map(), varsStack) } @@ -381,7 +381,7 @@ func (c *compiler) resolveSubstitution(vars *Map, node Node, substitution *d2ast parent := ParentField(node) for i, p := range substitution.Path { - f := vars.GetField(p.Unbox().ScalarString()) + f := vars.GetField(p.Unbox()) if f == nil { return nil } @@ -445,7 +445,7 @@ func (g *globContext) copyApplied(from *globContext) { func (g *globContext) prefixed(dst *Map) *globContext { g2 := g.copy() - prefix := d2ast.MakeKeyPath(RelIDA(g2.refctx.ScopeMap, dst)) + prefix := d2ast.MakeKeyPathString(RelIDA(g2.refctx.ScopeMap, dst)) g2.refctx.Key = g2.refctx.Key.Copy() if g2.refctx.Key.Key != nil { prefix.Path = append(prefix.Path, g2.refctx.Key.Key.Path...) @@ -483,9 +483,9 @@ func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool { } var ks string if gctx.refctx.Key.HasMultiGlob() { - ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst))) + ks = d2format.Format(d2ast.MakeKeyPathString(IDA(dst))) } else { - ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst))) + ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(dst))) } delete(gctx.appliedFields, ks) delete(gctx.appliedEdges, ks) @@ -992,11 +992,11 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) { } for _, id := range linkIDA[1:] { - if id == "_" { + if id.ScalarString() == "_" && id.IsUnquoted() { if len(linkIDA) < 2 || len(importIDA) < 2 { break } - linkIDA = append([]string{linkIDA[0]}, linkIDA[2:]...) + linkIDA = append([]d2ast.String{linkIDA[0]}, linkIDA[2:]...) importIDA = importIDA[:len(importIDA)-2] } else { break @@ -1004,7 +1004,7 @@ func (c *compiler) extendLinks(m *Map, importF *Field, importDir string) { } extendedIDA := append(importIDA, linkIDA[1:]...) - kp := d2ast.MakeKeyPath(extendedIDA) + kp := d2ast.MakeKeyPathString(extendedIDA) s := d2format.Format(kp) f.Primary_.Value = d2ast.MakeValueBox(d2ast.FlatUnquotedString(s)).ScalarBox().Unbox() } @@ -1046,30 +1046,34 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) { return } - if linkIDA[0] == "root" { + if linkIDA[0].ScalarString() == "root" && linkIDA[0].IsUnquoted() { c.errorf(refctx.Key.Key, "cannot refer to root in link") return } + if !linkIDA[0].IsUnquoted() { + return + } + // If it doesn't start with one of these reserved words, the link is definitely not a board link. - if !strings.EqualFold(linkIDA[0], "layers") && !strings.EqualFold(linkIDA[0], "scenarios") && !strings.EqualFold(linkIDA[0], "steps") && linkIDA[0] != "_" { + if !strings.EqualFold(linkIDA[0].ScalarString(), "layers") && !strings.EqualFold(linkIDA[0].ScalarString(), "scenarios") && !strings.EqualFold(linkIDA[0].ScalarString(), "steps") && linkIDA[0].ScalarString() != "_" { return } // Chop off the non-board portion of the scope, like if this is being defined on a nested object (e.g. `x.y.z`) for i := len(scopeIDA) - 1; i > 0; i-- { - if strings.EqualFold(scopeIDA[i-1], "layers") || strings.EqualFold(scopeIDA[i-1], "scenarios") || strings.EqualFold(scopeIDA[i-1], "steps") { + if scopeIDA[i-1].IsUnquoted() && (strings.EqualFold(scopeIDA[i-1].ScalarString(), "layers") || strings.EqualFold(scopeIDA[i-1].ScalarString(), "scenarios") || strings.EqualFold(scopeIDA[i-1].ScalarString(), "steps")) { scopeIDA = scopeIDA[:i+1] break } - if scopeIDA[i-1] == "root" { + if scopeIDA[i-1].ScalarString() == "root" && scopeIDA[i-1].IsUnquoted() { scopeIDA = scopeIDA[:i] break } } // Resolve underscores - for len(linkIDA) > 0 && linkIDA[0] == "_" { + for len(linkIDA) > 0 && linkIDA[0].ScalarString() == "_" && linkIDA[0].IsUnquoted() { if len(scopeIDA) < 2 { // Leave the underscore. It will fail in compiler as a standalone board, // but if imported, will get further resolved in extendLinks @@ -1080,12 +1084,12 @@ func (c *compiler) compileLink(f *Field, refctx *RefContext) { linkIDA = linkIDA[1:] } if len(scopeIDA) == 0 { - scopeIDA = []string{"root"} + scopeIDA = []d2ast.String{d2ast.FlatUnquotedString("root")} } // Create the absolute path by appending scope path with value specified scopeIDA = append(scopeIDA, linkIDA...) - kp := d2ast.MakeKeyPath(scopeIDA) + kp := d2ast.MakeKeyPathString(scopeIDA) f.Primary_.Value = d2ast.FlatUnquotedString(d2format.Format(kp)) } diff --git a/d2ir/d2ir.go b/d2ir/d2ir.go index 690b5acb7..9400761ee 100644 --- a/d2ir/d2ir.go +++ b/d2ir/d2ir.go @@ -377,11 +377,11 @@ func (f *Field) LastRef() Reference { } type EdgeID struct { - SrcPath []string `json:"src_path"` - SrcArrow bool `json:"src_arrow"` + SrcPath []d2ast.String `json:"src_path"` + SrcArrow bool `json:"src_arrow"` - DstPath []string `json:"dst_path"` - DstArrow bool `json:"dst_arrow"` + DstPath []d2ast.String `json:"dst_path"` + DstArrow bool `json:"dst_arrow"` // If nil, then any EdgeID with equal src/dst/arrows matches. Index *int `json:"index"` @@ -409,8 +409,8 @@ func (eid *EdgeID) Copy() *EdgeID { tmp := *eid eid = &tmp - eid.SrcPath = append([]string(nil), eid.SrcPath...) - eid.DstPath = append([]string(nil), eid.DstPath...) + eid.SrcPath = append([]d2ast.String(nil), eid.SrcPath...) + eid.DstPath = append([]d2ast.String(nil), eid.DstPath...) return eid } @@ -428,7 +428,7 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool { return false } for i, s := range eid.SrcPath { - if !strings.EqualFold(s, eid2.SrcPath[i]) { + if !strings.EqualFold(s.ScalarString(), eid2.SrcPath[i].ScalarString()) { return false } } @@ -440,7 +440,7 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool { return false } for i, s := range eid.DstPath { - if !strings.EqualFold(s, eid2.DstPath[i]) { + if !strings.EqualFold(s.ScalarString(), eid2.DstPath[i].ScalarString()) { return false } } @@ -450,21 +450,21 @@ func (eid *EdgeID) Match(eid2 *EdgeID) bool { // resolve resolves both underscores and commons in eid. // It returns the new eid, containing map adjusted for underscores and common ida. -func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error) { +func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []d2ast.String, _ error) { eid = eid.Copy() maxUnderscores := go2.Max(countUnderscores(eid.SrcPath), countUnderscores(eid.DstPath)) for i := 0; i < maxUnderscores; i++ { - if eid.SrcPath[0] == "_" { + if eid.SrcPath[0].ScalarString() == "_" && eid.SrcPath[0].IsUnquoted() { eid.SrcPath = eid.SrcPath[1:] } else { mf := ParentField(m) - eid.SrcPath = append([]string{mf.Name.ScalarString()}, eid.SrcPath...) + eid.SrcPath = append([]d2ast.String{mf.Name}, eid.SrcPath...) } - if eid.DstPath[0] == "_" { + if eid.DstPath[0].ScalarString() == "_" && eid.DstPath[0].IsUnquoted() { eid.DstPath = eid.DstPath[1:] } else { mf := ParentField(m) - eid.DstPath = append([]string{mf.Name.ScalarString()}, eid.DstPath...) + eid.DstPath = append([]d2ast.String{mf.Name}, eid.DstPath...) } m = ParentMap(m) if m == nil { @@ -473,7 +473,7 @@ func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []string, _ error) } for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 { - if !strings.EqualFold(eid.SrcPath[0], eid.DstPath[0]) || strings.Contains(eid.SrcPath[0], "*") { + if !strings.EqualFold(eid.SrcPath[0].ScalarString(), eid.DstPath[0].ScalarString()) || strings.Contains(eid.SrcPath[0].ScalarString(), "*") { return eid, m, common, nil } common = append(common, eid.SrcPath[0]) @@ -702,9 +702,9 @@ func (m *Map) EdgeCountRecursive() int { func (m *Map) GetClassMap(name string) *Map { root := RootMap(m) - classes := root.Map().GetField("classes") + classes := root.Map().GetField(d2ast.FlatUnquotedString("classes")) if classes != nil && classes.Map() != nil { - class := classes.Map().GetField(name) + class := classes.Map().GetField(d2ast.FlatUnquotedString(name)) if class != nil && class.Map() != nil { return class.Map() } @@ -712,8 +712,8 @@ func (m *Map) GetClassMap(name string) *Map { return nil } -func (m *Map) GetField(ida ...string) *Field { - for len(ida) > 0 && ida[0] == "_" { +func (m *Map) GetField(ida ...d2ast.String) *Field { + for len(ida) > 0 && ida[0].ScalarString() == "_" && ida[0].IsUnquoted() { m = ParentMap(m) if m == nil { return nil @@ -722,7 +722,7 @@ func (m *Map) GetField(ida ...string) *Field { return m.getField(ida) } -func (m *Map) getField(ida []string) *Field { +func (m *Map) getField(ida []d2ast.String) *Field { if len(ida) == 0 { return nil } @@ -730,7 +730,7 @@ func (m *Map) getField(ida []string) *Field { s := ida[0] rest := ida[1:] - if s == "_" { + if s.ScalarString() == "_" && s.IsUnquoted() { return nil } @@ -738,7 +738,10 @@ func (m *Map) getField(ida []string) *Field { if f.Name == nil { continue } - if !strings.EqualFold(f.Name.ScalarString(), s) { + if !strings.EqualFold(f.Name.ScalarString(), s.ScalarString()) { + continue + } + if f.Name.IsUnquoted() != s.IsUnquoted() { continue } if len(rest) == 0 { @@ -788,9 +791,9 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b if gctx != nil { var ks string if refctx.Key.HasMultiGlob() { - ks = d2format.Format(d2ast.MakeKeyPath(IDA(f))) + ks = d2format.Format(d2ast.MakeKeyPathString(IDA(f))) } else { - ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f))) + ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(f))) } if !kp.HasGlob() { if !passthrough { @@ -887,7 +890,7 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString) } - if findBoardKeyword(headString) != -1 && head.IsUnquoted() && NodeBoardKind(m) == "" { + if findBoardKeyword(head) != -1 && head.IsUnquoted() && NodeBoardKind(m) == "" { return d2parser.Errorf(kp.Path[i].Unbox(), "%s is only allowed at a board root", headString) } @@ -945,9 +948,9 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b for _, grefctx := range c.globRefContextStack { var ks string if grefctx.Key.HasMultiGlob() { - ks = d2format.Format(d2ast.MakeKeyPath(IDA(f))) + ks = d2format.Format(d2ast.MakeKeyPathString(IDA(f))) } else { - ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(f))) + ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(f))) } gctx2 := c.getGlobContext(grefctx) gctx2.appliedFields[ks] = struct{}{} @@ -1090,7 +1093,7 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[ } if len(common) > 0 { - commonKP := d2ast.MakeKeyPath(common) + commonKP := d2ast.MakeKeyPathString(common) lastMatch := 0 for i, el := range commonKP.Path { for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ { @@ -1142,9 +1145,9 @@ func (m *Map) getEdges(eid *EdgeID, refctx *RefContext, gctx *globContext, ea *[ if gctx != nil { var ks string if refctx.Key.HasMultiGlob() { - ks = d2format.Format(d2ast.MakeKeyPath(IDA(e))) + ks = d2format.Format(d2ast.MakeKeyPathString(IDA(e))) } else { - ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e))) + ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(e))) } if _, ok := gctx.appliedEdges[ks]; ok { continue @@ -1186,7 +1189,7 @@ func (m *Map) createEdge(eid *EdgeID, refctx *RefContext, gctx *globContext, c * return d2parser.Errorf(refctx.Edge, err.Error()) } if len(common) > 0 { - commonKP := d2ast.MakeKeyPath(common) + commonKP := d2ast.MakeKeyPathString(common) lastMatch := 0 for i, el := range commonKP.Path { for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ { @@ -1303,7 +1306,7 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c return nil, d2parser.Errorf(refctx.Edge, err.Error()) } if len(common) > 0 { - commonKP := d2ast.MakeKeyPath(common) + commonKP := d2ast.MakeKeyPathString(common) lastMatch := 0 for i, el := range commonKP.Path { for j := lastMatch; j < len(refctx.Edge.Src.Path); j++ { @@ -1360,9 +1363,9 @@ func (m *Map) createEdge2(eid *EdgeID, refctx *RefContext, gctx *globContext, c e2.ID = e2.ID.Copy() e2.ID.Index = nil if refctx.Key.HasMultiGlob() { - ks = d2format.Format(d2ast.MakeKeyPath(IDA(e2))) + ks = d2format.Format(d2ast.MakeKeyPathString(IDA(e2))) } else { - ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(e2))) + ks = d2format.Format(d2ast.MakeKeyPathString(BoardIDA(e2))) } if _, ok := gctx.appliedEdges[ks]; ok { return nil, nil @@ -1401,11 +1404,11 @@ func (f *Field) AST() d2ast.Node { func (e *Edge) AST() d2ast.Node { astEdge := &d2ast.Edge{} - astEdge.Src = d2ast.MakeKeyPath(e.ID.SrcPath) + astEdge.Src = d2ast.MakeKeyPathString(e.ID.SrcPath) if e.ID.SrcArrow { astEdge.SrcArrow = "<" } - astEdge.Dst = d2ast.MakeKeyPath(e.ID.DstPath) + astEdge.Dst = d2ast.MakeKeyPathString(e.ID.DstPath) if e.ID.DstArrow { astEdge.DstArrow = ">" } @@ -1424,7 +1427,7 @@ func (e *Edge) AST() d2ast.Node { return k } -func (e *Edge) IDString() string { +func (e *Edge) IDString() d2ast.String { ast := e.AST().(*d2ast.Key) if e.ID.Index != nil { ast.EdgeIndex = &d2ast.EdgeIndex{ @@ -1433,7 +1436,8 @@ func (e *Edge) IDString() string { } ast.Primary = d2ast.ScalarBox{} ast.Value = d2ast.ValueBox{} - return d2format.Format(ast) + formatted := d2format.Format(ast) + return d2ast.FlatUnquotedString(formatted) } func (a *Array) AST() d2ast.Node { @@ -1465,7 +1469,7 @@ func (m *Map) AST() d2ast.Node { func (m *Map) appendFieldReferences(i int, kp *d2ast.KeyPath, refctx *RefContext, c *compiler) { sb := kp.Path[i] - f := m.GetField(sb.Unbox().ScalarString()) + f := m.GetField(sb.Unbox()) if f == nil { return } @@ -1563,7 +1567,7 @@ func ParentShape(n Node) string { f, ok := n.(*Field) if ok { if f.Map() != nil { - shapef := f.Map().GetField("shape") + shapef := f.Map().GetField(d2ast.FlatUnquotedString("shape")) if shapef != nil && shapef.Primary() != nil { return shapef.Primary().Value.ScalarString() } @@ -1576,30 +1580,30 @@ func ParentShape(n Node) string { } } -func countUnderscores(p []string) int { +func countUnderscores(p []d2ast.String) int { for i, el := range p { - if el != "_" { + if el.ScalarString() != "_" || el.IsUnquoted() { return i } } return 0 } -func findBoardKeyword(ida ...string) int { +func findBoardKeyword(ida ...d2ast.String) int { for i := range ida { - if _, ok := d2ast.BoardKeywords[ida[i]]; ok { + if _, ok := d2ast.BoardKeywords[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() { return i } } return -1 } -func findProhibitedEdgeKeyword(ida ...string) int { +func findProhibitedEdgeKeyword(ida ...d2ast.String) int { for i := range ida { - if _, ok := d2ast.SimpleReservedKeywords[ida[i]]; ok { + if _, ok := d2ast.SimpleReservedKeywords[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() { return i } - if _, ok := d2ast.ReservedKeywordHolders[ida[i]]; ok { + if _, ok := d2ast.ReservedKeywordHolders[ida[i].ScalarString()]; ok && ida[i].IsUnquoted() { return i } } @@ -1643,7 +1647,7 @@ func parentPrimaryKey(n Node) *d2ast.Key { } // BoardIDA returns the absolute path to n from the nearest board root. -func BoardIDA(n Node) (ida []string) { +func BoardIDA(n Node) (ida []d2ast.String) { for { switch n := n.(type) { case *Field: @@ -1651,7 +1655,7 @@ func BoardIDA(n Node) (ida []string) { reverseIDA(ida) return ida } - ida = append(ida, n.Name.ScalarString()) + ida = append(ida, n.Name) case *Edge: ida = append(ida, n.IDString()) } @@ -1664,11 +1668,11 @@ func BoardIDA(n Node) (ida []string) { } // IDA returns the absolute path to n. -func IDA(n Node) (ida []string) { +func IDA(n Node) (ida []d2ast.String) { for { switch n := n.(type) { case *Field: - ida = append(ida, n.Name.ScalarString()) + ida = append(ida, n.Name) if n.Root() { reverseIDA(ida) return ida @@ -1685,17 +1689,17 @@ func IDA(n Node) (ida []string) { } // RelIDA returns the path to n relative to p. -func RelIDA(p, n Node) (ida []string) { +func RelIDA(p, n Node) (ida []d2ast.String) { for { switch n := n.(type) { case *Field: - ida = append(ida, n.Name.ScalarString()) + ida = append(ida, n.Name) if n.Root() { reverseIDA(ida) return ida } case *Edge: - ida = append(ida, n.String()) + ida = append(ida, d2ast.FlatUnquotedString(n.String())) } n = n.Parent() f, fok := n.(*Field) @@ -1707,11 +1711,11 @@ func RelIDA(p, n Node) (ida []string) { } } -func reverseIDA(ida []string) { - for i := 0; i < len(ida)/2; i++ { - tmp := ida[i] - ida[i] = ida[len(ida)-i-1] - ida[len(ida)-i-1] = tmp +func reverseIDA[T any](slice []T) { + for i := 0; i < len(slice)/2; i++ { + tmp := slice[i] + slice[i] = slice[len(slice)-i-1] + slice[len(slice)-i-1] = tmp } } @@ -1786,7 +1790,7 @@ func (m *Map) Equal(n2 Node) bool { } func (m *Map) InClass(key *d2ast.Key) bool { - classes := m.Map().GetField("classes") + classes := m.Map().GetField(d2ast.FlatUnquotedString("classes")) if classes == nil || classes.Map() == nil { return false } @@ -1814,7 +1818,7 @@ func (m *Map) IsClass() bool { if parentBoard.Map() == nil { return false } - classes := parentBoard.Map().GetField("classes") + classes := parentBoard.Map().GetField(d2ast.FlatUnquotedString("classes")) if classes == nil || classes.Map() == nil { return false } @@ -1835,9 +1839,9 @@ func (m *Map) FindBoardRoot(path []string) *Map { return m } - layersf := m.GetField("layers") - scenariosf := m.GetField("scenarios") - stepsf := m.GetField("steps") + layersf := m.GetField(d2ast.FlatUnquotedString("layers")) + scenariosf := m.GetField(d2ast.FlatUnquotedString("scenarios")) + stepsf := m.GetField(d2ast.FlatUnquotedString("steps")) if layersf != nil && layersf.Map() != nil { for _, f := range layersf.Map().Fields { diff --git a/d2ir/merge.go b/d2ir/merge.go index dd2e77be0..3b22b27df 100644 --- a/d2ir/merge.go +++ b/d2ir/merge.go @@ -2,7 +2,7 @@ package d2ir func OverlayMap(base, overlay *Map) { for _, of := range overlay.Fields { - bf := base.GetField(of.Name.ScalarString()) + bf := base.GetField(of.Name) if bf == nil { base.Fields = append(base.Fields, of.Copy(base).(*Field)) continue @@ -31,7 +31,7 @@ func ExpandSubstitution(m, resolved *Map, placeholder *Field) { } for _, of := range resolved.Fields { - bf := m.GetField(of.Name.ScalarString()) + bf := m.GetField(of.Name) if bf == nil { m.Fields = append(m.Fields[:fi], append([]*Field{of.Copy(m).(*Field)}, m.Fields[fi:]...)...) fi++ diff --git a/e2etests/testdata/txtar/sql-table-reserved/dagre/board.exp.json b/e2etests/testdata/txtar/sql-table-reserved/dagre/board.exp.json new file mode 100644 index 000000000..05f561842 --- /dev/null +++ b/e2etests/testdata/txtar/sql-table-reserved/dagre/board.exp.json @@ -0,0 +1,305 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "my_table", + "type": "sql_table", + "pos": { + "x": 0, + "y": 166 + }, + "width": 200, + "height": 200, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N1", + "stroke": "N7", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": { + "Scheme": "https", + "Opaque": "", + "User": null, + "Host": "static.wikia.nocookie.net", + "Path": "/tomandjerry/images/4/46/JerryJumbo3-1-.jpg", + "RawPath": "", + "OmitHost": false, + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "iconPosition": "OUTSIDE_TOP_LEFT", + "blend": false, + "fields": null, + "methods": null, + "columns": [ + { + "name": { + "label": "shape", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 51, + "labelHeight": 26 + }, + "type": { + "label": "string", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "icon", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 35, + "labelHeight": 26 + }, + "type": { + "label": "string", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "width", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "type": { + "label": "int", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 23, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "height", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 53, + "labelHeight": 26 + }, + "type": { + "label": "int", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 23, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + } + ], + "label": "my_table", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 99, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "B2", + "secondaryAccentColor": "AA2", + "neutralAccentColor": "N2" + }, + { + "id": "x", + "type": "rectangle", + "pos": { + "x": 74, + "y": 0 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(x -> my_table)[0]", + "src": "x", + "srcArrow": "none", + "dst": "my_table", + "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, + "route": [ + { + "x": 100, + "y": 66 + }, + { + "x": 100, + "y": 106 + }, + { + "x": 100, + "y": 126 + }, + { + "x": 100, + "y": 166 + } + ], + "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": "", + "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/sql-table-reserved/dagre/sketch.exp.svg b/e2etests/testdata/txtar/sql-table-reserved/dagre/sketch.exp.svg new file mode 100644 index 000000000..47427da18 --- /dev/null +++ b/e2etests/testdata/txtar/sql-table-reserved/dagre/sketch.exp.svg @@ -0,0 +1,102 @@ +my_tableshapestringiconstringwidthintheightintx + + + \ No newline at end of file diff --git a/e2etests/testdata/txtar/sql-table-reserved/elk/board.exp.json b/e2etests/testdata/txtar/sql-table-reserved/elk/board.exp.json new file mode 100644 index 000000000..c53ead7f7 --- /dev/null +++ b/e2etests/testdata/txtar/sql-table-reserved/elk/board.exp.json @@ -0,0 +1,300 @@ +{ + "name": "", + "isFolderOnly": false, + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "my_table", + "type": "sql_table", + "pos": { + "x": 78, + "y": 217 + }, + "width": 200, + "height": 200, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "N1", + "stroke": "N7", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": { + "Scheme": "https", + "Opaque": "", + "User": null, + "Host": "static.wikia.nocookie.net", + "Path": "/tomandjerry/images/4/46/JerryJumbo3-1-.jpg", + "RawPath": "", + "OmitHost": false, + "ForceQuery": false, + "RawQuery": "", + "Fragment": "", + "RawFragment": "" + }, + "iconPosition": "OUTSIDE_TOP_LEFT", + "blend": false, + "fields": null, + "methods": null, + "columns": [ + { + "name": { + "label": "shape", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 51, + "labelHeight": 26 + }, + "type": { + "label": "string", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "icon", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 35, + "labelHeight": 26 + }, + "type": { + "label": "string", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "width", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 48, + "labelHeight": 26 + }, + "type": { + "label": "int", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 23, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "height", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 53, + "labelHeight": 26 + }, + "type": { + "label": "int", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 23, + "labelHeight": 26 + }, + "constraint": null, + "reference": "" + } + ], + "label": "my_table", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 99, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "B2", + "secondaryAccentColor": "AA2", + "neutralAccentColor": "N2" + }, + { + "id": "x", + "type": "rectangle", + "pos": { + "x": 12, + "y": 12 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "B6", + "stroke": "B1", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "N1", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(x -> my_table)[0]", + "src": "x", + "srcArrow": "none", + "dst": "my_table", + "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, + "route": [ + { + "x": 38.5, + "y": 77.6989974975586 + }, + { + "x": 38.5, + "y": 228.6999969482422 + }, + { + "x": 78.5, + "y": 228.6999969482422 + } + ], + "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": "", + "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/sql-table-reserved/elk/sketch.exp.svg b/e2etests/testdata/txtar/sql-table-reserved/elk/sketch.exp.svg new file mode 100644 index 000000000..1f6898cdd --- /dev/null +++ b/e2etests/testdata/txtar/sql-table-reserved/elk/sketch.exp.svg @@ -0,0 +1,102 @@ +my_tableshapestringiconstringwidthintheightintx + + + \ No newline at end of file diff --git a/e2etests/txtar.txt b/e2etests/txtar.txt index a9d4bffa8..38b0ec213 100644 --- a/e2etests/txtar.txt +++ b/e2etests/txtar.txt @@ -643,3 +643,17 @@ financial.style.fill: "#e8f4f8" monitoring.style.fill: "#f8e8e8" projects.style.fill: "#e8f8e8" team.style.fill: "#f8f0e8" + +-- sql-table-reserved -- +my_table: { + shape: sql_table + icon: https://static.wikia.nocookie.net/tomandjerry/images/4/46/JerryJumbo3-1-.jpg + width: 200 + height: 200 + "shape": string + "icon": string + "width": int + "height": int +} + +x -> my_table."shape"