1
0
Fork 0
mirror of https://github.com/veekun/pokedex.git synced 2024-08-20 18:16:34 +00:00

Scrap docutils for markdown.

This commit is contained in:
Eevee 2010-05-31 15:13:34 -07:00
parent 00ac500da8
commit 1fbba5476c
7 changed files with 995 additions and 1163 deletions

100
pokedex/db/markdown.py Normal file
View file

@ -0,0 +1,100 @@
# encoding: utf8
u"""Implements the markup used for description and effect text in the database.
The language used is a variation of Markdown and Markdown Extra. There are
docs for each at http://daringfireball.net/projects/markdown/ and
http://michelf.com/projects/php-markdown/extra/ respectively.
Pokédex links are represented with the extended syntax `[name]{type}`, e.g.,
`[Eevee]{pokemon}`. The actual code that parses these is in spline-pokedex.
"""
import markdown
import sqlalchemy.types
class MarkdownString(object):
"""Wraps a Markdown string. Stringifies to the original text, but .as_html
will return an HTML rendering.
To add extensions to the rendering (which is necessary for rendering links
correctly, and which spline-pokedex does), you must append to this class's
`markdown_extensions` list. Yep, that's gross.
"""
markdown_extensions = ['extra']
def __init__(self, source_text):
self.source_text = source_text
self._as_html = None
def __unicode__(self):
return self.source_text
@property
def as_html(self):
"""Returns the string as HTML4."""
if self._as_html:
return self._as_html
md = markdown.Markdown(
extensions=self.markdown_extensions,
safe_mode='escape',
output_format='xhtml1',
)
self._as_html = md.convert(self.source_text)
return self._as_html
@property
def as_text(self):
"""Returns the string in a plaintext-friendly form.
At the moment, this is just the original source text.
"""
return self.source_text
class MoveEffectProperty(object):
"""Property that wraps a move effect. Used like this:
MoveClass.effect = MoveEffectProperty('effect')
some_move.effect # returns a MarkdownString
some_move.effect.as_html # returns a chunk of HTML
This class also performs simple substitution on the effect, replacing
`$effect_chance` with the move's actual effect chance.
"""
def __init__(self, effect_column):
self.effect_column = effect_column
def __get__(self, move, move_class):
effect_text = getattr(move.move_effect, self.effect_column)
effect_text = effect_text.replace(
u'$effect_chance',
unicode(move.effect_chance),
)
return MarkdownString(effect_text)
class MarkdownColumn(sqlalchemy.types.TypeDecorator):
"""Generic SQLAlchemy column type for Markdown text.
Do NOT use this for move effects! They need to know what move they belong
to so they can fill in, e.g., effect chances. Use the MoveEffectProperty
property class above.
"""
impl = sqlalchemy.types.Unicode
def process_bind_param(self, value, dialect):
if not isinstance(value, basestring):
# Can't assign, e.g., MarkdownString objects yet
raise NotImplementedError
return unicode(value)
def process_result_value(self, value, dialect):
return MarkdownString(value)

View file

