From 64af42918dad594a2e869427d9bb570a5131009a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:23:07 +0000 Subject: [PATCH] refactor(view): clear the Sonar findings in the draw.io writer and reader The draw.io writer owns its cell indentation through openCell/closeCell, openObject/closeObject and openGeometry/closeGeometry instead of repeating the indentation literals; the page scanner and model reader of the import are a struct each with one method per token kind and per element, so no function passes the cognitive-complexity threshold; the matcher indexes the baseline in a helper of its own; identifiers that shadowed the predeclared any and real are renamed. The emitted bytes and every golden are unchanged. Co-Authored-By: jason.han --- cmd/sysml/import_diagram_test.go | 10 +- internal/ir/view/drawio.go | 118 ++++--- internal/ir/view/drawio_import.go | 439 +++++++++++++++---------- internal/ir/view/drawio_import_diff.go | 37 ++- 4 files changed, 368 insertions(+), 236 deletions(-) diff --git a/cmd/sysml/import_diagram_test.go b/cmd/sysml/import_diagram_test.go index 1aba7df2a0..5d76cdc990 100644 --- a/cmd/sysml/import_diagram_test.go +++ b/cmd/sysml/import_diagram_test.go @@ -122,15 +122,15 @@ func TestImportDiagramFlagsNeedTheFile(t *testing.T) { func TestImportDiagramWritesThroughSymlinksAndLeavesUnchangedFilesAlone(t *testing.T) { binary := buildCLI(t) dir := t.TempDir() - real := filepath.Join(dir, "src", "plant.sysml") - if err := os.MkdirAll(filepath.Dir(real), 0o755); err != nil { + realPath := filepath.Join(dir, "src", "plant.sysml") + if err := os.MkdirAll(filepath.Dir(realPath), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(real, []byte(importDiagramModel), 0o644); err != nil { + if err := os.WriteFile(realPath, []byte(importDiagramModel), 0o644); err != nil { t.Fatal(err) } link := filepath.Join(dir, "plant.sysml") - if err := os.Symlink(real, link); err != nil { + if err := os.Symlink(realPath, link); err != nil { t.Skipf("symlinks unavailable: %v", err) } other := filepath.Join(dir, "other.sysml") @@ -149,7 +149,7 @@ func TestImportDiagramWritesThroughSymlinksAndLeavesUnchangedFilesAlone(t *testi if info, err := os.Lstat(link); err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("plant.sysml is no longer a symlink: %v %v", info, err) } - if text, _ := os.ReadFile(real); !strings.Contains(string(text), "Layout about Plant::Loop::pump") { + if text, _ := os.ReadFile(realPath); !strings.Contains(string(text), "Layout about Plant::Loop::pump") { t.Errorf("linked model not edited:\n%s", text) } if after, err := os.Stat(other); err != nil || !os.SameFile(before, after) || !after.ModTime().Equal(before.ModTime()) { diff --git a/internal/ir/view/drawio.go b/internal/ir/view/drawio.go index ad07d42056..1c16e20907 100644 --- a/internal/ir/view/drawio.go +++ b/internal/ir/view/drawio.go @@ -276,13 +276,13 @@ func (w *drawioWriter) childShift(node *Node, box drawioBox) drawioBox { if node.Geometry.placed() { return drawioBox{} } - minX, minY, any := math.Inf(1), math.Inf(1), false + minX, minY, found := math.Inf(1), math.Inf(1), false for _, child := range node.Children { if child.Geometry.placed() { - minX, minY, any = math.Min(minX, child.Geometry.X), math.Min(minY, child.Geometry.Y), true + minX, minY, found = math.Min(minX, child.Geometry.X), math.Min(minY, child.Geometry.Y), true } } - if !any { + if !found { return drawioBox{} } return drawioBox{X: box.X + drawioNestedPad - minX, Y: box.Y + w.headroom(node) + drawioNestedGap - minY} @@ -515,6 +515,24 @@ func (w *drawioWriter) attr(name, value string) { w.b.WriteString(`"`) } +// The indentation of what the writer puts under : a cell or the +// wrapping one, what sits inside it, and what sits inside the wrapped cell. +const ( + drawioCellIndent = " " + drawioCellBodyIndent = drawioCellIndent + " " + drawioWrappedBodyIndent = drawioCellBodyIndent + " " +) + +// openCell opens a plain cell under , up to its attributes. +func (w *drawioWriter) openCell() { + w.b.WriteString(drawioCellIndent + ". +func (w *drawioWriter) closeCell() { + w.b.WriteString(drawioCellIndent + "\n") +} + // drawioObject are the attributes a semantic cell's carries. type drawioObject struct { id, label, fqn, kind, link string @@ -527,10 +545,10 @@ type drawioName struct { Offset, Len int } -// openObject writes the wrapper of a semantic cell; the view is every -// cell's, the fqn and link those with one. -func (w *drawioWriter) openObject(indent string, o drawioObject) { - w.b.WriteString(indent + " wrapper of a semantic cell and opens the cell +// it wraps, up to its attributes; the view is every cell's, the fqn and link those with one. +func (w *drawioWriter) openObject(o drawioObject) { + w.b.WriteString(drawioCellIndent + " 0 && w.names != nil { w.names[o.id] = o.name } - w.b.WriteString(">\n") + w.b.WriteString(">\n" + drawioCellBodyIndent + " wrapper. +func (w *drawioWriter) closeObject() { + w.b.WriteString(drawioCellBodyIndent + "\n" + drawioCellIndent + "\n") } // nodeName locates shown(node)'s token in the label headerLines joins: after @@ -651,8 +674,7 @@ func (w *drawioWriter) writeNode(node *Node, parent, parentFQN string) { } compartments := w.compartments(node) header := w.headerHeight(node) - w.openObject(" ", drawioObject{id: node.ID, label: strings.Join(w.headerLines(node), "\n"), fqn: fqn, kind: node.Kind, link: w.link(node.Origin), name: w.nodeName(node)}) - w.b.WriteString(" 0)) w.b.WriteString(` vertex="1"`) if collapsed(node) { @@ -661,11 +683,11 @@ func (w *drawioWriter) writeNode(node *Node, parent, parentFQN string) { w.attr("parent", parent) w.b.WriteString(">\n") if collapsed(node) { - w.geometry(" ", drawioBox{X: rel.X, Y: rel.Y, Width: rel.Width, Height: header}, nil, &rel) + w.geometry(drawioWrappedBodyIndent, drawioBox{X: rel.X, Y: rel.Y, Width: rel.Width, Height: header}, nil, &rel) } else { - w.geometry(" ", rel, nil, nil) + w.geometry(drawioWrappedBodyIndent, rel, nil, nil) } - w.b.WriteString(" \n \n") + w.closeObject() w.cells[node.ID] = true y := header for i, compartment := range compartments { @@ -681,15 +703,15 @@ func (w *drawioWriter) writeNode(node *Node, parent, parentFQN string) { // writeCompartment writes one compartment as a text cell stacked in its node. func (w *drawioWriter) writeCompartment(id string, node *Node, compartment Compartment, box drawioBox) { - w.b.WriteString(" \n") - w.geometry(" ", box, nil, nil) - w.b.WriteString(" \n") + w.geometry(drawioCellBodyIndent, box, nil, nil) + w.closeCell() } // writePorts writes a node's drawn ports as small border cells, inputs left, outputs @@ -713,8 +735,7 @@ func (w *drawioWriter) writePorts(node *Node, fqn string) { if portFQN == "" && fqn != "" && port.Name != "" && !port.NameSynthesized { portFQN = fqn + "::" + port.Name } - w.openObject(" ", drawioObject{id: id, label: w.ports.pinLabel(port), fqn: portFQN, kind: drawioPortKind(port), link: w.link(port.Origin), name: drawioPortName(port)}) - w.b.WriteString(" \n \n") + w.closeObject() w.cells[id] = true } } @@ -814,20 +835,30 @@ func (w *drawioWriter) relativeGeometry(indent string, side PortDirection, fract case PortOut: x, y = 1, fraction } - w.b.WriteString(indent + "` + "\n") w.b.WriteString(indent + ` ` + "\n") + w.closeGeometry(indent) +} + +// openGeometry opens a cell's mxGeometry at indent, up to its attributes. +func (w *drawioWriter) openGeometry(indent string) { + w.b.WriteString(indent + "\n") } // geometry writes an mxGeometry: the box, any route points, and the full box a // collapsed cell expands to. func (w *drawioWriter) geometry(indent string, box drawioBox, points []Point, alternate *drawioBox) { - w.b.WriteString(indent + "` + "\n") } w.writePoints(indent+" ", points) - w.b.WriteString(indent + "\n") + w.closeGeometry(indent) } // writePoints writes a route's waypoints. @@ -1026,23 +1057,23 @@ func (w *drawioWriter) writeEdge(i int, edge Edge) { // No route, but an end at a port still leaves through the port's face. anchors = w.faceAnchor("exit", source, w.inward[i]&drawioFrom != 0) + w.faceAnchor("entry", target, w.inward[i]&drawioTo != 0) } - w.openObject(" ", drawioObject{id: id, label: edge.Label, fqn: w.edgeFQN(edge), kind: edge.Kind.String(), link: w.link(edge.Origin), name: drawioEdgeName(edge)}) - w.b.WriteString(" \n") - w.b.WriteString(` \n") } else { w.b.WriteString(">\n") - w.writePoints(" ", points) - w.b.WriteString(" \n") + w.writePoints(drawioWrappedBodyIndent+" ", points) + w.closeGeometry(drawioWrappedBodyIndent) } - w.b.WriteString(" \n \n") + w.closeObject() w.endLabel(id+":from", id, edge.FromLabel, -0.8, edge.Style) w.endLabel(id+":to", id, edge.ToLabel, 0.8, edge.Style) } @@ -1102,7 +1133,7 @@ func (w *drawioWriter) faceAnchor(end, cell string, inward bool) string { if d := box.bottom() - cy; d < best { fx, fy, best = 0.5, 1, d } - if d := cx - box.X; d < best { + if cx-box.X < best { fx, fy = 0, 0.5 } if inward { @@ -1142,15 +1173,17 @@ func (w *drawioWriter) endLabel(id, edge, text string, position float64, style * if text == "" { return } - w.b.WriteString(" \n \n") + w.openGeometry(drawioCellBodyIndent) w.attr("x", formatCoord(position)) - w.b.WriteString(` relative="1" as="geometry"/>` + "\n \n") + w.b.WriteString(` relative="1" as="geometry"/>` + "\n") + w.closeCell() } // drawioEdgeFullStyle is an edge's whole style: orthogonal routing unless a @@ -1265,13 +1298,13 @@ func drawioNoteGroups(notes []Note) [][]int { // each subject the group names. func (w *drawioWriter) writeNote(group []int, notes []Note, box drawioBox) { id := fmt.Sprintf("note%d", group[0]) - w.b.WriteString(" ` + "\n") - w.geometry(" ", box, nil, nil) - w.b.WriteString(" \n") + w.geometry(drawioCellBodyIndent, box, nil, nil) + w.closeCell() anchored := map[string]bool{} for _, i := range group { note := notes[i] @@ -1286,13 +1319,14 @@ func (w *drawioWriter) writeNote(group []int, notes []Note, box drawioBox) { continue } anchored[target] = true - w.b.WriteString(" \n \n \n") + w.b.WriteString(">\n" + drawioCellBodyIndent + `` + "\n") + w.closeCell() } } @@ -1302,13 +1336,13 @@ func (w *drawioWriter) writePicture(i int, picture Picture) { if !ok { return } - w.b.WriteString(" ` + "\n") - w.geometry(" ", drawioBox{X: picture.X, Y: picture.Y, Width: picture.Width, Height: picture.Height}, nil, nil) - w.b.WriteString(" \n") + w.geometry(drawioCellBodyIndent, drawioBox{X: picture.X, Y: picture.Y, Width: picture.Width, Height: picture.Height}, nil, nil) + w.closeCell() } // drawioPictureSource is a picture's image as draw.io keeps one in a style, diff --git a/internal/ir/view/drawio_import.go b/internal/ir/view/drawio_import.go index 2452c0ac8b..e1aa27a9b6 100644 --- a/internal/ir/view/drawio_import.go +++ b/internal/ir/view/drawio_import.go @@ -159,97 +159,136 @@ func ReadDrawio(data []byte, view string) (*DrawioFile, error) { // inline for an uncompressed file, decoded for a compressed one. func drawioPages(data []byte) ([]drawioPageData, error) { decoder := xml.NewDecoder(bytes.NewReader(data)) - var pages []drawioPageData - var ( - root string - inFile bool - current *drawioPageData - depth int - text strings.Builder - model bytes.Buffer - enc *xml.Encoder - ) + var scanner drawioPageScanner for { token, err := decoder.Token() if err == io.EOF { break } if err != nil { - return nil, &DrawioFileError{Message: "malformed XML", Err: err} + return nil, drawioMalformedXML(err) } - switch tok := token.(type) { - case xml.StartElement: - if root == "" { - root = tok.Name.Local - if root != "mxfile" { - return nil, &DrawioFileError{Message: fmt.Sprintf("the root element is <%s>, not ", root)} - } - inFile = true - continue - } - if current == nil { - if !inFile || tok.Name.Local != "diagram" { - return nil, &DrawioFileError{Message: fmt.Sprintf("<%s> under ; a file holds pages", tok.Name.Local)} - } - current = &drawioPageData{name: attrValue(tok.Attr, "name")} - text.Reset() - model.Reset() - enc = nil - continue - } - if depth == 0 { - if tok.Name.Local != "mxGraphModel" { - return nil, &DrawioFileError{Message: fmt.Sprintf("<%s> under ; a page holds one ", tok.Name.Local)} - } - enc = xml.NewEncoder(&model) - } - depth++ - if err := enc.EncodeToken(tok); err != nil { - return nil, &DrawioFileError{Message: "malformed XML", Err: err} - } - case xml.EndElement: - switch { - case current != nil && depth > 0: - depth-- - if err := enc.EncodeToken(tok); err != nil { - return nil, &DrawioFileError{Message: "malformed XML", Err: err} - } - if depth == 0 { - if err := enc.Flush(); err != nil { - return nil, &DrawioFileError{Message: "malformed XML", Err: err} - } - current.model = append([]byte(nil), model.Bytes()...) - } - case current != nil: - if current.model == nil { - decoded, err := decodeDrawioDiagram(text.String()) - if err != nil { - return nil, &DrawioFileError{Message: "page " + strconv.Quote(current.name) + " holds no model", Err: err} - } - current.model = decoded - } - pages = append(pages, *current) - current = nil - default: - inFile = false - } - case xml.CharData: - if current != nil && depth == 0 { - text.Write(tok) - } else if current != nil { - if err := enc.EncodeToken(tok); err != nil { - return nil, &DrawioFileError{Message: "malformed XML", Err: err} - } - } + if err := scanner.token(token); err != nil { + return nil, err } } - if root == "" { + if scanner.root == "" { return nil, &DrawioFileError{Message: "the file is empty"} } - if len(pages) == 0 { + if len(scanner.pages) == 0 { return nil, &DrawioFileError{Message: "the file has no page"} } - return pages, nil + return scanner.pages, nil +} + +// drawioMalformedXML is the file error for XML encoding/xml refuses. +func drawioMalformedXML(err error) error { + return &DrawioFileError{Message: "malformed XML", Err: err} +} + +// drawioPageScanner reads an token by token: the it is in, +// whose it re-encodes verbatim, else whose compressed text it collects. +type drawioPageScanner struct { + pages []drawioPageData + root string + inFile bool + current *drawioPageData + depth int + text strings.Builder + model bytes.Buffer + enc *xml.Encoder +} + +func (s *drawioPageScanner) token(token xml.Token) error { + switch tok := token.(type) { + case xml.StartElement: + return s.start(tok) + case xml.EndElement: + return s.end(tok) + case xml.CharData: + return s.chars(tok) + } + return nil +} + +// start opens the root, a page, or an element of the page's model. +func (s *drawioPageScanner) start(tok xml.StartElement) error { + if s.root == "" { + s.root = tok.Name.Local + if s.root != "mxfile" { + return &DrawioFileError{Message: fmt.Sprintf("the root element is <%s>, not ", s.root)} + } + s.inFile = true + return nil + } + if s.current == nil { + if !s.inFile || tok.Name.Local != "diagram" { + return &DrawioFileError{Message: fmt.Sprintf("<%s> under ; a file holds pages", tok.Name.Local)} + } + s.current = &drawioPageData{name: attrValue(tok.Attr, "name")} + s.text.Reset() + s.model.Reset() + s.enc = nil + return nil + } + if s.depth == 0 { + if tok.Name.Local != "mxGraphModel" { + return &DrawioFileError{Message: fmt.Sprintf("<%s> under ; a page holds one ", tok.Name.Local)} + } + s.enc = xml.NewEncoder(&s.model) + } + s.depth++ + return s.encode(tok) +} + +// end closes an element of the model, finishing it at the model's own close, +// or a page, decoding its text when it held no inline model, or the root. +func (s *drawioPageScanner) end(tok xml.EndElement) error { + switch { + case s.current != nil && s.depth > 0: + s.depth-- + if err := s.encode(tok); err != nil { + return err + } + if s.depth == 0 { + if err := s.enc.Flush(); err != nil { + return drawioMalformedXML(err) + } + s.current.model = append([]byte(nil), s.model.Bytes()...) + } + case s.current != nil: + if s.current.model == nil { + decoded, err := decodeDrawioDiagram(s.text.String()) + if err != nil { + return &DrawioFileError{Message: "page " + strconv.Quote(s.current.name) + " holds no model", Err: err} + } + s.current.model = decoded + } + s.pages = append(s.pages, *s.current) + s.current = nil + default: + s.inFile = false + } + return nil +} + +// chars is a page's compressed text, or character data inside its model. +func (s *drawioPageScanner) chars(tok xml.CharData) error { + if s.current == nil { + return nil + } + if s.depth == 0 { + s.text.Write(tok) + return nil + } + return s.encode(tok) +} + +func (s *drawioPageScanner) encode(tok xml.Token) error { + if err := s.enc.EncodeToken(tok); err != nil { + return drawioMalformedXML(err) + } + return nil } // drawioInflateLimit bounds the XML a compressed page may inflate to, so a @@ -285,13 +324,7 @@ func decodeDrawioDiagram(text string) ([]byte, error) { // handle on each semantic cell and every vertex geometry made absolute. func readDrawioModel(model []byte) (*DrawioFile, error) { decoder := xml.NewDecoder(bytes.NewReader(model)) - f := &DrawioFile{byID: map[string]*DrawioCell{}} - var ( - objects []map[string]string - open []*DrawioCell - points bool - seen bool - ) + r := drawioModelReader{f: &DrawioFile{byID: map[string]*DrawioCell{}}} for { token, err := decoder.Token() if err == io.EOF { @@ -302,100 +335,18 @@ func readDrawioModel(model []byte) (*DrawioFile, error) { } switch tok := token.(type) { case xml.StartElement: - attrs := map[string]string{} - for _, a := range tok.Attr { - attrs[a.Name.Local] = a.Value - } - switch tok.Name.Local { - case "mxGraphModel": - seen = true - f.PageWidth, _ = strconv.ParseFloat(attrs["pageWidth"], 64) - f.PageHeight, _ = strconv.ParseFloat(attrs["pageHeight"], 64) - f.Unit = attrs["unit"] - case "object", "UserObject": - objects = append(objects, attrs) - case "mxCell": - cell := &DrawioCell{ID: attrs["id"], Parent: attrs["parent"], Value: attrs["value"], - Vertex: attrs["vertex"] == "1", Edge: attrs["edge"] == "1", Collapsed: attrs["collapsed"] == "1", - Source: attrs["source"], Target: attrs["target"], StyleText: attrs["style"], Style: drawioStyleMap(attrs["style"])} - if len(objects) > 0 { - o := objects[len(objects)-1] - cell.ID, cell.FQN, cell.Kind, cell.View, cell.Link, cell.Label = o["id"], o["fqn"], o["kind"], o["view"], o["link"], o["label"] - objects[len(objects)-1] = nil - } - if cell.ID == "" { - return nil, errors.New("an mxCell has no id") - } - if _, dup := f.byID[cell.ID]; dup { - return nil, fmt.Errorf("cell id %q written twice", cell.ID) - } - open = append(open, cell) - case "mxGeometry": - if len(open) == 0 { - return nil, errors.New("an mxGeometry outside every mxCell") - } - cell := open[len(open)-1] - cell.HasGeometry = true - cell.Relative = attrs["relative"] == "1" - cell.Geometry, err = drawioBoxOf(attrs) - if err != nil { - return nil, fmt.Errorf("cell %s: geometry: %w", cell.ID, err) - } - case "mxRectangle": - if len(open) > 0 && attrs["as"] == "alternateBounds" { - box, err := drawioBoxOf(attrs) - if err != nil { - return nil, fmt.Errorf("cell %s: alternate bounds: %w", open[len(open)-1].ID, err) - } - open[len(open)-1].Alternate = &box - } - case "Array": - points = len(open) > 0 && attrs["as"] == "points" - case "mxPoint": - if len(open) == 0 { - break - } - cell := open[len(open)-1] - if points { - box, err := drawioBoxOf(attrs) - if err != nil { - return nil, fmt.Errorf("cell %s: waypoint: %w", cell.ID, err) - } - cell.Points = append(cell.Points, Point{X: box.X, Y: box.Y}) - } else if attrs["as"] == "offset" { - box, err := drawioBoxOf(attrs) - if err != nil { - return nil, fmt.Errorf("cell %s: offset: %w", cell.ID, err) - } - cell.Offset = Point{X: box.X, Y: box.Y} - } - } + err = r.start(tok) case xml.EndElement: - switch tok.Name.Local { - case "object", "UserObject": - if len(objects) > 0 { - if objects[len(objects)-1] != nil { - return nil, fmt.Errorf("object %q wraps no mxCell", objects[len(objects)-1]["id"]) - } - objects = objects[:len(objects)-1] - } - case "mxCell": - if len(open) > 0 { - cell := open[len(open)-1] - open = open[:len(open)-1] - if cell.ID != "0" && cell.ID != "1" { - f.Cells = append(f.Cells, *cell) - } - f.byID[cell.ID] = nil - } - case "Array": - points = false - } + err = r.end(tok) + } + if err != nil { + return nil, err } } - if !seen { + if !r.seen { return nil, errors.New("no ") } + f := r.f for i := range f.Cells { f.byID[f.Cells[i].ID] = &f.Cells[i] } @@ -407,6 +358,146 @@ func readDrawioModel(model []byte) (*DrawioFile, error) { return f, nil } +// drawioModelReader reads an element by element: the +// wrappers and cells open around the current element, and whether a waypoint Array is. +type drawioModelReader struct { + f *DrawioFile + objects []map[string]string + open []*DrawioCell + points bool + seen bool +} + +func (r *drawioModelReader) start(tok xml.StartElement) error { + attrs := map[string]string{} + for _, a := range tok.Attr { + attrs[a.Name.Local] = a.Value + } + switch tok.Name.Local { + case "mxGraphModel": + r.seen = true + r.f.PageWidth, _ = strconv.ParseFloat(attrs["pageWidth"], 64) + r.f.PageHeight, _ = strconv.ParseFloat(attrs["pageHeight"], 64) + r.f.Unit = attrs["unit"] + case "object", "UserObject": + r.objects = append(r.objects, attrs) + case "mxCell": + return r.openCell(attrs) + case "mxGeometry": + return r.geometry(attrs) + case "mxRectangle": + return r.rectangle(attrs) + case "Array": + r.points = len(r.open) > 0 && attrs["as"] == "points" + case "mxPoint": + return r.point(attrs) + } + return nil +} + +func (r *drawioModelReader) end(tok xml.EndElement) error { + switch tok.Name.Local { + case "object", "UserObject": + if n := len(r.objects); n > 0 { + if r.objects[n-1] != nil { + return fmt.Errorf("object %q wraps no mxCell", r.objects[n-1]["id"]) + } + r.objects = r.objects[:n-1] + } + case "mxCell": + if cell := r.innermost(); cell != nil { + r.open = r.open[:len(r.open)-1] + if cell.ID != "0" && cell.ID != "1" { + r.f.Cells = append(r.f.Cells, *cell) + } + r.f.byID[cell.ID] = nil + } + case "Array": + r.points = false + } + return nil +} + +// innermost is the cell open around the element being read, nil outside every cell. +func (r *drawioModelReader) innermost() *DrawioCell { + if len(r.open) == 0 { + return nil + } + return r.open[len(r.open)-1] +} + +// openCell opens an mxCell, taking the handle of the wrapping it when one does. +func (r *drawioModelReader) openCell(attrs map[string]string) error { + cell := &DrawioCell{ID: attrs["id"], Parent: attrs["parent"], Value: attrs["value"], + Vertex: attrs["vertex"] == "1", Edge: attrs["edge"] == "1", Collapsed: attrs["collapsed"] == "1", + Source: attrs["source"], Target: attrs["target"], StyleText: attrs["style"], Style: drawioStyleMap(attrs["style"])} + if n := len(r.objects); n > 0 { + o := r.objects[n-1] + cell.ID, cell.FQN, cell.Kind, cell.View, cell.Link, cell.Label = o["id"], o["fqn"], o["kind"], o["view"], o["link"], o["label"] + r.objects[n-1] = nil + } + if cell.ID == "" { + return errors.New("an mxCell has no id") + } + if _, dup := r.f.byID[cell.ID]; dup { + return fmt.Errorf("cell id %q written twice", cell.ID) + } + r.open = append(r.open, cell) + return nil +} + +// geometry is the open cell's mxGeometry. +func (r *drawioModelReader) geometry(attrs map[string]string) error { + cell := r.innermost() + if cell == nil { + return errors.New("an mxGeometry outside every mxCell") + } + cell.HasGeometry = true + cell.Relative = attrs["relative"] == "1" + var err error + if cell.Geometry, err = drawioBoxOf(attrs); err != nil { + return fmt.Errorf("cell %s: geometry: %w", cell.ID, err) + } + return nil +} + +// rectangle is the open cell's alternate bounds, the box a collapsed cell expands to. +func (r *drawioModelReader) rectangle(attrs map[string]string) error { + cell := r.innermost() + if cell == nil || attrs["as"] != "alternateBounds" { + return nil + } + box, err := drawioBoxOf(attrs) + if err != nil { + return fmt.Errorf("cell %s: alternate bounds: %w", cell.ID, err) + } + cell.Alternate = &box + return nil +} + +// point is one of the open cell's waypoints inside a points Array, else its label offset. +func (r *drawioModelReader) point(attrs map[string]string) error { + cell := r.innermost() + if cell == nil { + return nil + } + switch { + case r.points: + box, err := drawioBoxOf(attrs) + if err != nil { + return fmt.Errorf("cell %s: waypoint: %w", cell.ID, err) + } + cell.Points = append(cell.Points, Point{X: box.X, Y: box.Y}) + case attrs["as"] == "offset": + box, err := drawioBoxOf(attrs) + if err != nil { + return fmt.Errorf("cell %s: offset: %w", cell.ID, err) + } + cell.Offset = Point{X: box.X, Y: box.Y} + } + return nil +} + // pageView is the view the page's semantic cells name; the one they agree on. func (f *DrawioFile) pageView() string { views := map[string]bool{} diff --git a/internal/ir/view/drawio_import_diff.go b/internal/ir/view/drawio_import_diff.go index 2cc99f22bf..c9bff5739e 100644 --- a/internal/ir/view/drawio_import_diff.go +++ b/internal/ir/view/drawio_import_diff.go @@ -216,20 +216,8 @@ type drawioMatcher struct { // position, and diffs each pair. func (m *drawioMatcher) match() { m.matched = map[string]bool{} - byFQN := map[string][]*DrawioCell{} - byKey := map[DrawioEdgeKey][]*DrawioCell{} baseKeys, fileKeys := m.edgesByKey(m.base), m.edgesByKey(m.file) - for i := range m.base.Cells { - c := &m.base.Cells[i] - switch { - case c.Vertex && c.FQN != "": - byFQN[c.FQN] = append(byFQN[c.FQN], c) - case c.Edge && c.Kind != "": - if key, ok := baseKeys[c.ID]; ok { - byKey[key.unordered()] = append(byKey[key.unordered()], c) - } - } - } + byFQN, byKey := m.baselineIndex(baseKeys) var pending []*DrawioCell baseOf := map[*DrawioCell]*DrawioCell{} pair := func(cell, base *DrawioCell) { @@ -285,6 +273,25 @@ func (m *drawioMatcher) match() { m.deleted() } +// baselineIndex lists the baseline's named vertices by qualified name and its +// semantic edges by their unordered ends and kind, each in the baseline's order. +func (m *drawioMatcher) baselineIndex(baseKeys map[string]DrawioEdgeKey) (map[string][]*DrawioCell, map[DrawioEdgeKey][]*DrawioCell) { + byFQN := map[string][]*DrawioCell{} + byKey := map[DrawioEdgeKey][]*DrawioCell{} + for i := range m.base.Cells { + c := &m.base.Cells[i] + switch { + case c.Vertex && c.FQN != "": + byFQN[c.FQN] = append(byFQN[c.FQN], c) + case c.Edge && c.Kind != "": + if key, ok := baseKeys[c.ID]; ok { + byKey[key.unordered()] = append(byKey[key.unordered()], c) + } + } + } + return byFQN, byKey +} + // sameDrawioElement reports whether the file's cell and the baseline's of the // same id still stand for one element: the same qualified name and kind — the // same kind and label for a node no name reaches —, or the same ends and kind. @@ -816,8 +823,8 @@ func drawioPictureWithin(location, dir string) (string, bool) { } // The file a symlink reaches must be under dir too; the location kept is the link the user named. if realBase, err := filepath.EvalSymlinks(base); err == nil { - if real, err := filepath.EvalSymlinks(path); err == nil { - if realRel, err := filepath.Rel(realBase, real); err != nil || drawioOutside(realRel) { + if realPath, err := filepath.EvalSymlinks(path); err == nil { + if realRel, err := filepath.Rel(realBase, realPath); err != nil || drawioOutside(realRel) { return "", false } }