1#!/usr/bin/python 2## 3## license:BSD-3-Clause 4## copyright-holders:Vas Crabb 5 6import os 7import os.path 8import re 9import sys 10import xml.sax 11import xml.sax.saxutils 12import zlib 13 14 15# workaround for version incompatibility 16if sys.version_info > (3, ): 17 long = int 18 19 20class ErrorHandler(object): 21 def __init__(self, **kwargs): 22 super(ErrorHandler, self).__init__(**kwargs) 23 self.errors = 0 24 self.warnings = 0 25 26 def error(self, exception): 27 self.errors += 1 28 sys.stderr.write('error: %s' % (exception)) 29 30 def fatalError(self, exception): 31 raise exception 32 33 def warning(self, exception): 34 self.warnings += 1 35 sys.stderr.write('warning: %s' % (exception)) 36 37 38class Minifyer(object): 39 def __init__(self, output, **kwargs): 40 super(Minifyer, self).__init__(**kwargs) 41 42 self.output = output 43 self.incomplete_tag = False 44 self.element_content = '' 45 46 def setDocumentLocator(self, locator): 47 pass 48 49 def startDocument(self): 50 self.output('<?xml version="1.0"?>') 51 52 def endDocument(self): 53 self.output('\n') 54 55 def startElement(self, name, attrs): 56 self.flushElementContent() 57 if self.incomplete_tag: 58 self.output('>') 59 self.output('<%s' % (name)) 60 for name in attrs.getNames(): 61 self.output(' %s=%s' % (name, xml.sax.saxutils.quoteattr(attrs[name]))) 62 self.incomplete_tag = True 63 64 def endElement(self, name): 65 self.flushElementContent() 66 if self.incomplete_tag: 67 self.output('/>') 68 else: 69 self.output('</%s>' % (name)) 70 self.incomplete_tag = False 71 72 def characters(self, content): 73 self.element_content += content 74 75 def ignorableWhitespace(self, whitespace): 76 pass 77 78 def processingInstruction(self, target, data): 79 pass 80 81 def flushElementContent(self): 82 self.element_content = self.element_content.strip() 83 if self.element_content: 84 if self.incomplete_tag: 85 self.output('>') 86 self.incomplete_tag = False 87 self.output(xml.sax.saxutils.escape(self.element_content)) 88 self.element_content = '' 89 90 91class XmlError(Exception): 92 pass 93 94 95class LayoutChecker(Minifyer): 96 BADTAGPATTERN = re.compile('[^abcdefghijklmnopqrstuvwxyz0123456789_.:^$]') 97 VARPATTERN = re.compile('^.*~[0-9A-Za-z_]+~.*$') 98 FLOATCHARS = re.compile('^.*[.eE].*$') 99 SHAPES = frozenset(('disk', 'led14seg', 'led14segsc', 'led16seg', 'led16segsc', 'led7seg', 'led8seg_gts1', 'rect')) 100 ORIENTATIONS = frozenset((0, 90, 180, 270)) 101 YESNO = frozenset(('yes', 'no')) 102 BLENDMODES = frozenset(('none', 'alpha', 'multiply', 'add')) 103 104 def __init__(self, output, **kwargs): 105 super(LayoutChecker, self).__init__(output=output, **kwargs) 106 self.locator = None 107 self.errors = 0 108 self.elements = { } 109 self.groups = { } 110 self.views = { } 111 self.referenced_elements = { } 112 self.referenced_groups = { } 113 self.group_collections = { } 114 self.current_collections = None 115 116 def formatLocation(self): 117 return '%s:%d:%d' % (self.locator.getSystemId(), self.locator.getLineNumber(), self.locator.getColumnNumber()) 118 119 def handleError(self, msg): 120 self.errors += 1 121 sys.stderr.write('error: %s: %s\n' % (self.formatLocation(), msg)) 122 123 def checkIntAttribute(self, name, attrs, key, default): 124 if key not in attrs: 125 return default 126 val = attrs[key] 127 if self.VARPATTERN.match(val): 128 return None 129 base = 10 130 offs = 0 131 if (len(val) >= 1) and ('$' == val[0]): 132 base = 16 133 offs = 1 134 elif (len(val) >= 2) and ('0' == val[0]) and (('x' == val[1]) or ('X' == val[1])): 135 base = 16 136 offs = 2 137 elif (len(val) >= 1) and ('#' == val[0]): 138 offs = 1 139 try: 140 return int(val[offs:], base) 141 except: 142 self.handleError('Element %s attribute %s "%s" is not an integer' % (name, key, val)) 143 return None 144 145 def checkFloatAttribute(self, name, attrs, key, default): 146 if key not in attrs: 147 return default 148 val = attrs[key] 149 if self.VARPATTERN.match(val): 150 return None 151 try: 152 return float(val) 153 except: 154 self.handleError('Element %s attribute %s "%s" is not a floating point number' % (name, key, val)) 155 return None 156 157 def checkNumericAttribute(self, name, attrs, key, default): 158 if key not in attrs: 159 return default 160 val = attrs[key] 161 if self.VARPATTERN.match(val): 162 return None 163 base = 0 164 offs = 0 165 try: 166 if (len(val) >= 1) and ('$' == val[0]): 167 base = 16 168 offs = 1 169 elif (len(val) >= 2) and ('0' == val[0]) and (('x' == val[1]) or ('X' == val[1])): 170 base = 16 171 offs = 2 172 elif (len(val) >= 1) and ('#' == val[0]): 173 base = 10 174 offs = 1 175 elif self.FLOATCHARS.match(val): 176 return float(val) 177 return int(val[offs:], base) 178 except: 179 self.handleError('Element %s attribute %s "%s" is not a number' % (name, key, val)) 180 return None 181 182 def checkParameter(self, attrs): 183 if 'name' not in attrs: 184 self.handleError('Element param missing attribute name') 185 else: 186 name = attrs['name'] 187 self.checkNumericAttribute('param', attrs, 'increment', None) 188 lshift = self.checkIntAttribute('param', attrs, 'lshift', None) 189 if (lshift is not None) and (0 > lshift): 190 self.handleError('Element param attribute lshift "%s" is negative' % (attrs['lshift'], )) 191 rshift = self.checkIntAttribute('param', attrs, 'rshift', None) 192 if (rshift is not None) and (0 > rshift): 193 self.handleError('Element param attribute rshift "%s" is negative' % (attrs['rshift'], )) 194 if self.repeat_depth and self.repeat_depth[-1]: 195 if 'start' in attrs: 196 if 'value' in attrs: 197 self.handleError('Element param has both start and value attributes') 198 if 'name' in attrs: 199 if name not in self.variable_scopes[-1]: 200 self.variable_scopes[-1][name] = True 201 elif not self.VARPATTERN.match(name): 202 self.handleError('Generator parameter "%s" redefined' % (name, )) 203 else: 204 if 'value' not in attrs: 205 self.handleError('Element param missing attribute value') 206 if ('increment' in attrs) or ('lshift' in attrs) or ('rshift' in attrs): 207 self.handleError('Element param has increment/lshift/rshift attribute(s) without start attribute') 208 if 'name' in attrs: 209 if not self.variable_scopes[-1].get(name, False): 210 self.variable_scopes[-1][name] = False 211 elif not self.VARPATTERN.match(name): 212 self.handleError('Generator parameter "%s" redefined' % (name, )) 213 else: 214 if ('start' in attrs) or ('increment' in attrs) or ('lshift' in attrs) or ('rshift' in attrs): 215 self.handleError('Element param with start/increment/lshift/rshift attribute(s) not in repeat scope') 216 if 'value' not in attrs: 217 self.handleError('Element param missing attribute value') 218 if 'name' in attrs: 219 self.variable_scopes[-1][attrs['name']] = False 220 221 def checkBounds(self, attrs): 222 left = self.checkFloatAttribute('bounds', attrs, 'left', 0.0) 223 top = self.checkFloatAttribute('bounds', attrs, 'top', 0.0) 224 right = self.checkFloatAttribute('bounds', attrs, 'right', 1.0) 225 bottom = self.checkFloatAttribute('bounds', attrs, 'bottom', 1.0) 226 x = self.checkFloatAttribute('bounds', attrs, 'x', 0.0) 227 y = self.checkFloatAttribute('bounds', attrs, 'y', 0.0) 228 xc = self.checkFloatAttribute('bounds', attrs, 'xc', 0.0) 229 yc = self.checkFloatAttribute('bounds', attrs, 'yc', 0.0) 230 width = self.checkFloatAttribute('bounds', attrs, 'width', 1.0) 231 height = self.checkFloatAttribute('bounds', attrs, 'height', 1.0) 232 if (left is not None) and (right is not None) and (left > right): 233 self.handleError('Element bounds attribute left "%s" is greater than attribute right "%s"' % ( 234 attrs.get('left', 0.0), 235 attrs.get('right', 1.0))) 236 if (top is not None) and (bottom is not None) and (top > bottom): 237 self.handleError('Element bounds attribute top "%s" is greater than attribute bottom "%s"' % ( 238 attrs.get('top', 0.0), 239 attrs.get('bottom', 1.0))) 240 if (width is not None) and (0.0 > width): 241 self.handleError('Element bounds attribute width "%s" is negative' % (attrs['width'], )) 242 if (height is not None) and (0.0 > height): 243 self.handleError('Element bounds attribute height "%s" is negative' % (attrs['height'], )) 244 if (('left' in attrs) and (('x' in attrs) or ('xc' in attrs))) or (('x' in attrs) and ('xc' in attrs)): 245 self.handleError('Element bounds has multiple horizontal origin attributes (left/x/xc)') 246 if (('left' in attrs) and ('width' in attrs)) or ((('x' in attrs) or ('xc' in attrs)) and ('right' in attrs)): 247 self.handleError('Element bounds has both left/right and x/xc/width attributes') 248 if (('top' in attrs) and (('y' in attrs) or ('yc' in attrs))) or (('y' in attrs) and ('yc' in attrs)): 249 self.handleError('Element bounds has multiple vertical origin attributes (top/y/yc)') 250 if (('top' in attrs) and ('height' in attrs)) or ((('y' in attrs) or ('yc' in attrs)) and ('bottom' in attrs)): 251 self.handleError('Element bounds has both top/bottom and y/yc/height attributes') 252 253 def checkOrientation(self, attrs): 254 if self.have_orientation[-1]: 255 self.handleError('Duplicate element orientation') 256 else: 257 self.have_orientation[-1] = True 258 if self.checkIntAttribute('orientation', attrs, 'rotate', 0) not in self.ORIENTATIONS: 259 self.handleError('Element orientation attribute rotate "%s" is unsupported' % (attrs['rotate'], )) 260 for name in ('swapxy', 'flipx', 'flipy'): 261 if (attrs.get(name, 'no') not in self.YESNO) and (not self.VARPATTERN.match(attrs['yesno'])): 262 self.handleError('Element orientation attribute %s "%s" is not "yes" or "no"' % (name, attrs[name])) 263 264 def checkColor(self, attrs): 265 self.checkColorChannel(attrs, 'red') 266 self.checkColorChannel(attrs, 'green') 267 self.checkColorChannel(attrs, 'blue') 268 self.checkColorChannel(attrs, 'alpha') 269 270 def checkColorChannel(self, attrs, name): 271 channel = self.checkFloatAttribute('color', attrs, name, None) 272 if (channel is not None) and ((0.0 > channel) or (1.0 < channel)): 273 self.handleError('Element color attribute %s "%s" outside valid range 0.0-1.0' % (name, attrs[name])) 274 275 def checkTag(self, tag, element, attr): 276 if '' == tag: 277 self.handleError('Element %s attribute %s is empty' % (element, attr)) 278 else: 279 if tag.find('^') >= 0: 280 self.handleError('Element %s attribute %s "%s" contains parent device reference' % (element, attr, tag)) 281 if ':' == tag[-1]: 282 self.handleError('Element %s attribute %s "%s" ends with separator' % (element, attr, tag)) 283 if tag.find('::') >= 0: 284 self.handleError('Element %s attribute %s "%s" contains double separator' % (element, attr, tag)) 285 286 def checkComponent(self, name, attrs): 287 statemask = self.checkIntAttribute(name, attrs, 'statemask', None) 288 stateval = self.checkIntAttribute(name, attrs, 'state', None) 289 if stateval is not None: 290 if 0 > stateval: 291 self.handleError('Element %s attribute state "%s" is negative' % (name, attrs['state'])) 292 if (statemask is not None) and (stateval & ~statemask): 293 self.handleError('Element %s attribute state "%s" has bits set that are clear in attribute statemask "%s"' % (name, attrs['state'], attrs['statemask'])) 294 if 'image' == name: 295 self.handlers.append((self.imageComponentStartHandler, self.imageComponentEndHandler)) 296 else: 297 self.handlers.append((self.componentStartHandler, self.componentEndHandler)) 298 self.have_bounds.append({ }) 299 self.have_color.append({ }) 300 301 def checkViewItem(self, name, attrs): 302 if ('blend' in attrs) and (attrs['blend'] not in self.BLENDMODES) and not self.VARPATTERN.match(attrs['blend']): 303 self.handleError('Element %s attribute blend "%s" is unsupported' % (name, attrs['blend'])) 304 if 'inputtag' in attrs: 305 if 'inputmask' not in attrs: 306 self.handleError('Element %s has inputtag attribute without inputmask attribute' % (name, )) 307 self.checkTag(attrs['inputtag'], name, 'inputtag') 308 elif 'inputmask' in attrs: 309 self.handleError('Element %s has inputmask attribute without inputtag attribute' % (name, )) 310 inputraw = None 311 if 'inputraw' in attrs: 312 if (attrs['inputraw'] not in self.YESNO) and (not self.VARPATTERN.match(attrs['inputraw'])): 313 self.handleError('Element %s attribute inputraw "%s" is not "yes" or "no"' % (name, attrs['inputraw'])) 314 else: 315 inputraw = 'yes' == attrs['inputraw'] 316 if 'inputmask' not in attrs: 317 self.handleError('Element %s has inputraw attribute without inputmask attribute' % (name, )) 318 if 'inputtag' not in attrs: 319 self.handleError('Element %s has inputraw attribute without inputtag attribute' % (name, )) 320 inputmask = self.checkIntAttribute(name, attrs, 'inputmask', None) 321 if (inputmask is not None) and (not inputmask): 322 if (inputraw is None) or (not inputraw): 323 self.handleError('Element %s attribute inputmask "%s" is zero' % (name, attrs['inputmask'])) 324 325 def startViewItem(self, name): 326 self.handlers.append((self.viewItemStartHandler, self.viewItemEndHandler)) 327 self.have_bounds.append(None if 'group' == name else { }) 328 self.have_orientation.append(False) 329 self.have_color.append(None if 'group' == name else { }) 330 331 def rootStartHandler(self, name, attrs): 332 if 'mamelayout' != name: 333 self.ignored_depth = 1 334 self.handleError('Expected root element mamelayout but found %s' % (name, )) 335 else: 336 if 'version' not in attrs: 337 self.handleError('Element mamelayout missing attribute version') 338 else: 339 try: 340 long(attrs['version']) 341 except: 342 self.handleError('Element mamelayout attribute version "%s" is not an integer' % (attrs['version'], )) 343 self.variable_scopes.append({ }) 344 self.repeat_depth.append(0) 345 self.handlers.append((self.layoutStartHandler, self.layoutEndHandler)) 346 347 def rootEndHandler(self, name, attrs): 348 pass # should be unreachable 349 350 def layoutStartHandler(self, name, attrs): 351 if 'element' == name: 352 if 'name' not in attrs: 353 self.handleError('Element element missing attribute name') 354 else: 355 generated_name = self.VARPATTERN.match(attrs['name']) 356 if generated_name: 357 self.generated_element_names = True 358 if attrs['name'] not in self.elements: 359 self.elements[attrs['name']] = self.formatLocation() 360 elif not generated_name: 361 self.handleError('Element element has duplicate name (previous %s)' % (self.elements[attrs['name']], )) 362 defstate = self.checkIntAttribute(name, attrs, 'defstate', None) 363 if (defstate is not None) and (0 > defstate): 364 self.handleError('Element element attribute defstate "%s" is negative' % (attrs['defstate'], )) 365 self.handlers.append((self.elementStartHandler, self.elementEndHandler)) 366 elif 'group' == name: 367 self.current_collections = { } 368 if 'name' not in attrs: 369 self.handleError('Element group missing attribute name') 370 else: 371 generated_name = self.VARPATTERN.match(attrs['name']) 372 if generated_name: 373 self.generated_group_names = True 374 if attrs['name'] not in self.groups: 375 self.groups[attrs['name']] = self.formatLocation() 376 if not generated_name: 377 self.group_collections[attrs['name']] = self.current_collections 378 elif not generated_name: 379 self.handleError('Element group has duplicate name (previous %s)' % (self.groups[attrs['name']], )) 380 self.handlers.append((self.groupViewStartHandler, self.groupViewEndHandler)) 381 self.variable_scopes.append({ }) 382 self.repeat_depth.append(0) 383 self.have_bounds.append(None) 384 elif ('view' == name) and (not self.repeat_depth[-1]): 385 self.current_collections = { } 386 if 'name' not in attrs: 387 self.handleError('Element view missing attribute name') 388 else: 389 if attrs['name'] not in self.views: 390 self.views[attrs['name']] = self.formatLocation() 391 elif not self.VARPATTERN.match(attrs['name']): 392 self.handleError('Element view has duplicate name (previous %s)' % (self.views[attrs['name']], )) 393 self.handlers.append((self.groupViewStartHandler, self.groupViewEndHandler)) 394 self.variable_scopes.append({ }) 395 self.repeat_depth.append(0) 396 self.have_bounds.append(None) 397 elif 'repeat' == name: 398 if 'count' not in attrs: 399 self.handleError('Element repeat missing attribute count') 400 else: 401 count = self.checkIntAttribute(name, attrs, 'count', None) 402 if (count is not None) and (0 >= count): 403 self.handleError('Element repeat attribute count "%s" is not positive' % (attrs['count'], )) 404 self.variable_scopes.append({ }) 405 self.repeat_depth[-1] += 1 406 elif 'param' == name: 407 self.checkParameter(attrs) 408 self.ignored_depth = 1 409 elif ('script' == name) and (not self.repeat_depth[-1]): 410 self.ignored_depth = 1 411 else: 412 self.handleError('Encountered unexpected element %s' % (name, )) 413 self.ignored_depth = 1 414 415 def layoutEndHandler(self, name): 416 self.variable_scopes.pop() 417 if self.repeat_depth[-1]: 418 self.repeat_depth[-1] -= 1 419 else: 420 if not self.generated_element_names: 421 for element in self.referenced_elements: 422 if (element not in self.elements) and (not self.VARPATTERN.match(element)): 423 self.handleError('Element "%s" not found (first referenced at %s)' % (element, self.referenced_elements[element])) 424 if not self.generated_group_names: 425 for group in self.referenced_groups: 426 if (group not in self.groups) and (not self.VARPATTERN.match(group)): 427 self.handleError('Group "%s" not found (first referenced at %s)' % (group, self.referenced_groups[group])) 428 if not self.views: 429 self.handleError('No view elements found') 430 self.handlers.pop() 431 432 def elementStartHandler(self, name, attrs): 433 if name in self.SHAPES: 434 self.checkComponent(name, attrs) 435 elif 'text' == name: 436 if 'string' not in attrs: 437 self.handleError('Element text missing attribute string') 438 align = self.checkIntAttribute(name, attrs, 'align', None) 439 if (align is not None) and ((0 > align) or (2 < align)): 440 self.handleError('Element text attribute align "%s" not in valid range 0-2' % (attrs['align'], )) 441 self.checkComponent(name, attrs) 442 elif 'simplecounter' == name: 443 maxstate = self.checkIntAttribute(name, attrs, 'maxstate', None) 444 if (maxstate is not None) and (0 > maxstate): 445 self.handleError('Element simplecounter attribute maxstate "%s" is negative' % (attrs['maxstate'], )) 446 digits = self.checkIntAttribute(name, attrs, 'digits', None) 447 if (digits is not None) and (0 >= digits): 448 self.handleError('Element simplecounter attribute digits "%s" is not positive' % (attrs['digits'], )) 449 align = self.checkIntAttribute(name, attrs, 'align', None) 450 if (align is not None) and ((0 > align) or (2 < align)): 451 self.handleError('Element simplecounter attribute align "%s" not in valid range 0-2' % (attrs['align'], )) 452 self.checkComponent(name, attrs) 453 elif 'image' == name: 454 self.have_file = 'file' in attrs 455 self.have_data = None 456 self.checkComponent(name, attrs) 457 elif 'reel' == name: 458 # TODO: validate symbollist and improve validation of other attributes 459 self.checkIntAttribute(name, attrs, 'stateoffset', None) 460 numsymbolsvisible = self.checkIntAttribute(name, attrs, 'numsymbolsvisible', None) 461 if (numsymbolsvisible is not None) and (0 >= numsymbolsvisible): 462 self.handleError('Element reel attribute numsymbolsvisible "%s" not positive' % (attrs['numsymbolsvisible'], )) 463 reelreversed = self.checkIntAttribute(name, attrs, 'reelreversed', None) 464 if (reelreversed is not None) and ((0 > reelreversed) or (1 < reelreversed)): 465 self.handleError('Element reel attribute reelreversed "%s" not in valid range 0-1' % (attrs['reelreversed'], )) 466 beltreel = self.checkIntAttribute(name, attrs, 'beltreel', None) 467 if (beltreel is not None) and ((0 > beltreel) or (1 < beltreel)): 468 self.handleError('Element reel attribute beltreel "%s" not in valid range 0-1' % (attrs['beltreel'], )) 469 self.checkComponent(name, attrs) 470 else: 471 self.handleError('Encountered unexpected element %s' % (name, )) 472 self.ignored_depth = 1 473 474 def elementEndHandler(self, name): 475 self.handlers.pop() 476 477 def componentStartHandler(self, name, attrs): 478 if 'bounds' == name: 479 state = self.checkIntAttribute(name, attrs, 'state', 0) 480 if state is not None: 481 if 0 > state: 482 self.handleError('Element bounds attribute state "%s" is negative' % (attrs['state'], )) 483 if state in self.have_bounds[-1]: 484 self.handleError('Duplicate bounds for state %d (previous %s)' % (state, self.have_bounds[-1][state])) 485 else: 486 self.have_bounds[-1][state] = self.formatLocation() 487 self.checkBounds(attrs) 488 elif 'color' == name: 489 state = self.checkIntAttribute(name, attrs, 'state', 0) 490 if state is not None: 491 if 0 > state: 492 self.handleError('Element color attribute state "%s" is negative' % (attrs['state'], )) 493 if state in self.have_color[-1]: 494 self.handleError('Duplicate color for state %d (previous %s)' % (state, self.have_color[-1][state])) 495 else: 496 self.have_color[-1][state] = self.formatLocation() 497 self.checkColor(attrs) 498 self.ignored_depth = 1 499 500 def componentEndHandler(self, name): 501 self.have_bounds.pop() 502 self.have_color.pop() 503 self.handlers.pop() 504 505 def imageComponentStartHandler(self, name, attrs): 506 if 'data' == name: 507 if self.have_data is not None: 508 self.handleError('Element image has multiple data child elements (previous %s)' % (self.have_data)) 509 else: 510 self.have_data = self.formatLocation() 511 if self.have_file: 512 self.handleError('Element image has attribute file and child element data') 513 self.ignored_depth = 1 514 else: 515 self.componentStartHandler(name, attrs) 516 517 def imageComponentEndHandler(self, name): 518 if (not self.have_file) and (self.have_data is None): 519 self.handleError('Element image missing attribute file or child element data') 520 del self.have_file 521 del self.have_data 522 self.componentEndHandler(name) 523 524 def groupViewStartHandler(self, name, attrs): 525 if 'element' == name: 526 if 'ref' not in attrs: 527 self.handleError('Element %s missing attribute ref' % (name, )) 528 elif attrs['ref'] not in self.referenced_elements: 529 self.referenced_elements[attrs['ref']] = self.formatLocation() 530 self.checkViewItem(name, attrs) 531 self.startViewItem(name) 532 elif 'screen' == name: 533 if 'index' in attrs: 534 index = self.checkIntAttribute(name, attrs, 'index', None) 535 if (index is not None) and (0 > index): 536 self.handleError('Element screen attribute index "%s" is negative' % (attrs['index'], )) 537 if 'tag' in attrs: 538 self.handleError('Element screen has both index and tag attributes') 539 if 'tag' in attrs: 540 tag = attrs['tag'] 541 self.checkTag(tag, name, 'tag') 542 if self.BADTAGPATTERN.search(tag): 543 self.handleError('Element screen attribute tag "%s" contains invalid characters' % (tag, )) 544 self.checkViewItem(name, attrs) 545 self.startViewItem(name) 546 elif 'group' == name: 547 if 'ref' not in attrs: 548 self.handleError('Element group missing attribute ref') 549 else: 550 if attrs['ref'] not in self.referenced_groups: 551 self.referenced_groups[attrs['ref']] = self.formatLocation() 552 if (not self.VARPATTERN.match(attrs['ref'])) and (attrs['ref'] in self.group_collections): 553 for n, l in self.group_collections[attrs['ref']].items(): 554 if n not in self.current_collections: 555 self.current_collections[n] = l 556 else: 557 self.handleError('Element group instantiates collection with duplicate name "%s" from %s (previous %s)' % (n, l, self.current_collections[n])) 558 self.startViewItem(name) 559 elif 'repeat' == name: 560 if 'count' not in attrs: 561 self.handleError('Element repeat missing attribute count') 562 else: 563 count = self.checkIntAttribute(name, attrs, 'count', None) 564 if (count is not None) and (0 >= count): 565 self.handleError('Element repeat attribute count "%s" is negative' % (attrs['count'], )) 566 self.variable_scopes.append({ }) 567 self.repeat_depth[-1] += 1 568 elif 'collection' == name: 569 if 'name' not in attrs: 570 self.handleError('Element collection missing attribute name') 571 elif not self.VARPATTERN.match(attrs['name']): 572 if attrs['name'] not in self.current_collections: 573 self.current_collections[attrs['name']] = self.formatLocation() 574 else: 575 self.handleError('Element collection has duplicate name (previous %s)' % (self.current_collections[attrs['name']], )) 576 if attrs.get('visible', 'yes') not in self.YESNO: 577 self.handleError('Element collection attribute visible "%s" is not "yes" or "no"' % (attrs['visible'], )) 578 self.variable_scopes.append({ }) 579 self.collection_depth += 1 580 elif 'param' == name: 581 self.checkParameter(attrs) 582 self.ignored_depth = 1 583 elif 'bounds' == name: 584 if self.have_bounds[-1] is not None: 585 self.handleError('Duplicate element bounds (previous %s)' % (self.have_bounds[-1], )) 586 else: 587 self.have_bounds[-1] = self.formatLocation() 588 self.checkBounds(attrs) 589 if self.repeat_depth[-1]: 590 self.handleError('Element bounds inside repeat') 591 elif self.collection_depth: 592 self.handleError('Element bounds inside collection') 593 self.ignored_depth = 1 594 else: 595 self.handleError('Encountered unexpected element %s' % (name, )) 596 self.ignored_depth = 1 597 598 def groupViewEndHandler(self, name): 599 self.variable_scopes.pop() 600 if 'collection' == name: 601 self.collection_depth -= 1 602 elif self.repeat_depth[-1]: 603 self.repeat_depth[-1] -= 1 604 else: 605 self.current_collections = None 606 self.repeat_depth.pop() 607 self.have_bounds.pop() 608 self.handlers.pop() 609 610 def viewItemStartHandler(self, name, attrs): 611 if 'animate' == name: 612 if isinstance(self.have_bounds[-1], dict): 613 if 'inputtag' in attrs: 614 if 'name' in attrs: 615 self.handleError('Element animate has both attribute inputtag and attribute name') 616 self.checkTag(attrs['inputtag'], name, 'inputtag') 617 elif 'name' not in attrs: 618 self.handleError('Element animate has neither attribute inputtag nor attribute name') 619 self.checkIntAttribute(name, attrs, 'mask', None) 620 else: 621 self.handleError('Encountered unexpected element %s' % (name, )) 622 elif 'bounds' == name: 623 if self.have_bounds[-1] is None: 624 self.have_bounds[-1] = self.formatLocation() 625 elif isinstance(self.have_bounds[-1], dict): 626 state = self.checkIntAttribute(name, attrs, 'state', 0) 627 if state is not None: 628 if 0 > state: 629 self.handleError('Element bounds attribute state "%s" is negative' % (attrs['state'], )) 630 if state in self.have_bounds[-1]: 631 self.handleError('Duplicate bounds for state %d (previous %s)' % (state, self.have_bounds[-1][state])) 632 else: 633 self.have_bounds[-1][state] = self.formatLocation() 634 else: 635 self.handleError('Duplicate element bounds (previous %s)' % (self.have_bounds[-1], )) 636 self.checkBounds(attrs) 637 elif 'orientation' == name: 638 self.checkOrientation(attrs) 639 elif 'color' == name: 640 if self.have_color[-1] is None: 641 self.have_color[-1] = self.formatLocation() 642 elif isinstance(self.have_color[-1], dict): 643 state = self.checkIntAttribute(name, attrs, 'state', 0) 644 if state is not None: 645 if 0 > state: 646 self.handleError('Element color attribute state "%s" is negative' % (attrs['state'], )) 647 if state in self.have_color[-1]: 648 self.handleError('Duplicate color for state %d (previous %s)' % (state, self.have_color[-1][state])) 649 else: 650 self.have_color[-1][state] = self.formatLocation() 651 else: 652 self.handleError('Duplicate element color (previous %s)' % (self.have_color[-1], )) 653 self.checkColor(attrs) 654 else: 655 self.handleError('Encountered unexpected element %s' % (name, )) 656 self.ignored_depth = 1 657 658 def viewItemEndHandler(self, name): 659 self.have_bounds.pop() 660 self.have_orientation.pop() 661 self.have_color.pop() 662 self.handlers.pop() 663 664 def setDocumentLocator(self, locator): 665 self.locator = locator 666 super(LayoutChecker, self).setDocumentLocator(locator) 667 668 def startDocument(self): 669 self.handlers = [(self.rootStartHandler, self.rootEndHandler)] 670 self.ignored_depth = 0 671 self.variable_scopes = [ ] 672 self.repeat_depth = [ ] 673 self.collection_depth = 0 674 self.have_bounds = [ ] 675 self.have_orientation = [ ] 676 self.have_color = [ ] 677 self.generated_element_names = False 678 self.generated_group_names = False 679 super(LayoutChecker, self).startDocument() 680 681 def endDocument(self): 682 self.locator = None 683 self.elements.clear() 684 self.groups.clear() 685 self.views.clear() 686 self.referenced_elements.clear() 687 self.referenced_groups.clear() 688 self.group_collections.clear() 689 self.current_collections = None 690 del self.handlers 691 del self.ignored_depth 692 del self.variable_scopes 693 del self.repeat_depth 694 del self.collection_depth 695 del self.have_bounds 696 del self.have_orientation 697 del self.have_color 698 del self.generated_element_names 699 del self.generated_group_names 700 super(LayoutChecker, self).endDocument() 701 702 def startElement(self, name, attrs): 703 if 0 < self.ignored_depth: 704 self.ignored_depth += 1 705 else: 706 self.handlers[-1][0](name, attrs) 707 super(LayoutChecker, self).startElement(name, attrs) 708 709 def endElement(self, name): 710 if 0 < self.ignored_depth: 711 self.ignored_depth -= 1 712 else: 713 self.handlers[-1][1](name) 714 super(LayoutChecker, self).endElement(name) 715 716 717def compressLayout(src, dst, comp): 718 state = [0, 0] 719 def write(block): 720 for ch in bytearray(block): 721 if 0 == state[0]: 722 dst('\t') 723 elif 0 == (state[0] % 32): 724 dst(',\n\t') 725 else: 726 dst(', ') 727 state[0] += 1 728 dst('%3u' % (ch)) 729 730 def output(text): 731 block = text.encode('UTF-8') 732 state[1] += len(block) 733 write(comp.compress(block)) 734 735 error_handler = ErrorHandler() 736 content_handler = LayoutChecker(output) 737 parser = xml.sax.make_parser() 738 parser.setErrorHandler(error_handler) 739 parser.setContentHandler(content_handler) 740 try: 741 parser.parse(src) 742 write(comp.flush()) 743 dst('\n') 744 except xml.sax.SAXException as exception: 745 print('fatal error: %s' % (exception)) 746 raise XmlError('Fatal error parsing XML') 747 if (content_handler.errors > 0) or (error_handler.errors > 0) or (error_handler.warnings > 0): 748 raise XmlError('Error(s) and/or warning(s) parsing XML') 749 750 return state[1], state[0] 751 752 753class BlackHole(object): 754 def write(self, *args): 755 pass 756 def close(self): 757 pass 758 759 760if __name__ == '__main__': 761 if (len(sys.argv) > 4) or (len(sys.argv) < 2): 762 print('Usage:') 763 print(' complay <source.lay> [<output.h> [<varname>]]') 764 sys.exit(0 if len(sys.argv) <= 1 else 1) 765 766 srcfile = sys.argv[1] 767 dstfile = sys.argv[2] if len(sys.argv) >= 3 else None 768 if len(sys.argv) >= 4: 769 varname = sys.argv[3] 770 else: 771 varname = os.path.basename(srcfile) 772 base, ext = os.path.splitext(varname) 773 if ext.lower() == '.lay': 774 varname = base 775 varname = 'layout_' + re.sub('[^0-9A-Za-z_]', '_', varname) 776 777 comp_type = 'internal_layout::compression::ZLIB' 778 try: 779 dst = open(dstfile,'w') if dstfile is not None else BlackHole() 780 dst.write('static const unsigned char %s_data[] = {\n' % (varname)) 781 byte_count, comp_size = compressLayout(srcfile, lambda x: dst.write(x), zlib.compressobj()) 782 dst.write('};\n\n') 783 dst.write('const internal_layout %s = {\n' % (varname)) 784 dst.write('\t%d, sizeof(%s_data), %s, %s_data\n' % (byte_count, varname, comp_type, varname)) 785 dst.write('};\n') 786 dst.close() 787 except XmlError: 788 dst.close() 789 if dstfile is not None: 790 os.remove(dstfile) 791 sys.exit(2) 792 except IOError: 793 sys.stderr.write("Unable to open output file '%s'\n" % dstfile) 794 dst.close() 795 if dstfile is not None: 796 os.remove(dstfile) 797 sys.exit(3) 798