-# 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 <body>,
- <head>, and <html> 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('location', 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"""
- <p><em>Error in markup! Raw source is below.</em></p>
- <pre>{0}</pre>
- """.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)