|
|
@@ -0,0 +1,76 @@
|
|
|
+# Some functions for dealing with Draw.io shape libraries (that are shown in the left pane)
|
|
|
+
|
|
|
+from typing import List, Dict, Tuple
|
|
|
+from decimal import Decimal
|
|
|
+import json
|
|
|
+import secrets
|
|
|
+import xml.etree.ElementTree as ET
|
|
|
+import copy
|
|
|
+
|
|
|
+from drawio2py.abstract_syntax import *
|
|
|
+from drawio2py.parser import Parser
|
|
|
+from drawio2py import util
|
|
|
+
|
|
|
+# Generates (more or less globally unique) Cell IDs in a format similar to how drawio does it:
|
|
|
+class DrawioIDGenerator:
|
|
|
+ def __init__(self):
|
|
|
+ self.next_id = 0
|
|
|
+ self.prefix = secrets.token_urlsafe(20) # a random token of similar complexity to Drawio's cell IDs.
|
|
|
+
|
|
|
+ def gen(self) -> str:
|
|
|
+ id = self.next_id
|
|
|
+ self.next_id += 1
|
|
|
+ return self.prefix + "-" + str(id)
|
|
|
+
|
|
|
+def parse_library(path) -> Dict[str, Cell]:
|
|
|
+ # A library is at the highest level an XML tree, with only one node: <mxlibrary>
|
|
|
+ tree = ET.parse(path)
|
|
|
+ # The "text" in this node is a JSON array:
|
|
|
+ elements = json.loads(tree.getroot().text)
|
|
|
+ library = {}
|
|
|
+ for el in elements:
|
|
|
+ # Every element in the array has a "title" (shown to the user when hovering the shape) and "xml", which is actually Base64-encoded compressed URL-encoded XML (lol) (of an <mxgraphmodel>), which is the same format as what we encounter in a .drawio file:
|
|
|
+ mxgm = Parser.decode_and_deflate(el["xml"])
|
|
|
+ # We create a dictionary mapping title to <mxgraphmodel>:
|
|
|
+ shape_root = Parser.parse_mxgraphmodel(mxgm)
|
|
|
+
|
|
|
+ # Some assertions on what we expect from shape libraries:
|
|
|
+ if len(shape_root.children) != 1:
|
|
|
+ raise Exception("Library shape '" + el["title"] + "': The root does not contain one layer, but " + len(shape_root.children))
|
|
|
+ [shape_layer] = shape_root.children
|
|
|
+ if len(shape_layer.children) != 1:
|
|
|
+ raise Exception("Library shape '" + el["title"] + "': Layer does not contain one cell, but " + len(shape_layer.children))
|
|
|
+ [shape_cell] = shape_layer.children
|
|
|
+ library[el["title"]] = shape_cell
|
|
|
+ return library
|
|
|
+
|
|
|
+class ShapeCloner:
|
|
|
+ def __init__(self, id_gen: DrawioIDGenerator):
|
|
|
+ self.id_gen = id_gen
|
|
|
+
|
|
|
+ def clone_cell(self, template: Cell, parent: Cell) -> Cell:
|
|
|
+ cell = copy.deepcopy(template)
|
|
|
+ cell.id = self.id_gen.gen()
|
|
|
+ cell.parent = parent
|
|
|
+ parent.children.append(cell)
|
|
|
+ return cell
|
|
|
+
|
|
|
+ def clone_vertex(self, template: Cell, parent: Cell, x: Decimal, y: Decimal) -> Cell:
|
|
|
+ cell = self.clone_cell(template, parent)
|
|
|
+ if type(cell) != Vertex:
|
|
|
+ raise Exception("Expected template to contain Vertex, instead the type was " + str(type(cell)))
|
|
|
+ cell.geometry.x = x
|
|
|
+ cell.geometry.y = y
|
|
|
+ return cell
|
|
|
+
|
|
|
+ def clone_edge(self, template: Cell, parent: Cell, source: Cell, target: Cell):
|
|
|
+ if util.find_lca(source, target) != parent:
|
|
|
+ raise Exception("Drawio invariant violation: The parent of an edge must always be the LCA of the source and target of that edge.")
|
|
|
+ cell = self.clone_cell(template, parent)
|
|
|
+ if type(cell) != Edge:
|
|
|
+ raise Exception("Expected template to contain Edge, instead the type was " + str(type(cell)))
|
|
|
+ cell.source = source
|
|
|
+ cell.target = target
|
|
|
+ cell.geometry.source_point = None
|
|
|
+ cell.geometry.target_point = None
|
|
|
+ return cell
|