@ -1,250 +0,0 @@
# encoding: utf8
r"""Functionality for handling reStructuredText fields in the database.
This module defines the following extra text roles. By default, they merely
bold the contents of the tag. Calling code may redefine them with
`docutils.parsers.rst.roles.register_local_role`. Docutils role extensions
are, apparently, global.
`ability`
`item`
`move`
`pokemon`
These all wrap objects of the corresponding type. They're intended to be
used to link to these items.
`mechanic`
This is a general-purpose reference role. The Web Pokédex uses these to
link to pages on mechanics. Amongst the things tagged with this are:
* Stats, e.g., Attack, Speed
* Major status effects, e.g., paralysis, freezing
* Minor status effects not unique to a single move, e.g., confusion
* Battle mechanics, e.g., "regular damage", "lowers/raises" a stat
`data`
Depends on context. Created for move effect chances; some effects contain
text like "Has a \:data\:\`move.effect_chance\` chance to...". Here, the
enclosed text is taken as a reference to a column on the associated move.
Other contexts may someday invent their own constructs.
This is actually implemented by adding a `_pokedex_handle_data` attribute
to the reST document itself, which the `data` role handler attempts to
call. This function takes `rawtext` and `text` as arguments and should
return a reST node.
"""
import cgi
from docutils.frontend import OptionParser
from docutils.io import Output
import docutils.nodes
from docutils.parsers.rst import Parser, roles
import docutils.utils
from docutils.writers.html4css1 import Writer as HTMLWriter
from docutils.writers import UnfilteredWriter
import sqlalchemy.types
### Subclasses of bits of docutils, to munge it into doing what I want
class HTMLFragmentWriter(HTMLWriter):
"""Translates reST to HTML, but only as a fragment. Enclosing <body>,
<head>, and <html> tags are omitted.
"""
def apply_template(self):
subs = self.interpolation_dict()
return subs['body']
class TextishTranslator(docutils.nodes.SparseNodeVisitor):
"""A simple translator that tries to return plain text that still captures
the spirit of the original (basic) formatting.
This will probably not be useful for anything complicated; it's only meant
for extremely simple text.
"""
def __init__(self, document):
self.document = document
self.translated = u''
def visit_Text(self, node):
"""Text is left alone."""
self.translated += node.astext()
def depart_paragraph(self, node):
"""Append a blank line after a paragraph, unless it's the last of its
siblings.
"""
if not node.parent:
return
# Loop over siblings. If we see a sibling after we see this node, then
# append the blank line
seen_node = False
for sibling in node.parent:
if sibling is node:
seen_node = True
continue
if seen_node:
self.translated += u'\n\n'
return
class TextishWriter(UnfilteredWriter):
"""Translates reST back into plain text, aka more reST. Difference is that
custom roles are handled, so you get "50% chance" instead of junk.
"""
def translate(self):
visitor = TextishTranslator(self.document)
self.document.walkabout(visitor)
self.output = visitor.translated
class UnicodeOutput(Output):
"""reST Unicode output. The distribution only has a StringOutput, and I
want me some Unicode.
"""
def write(self, data):
"""Returns data (a Unicode string) unaltered."""
return data
### Text roles
def generic_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
node = docutils.nodes.emphasis(rawtext, text, **options)
return [node], []
roles.register_local_role('ability', generic_role)
roles.register_local_role('item', generic_role)
roles.register_local_role('location', generic_role)
roles.register_local_role('move', generic_role)
roles.register_local_role('type', generic_role)
roles.register_local_role('pokemon', generic_role)
roles.register_local_role('mechanic', generic_role)
def data_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
document = inliner.document
node = document._pokedex_handle_data(rawtext, text)
return [node], []
roles.register_local_role('data', data_role)
### Public classes
class RstString(object):
"""Wraps a reStructuredText string. Stringifies to the original text, but
may be translated to HTML with .as_html().
"""
def __init__(self, source_text, document_properties={}):
"""
`document_properties`
List of extra properties to attach to the reST document object.
"""
self.source_text = source_text
self.document_properties = document_properties
self._rest_document = None
def __unicode__(self):
return self.source_text
@property
def rest_document(self):
"""reST parse tree of the source text.
This property is lazy-loaded.
"""
# Return it if we have it
if self._rest_document:
return self._rest_document
parser = Parser()
settings = OptionParser(components=(Parser,HTMLWriter)).get_default_values()
document = docutils.utils.new_document('pokedex', settings)
# Add properties (in this case, probably just the data role handler)
document.__dict__.update(self.document_properties)
# PARSE
parser.parse(self.source_text, document)
self._rest_document = document
return document
@property
def as_html(self):
"""Returns the string as HTML4."""
document = self.rest_document
# Check for errors; don't want to leave the default error message cruft
# in here
if document.next_node(condition=docutils.nodes.system_message):
# Boo! Cruft.
return u"""
<p><em>Error in markup! Raw source is below.</em></p>
<pre>{0}</pre>
""".format( cgi.escape(self.source_text) )
destination = UnicodeOutput()
writer = HTMLFragmentWriter()
return writer.write(document, destination)
@property
def as_text(self):
"""Returns the string mostly unchanged, save for our custom roles."""
document = self.rest_document
destination = UnicodeOutput()
writer = TextishWriter()
return writer.write(document, destination)
class MoveEffectProperty(object):
"""Property that wraps a move effect. Used like this:
MoveClass.effect = MoveEffectProperty('effect')
some_move.effect # returns an RstString
some_move.effect.as_html # returns a chunk of HTML
This class also performs `%` substitution on the effect, replacing
`%(effect_chance)d` with the move's actual effect chance. Also this is a
lie and it doesn't yet.
"""
def __init__(self, effect_column):
self.effect_column = effect_column
def __get__(self, move, move_class):
# Attach a function for handling the `data` role
# XXX make this a little more fault-tolerant.. maybe..
def data_role_func(rawtext, text):
assert text[0:5] == 'move.'
newtext = getattr(move, text[5:])
return docutils.nodes.Text(newtext, rawtext)
return RstString(getattr(move.move_effect, self.effect_column),
document_properties=dict(
_pokedex_handle_data=data_role_func))
class RstTextColumn(sqlalchemy.types.TypeDecorator):
"""Generic column type for reST text.
Do NOT use this for move effects! They need to know what move they belong
to so they can fill in, e.g., effect chances.
"""
impl = sqlalchemy.types.Unicode
def process_bind_param(self, value, dialect):
return unicode(value)
def process_result_value(self, value, dialect):
return RstString(value)

