diff --git a/pokedex/db/rst.py b/pokedex/db/rst.py new file mode 100644 index 0000000..e70af2c --- /dev/null +++ b/pokedex/db/rst.py @@ -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 , + , and 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)) diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index 6019c30..a7dd9c8 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -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')