ソースを参照

Abstract syntax contains the actual diagram tree (parent, child, source, target relations resolved)

Joeri Exelmans 2 年 前
コミット
79c98fa004
5 ファイル変更165 行追加136 行削除
  1. 39 35
      drawio2py/abstract_syntax.py
  2. 33 29
      drawio2py/generator.py
  3. 38 10
      drawio2py/parser.py
  4. 54 61
      drawio2py/template.oml
  5. 1 1
      test/run_tests.py

+ 39 - 35
drawio2py/abstract_syntax.py

@@ -8,10 +8,12 @@ class Style:
 	data: dict[str,str]
 
 @dataclass
-class _CellEdgeBase:
+class Cell:
+	""" Instances of Cell are: the root cell and its layers """
 	id: str
 	value: str
-	parent: Optional[str]     # ID of cell. Every cell has a parent, except for the root.
+	parent: Optional['Cell']    # Every cell has a parent, except for the root.
+	children: List['Cell']
 	properties: dict[str,str] # User-defined properties
 	style: Optional[Style]
 	attributes: dict[str,str] # Any extra XML attributes
@@ -22,13 +24,13 @@ class Point:
 	y: Decimal
 
 @dataclass
-class CellGeometry(Point):
+class VertexGeometry(Point):
 	width: Decimal
 	height: Decimal
 
 @dataclass
-class Cell(_CellEdgeBase):
-	geometry: Optional[CellGeometry]
+class Vertex(Cell):
+	geometry: VertexGeometry
 
 @dataclass
 class EdgeGeometry:
@@ -37,10 +39,10 @@ class EdgeGeometry:
 	target_point: Optional[Point] = None
 
 @dataclass
-class Edge(_CellEdgeBase):
-	geometry: Optional[EdgeGeometry]
-	source: str
-	target: str
+class Edge(Cell):
+	geometry: EdgeGeometry
+	source: Vertex
+	target: Vertex
 
 @dataclass
 class Page:
@@ -48,32 +50,34 @@ class Page:
 	name: str                 # Page name, as shown in GUI
 	attributes: dict[str,str] # Attributes of the <mxGraphModel> of the page
 
-	cells: List[Cell] = field(default_factory=list) # All the cells that are not edges.
-	edges: List[Edge] = field(default_factory=list) # All the edges.
-
-	def find_cell(self, identifier):
-		for cell in self.cells:
-			if cell.id == identifier:
-				return cell
-		return None
-
-	def find_edge(self, identifier):
-		for edge in self.edges:
-			if edge.id == identifier:
-				return edge
-		return None
-
-	def find_cell_index(self, id) -> Optional[int]:
-		for i,cell in enumerate(self.cells):
-			if cell.id == id:
-				return i
-		return None
-
-	def find_edge_index(self, id) -> Optional[int]:
-		for i,edge in enumerate(self.edges):
-			if edge.id == id:
-				return i
-		return None
+	root: Cell
+
+	# cells: List[Cell] = field(default_factory=list) # All the cells that are not edges.
+	# edges: List[Edge] = field(default_factory=list) # All the edges.
+
+	# def find_cell(self, identifier):
+	# 	for cell in self.cells:
+	# 		if cell.id == identifier:
+	# 			return cell
+	# 	return None
+
+	# def find_edge(self, identifier):
+	# 	for edge in self.edges:
+	# 		if edge.id == identifier:
+	# 			return edge
+	# 	return None
+
+	# def find_cell_index(self, id) -> Optional[int]:
+	# 	for i,cell in enumerate(self.cells):
+	# 		if cell.id == id:
+	# 			return i
+	# 	return None
+
+	# def find_edge_index(self, id) -> Optional[int]:
+	# 	for i,edge in enumerate(self.edges):
+	# 		if edge.id == id:
+	# 			return i
+	# 	return None
 
 	def get_sanitized_name(self):
 		if re.match(r"Page-\d+", self.name):

+ 33 - 29
drawio2py/generator.py

@@ -28,23 +28,34 @@ def generate(drawio: DrawIOFile, file_object):
 		mxgm = ET.Element("mxGraphModel", page.attributes)
 		root = ET.SubElement(mxgm, "root")
 
-		for cell in page.cells:
-			if cell.properties:
-				# If there is a surrounding <object>, then drawio expects the ID of the enclosed <mxCell> to be an attribute of the <object>:
-				properties_with_id = cell.properties.copy()
-				properties_with_id['id'] = cell.id
-				par = ET.SubElement(root, "object", properties_with_id)
-			else:
-				par = root
+		def write_cell(cell):
 			attrs = {
 				"id": "" if cell.properties else str(cell.id),
 				"value": cell.value,
 				"style": generate_style(cell.style),
 			}
 			if cell.parent:
