3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
10 from __future__
import division
14 from xml
.etree
.ElementTree
import ElementTree
16 # Python 3 moved this for no really good reason.
18 from sys
import intern
23 from io
import StringIO
26 from cStringIO
import StringIO
28 from StringIO
import StringIO
30 from bulletml
.errors
import Error
31 from bulletml
.expr
import NumberDef
, INumberDef
34 __all__
= ["ParseError", "BulletML"]
36 class ParseError(Error
):
37 """Raised when an error occurs parsing the XML structure."""
41 """Strip namespace poop off the front of a tag."""
43 return element
.tag
.rsplit('}', 1)[1]
47 class ParamList(object):
48 """List of parameter definitions."""
50 def __init__(self
, params
=()):
51 self
.params
= list(params
)
54 def FromXML(cls
, doc
, element
):
55 """Construct using an ElementTree-style element."""
56 return cls([NumberDef(subelem
.text
) for subelem
in element
57 if realtag(subelem
) == "param"])
59 def __call__(self
, params
, rank
):
60 return [param(params
, rank
) for param
in self
.params
]
63 return "%s(%r)" % (type(self
).__name
__, self
.params
)
65 class Direction(object):
66 """Raw direction value."""
68 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
70 def __init__(self
, type, value
):
71 if type not in self
.VALID_TYPES
:
72 raise ValueError("invalid type %r" % type)
73 self
.type = intern(type)
76 def __getstate__(self
):
77 return [('type', self
.type), ('value', self
.value
.expr
)]
79 def __setstate__(self
, state
):
81 self
.__init
__(state
["type"], NumberDef(state
["value"]))
84 def FromXML(cls
, doc
, element
, default
="absolute"):
85 """Construct using an ElementTree-style element."""
86 return cls(element
.get("type", default
), NumberDef(element
.text
))
88 def __call__(self
, params
, rank
):
89 return (math
.radians(self
.value(params
, rank
)), self
.type)
92 return "%s(%r, type=%r)" % (
93 type(self
).__name
__, self
.value
, self
.type)
95 class ChangeDirection(object):
96 """Direction change over time."""
98 def __init__(self
, term
, direction
):
100 self
.direction
= direction
102 def __getstate__(self
):
103 return [('frames', self
.term
.expr
),
104 ('type', self
.direction
.type),
105 ('value', self
.direction
.value
.expr
)]
107 def __setstate__(self
, state
):
109 self
.__init
__(INumberDef(state
["frames"]),
110 Direction(state
["type"], NumberDef(state
["value"])))
113 def FromXML(cls
, doc
, element
):
114 """Construct using an ElementTree-style element."""
115 for subelem
in element
.getchildren():
116 tag
= realtag(subelem
)
117 if tag
== "direction":
118 direction
= Direction
.FromXML(doc
, subelem
)
120 term
= INumberDef(subelem
.text
)
122 return cls(term
, direction
)
123 except UnboundLocalError as exc
:
124 raise ParseError(str(exc
))
126 def __call__(self
, params
, rank
):
127 return self
.term(params
, rank
), self
.direction(params
, rank
)
130 return "%s(term=%r, direction=%r)" % (
131 type(self
).__name
__, self
.term
, self
.direction
)
134 """Raw speed value."""
136 VALID_TYPES
= ["relative", "absolute", "sequence"]
138 def __init__(self
, type, value
):
139 if type not in self
.VALID_TYPES
:
140 raise ValueError("invalid type %r" % type)
141 self
.type = intern(type)
144 def __getstate__(self
):
145 return [('type', self
.type), ('value', self
.value
.expr
)]
147 def __setstate__(self
, state
):
149 self
.__init
__(state
["type"], NumberDef(state
["value"]))
152 def FromXML(cls
, doc
, element
):
153 """Construct using an ElementTree-style element."""
154 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
156 def __call__(self
, params
, rank
):
157 return (self
.value(params
, rank
), self
.type)
160 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
162 class ChangeSpeed(object):
163 """Speed change over time."""
165 def __init__(self
, term
, speed
):
169 def __getstate__(self
):
170 return [('frames', self
.term
.expr
),
171 ('type', self
.speed
.type),
172 ('value', self
.speed
.value
.expr
)]
174 def __setstate__(self
, state
):
176 self
.__init
__(INumberDef(state
["frames"]),
177 Speed(state
["type"], NumberDef(state
["value"])))
180 def FromXML(cls
, doc
, element
):
181 """Construct using an ElementTree-style element."""
182 for subelem
in element
.getchildren():
183 tag
= realtag(subelem
)
185 speed
= Speed
.FromXML(doc
, subelem
)
187 term
= INumberDef(subelem
.text
)
189 return cls(term
, speed
)
190 except UnboundLocalError as exc
:
191 raise ParseError(str(exc
))
193 def __call__(self
, params
, rank
):
194 return self
.term(params
, rank
), self
.speed(params
, rank
)
197 return "%s(term=%r, speed=%r)" % (
198 type(self
).__name
__, self
.term
, self
.speed
)
201 """Wait for some frames."""
203 def __init__(self
, frames
):
206 def __getstate__(self
):
207 return dict(frames
=self
.frames
.expr
)
209 def __setstate__(self
, state
):
210 self
.__init
__(INumberDef(state
["frames"]))
213 def FromXML(cls
, doc
, element
):
214 """Construct using an ElementTree-style element."""
215 return cls(INumberDef(element
.text
))
217 def __call__(self
, params
, rank
):
218 return self
.frames(params
, rank
)
221 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
224 """Set a bullet tag."""
226 def __init__(self
, tag
):
229 def __getstate__(self
):
230 return dict(tag
=self
.tag
)
232 def __setstate__(self
, state
):
233 self
.__init
__(state
["tag"])
236 def FromXML(cls
, doc
, element
):
237 """Construct using an ElementTree-style element."""
238 return cls(element
.text
)
241 """Unset a bullet tag."""
243 def __init__(self
, tag
):
246 def __getstate__(self
):
247 return dict(tag
=self
.tag
)
249 def __setstate__(self
, state
):
250 self
.__init
__(state
["tag"])
253 def FromXML(cls
, doc
, element
):
254 """Construct using an ElementTree-style element."""
255 return cls(element
.text
)
257 class Appearance(object):
258 """Set a bullet appearance."""
260 def __init__(self
, appearance
):
261 self
.appearance
= appearance
263 def __getstate__(self
):
264 return dict(appearance
=self
.appearance
)
266 def __setstate__(self
, state
):
267 self
.__init
__(state
["appearance"])
270 def FromXML(cls
, doc
, element
):
271 """Construct using an ElementTree-style element."""
272 return cls(element
.text
)
274 class Vanish(object):
275 """Make the owner disappear."""
281 def FromXML(cls
, doc
, element
):
282 """Construct using an ElementTree-style element."""
286 return "%s()" % (type(self
).__name
__)
288 class Repeat(object):
289 """Repeat an action definition."""
291 def __init__(self
, times
, action
):
295 def __getstate__(self
):
296 return [('times', self
.times
.expr
), ('action', self
.action
)]
298 def __setstate__(self
, state
):
300 self
.__init
__(INumberDef(state
["times"]), state
["action"])
303 def FromXML(cls
, doc
, element
):
304 """Construct using an ElementTree-style element."""
305 for subelem
in element
.getchildren():
306 tag
= realtag(subelem
)
308 times
= INumberDef(subelem
.text
)
309 elif tag
== "action":
310 action
= ActionDef
.FromXML(doc
, subelem
)
311 elif tag
== "actionRef":
312 action
= ActionRef
.FromXML(doc
, subelem
)
314 return cls(times
, action
)
315 except UnboundLocalError as exc
:
316 raise ParseError(str(exc
))
318 def __call__(self
, params
, rank
):
319 return self
.times(params
, rank
), self
.action(params
, rank
)
322 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
325 """Accelerate over some time."""
330 def __init__(self
, term
, horizontal
=None, vertical
=None):
332 self
.horizontal
= horizontal
333 self
.vertical
= vertical
335 def __getstate__(self
):
336 state
= [('frames', self
.term
.expr
)]
338 state
.append(('horizontal', self
.horizontal
))
340 state
.append(('vertical', self
.vertical
))
343 def __setstate__(self
, state
):
345 self
.__init
__(INumberDef(state
["frames"]), state
.get("horizontal"),
346 state
.get("vertical"))
349 def FromXML(cls
, doc
, element
):
350 """Construct using an ElementTree-style element."""
354 for subelem
in element
.getchildren():
355 tag
= realtag(subelem
)
357 term
= INumberDef(subelem
.text
)
358 elif tag
== "horizontal":
359 horizontal
= Speed
.FromXML(doc
, subelem
)
360 elif tag
== "vertical":
361 vertical
= Speed
.FromXML(doc
, subelem
)
364 return cls(term
, horizontal
, vertical
)
365 except AttributeError:
368 def __call__(self
, params
, rank
):
369 frames
= self
.term(params
, rank
)
370 horizontal
= self
.horizontal
and self
.horizontal(params
, rank
)
371 vertical
= self
.vertical
and self
.vertical(params
, rank
)
372 return frames
, horizontal
, vertical
375 return "%s(%r, horizontal=%r, vertical=%r)" % (
376 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
378 class BulletDef(object):
379 """Bullet definition."""
381 def __init__(self
, actions
=(), direction
=None, speed
=None, tags
=(),
383 self
.direction
= direction
385 self
.actions
= list(actions
)
386 self
.tags
= set(tags
)
387 self
.appearance
= appearance
389 def __getstate__(self
):
392 state
.append(("direction", self
.direction
))
394 state
.append(("speed", self
.speed
))
396 state
.append(("actions", self
.actions
))
398 state
.append(("tags", list(self
.tags
)))
400 state
.append(("appearance", self
.appearance
))
403 def __setstate__(self
, state
):
405 self
.__init
__(**state
)
408 def FromXML(cls
, doc
, element
):
409 """Construct using an ElementTree-style element."""
414 for subelem
in element
.getchildren():
415 tag
= realtag(subelem
)
416 if tag
== "direction":
417 direction
= Direction
.FromXML(doc
, subelem
)
419 speed
= Speed
.FromXML(doc
, subelem
)
420 elif tag
== "action":
421 actions
.append(ActionDef
.FromXML(doc
, subelem
))
422 elif tag
== "actionRef":
423 actions
.append(ActionRef
.FromXML(doc
, subelem
))
425 tags
.add(subelem
.text
)
426 dfn
= cls(actions
, direction
, speed
, tags
)
427 doc
._bullets
[element
.get("label")] = dfn
430 def __call__(self
, params
, rank
):
431 actions
= [action(params
, rank
) for action
in self
.actions
]
433 self
.direction
and self
.direction(params
, rank
),
434 self
.speed
and self
.speed(params
, rank
),
440 return "%s(direction=%r, speed=%r, actions=%r)" % (
441 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
443 class BulletRef(object):
444 """Create a bullet by name with parameters."""
446 def __init__(self
, bullet
, params
=None):
448 self
.params
= ParamList() if params
is None else params
450 def __getstate__(self
):
452 if self
.params
.params
:
453 params
= [param
.expr
for param
in self
.params
.params
]
454 state
.append(("params", params
))
455 state
.append(('bullet', self
.bullet
))
458 def __setstate__(self
, state
):
460 bullet
= state
["bullet"]
461 params
= [NumberDef(param
) for param
in state
.get("params", [])]
462 self
.__init
__(bullet
, ParamList(params
))
465 def FromXML(cls
, doc
, element
):
466 """Construct using an ElementTree-style element."""
467 bullet
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
468 doc
._bullet
_refs
.append(bullet
)
471 def __call__(self
, params
, rank
):
472 return self
.bullet(self
.params(params
, rank
), rank
)
475 return "%s(params=%r, bullet=%r)" % (
476 type(self
).__name
__, self
.params
, self
.bullet
)
478 class ActionDef(object):
479 """Action definition.
481 To support parsing new actions, add tags to
482 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
483 FromXML classmethod, which take the BulletML instance and
484 ElementTree element as arguments.
487 # This is self-referential, so it's filled in later.
488 CONSTRUCTORS
= dict()
490 def __init__(self
, actions
):
491 self
.actions
= list(actions
)
493 def __getstate__(self
):
494 return dict(actions
=self
.actions
)
496 def __setstate__(self
, state
):
498 self
.__init
__(state
["actions"])
501 def FromXML(cls
, doc
, element
):
502 """Construct using an ElementTree-style element."""
504 for subelem
in element
.getchildren():
505 tag
= realtag(subelem
)
507 ctr
= cls
.CONSTRUCTORS
[tag
]
511 actions
.append(ctr
.FromXML(doc
, subelem
))
513 doc
._actions
[element
.get("label")] = dfn
516 def __call__(self
, params
, rank
):
517 return self
.actions
, params
520 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
522 class ActionRef(object):
523 """Run an action by name with parameters."""
525 def __init__(self
, action
, params
=None):
527 self
.params
= params
or ParamList()
529 def __getstate__(self
):
531 if self
.params
.params
:
532 params
= [param
.expr
for param
in self
.params
.params
]
533 state
.append(("params", params
))
534 state
.append(('action', self
.action
))
537 def __setstate__(self
, state
):
539 action
= state
["action"]
540 params
= [NumberDef(param
) for param
in state
.get("params", [])]
541 self
.__init
__(action
, ParamList(params
))
544 def FromXML(cls
, doc
, element
):
545 """Construct using an ElementTree-style element."""
546 action
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
547 doc
._action
_refs
.append(action
)
550 def __call__(self
, params
, rank
):
551 return self
.action(self
.params(params
, rank
), rank
)
554 return "%s(params=%r, action=%r)" % (
555 type(self
).__name
__, self
.params
, self
.action
)
557 class Offset(object):
558 """Provide an offset to a bullet's initial position."""
560 VALID_TYPES
= ["relative", "absolute"]
562 def __init__(self
, type, x
, y
):
563 if type not in self
.VALID_TYPES
:
564 raise ValueError("invalid type %r" % type)
565 self
.type = intern(type)
569 def __getstate__(self
):
570 state
= [('type', self
.type)]
572 state
.append(('x', self
.x
.expr
))
574 state
.append(('y', self
.y
.expr
))
577 def __setstate__(self
, state
):
579 x
= NumberDef(state
["x"]) if "x" in state
else None
580 y
= NumberDef(state
["y"]) if "y" in state
else None
581 self
.__init
__(state
["type"], x
, y
)
584 def FromXML(cls
, doc
, element
):
585 """Construct using an ElementTree-style element."""
586 type = element
.get("type", "relative")
589 for subelem
in element
:
590 tag
= realtag(subelem
)
592 x
= NumberDef(subelem
.text
)
594 y
= NumberDef(subelem
.text
)
595 return cls(type, x
, y
)
597 def __call__(self
, params
, rank
):
598 return (self
.x(params
, rank
) if self
.x
else 0,
599 self
.y(params
, rank
) if self
.y
else 0)
601 class FireDef(object):
602 """Fire definition (creates a bullet)."""
604 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None,
605 tags
=(), appearance
=None):
607 self
.direction
= direction
610 self
.tags
= set(tags
)
611 self
.appearance
= appearance
613 def __getstate__(self
):
616 state
.append(("direction", self
.direction
))
618 state
.append(("speed", self
.speed
))
620 state
.append(("offset", self
.offset
))
622 state
.append(("tags", list(self
.tags
)))
624 state
.append(("appearance", self
.appearance
))
626 params
= self
.bullet
.params
627 except AttributeError:
628 state
.append(('bullet', self
.bullet
))
631 state
.append(('bullet', self
.bullet
))
633 # Strip out empty BulletRefs.
634 state
.append(('bullet', self
.bullet
.bullet
))
637 def __setstate__(self
, state
):
639 self
.__init
__(**state
)
642 def FromXML(cls
, doc
, element
):
643 """Construct using an ElementTree-style element."""
650 for subelem
in element
.getchildren():
651 tag
= realtag(subelem
)
652 if tag
== "direction":
653 direction
= Direction
.FromXML(doc
, subelem
, "aim")
655 speed
= Speed
.FromXML(doc
, subelem
)
656 elif tag
== "bullet":
657 bullet
= BulletDef
.FromXML(doc
, subelem
)
658 elif tag
== "bulletRef":
659 bullet
= BulletRef
.FromXML(doc
, subelem
)
660 elif tag
== "offset":
661 offset
= Offset
.FromXML(doc
, subelem
)
663 tags
.add(subelem
.text
)
664 elif tag
== "appearance":
665 appearance
= subelem
.text
667 fire
= cls(bullet
, direction
, speed
, offset
, tags
, appearance
)
668 except UnboundLocalError as exc
:
669 raise ParseError(str(exc
))
671 doc
._fires
[element
.get("label")] = fire
674 def __call__(self
, params
, rank
):
675 direction
, speed
, tags
, appearance
, actions
= self
.bullet(params
, rank
)
677 direction
= self
.direction(params
, rank
)
679 speed
= self
.speed(params
, rank
)
680 tags
= tags
.union(self
.tags
)
682 appearance
= self
.appearance
683 return direction
, speed
, self
.offset
, tags
, appearance
, actions
686 return "%s(direction=%r, speed=%r, bullet=%r)" % (
687 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
689 class FireRef(object):
690 """Fire a bullet by name with parameters."""
692 def __init__(self
, fire
, params
=None):
694 self
.params
= params
or ParamList()
696 def __getstate__(self
):
698 if self
.params
.params
:
699 params
= [param
.expr
for param
in self
.params
.params
]
700 state
.append(("params", params
))
701 state
.append(('fire', self
.fire
))
704 def __setstate__(self
, state
):
707 params
= [NumberDef(param
) for param
in state
.get("params", [])]
708 self
.__init
__(fire
, ParamList(params
))
711 def FromXML(cls
, doc
, element
):
712 """Construct using an ElementTree-style element."""
713 fired
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
714 doc
._fire
_refs
.append(fired
)
717 def __call__(self
, params
, rank
):
718 return self
.fire(self
.params(params
, rank
), rank
)
721 return "%s(params=%r, fire=%r)" % (
722 type(self
).__name
__, self
.params
, self
.fire
)
724 class BulletML(object):
725 """BulletML document.
727 A BulletML document is a collection of top-level actions and the
730 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
731 its parsing. It maps tag names to classes with a FromXML
732 classmethod, which take the BulletML instance and ElementTree
733 element as arguments.
743 def __init__(self
, type="none", actions
=None):
744 self
.type = intern(type)
745 self
.actions
= [] if actions
is None else actions
747 def __getstate__(self
):
748 return [('type', self
.type), ('actions', self
.actions
)]
750 def __setstate__(self
, state
):
752 self
.__init
__(state
["type"], actions
=state
.get("actions"))
755 def FromXML(cls
, source
):
756 """Return a BulletML instance based on XML."""
757 if not hasattr(source
, 'read'):
758 source
= StringIO(source
)
761 root
= tree
.parse(source
)
763 doc
= cls(type=root
.get("type", "none"))
768 doc
._bullet
_refs
= []
769 doc
._action
_refs
= []
772 for element
in root
.getchildren():
773 tag
= realtag(element
)
774 if tag
in doc
.CONSTRUCTORS
:
775 doc
.CONSTRUCTORS
[tag
].FromXML(doc
, element
)
778 for ref
in doc
._bullet
_refs
:
779 ref
.bullet
= doc
._bullets
[ref
.bullet
]
780 for ref
in doc
._fire
_refs
:
781 ref
.fire
= doc
._fires
[ref
.fire
]
782 for ref
in doc
._action
_refs
:
783 ref
.action
= doc
._actions
[ref
.action
]
784 except KeyError as exc
:
785 raise ParseError("unknown reference %s" % exc
)
787 doc
.actions
= [act
for name
, act
in doc
._actions
.items()
788 if name
and name
.startswith("top")]
790 del(doc
._bullet
_refs
)
791 del(doc
._action
_refs
)
800 def FromYAML(cls
, source
):
801 """Create a BulletML instance based on YAML."""
803 # Late import to avoid a circular dependency.
805 import bulletml
.bulletyaml
808 raise ParseError("PyYAML is not available")
811 return yaml
.load(source
)
812 except Exception as exc
:
813 raise ParseError(str(exc
))
816 def FromDocument(cls
, source
):
817 """Create a BulletML instance based on a seekable file or string.
819 This attempts to autodetect if the stream is XML or YAML.
821 if not hasattr(source
, 'read'):
822 source
= StringIO(source
)
823 start
= source
.read(1)
826 return cls
.FromXML(source
)
827 elif start
== "!" or start
== "#":
828 return cls
.FromYAML(source
)
830 raise ParseError("unknown initial character %r" % start
)
833 return "%s(type=%r, actions=%r)" % (
834 type(self
).__name
__, self
.type, self
.actions
)
836 ActionDef
.CONSTRUCTORS
= dict(
840 changeSpeed
=ChangeSpeed
,
841 changeDirection
=ChangeDirection
,
846 appearance
=Appearance
,