2 r
"""Functionality for handling reStructuredText fields in the database.
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.
13 These all wrap objects of the corresponding type. They're intended to be
14 used to link to these items.
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
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.
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
38 from docutils
.frontend
import OptionParser
39 from docutils
.io
import Output
41 from docutils
.parsers
.rst
import Parser
, roles
43 from docutils
.writers
.html4css1
import Writer
as HTMLWriter
44 from docutils
.writers
import UnfilteredWriter
46 import sqlalchemy
.types
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.
54 def apply_template(self
):
55 subs
= self
.interpolation_dict()
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.
63 This will probably not be useful for anything complicated; it's only meant
64 for extremely simple text.
67 def __init__(self
, document
):
68 self
.document
= document
71 def visit_Text(self
, node
):
72 """Text is left alone."""
73 self
.translated
+= node
.astext()
75 def depart_paragraph(self
, node
):
76 """Append a blank line after a paragraph, unless it's the last of its
82 # Loop over siblings. If we see a sibling after we see this node, then
83 # append the blank line
85 for sibling
in node
.parent
:
91 self
.translated
+= u
'\n\n'
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.
100 visitor
= TextishTranslator(self
.document
)
101 self
.document
.walkabout(visitor
)
102 self
.output
= visitor
.translated
105 class UnicodeOutput(Output
):
106 """reST Unicode output. The distribution only has a StringOutput, and I
107 want me some Unicode.
110 def write(self
, data
):
111 """Returns data (a Unicode string) unaltered."""
117 def generic_role(name
, rawtext
, text
, lineno
, inliner
, options
={}, content
=[]):
118 node
= docutils
.nodes
.emphasis(rawtext
, text
, **options
)
121 roles
.register_local_role('ability', generic_role
)
122 roles
.register_local_role('item', generic_role
)
123 roles
.register_local_role('move', generic_role
)
124 roles
.register_local_role('type', generic_role
)
125 roles
.register_local_role('pokemon', generic_role
)
126 roles
.register_local_role('mechanic', generic_role
)
128 def data_role(name
, rawtext
, text
, lineno
, inliner
, options
={}, content
=[]):
129 document
= inliner
.document
130 node
= document
._pokedex_handle_data(rawtext
, text
)
133 roles
.register_local_role('data', data_role
)
138 class RstString(object):
139 """Wraps a reStructuredText string. Stringifies to the original text, but
140 may be translated to HTML with .as_html().
143 def __init__(self
, source_text
, document_properties
={}):
145 `document_properties`
146 List of extra properties to attach to the reST document object.
148 self
.source_text
= source_text
149 self
.document_properties
= document_properties
150 self
._rest_document
= None
152 def __unicode__(self
):
153 return self
.source_text
156 def rest_document(self
):
157 """reST parse tree of the source text.
159 This property is lazy-loaded.
162 # Return it if we have it
163 if self
._rest_document
:
164 return self
._rest_document
167 settings
= OptionParser(components
=(Parser
,HTMLWriter
)).get_default_values()
168 document
= docutils
.utils
.new_document('pokedex', settings
)
170 # Add properties (in this case, probably just the data role handler)
171 document
.__dict__
.update(self
.document_properties
)
174 parser
.parse(self
.source_text
, document
)
176 self
._rest_document
= document
181 """Returns the string as HTML4."""
183 document
= self
.rest_document
185 # Check for errors; don't want to leave the default error message cruft
187 if document
.next_node(condition
=docutils
.nodes
.system_message
):
190 <p><em>Error in markup! Raw source is below.</em></p>
192 """.format( cgi
.escape(self
.source_text
) )
194 destination
= UnicodeOutput()
195 writer
= HTMLFragmentWriter()
196 return writer
.write(document
, destination
)
200 """Returns the string mostly unchanged, save for our custom roles."""
202 document
= self
.rest_document
204 destination
= UnicodeOutput()
205 writer
= TextishWriter()
206 return writer
.write(document
, destination
)
209 class MoveEffectProperty(object):
210 """Property that wraps a move effect. Used like this:
212 MoveClass.effect = MoveEffectProperty('effect')
214 some_move.effect # returns an RstString
215 some_move.effect.as_html # returns a chunk of HTML
217 This class also performs `%` substitution on the effect, replacing
218 `%(effect_chance)d` with the move's actual effect chance. Also this is a
219 lie and it doesn't yet.
222 def __init__(self
, effect_column
):
223 self
.effect_column
= effect_column
225 def __get__(self
, move
, move_class
):
226 # Attach a function for handling the `data` role
227 # XXX make this a little more fault-tolerant.. maybe..
228 def data_role_func(rawtext
, text
):
229 assert text
[0:5] == 'move.'
230 newtext
= getattr(move
, text
[5:])
231 return docutils
.nodes
.Text(newtext
, rawtext
)
233 return RstString(getattr(move
.move_effect
, self
.effect_column
),
234 document_properties
=dict(
235 _pokedex_handle_data
=data_role_func
))
237 class RstTextColumn(sqlalchemy
.types
.TypeDecorator
):
238 """Generic column type for reST text.
240 Do NOT use this for move effects! They need to know what move they belong
241 to so they can fill in, e.g., effect chances.
243 impl
= sqlalchemy
.types
.Unicode
245 def process_bind_param(self
, value
, dialect
):
246 return unicode(value
)
248 def process_result_value(self
, value
, dialect
):
249 return RstString(value
)