Setting the file. One moment.
Validate Drawio · Draw IO Diagram Generator · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page scripts/validate-drawio.py
scripts/ validate-drawio.py
Python · 214 lines · 7 KB
as
ET
16 from pathlib import Path
17
18
19 def _error (msg: str , errors: list ) -> None :
20 errors.append(msg)
21 print ( f " ERROR: { msg } " )
22
23
24 def validate_file (path: Path) -> list[ str ]:
25 """Parse and validate a single .drawio file. Returns list of error strings."""
26 errors: list[ str ] = []
27
28 # --- XML well-formedness ---
29 try :
30 tree = ET .parse(path)
31 except ET .ParseError as exc:
32 return [ f "XML parse error: { exc } " ]
33
34 root = tree.getroot()
35 if root.tag != "mxfile" :
36 _error( f "Root element must be <mxfile>, got < { root.tag } >" , errors)
37 return errors
38
39 diagrams = root.findall( "diagram" )
40 if not diagrams:
41 _error( "No <diagram> elements found inside <mxfile>" , errors)
42 return errors
43
44 for d_idx, diagram in enumerate (diagrams):
45 d_name = diagram.get( "name" , f "page- { d_idx } " )
46 prefix = f "[diagram ' { d_name } ']"
47
48 # Find mxGraphModel (may be direct child or base64-encoded; we handle direct only)
49 graph_model = diagram.find( "mxGraphModel" )
50 if graph_model is None :
51 print ( f " SKIP { prefix } : mxGraphModel not found as direct child (may be compressed)" )
52 continue
53
54 root_elem = graph_model.find( "root" )
55 if root_elem is None :
56 _error( f " { prefix } Missing <root> element inside <mxGraphModel>" , errors)
57 continue
58
59 cells = root_elem.findall( "mxCell" )
60 cell_ids: dict[ str , ET .Element] = {}
61 has_id0 = False
62 has_id1 = False
63
64 # --- Collect all IDs, check for root cells ---
65 for cell in cells:
66 cid = cell.get( "id" )
67 if cid is None :
68 _error( f " { prefix } Found <mxCell> without an 'id' attribute" , errors)
69 continue
70 if cid in cell_ids:
71 _error( f " { prefix } Duplicate cell id=' { cid } '" , errors)
72 cell_ids[cid] = cell
73 if cid == "0" :
74 has_id0 = True
75 if cid == "1" :
76 has_id1 = True
77
78 if not has_id0:
79 _error( f " { prefix } Missing required root cell id='0'" , errors)
80 if not has_id1:
81 _error( f " { prefix } Missing required default-layer cell id='1'" , errors)
82
83 # L2: id="0" must be the first cell, id="1" must be the second cell
84 if len (cells) >= 1 and cells[ 0 ].get( "id" ) != "0" :
85 _error(
86 f " { prefix } First <mxCell> must have id='0', "
87 f "got id=' { cells[ 0 ].get( 'id' ) } '" ,
88 errors,
89 )
90 if len (cells) >= 2 and cells[ 1 ].get( "id" ) != "1" :
91 _error(
92 f " { prefix } Second <mxCell> must have id='1', "
93 f "got id=' { cells[ 1 ].get( 'id' ) } '" ,
94 errors,
95 )
96 # L3: id="1" must have parent="0"
97 for cell in cells:
98 if cell.get( "id" ) == "1" and cell.get( "parent" ) != "0" :
99 _error(
100 f " { prefix } Cell id='1' must have parent='0', "
101 f "got parent=' { cell.get( 'parent' ) } '" ,
102 errors,
103 )
104 # H2: Every diagram page must contain a title cell
105 # (a vertex with style containing 'text;' and 'fontSize=18')
106 def _is_title_style (style: str ) -> bool :
107 """Return True if the style string identifies a draw.io title text cell."""
108 return (
109 (style.startswith( "text;" ) or ";text;" in style)
110 and "fontSize=18" in style
111 )
112
113 has_title_cell = any (
114 c.get( "vertex" ) == "1" and _is_title_style(c.get( "style" ) or "" )
115 for c in cells
116 )
117 if not has_title_cell:
118 _error(
119 f " { prefix } No title cell found — add a vertex with style "
120 "containing 'text;' and 'fontSize=18' at the top of the page" ,
121 errors,
122 )
123
124 # --- Check each cell for structural validity ---
125 for cell in cells:
126 cid = cell.get( "id" , "<unknown>" )
127 is_vertex = cell.get( "vertex" ) == "1"
128 is_edge = cell.get( "edge" ) == "1"
129
130 # Parent must exist (skip the root cell id=0 which has no parent)
131 parent = cell.get( "parent" )
132 if cid != "0" :
133 if parent is None :
134 _error( f " { prefix } Cell id=' { cid } ' is missing a 'parent' attribute" , errors)
135 elif parent not in cell_ids:
136 _error(
137 f " { prefix } Cell id=' { cid } ' references unknown parent=' { parent } '" ,
138 errors,
139 )
140
141 # Vertex cells must have mxGeometry
142 if is_vertex:
143 geom = cell.find( "mxGeometry" )
144 if geom is None :
145 _error(
146 f " { prefix } Vertex cell id=' { cid } ' is missing <mxGeometry>" ,
147 errors,
148 )
149
150 # Edge cells must have source and target, both must exist.
151 # Exception: floating edges (e.g. sequence diagram lifelines) use
152 # sourcePoint/targetPoint in mxGeometry instead of source/target attributes.
153 if is_edge:
154 source = cell.get( "source" )
155 target = cell.get( "target" )
156 geom = cell.find( "mxGeometry" )
157 has_source_point = geom is not None and any (
158 p.get( "as" ) == "sourcePoint" for p in geom.findall( "mxPoint" )
159 )
160 has_target_point = geom is not None and any (
161 p.get( "as" ) == "targetPoint" for p in geom.findall( "mxPoint" )
162 )
163 if source is None and not has_source_point:
164 _error(
165 f " { prefix } Edge cell id=' { cid } ' is missing 'source' attribute "
166 f "(and no sourcePoint in mxGeometry)" ,
167 errors,
168 )
169 elif source is not None and source not in cell_ids:
170 _error(
171 f " { prefix } Edge id=' { cid } ' references unknown source=' { source } '" ,
172 errors,
173 )
174 if target is None and not has_target_point:
175 _error(
176 f " { prefix } Edge cell id=' { cid } ' is missing 'target' attribute "
177 f "(and no targetPoint in mxGeometry)" ,
178 errors,
179 )
180 elif target is not None and target not in cell_ids:
181 _error(
182 f " { prefix } Edge id=' { cid } ' references unknown target=' { target } '" ,
183 errors,
184 )
185
186 return errors
187
188
189 def main () -> int :
190 if len (sys.argv) < 2 :
191 print ( "Usage: python validate-drawio.py <diagram.drawio>" )
192 return 1
193
194 path = Path(sys.argv[ 1 ])
195 if not path.exists():
196 print ( f "File not found: { path } " )
197 return 1
198 if not path.is_file():
199 print ( f "Not a file: { path } " )
200 return 1
201
202 print ( f "Validating: { path } " )
203 errors = validate_file(path)
204
205 if errors:
206 print ( f " \n FAIL — { len (errors) } error(s) found." )
207 return 1
208
209 print ( "PASS — No errors found." )
210 return 0
211
212
213 if __name__ == "__main__" :
214 sys.exit(main())