Restored Item.appears_underground and added location support to rst.
[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 from docutils.writers import UnfilteredWriter
45
46 import sqlalchemy.types
47
48 ### Subclasses of bits of docutils, to munge it into doing what I want
49 class HTMLFragmentWriter(HTMLWriter):
50 """Translates reST to HTML, but only as a fragment. Enclosing <body>,
51 <head>, and <html> tags are omitted.
52 """
53
54 def apply_template(self):
55 subs = self.interpolation_dict()
56 return subs['body']
57
58
59 class TextishTranslator(docutils.nodes.SparseNodeVisitor):
60 """A simple translator that tries to return plain text that still captures
61 the spirit of the original (basic) formatting.
62
63 This will probably not be useful for anything complicated; it's only meant
64 for extremely simple text.
65 """
66
67 def __init__(self, document):
68 self.document = document
69 self.translated = u''
70
71 def visit_Text(self, node):
72 """Text is left alone."""
73 self.translated += node.astext()
74
75 def depart_paragraph(self, node):
76 """Append a blank line after a paragraph, unless it's the last of its
77 siblings.
78 """
79 if not node.parent:
80 return
81
82 # Loop over siblings. If we see a sibling after we see this node, then
83 # append the blank line
84 seen_node = False
85 for sibling in node.parent:
86 if sibling is node:
87 seen_node = True
88 continue
89
90 if seen_node:
91 self.translated += u'\n\n'
92 return
93
94 class TextishWriter(UnfilteredWriter):
95 """Translates reST back into plain text, aka more reST. Difference is that
96 custom roles are handled, so you get "50% chance" instead of junk.
97 """
98
99 def translate(self):
100 visitor = TextishTranslator(self.document)
101 self.document.walkabout(visitor)
102 self.output = visitor.translated
103
104
105 class UnicodeOutput(Output):
106 """reST Unicode output. The distribution only has a StringOutput, and I
107 want me some Unicode.
108 """
109
110 def write(self, data):
111 """Returns data (a Unicode string) unaltered."""
112 return data
113
114
115 ### Text roles
116
117 def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
118 node = docutils.nodes.emphasis(rawtext, text, **options)
119 return [node], []
120
121 roles.register_local_role('ability', generic_role)
122 roles.register_local_role('item', generic_role)
123 roles.register_local_role('location', generic_role)
124 roles.register_local_role('move', generic_role)
125 roles.register_local_role('type', generic_role)
126 roles.register_local_role('pokemon', generic_role)
127 roles.register_local_role('mechanic', generic_role)
128
129 def data_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
130 document = inliner.document
131 node = document._pokedex_handle_data(rawtext, text)
132 return [node], []
133
134 roles.register_local_role('data', data_role)
135
136
137 ### Public classes
138
139 class RstString(object):
140 """Wraps a reStructuredText string. Stringifies to the original text, but
141 may be translated to HTML with .as_html().
142 """
143
144 def __init__(self, source_text, document_properties={}):
145 """
146 `document_properties`
147 List of extra properties to attach to the reST document object.
148 """
149 self.source_text = source_text
150 self.document_properties = document_properties
151 self._rest_document = None
152
153 def __unicode__(self):
154 return self.source_text
155
156 @property
157 def rest_document(self):
158 """reST parse tree of the source text.
159
160 This property is lazy-loaded.
161 """
162
163 # Return it if we have it
164 if self._rest_document:
165 return self._rest_document
166
167 parser = Parser()
168 settings = OptionParser(components=(Parser,HTMLWriter)).get_default_values()
169 document = docutils.utils.new_document('pokedex', settings)
170
171 # Add properties (in this case, probably just the data role handler)
172 document.__dict__.update(self.document_properties)
173
174 # PARSE
175 parser.parse(self.source_text, document)
176
177 self._rest_document = document
178 return document
179
180 @property
181 def as_html(self):
182 """Returns the string as HTML4."""
183
184 document = self.rest_document
185
186 # Check for errors; don't want to leave the default error message cruft
187 # in here
188 if document.next_node(condition=docutils.nodes.system_message):
189 # Boo! Cruft.
190 return u"""
191 <p><em>Error in markup! Raw source is below.</em></p>
192 <pre>{0}</pre>
193 """.format( cgi.escape(self.source_text) )
194
195 destination = UnicodeOutput()
196 writer = HTMLFragmentWriter()
197 return writer.write(document, destination)
198
199 @property
200 def as_text(self):
201 """Returns the string mostly unchanged, save for our custom roles."""
202
203 document = self.rest_document
204
205 destination = UnicodeOutput()
206 writer = TextishWriter()
207 return writer.write(document, destination)
208
209
210 class MoveEffectProperty(object):
211 """Property that wraps a move effect. Used like this:
212
213 MoveClass.effect = MoveEffectProperty('effect')
214
215 some_move.effect # returns an RstString
216 some_move.effect.as_html # returns a chunk of HTML
217
218 This class also performs `%` substitution on the effect, replacing
219 `%(effect_chance)d` with the move's actual effect chance. Also this is a
220 lie and it doesn't yet.
221 """
222
223 def __init__(self, effect_column):
224 self.effect_column = effect_column
225
226 def __get__(self, move, move_class):
227 # Attach a function for handling the `data` role
228 # XXX make this a little more fault-tolerant.. maybe..
229 def data_role_func(rawtext, text):
230 assert text[0:5] == 'move.'
231 newtext = getattr(move, text[5:])
232 return docutils.nodes.Text(newtext, rawtext)
233
234 return RstString(getattr(move.move_effect, self.effect_column),
235 document_properties=dict(
236 _pokedex_handle_data=data_role_func))
237
238 class RstTextColumn(sqlalchemy.types.TypeDecorator):
239 """Generic column type for reST text.
240
241 Do NOT use this for move effects! They need to know what move they belong
242 to so they can fill in, e.g., effect chances.
243 """
244 impl = sqlalchemy.types.Unicode
245
246 def process_bind_param(self, value, dialect):
247 return unicode(value)
248
249 def process_result_value(self, value, dialect):
250 return RstString(value)