Merge remote branch 'veekun/media-reorganization'
[zzz-pokedex.git] / pokedex / db / markdown.py
1 # encoding: utf8
2 u"""Implements the markup used for description and effect text in the database.
3
4 The language used is a variation of Markdown and Markdown Extra. There are
5 docs for each at http://daringfireball.net/projects/markdown/ and
6 http://michelf.com/projects/php-markdown/extra/ respectively.
7
8 Pokédex links are represented with the syntax `[text]{type:identifier}`, e.g.,
9 `[Eevee]{pokemon:eevee}`. The actual code that parses these is in
10 spline-pokedex.
11 """
12 from __future__ import absolute_import
13
14 import markdown
15 import sqlalchemy.types
16
17 class MarkdownString(object):
18 """Wraps a Markdown string. Stringifies to the original text, but .as_html
19 will return an HTML rendering.
20
21 To add extensions to the rendering (which is necessary for rendering links
22 correctly, and which spline-pokedex does), you must append to this class's
23 `markdown_extensions` list. Yep, that's gross.
24 """
25
26 markdown_extensions = ['extra']
27
28 def __init__(self, source_text):
29 self.source_text = source_text
30 self._as_html = None
31
32 def __unicode__(self):
33 return self.source_text
34
35 def __str__(self):
36 return unicode(self.source_text).encode()
37
38 def __html__(self):
39 return self.as_html
40
41 @property
42 def as_html(self):
43 """Returns the string as HTML4."""
44
45 if self._as_html is not None:
46 return self._as_html
47
48 md = markdown.Markdown(
49 extensions=self.markdown_extensions,
50 safe_mode='escape',
51 output_format='xhtml1',
52 )
53
54 self._as_html = md.convert(self.source_text)
55
56 return self._as_html
57
58 @property
59 def as_text(self):
60 """Returns the string in a plaintext-friendly form.
61
62 At the moment, this is just the original source text.
63 """
64 return self.source_text
65
66 def _markdownify_effect_text(move, effect_text):
67 effect_text = effect_text.replace(
68 u'$effect_chance',
69 unicode(move.effect_chance),
70 )
71
72 return MarkdownString(effect_text)
73
74 class MoveEffectProperty(object):
75 """Property that wraps move effects. Used like this:
76
77 MoveClass.effect = MoveEffectProperty('effect')
78
79 some_move.effect # returns a MarkdownString
80 some_move.effect.as_html # returns a chunk of HTML
81
82 This class also performs simple substitution on the effect, replacing
83 `$effect_chance` with the move's actual effect chance.
84
85 Use `MoveEffectPropertyMap` for dict-like association proxies.
86 """
87
88 def __init__(self, effect_column):
89 self.effect_column = effect_column
90
91 def __get__(self, obj, cls):
92 prop = getattr(obj.move_effect, self.effect_column)
93 return _markdownify_effect_text(obj, prop)
94
95 class MoveEffectPropertyMap(MoveEffectProperty):
96 """Similar to `MoveEffectProperty`, but works on dict-like association
97 proxies.
98 """
99 def __get__(self, obj, cls):
100 prop = getattr(obj.move_effect, self.effect_column)
101 newdict = dict(prop)
102 for key in newdict:
103 newdict[key] = _markdownify_effect_text(obj, newdict[key])
104 return newdict
105
106 class MarkdownColumn(sqlalchemy.types.TypeDecorator):
107 """Generic SQLAlchemy column type for Markdown text.
108
109 Do NOT use this for move effects! They need to know what move they belong
110 to so they can fill in, e.g., effect chances. Use the MoveEffectProperty
111 property class above.
112 """
113 impl = sqlalchemy.types.Unicode
114
115 def process_bind_param(self, value, dialect):
116 if value is None:
117 return None
118
119 if not isinstance(value, basestring):
120 # Can't assign, e.g., MarkdownString objects yet
121 raise NotImplementedError
122
123 return unicode(value)
124
125 def process_result_value(self, value, dialect):
126 if value is None:
127 return None
128
129 return MarkdownString(value)