View file

@ -8,7 +8,7 @@ from sqlalchemy.orm.session import Session
from sqlalchemy.sql import and_
from sqlalchemy.types import *
from pokedex.db import rst
from pokedex.db import markdown
metadata = MetaData()
TableBase = declarative_base(metadata=metadata)
@ -19,8 +19,8 @@ class Ability(TableBase):
id = Column(Integer, primary_key=True, nullable=False)
name = Column(Unicode(24), nullable=False)
generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False)
effect = Column(rst.RstTextColumn(5120), nullable=False)
short_effect = Column(rst.RstTextColumn(255), nullable=False)
effect = Column(markdown.MarkdownColumn(5120), nullable=False)
short_effect = Column(markdown.MarkdownColumn(255), nullable=False)
class AbilityFlavorText(TableBase):
__tablename__ = 'ability_flavor_text'
@ -214,7 +214,7 @@ class Item(TableBase):
cost = Column(Integer, nullable=False)
fling_power = Column(Integer, nullable=True)
fling_effect_id = Column(Integer, ForeignKey('item_fling_effects.id'), nullable=True)
effect = Column(rst.RstTextColumn(5120), nullable=False)
effect = Column(markdown.MarkdownColumn(5120), nullable=False)
@property
def appears_underground(self):
@ -338,7 +338,7 @@ class MoveFlagType(TableBase):
__tablename__ = 'move_flag_types'
id = Column(Integer, primary_key=True, nullable=False)
name = Column(Unicode(32), nullable=False)
description = Column(rst.RstTextColumn(128), nullable=False)
description = Column(markdown.MarkdownColumn(128), nullable=False)
class MoveFlavorText(TableBase):
__tablename__ = 'move_flavor_text'
@ -771,9 +771,9 @@ Move.super_contest_combo_prev = association_proxy('super_contest_combo_second',
Move.target = relation(MoveTarget, backref='moves')
Move.type = relation(Type, backref='moves')
Move.effect = rst.MoveEffectProperty('effect')
Move.effect = markdown.MoveEffectProperty('effect')
Move.priority = association_proxy('move_effect', 'priority')
Move.short_effect = rst.MoveEffectProperty('short_effect')
Move.short_effect = markdown.MoveEffectProperty('short_effect')
MoveEffect.category_map = relation(MoveEffectCategoryMap)
MoveEffect.categories = association_proxy('category_map', 'category')