Loading big tables now shows a progress indicator. #180
[zzz-pokedex.git] / pokedex / db / rst.py
index e70af2c..9ee84ba 100644 (file)
@@ -33,12 +33,17 @@ are, apparently, global.
     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):
@@ -50,6 +55,53 @@ class HTMLFragmentWriter(HTMLWriter):
         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.
@@ -63,12 +115,13 @@ class UnicodeOutput(Output):
 ### Text roles
 
 def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
-    node = docutils.nodes.strong(text, rawtext, **options)
+    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)
 
@@ -84,7 +137,7 @@ roles.register_local_role('data', data_role)
 
 class RstString(object):
     """Wraps a reStructuredText string.  Stringifies to the original text, but
-    may be translated to HTML with .to_html().
+    may be translated to HTML with .as_html().
     """
 
     def __init__(self, source_text, document_properties={}):
@@ -128,16 +181,35 @@ class RstString(object):
         """Returns the string as HTML4."""
 
         document = self.rest_document
-        destination = UnicodeOutput()
 
+        # 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()
+        MoveClass.effect = MoveEffectProperty('effect')
 
         some_move.effect            # returns an RstString
         some_move.effect.as_html    # returns a chunk of HTML
@@ -147,6 +219,9 @@ class MoveEffectProperty(object):
     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..
@@ -155,6 +230,20 @@ class MoveEffectProperty(object):
             newtext = getattr(move, text[5:])
             return docutils.nodes.Text(newtext, rawtext)
 
-        return RstString(move.move_effect.effect,
+        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)