Added semi-magical reST parsing for move effects.
authorEevee <git@veekun.com>
Tue, 1 Sep 2009 04:37:29 +0000 (21:37 -0700)
committerEevee <git@veekun.com>
Tue, 1 Sep 2009 04:37:29 +0000 (21:37 -0700)
pokedex/db/rst.py [new file with mode: 0644]
pokedex/db/tables.py

diff --git a/pokedex/db/rst.py b/pokedex/db/rst.py
new file mode 100644 (file)
index 0000000..e70af2c
--- /dev/null
@@ -0,0 +1,160 @@
+# encoding: utf8
+r"""Functionality for handling reStructuredText fields in the database.
+
+This module defines the following extra text roles.  By default, they merely
+bold the contents of the tag.  Calling code may redefine them with
+`docutils.parsers.rst.roles.register_local_role`.  Docutils role extensions
+are, apparently, global.
+
+`ability`
+`item`
+`move`
+`pokemon`
+    These all wrap objects of the corresponding type.  They're intended to be
+    used to link to these items.
+
+`mechanic`
+    This is a general-purpose reference role.  The Web Pokédex uses these to
+    link to pages on mechanics.  Amongst the things tagged with this are:
+    * Stats, e.g., Attack, Speed
+    * Major status effects, e.g., paralysis, freezing
+    * Minor status effects not unique to a single move, e.g., confusion
+    * Battle mechanics, e.g., "regular damage", "lowers/raises" a stat
+
+`data`
+    Depends on context.  Created for move effect chances; some effects contain
+    text like "Has a \:data\:\`move.effect_chance\` chance to...".  Here, the
+    enclosed text is taken as a reference to a column on the associated move.
+    Other contexts may someday invent their own constructs.
+
+    This is actually implemented by adding a `_pokedex_handle_data` attribute
+    to the reST document itself, which the `data` role handler attempts to
+    call.  This function takes `rawtext` and `text` as arguments and should
+    return a reST node.
+"""
+
+from docutils.frontend import OptionParser
+from docutils.io import Output
+import docutils.nodes
+from docutils.parsers.rst import Parser, roles
+import docutils.utils
+from docutils.writers.html4css1 import Writer as HTMLWriter
+
+### Subclasses of bits of docutils, to munge it into doing what I want
+class HTMLFragmentWriter(HTMLWriter):
+    """Translates reST to HTML, but only as a fragment.  Enclosing <body>,
+    <head>, and <html> tags are omitted.
+    """
+
+    def apply_template(self):
+        subs = self.interpolation_dict()
+        return subs['body']
+
+class UnicodeOutput(Output):
+    """reST Unicode output.  The distribution only has a StringOutput, and I
+    want me some Unicode.
+    """
+
+    def write(self, data):
+        """Returns data (a Unicode string) unaltered."""
+        return data
+
+
+### Text roles
+
+def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+    node = docutils.nodes.strong(text, rawtext, **options)
+    return [node], []
+
+roles.register_local_role('ability', generic_role)
+roles.register_local_role('item', generic_role)
+roles.register_local_role('move', generic_role)
+roles.register_local_role('pokemon', generic_role)
+roles.register_local_role('mechanic', generic_role)
+
+def data_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+    document = inliner.document
+    node = document._pokedex_handle_data(rawtext, text)
+    return [node], []
+
+roles.register_local_role('data', data_role)
+
+
+### Public classes
+
+class RstString(object):
+    """Wraps a reStructuredText string.  Stringifies to the original text, but
+    may be translated to HTML with .to_html().
+    """
+
+    def __init__(self, source_text, document_properties={}):
+        """
+        `document_properties`
+            List of extra properties to attach to the reST document object.
+        """
+        self.source_text = source_text
+        self.document_properties = document_properties
+        self._rest_document = None
+
+    def __unicode__(self):
+        return self.source_text
+
+    @property
+    def rest_document(self):
+        """reST parse tree of the source text.
+
+        This property is lazy-loaded.
+        """
+
+        # Return it if we have it
+        if self._rest_document:
+            return self._rest_document
+
+        parser = Parser()
+        settings = OptionParser(components=(Parser,HTMLWriter)).get_default_values()
+        document = docutils.utils.new_document('pokedex', settings)
+
+        # Add properties (in this case, probably just the data role handler)
+        document.__dict__.update(self.document_properties)
+
+        # PARSE
+        parser.parse(self.source_text, document)
+
+        self._rest_document = document
+        return document
+
+    @property
+    def as_html(self):
+        """Returns the string as HTML4."""
+
+        document = self.rest_document
+        destination = UnicodeOutput()
+
+        writer = HTMLFragmentWriter()
+        return writer.write(document, destination)
+
+
+class MoveEffectProperty(object):
+    """Property that wraps a move effect.  Used like this:
+
+        MoveClass.effect = MoveEffectProperty()
+
+        some_move.effect            # returns an RstString
+        some_move.effect.as_html    # returns a chunk of HTML
+
+    This class also performs `%` substitution on the effect, replacing
+    `%(effect_chance)d` with the move's actual effect chance.  Also this is a
+    lie and it doesn't yet.
+    """
+
+    def __get__(self, move, move_class):
+        # Attach a function for handling the `data` role
+        # XXX make this a little more fault-tolerant..  maybe..
+        def data_role_func(rawtext, text):
+            assert text[0:5] == 'move.'
+            newtext = getattr(move, text[5:])
+            return docutils.nodes.Text(newtext, rawtext)
+
+        return RstString(move.move_effect.effect,
+                         document_properties=dict(
+                             _pokedex_handle_data=data_role_func))
index 6019c30..a7dd9c8 100644 (file)
@@ -7,6 +7,8 @@ from sqlalchemy.orm import backref, relation
 from sqlalchemy.types import *
 from sqlalchemy.databases.mysql import *
 
+from pokedex.db import rst
+
 metadata = MetaData()
 TableBase = declarative_base(metadata=metadata)
 
@@ -174,7 +176,7 @@ class MoveEffect(TableBase):
     id = Column(Integer, primary_key=True, nullable=False)
     priority = Column(SmallInteger, nullable=False)
     short_effect = Column(Unicode(128), nullable=False)
-    effect = Column(Unicode(255), nullable=False)
+    effect = Column(Unicode(5120), nullable=False)
 
 class MoveTarget(TableBase):
     __tablename__ = 'move_targets'
@@ -395,12 +397,16 @@ LocationArea.location = relation(Location, backref='areas')
 Machine.generation = relation(Generation)
 
 Move.damage_class = relation(MoveDamageClass, backref='moves')
-Move.effect = relation(MoveEffect, backref='moves')
+Move.move_effect = relation(MoveEffect, backref='moves')
 Move.generation = relation(Generation, backref='moves')
 Move.machines = relation(Machine, backref='move')
 Move.target = relation(MoveTarget, backref='moves')
 Move.type = relation(Type, backref='moves')
 
+Move.effect = rst.MoveEffectProperty()
+Move.priority = association_proxy('move_effect', 'priority')
+Move.short_effect = association_proxy('move_effect', 'short_effect')
+
 Pokemon.abilities = relation(Ability, secondary=PokemonAbility.__table__,
                                       order_by=PokemonAbility.slot,
                                       backref='pokemon')