-				attrs["parent"] = cell.parent
+				attrs["parent"] = cell.parent.id
+			if isinstance(cell, Edge):
+				attrs["edge"] = "1"
+				if cell.source:
+					attrs["source"] = cell.source.id
+				if cell.target:
+					attrs["target"] = cell.target.id
+					
+			if cell.properties:
+				# Wrap in <object> if there are properties
+				properties_with_id = cell.properties.copy()
+				properties_with_id['id'] = cell.id  # <object> gets the ID, not the <mxCell> (WTF drawio!)
+				par = ET.SubElement(root, "object", properties_with_id)
+			else:
+				par = root
+
+			# Create the actual <mxCell>
 			c = ET.SubElement(par, "mxCell", attrs, **cell.attributes)
-			if cell.geometry:
+
+			# Geometry
+			if isinstance(cell, Vertex):
 				ET.SubElement(c, "mxGeometry", {
 					"x": str(cell.geometry.x),
 					"y": str(cell.geometry.y),
@@ -52,34 +63,27 @@ def generate(drawio: DrawIOFile, file_object):
 					"height": str(cell.geometry.height),
 					"as": "geometry"
 				})
-		for edge in page.edges:
-			attrs = {
-				"id": str(edge.id),
-				"style": generate_style(edge.style),
-				"parent": edge.parent,
-				"edge": "1"
-			}
-			if edge.source:
-				attrs["source"] = edge.source
-			if edge.target:
-				attrs["target"] = edge.target
-			c = ET.SubElement(root, "mxCell", attrs)
-			if edge.geometry:
+			elif isinstance(cell, Edge):
 				g = ET.SubElement(c, "mxGeometry", {
 					"relative": "1",
 					"as": "geometry"
 				})
-				if len(edge.geometry.points) > 0:
+				if len(cell.geometry.points) > 0:
 					a = ET.SubElement(g, "Array", {"as": "points"})
-					for p in edge.geometry.points:
+					for p in cell.geometry.points:
 						ET.SubElement(a, "mxPoint", {"x": str(p.x), "y": str(p.y)})
-				if edge.geometry.source_point is not None:
-					sp = edge.geometry.source_point
+				if cell.geometry.source_point is not None:
+					sp = cell.geometry.source_point
 					ET.SubElement(g, "mxPoint", {"x": str(sp.x), "y": str(sp.y), "as": "sourcePoint"})
-				if edge.geometry.target_point is not None:
-					tp = edge.geometry.target_point
+				if cell.geometry.target_point is not None:
+					tp = cell.geometry.target_point
 					ET.SubElement(g, "mxPoint", {"x": str(tp.x), "y": str(tp.y), "as": "targetPoint"})
 
+			for child in cell.children:
+				write_cell(child)
+
+		write_cell(page.root)
+
 		dia.append(mxgm)
 
 	et = ET.ElementTree(graph)

+ 38 - 10
drawio2py/parser.py

@@ -60,7 +60,7 @@ class Parser:
 	@staticmethod
 	def parse_cell_geometry(groot):
 		if groot is None: return None
