# 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. """ import cgi 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 from docutils.writers import UnfilteredWriter import sqlalchemy.types ### 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 TextishTranslator(docutils.nodes.SparseNodeVisitor): """A simple translator that tries to return plain text that still captures the spirit of the original (basic) formatting. This will probably not be useful for anything complicated; it's only meant for extremely simple text. """ def __init__(self, document): self.document = document self.translated = u'' def visit_Text(self, node): """Text is left alone.""" self.translated += node.astext() def depart_paragraph(self, node): """Append a blank line after a paragraph, unless it's the last of its siblings. """ if not node.parent: return # Loop over siblings. If we see a sibling after we see this node, then # append the blank line seen_node = False for sibling in node.parent: if sibling is node: seen_node = True continue if seen_node: self.translated += u'\n\n' return class TextishWriter(UnfilteredWriter): """Translates reST back into plain text, aka more reST. Difference is that custom roles are handled, so you get "50% chance" instead of junk. """ def translate(self): visitor = TextishTranslator(self.document) self.document.walkabout(visitor) self.output = visitor.translated 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.emphasis(rawtext, text, **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('type', 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 .as_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 # Check for errors; don't want to leave the default error message cruft # in here if document.next_node(condition=docutils.nodes.system_message): # Boo! Cruft. return u"""Error in markup! Raw source is below.
{0}""".format( cgi.escape(self.source_text) ) destination = UnicodeOutput() writer = HTMLFragmentWriter() return writer.write(document, destination) @property def as_text(self): """Returns the string mostly unchanged, save for our custom roles.""" document = self.rest_document destination = UnicodeOutput() writer = TextishWriter() return writer.write(document, destination) class MoveEffectProperty(object): """Property that wraps a move effect. Used like this: MoveClass.effect = MoveEffectProperty('effect') 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 __init__(self, effect_column): self.effect_column = effect_column 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(getattr(move.move_effect, self.effect_column), document_properties=dict( _pokedex_handle_data=data_role_func)) class RstTextColumn(sqlalchemy.types.TypeDecorator): """Generic column type for reST text. Do NOT use this for move effects! They need to know what move they belong to so they can fill in, e.g., effect chances. """ impl = sqlalchemy.types.Unicode def process_bind_param(self, value, dialect): return unicode(value) def process_result_value(self, value, dialect): return RstString(value)