Require docutils.
[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 from docutils.frontend import OptionParser
37 from docutils.io import Output
38 import docutils.nodes
39 from docutils.parsers.rst import Parser, roles
40 import docutils.utils
41 from docutils.writers.html4css1 import Writer as HTMLWriter
42
43 import sqlalchemy.types
44
45 ### Subclasses of bits of docutils, to munge it into doing what I want
46 class HTMLFragmentWriter(HTMLWriter):
47 """Translates reST to HTML, but only as a fragment. Enclosing <body>,
48 <head>, and <html> tags are omitted.
49 """
50
51 def apply_template(self):
52 subs = self.interpolation_dict()
53 return subs['body']
54
55 class UnicodeOutput(Output):
56 """reST Unicode output. The distribution only has a StringOutput, and I
57 want me some Unicode.
58 """
59
60 def write(self, data):
61 """Returns data (a Unicode string) unaltered."""
62 return data
63
64
65 ### Text roles
66
67 def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
68 node = docutils.nodes.emphasis(rawtext, text, **options)
69 return [node], []
70
71 roles.register_local_role('ability', generic_role)
72 roles.register_local_role('item', generic_role)
73 roles.register_local_role('move', generic_role)
74 roles.register_local_role('type', generic_role)
75 roles.register_local_role('pokemon', generic_role)
76 roles.register_local_role('mechanic', generic_role)
77
78 def data_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
79 document = inliner.document
80 node = document._pokedex_handle_data(rawtext, text)
81 return [node], []
82
83 roles.register_local_role('data', data_role)
84
85
86 ### Public classes
87
88 class RstString(object):
89 """Wraps a reStructuredText string. Stringifies to the original text, but
90 may be translated to HTML with .to_html().
91 """
92
93 def __init__(self, source_text, document_properties={}):
94 """
95 `document_properties`
96 List of extra properties to attach to the reST document object.
97 """
98 self.source_text = source_text
99 self.document_properties = document_properties
100 self._rest_document = None
101
102 def __unicode__(self):
103 return self.source_text
104
105 @property
106 def rest_document(self):
107 """reST parse tree of the source text.
108
109 This property is lazy-loaded.
110 """
111
112 # Return it if we have it
113 if self._rest_document:
114 return self._rest_document
115
116 parser = Parser()
117 settings = OptionParser(components=(Parser,HTMLWriter)).get_default_values()
118 document = docutils.utils.new_document('pokedex', settings)
119
120 # Add properties (in this case, probably just the data role handler)
121 document.__dict__.update(self.document_properties)
122
123 # PARSE
124 parser.parse(self.source_text, document)
125
126 self._rest_document = document
127 return document
128
129 @property
130 def as_html(self):
131 """Returns the string as HTML4."""
132
133 document = self.rest_document
134 destination = UnicodeOutput()
135
136 writer = HTMLFragmentWriter()
137 return writer.write(document, destination)
138
139
140 class MoveEffectProperty(object):
141 """Property that wraps a move effect. Used like this:
142
143 MoveClass.effect = MoveEffectProperty('effect')
144
145 some_move.effect # returns an RstString
146 some_move.effect.as_html # returns a chunk of HTML
147
148 This class also performs `%` substitution on the effect, replacing
149 `%(effect_chance)d` with the move's actual effect chance. Also this is a
150 lie and it doesn't yet.
151 """
152
153 def __init__(self, effect_column):
154 self.effect_column = effect_column
155
156 def __get__(self, move, move_class):
157 # Attach a function for handling the `data` role
158 # XXX make this a little more fault-tolerant.. maybe..
159 def data_role_func(rawtext, text):
160 assert text[0:5] == 'move.'
161 newtext = getattr(move, text[5:])
162 return docutils.nodes.Text(newtext, rawtext)
163
164 return RstString(getattr(move.move_effect, self.effect_column),
165 document_properties=dict(
166 _pokedex_handle_data=data_role_func))
167
168 class RstTextColumn(sqlalchemy.types.TypeDecorator):
169 """Generic column type for reST text.
170
171 Do NOT use this for move effects! They need to know what move they belong
172 to so they can fill in, e.g., effect chances.
173 """
174 impl = sqlalchemy.types.Unicode
175
176 def process_bind_param(self, value, dialect):
177 return unicode(value)
178
179 def process_result_value(self, value, dialect):
180 return RstString(value)