-		return CellGeometry(groot.attrib["x"], groot.attrib["y"],
+		return VertexGeometry(groot.attrib["x"], groot.attrib["y"],
 		                    groot.attrib["width"], groot.attrib["height"])
 
 	@staticmethod
@@ -107,13 +107,16 @@ class Parser:
 			source = croot.attrib.get("source", None)
 			target = croot.attrib.get("target", None)
 
-			return Edge(cid, value, parent, {}, style, atts, geom, source, target)
+			return Edge(cid, value, parent, [], {}, style, atts, geom, source, target)
 
 		geom = Parser.parse_cell_geometry(croot.find(".//mxGeometry"))
-		return Cell(cid, value, parent, {}, style, atts, geom)
+		if geom != None:
+			return Vertex(cid, value, parent, [], {}, style, atts, geom)
+		else:
+			return Cell(cid, value, parent, [], {}, style, atts)
 
 	@staticmethod
-	def parse_components(page, nroot):
+	def parse_components(nroot, cell_dict):
 		for coix, cebj in enumerate(nroot):
 			if cebj.tag == "object":
 				cell = Parser.parse_object(cebj)
@@ -122,10 +125,12 @@ class Parser:
 			else:
 				raise ParseException("unknown component tag '%s'" % cebj.tag)
 
-			if isinstance(cell, Edge):
-				page.edges.append(cell)
-			else:
-				page.cells.append(cell)
+			cell_dict[cell.id] = cell
+
+			# if isinstance(cell, Edge):
+			# 	page.edges.append(cell)
+			# else:
+			# 	page.cells.append(cell)
 
 	@staticmethod
 	def parse(filename):
@@ -154,10 +159,33 @@ class Parser:
 			else:
 				nroot = page[0]
 			mxgm = nroot
+
+			cell_dict = {} # mapping from ID to cell
+			Parser.parse_components(mxgm[0], cell_dict)
+
+			# resolve parent/child references
+			root = None
+			for cell_id, cell in cell_dict.items():
+				parent_id = cell.parent # cell.parent will be overwritten with the actual cell
+				if parent_id == None:
+					root = cell
+				else:
+					cell_dict[parent_id].children.append(cell)
+					cell.parent = cell_dict[parent_id]
+				if isinstance(cell, Edge):
+					if cell.source:
+						cell.source = cell_dict[cell.source]
+					if cell.target:
+						cell.target = cell_dict[cell.target]
+
+			if root == None:
+				raise Exception("No root cell in page " + str(px) + " (every cell had a parent)")
+
 			pg = Page(
 				id=page.attrib.get("id", str(px)),
 				name=page.attrib.get("name", ""),
-				attributes=mxgm.attrib)
-			Parser.parse_components(pg, mxgm[0])
+				attributes=mxgm.attrib,
+				root = root)
+
 			dfile.pages.append(pg)
 		return dfile

+ 54 - 61
drawio2py/template.oml

@@ -1,23 +1,61 @@
-{%- macro cellbase(page_index, page, cell, cell_iri) %}
+// Warning: Generated code! Do not edit!
+// Input file: '{{file.filename}}'
+// Generator: https://msdl.uantwerpen.be/git/rparedis/DrawioConvert/src/library
+
+{%- macro point(p) -%}
+drawio:hasX {{p.x}}
+drawio:hasY {{p.y}}
+{%- endmacro -%}
+
+{%- macro write_cell(page_index, cell) %}
+  {%- set cell_iri = "p"+(page_index|string)+"_cell_"+(cell.id|string) -%}
+  ci {{cell_iri}} : drawio:{{cell.__class__.__name__}} [
     drawio:hasDrawioId {{cell.id|to_oml_string_literal}}
     {%- if cell.value != "" %}
     drawio:hasValue {{cell.value|to_oml_string_literal}}
     {%- endif %}
     {%- if cell.parent != None %}
-    {%- set parent_index = page.find_cell_index(cell.parent) %}
-    {%- if parent_index != None %}
-    drawio:hasParent p{{page_index}}_c{{parent_index}}
-    {%- endif %}
-    {%- set parent_index = page.find_edge_index(cell.parent) %}
-    {%- if parent_index != None %}
-    drawio:hasParent p{{page_index}}_e{{parent_index}}
-    {%- endif %}
+    drawio:hasParent p{{page_index}}_cell_{{cell.parent.id}}
     {%- else %}
     drawio:isRootOf p{{page_index}}
     {%- endif %}
     object_diagram:inModel model
-  ]
 
