Make rst as_html handle errors a little more nicely.
[zzz-pokedex.git] / pokedex / db / rst.py
1 # encoding: utf8
2 r"""Functionality for handling reStructuredText fields in the database.
3
4 This module defines the following extra text roles. By default, they merely
5 bold the contents of the tag. Calling code may redefine them with
6 `docutils.parsers.rst.roles.register_local_role`. Docutils role extensions
7 are, apparently, global.
8
9 `ability`
10 `item`
11 `move`
12 `pokemon`
13 These all wrap objects of the corresponding type. They're intended to be
14 used to link to these items.
15
16 `mechanic`
17 This is a general-purpose reference role. The Web Pokédex uses these to
18 link to pages on mechanics. Amongst the things tagged with this are:
19 * Stats, e.g., Attack, Speed
20 * Major status effects, e.g., paralysis, freezing
21 * Minor status effects not unique to a single move, e.g., confusion
22 * Battle mechanics, e.g., "regular damage", "lowers/raises" a stat
23
24 `data`
25 Depends on context. Created for move effect chances; some effects contain
26 text like "Has a \:data\:\`move.effect_chance\` chance to...". Here, the
27 enclosed text is taken as a reference to a column on the associated move.
28 Other contexts may someday invent their own constructs.
29
30 This is actually implemented by adding a `_pokedex_handle_data` attribute
31 to the reST document itself, which the `data` role handler attempts to
32 call. This function takes `rawtext` and `text` as arguments and should
33 return a reST node.
34 """
35
36 import cgi
37
38 from docutils.frontend import OptionParser
39 from docutils.io import Output
40 import docutils.nodes
41 from docutils.parsers.rst import Parser, roles
42 import docutils.utils
43 from docutils.writers.html4css1 import Writer as HTMLWriter
44
45 import sqlalchemy.types
46
47 ### Subclasses of bits of docutils, to munge it into doing what I want
48 class HTMLFragmentWriter(HTMLWriter):
49 """Translates reST to HTML, but only as a fragment. Enclosing <body>,
50 <head>, and <html> tags are omitted.
51 """
52
53 def apply_template(self):
54 subs = self.interpolation_dict()
55 return subs['body']
56
57 class UnicodeOutput(Output):
58 """reST Unicode output. The distribution only has a StringOutput, and I
59 want me some Unicode.
60 """
61
62 def write(self, data):
63 """Returns data (a Unicode string) unaltered."""
64 return data
65
66
67 ### Text roles
68
69 def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
70 node = docutils.nodes.emphasis(rawtext, text, **options)
71 return [node], []
72
73 roles.register_local_role('ability', generic_role)
74 roles.register_local_role('item', generic_role)
75 roles.register_local_role('move', generic_role)
76 roles.register_local_role('type', generic_role)
77 roles.register_local_role('pokemon', generic_role)
78 roles.register_local_role('mechanic', generic_role)
79
80 def data_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
81 document = inliner.document
82 node = document._pokedex_handle_data(rawtext, text)
83 return [node], []
84
85 roles.register_local_role('data', data_role)
86
87
88 ### Public classes
89
90 class RstString(object):
91 """Wraps a reStructuredText string. Stringifies to the original text, but
92 may be translated to HTML with .as_html().
93 """
94
95 def __init__(self, source_text, document_properties={}):
96 """
97 `document_properties`
98 List of extra properties to attach to the reST document object.
99 """
100 self.source_text = source_text
101 self.document_properties = document_properties
102 self._rest_document = None
103
104 def __unicode__(self):
105 return self.source_text
106
107 @property
108 def rest_document(self):
109 """reST parse tree of the source text.
110
111 This property is lazy-loaded.
112 """
113
114 # Return it if we have it
115 if self._rest_document:
116 return self._rest_document
117
118 parser = Parser()
119 settings = OptionParser(components=(Parser,HTMLWriter)).get_default_values()
120 document = docutils.utils.new_document('pokedex', settings)
121
122 # Add properties (in this case, probably just the data role handler)
123 document.__dict__.update(self.document_properties)
124
125 # PARSE
126 parser.parse(self.source_text, document)
127
128 self._rest_document = document
129 return document
130
131 @property
132 def as_html(self):
133 """Returns the string as HTML4."""
134
135 document = self.rest_document
136
137 # Check for errors; don't want to leave the default error message cruft
138 # in here
139 if document.next_node(condition=docutils.nodes.system_message):
140 # Boo! Cruft.
141 return u"""
142 <p><em>Error in markup! Raw source is below.</em></p>
143 <pre>{0}</pre>
144 """.format( cgi.escape(self.source_text) )
145
146 destination = UnicodeOutput()
147 writer = HTMLFragmentWriter()
148 return writer.write(document, destination)
149
150
151 class MoveEffectProperty(object):
152 """Property that wraps a move effect. Used like this:
153
154 MoveClass.effect = MoveEffectProperty('effect')
155
156 some_move.effect # returns an RstString
157 some_move.effect.as_html # returns a chunk of HTML
158
159 This class also performs `%` substitution on the effect, replacing
160 `%(effect_chance)d` with the move's actual effect chance. Also this is a
161 lie and it doesn't yet.
162 """
163
164 def __init__(self, effect_column):
165 self.effect_column = effect_column
166
167 def __get__(self, move, move_class):
168 # Attach a function for handling the `data` role
169 # XXX make this a little more fault-tolerant.. maybe..
170 def data_role_func(rawtext, text):
171 assert text[0:5] == 'move.'
172 newtext = getattr(move, text[5:])
173 return docutils.nodes.Text(newtext, rawtext)
174
175 return RstString(getattr(move.move_effect, self.effect_column),
176 document_properties=dict(
177 _pokedex_handle_data=data_role_func))
178
179 class RstTextColumn(sqlalchemy.types.TypeDecorator):
180 """Generic column type for reST text.
181
182 Do NOT use this for move effects! They need to know what move they belong
183 to so they can fill in, e.g., effect chances.
184 """
185 impl = sqlalchemy.types.Unicode
186
187 def process_bind_param(self, value, dialect):
188 return unicode(value)
189
190 def process_result_value(self, value, dialect):
191 return RstString(value)