+    {%- if cell.__class__.__name__ == "Vertex" %}
+    drawio:hasVertexGeometry drawio:VertexGeometry [
+      {{ point(cell.geometry)|indent(6) }}
+      drawio:hasWidth {{cell.geometry.width}}
+      drawio:hasHeight {{cell.geometry.height}}
+    ]
+    {% endif -%}
+
+    {%- if cell.__class__.__name__ == "Edge" %}
+    drawio:hasEdgeGeometry drawio:EdgeGeometry [
+      {%- for p in cell.geometry.points -%}
+      drawio:hasPoint drawio:PointListItem [
+        drawio:hasListIndex {{loop.index0}}
+        {{ point(p)|indent(8) }}
+      ]
+      {%- endfor %}
+      {%- if cell.geometry.source_point != None %}
+      drawio:hasSourcePoint drawio:Point [
+        {{ point(cell.geometry.source_point)|indent(8) }}
+      ]
+      {%- endif %}
+      {%- if cell.geometry.target_point != None %}
+      drawio:hasTargetPoint drawio:Point [
+        {{ point(cell.geometry.target_point)|indent(8) }}
+      ]
+      {%- endif %}
+    ]
+    {%- if cell.source %}
+    drawio:hasSource p{{page_index}}_cell_{{cell.source.id}}
+    {%- endif -%}
+    {%- if cell.target %}
+    drawio:hasTarget p{{page_index}}_cell_{{cell.target.id}}
+    {%- endif -%}
+    {%- endif %}
+  ]
   {# Cell properties #}
   {%-for prop_key,prop_val in cell.properties.items() %}
   ci {{cell_iri}}_prop_{{prop_key}} : drawio:CellProperty [
@@ -27,7 +65,6 @@
     object_diagram:inModel model
   ]
   {%- endfor %}
-
   {# Cell style #}
   {%- for style_key,style_val in cell.style.data.items() %}
   ci {{cell_iri}}_sty_{{style_key}} : drawio:CellStyleEntry [
@@ -37,7 +74,6 @@
     object_diagram:inModel model
   ]
   {%- endfor %}
-
   {# Cell attributes #}
   {%- for attr_key,attr_val in cell.attributes.items() %}
   ci {{cell_iri}}_attr_{{attr_key}} : drawio:CellAttribute [
@@ -47,16 +83,12 @@
     object_diagram:inModel model
   ]
   {%- endfor %}
-{%- endmacro -%}
-
-{%- macro point(p) %}
-drawio:hasX {{p.x}}
-drawio:hasY {{p.y}}
-{%- endmacro -%}
 
-// Warning: Generated code! Do not edit!
-// Input file: '{{file.filename}}'
-// Generator: https://msdl.uantwerpen.be/git/rparedis/DrawioConvert/src/library
+  {# Recursively write out children #}
+  {%- for child in cell.children %}
+  {{ write_cell(page_index, child) }}
+  {%- endfor -%}
+{%- endmacro %}
 
 description <{{namespaces.description}}#> as {{namespaces.shorthand}} {
 
@@ -88,47 +120,8 @@ description <{{namespaces.description}}#> as {{namespaces.shorthand}} {
   {%- endfor %}
 
   {# Cells #}
-  {%- for cell_index, cell in enumerate(page.cells) -%}
-  {%- set cell_iri = "p"+(page_index|string)+"_c"+(cell_index|string) %}
-  ci {{cell_iri}} : drawio:Cell [
-    {%- if cell.geometry != None %}
-    drawio:hasCellGeometry drawio:CellGeometry [
-      {{ point(cell.geometry)|indent(6) }}
-      drawio:hasWidth {{cell.geometry.width}}
-      drawio:hasHeight {{cell.geometry.height}}
-    ]
-    {%- endif %}
-    {{ cellbase(page_index, page, cell, cell_iri) }}
-  {# closing ']' is taken care of by cellbase #}
-  {%- endfor %}
+  {{ write_cell(page_index, page.root) }}
 
-  {# Edges #}
-  {%- for edge_index, edge in enumerate(page.edges) -%}
-  {%- set edge_iri = "p"+(page_index|string)+"_e"+(edge_index|string) %}
-  ci {{edge_iri}} : drawio:Edge [
-    {%-if edge.geometry != None %}
-    drawio:hasEdgeGeometry drawio:EdgeGeometry [
-      {%-for p in edge.geometry.points -%}
-      drawio:hasPoint drawio:PointListItem [
-        drawio:hasListIndex {{loop.index0}}
-        {{ point(p)|indent(8) }}
-      ]
-      {%- endfor %}
-      {%- if edge.geometry.source_point != None %}
-      drawio:hasSourcePoint drawio:Point [
-        {{ point(edge.geometry.source_point)|indent(8) }}
-      ]
-      {%- endif %}
-      {%- if edge.geometry.target_point != None %}
-      drawio:hasTargetPoint drawio:Point [
-        {{ point(edge.geometry.target_point)|indent(8) }}
-      ]
-      {%- endif %}
-    ]
-    {%- endif %}
-    {{ cellbase(page_index, page, edge, edge_iri) }}
-  {# closing ']' is taken care of by cellbase #}
-  {%- endfor %}
   {%- endfor %}
 
 }

+ 1 - 1
test/run_tests.py

@@ -42,7 +42,7 @@ if __name__ == "__main__":
             # print(csyntax2.getvalue())
             raise Exception("Files differ after round-trip!")
 
-        oml_generator.write_oml(drawio_file=asyntax, oml_namespace="my_drawio", ostream=DummyOutput())
+        oml_generator.write_oml(drawio_file=asyntax, ostream=DummyOutput())
         print(filename, "OK")
 
     # Just see if these files parse without throwing an exception :)