aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--screenplain/richstring.py111
-rw-r--r--tests/richstring_test.py122
-rw-r--r--tests/spmd_test.py4
3 files changed, 233 insertions, 4 deletions
diff --git a/screenplain/richstring.py b/screenplain/richstring.py
new file mode 100644
index 0000000..7543aaf
--- /dev/null
+++ b/screenplain/richstring.py
@@ -0,0 +1,111 @@
+import re
+
+_emphasis = re.compile(
+ r'(?:'
+ r'(\*\*)' # two stars
+ r'(?=\S)' # must not be followed by space
+ r'(.+?[*_]*)' # inside text
+ r'(?<=\S)\*\*' # finishing with two stars
+ r'|'
+ r'(\*)' # one star
+ r'(?=\S)' # must not be followed by space
+ r'(.+)' # inside text
+ r'(?<=\S)\*' # finishing with one star
+ r'(?!\*)' # must not be followed by star
+ r'|'
+ r'(_)' # underline
+ r'(?=\S)' # must not be followed by space
+ r'([^_]+)' # inside text
+ r'(?<=\S)_' # finishing with underline
+ r')'
+)
+
+class RichString(object):
+ def __init__(self, *segments):
+ self.segments = segments
+
+ def to_html(self):
+ result = ''
+ for segment in self.segments:
+ if isinstance(segment, basestring):
+ result += segment
+ else:
+ result += segment.to_html()
+ return result
+
+ def __unicode__(self):
+ return self.to_html()
+
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ ', '.join(repr(s) for s in self.segments)
+ )
+
+ def __eq__(self, other):
+ return (
+ self.__class__ == other.__class__ and
+ self.segments == other.segments
+ )
+
+ def __ne__(self, other):
+ return (
+ self.__class__ != other.__class__ or
+ self.segments != other.segments
+ )
+
+class Italic(RichString):
+ def to_html(self):
+ return '<em>' + super(Italic, self).to_html() + '</em>'
+
+class Bold(RichString):
+ def to_html(self):
+ return '<strong>' + super(Bold, self).to_html() + '</strong>'
+
+class Underline(RichString):
+ def to_html(self):
+ return '<u>' + super(Underline, self).to_html() + '</u>'
+
+def _parse(source):
+ segments = []
+
+ scanner = _emphasis.scanner(source)
+ pos = 0
+ while pos != len(source):
+ match = scanner.search()
+ if not match:
+ segments.append(source[pos:])
+ break
+ if match.start() != pos:
+ segments.append(source[pos:match.start()])
+
+ (
+ two_stars, two_stars_text,
+ one_star, one_star_text,
+ underline, underline_text
+ ) = match.groups()
+
+ if two_stars:
+ segments.append(Bold(*_parse(two_stars_text)))
+ elif one_star:
+ segments.append(Italic(*_parse(one_star_text)))
+ else:
+ segments.append(Underline(*_parse(underline_text)))
+ pos = match.end()
+
+ return segments
+
+def parse_emphasis(source):
+ """Parses emphasis markers like * and ** in a string
+ and returns a RichString object.
+
+ >>> parse_emphasis(u'**hello**')
+ Bold(u'hello')
+ >>> parse_emphasis(u'plain')
+ RichString(u'plain')
+ """
+ segments = _parse(source)
+ if len(segments) == 1 and isinstance(segments[0], RichString):
+ return segments[0]
+ else:
+ return RichString(*segments)
diff --git a/tests/richstring_test.py b/tests/richstring_test.py
new file mode 100644
index 0000000..bd7ea22
--- /dev/null
+++ b/tests/richstring_test.py
@@ -0,0 +1,122 @@
+import unittest2
+from screenplain.richstring import RichString, Bold, Italic, Underline
+from screenplain.richstring import parse_emphasis
+from screenplain.types import Slug, Action, Dialog, DualDialog, Transition
+
+class RichStringOperatorTests(unittest2.TestCase):
+
+ def test_repr(self):
+ s = RichString(Bold('Hello'), ' there ', Bold('folks'))
+ self.assertEquals(
+ "RichString(Bold('Hello'), ' there ', Bold('folks'))",
+ repr(s)
+ )
+
+ def test_eq(self):
+ self.assertEquals(Bold('Hello'), Bold('Hello'))
+ self.assertNotEquals(Bold('Hello'), Bold('Foo'))
+ self.assertNotEquals('Hello', Bold('Hello'))
+ self.assertEquals(
+ Bold('a', Italic('b'), 'c'),
+ Bold('a', Italic('b'), 'c')
+ )
+ self.assertNotEquals(
+ Bold('a', Italic('b'), 'c'),
+ Bold('a', Italic('b'), 'd')
+ )
+
+ def test_ne(self):
+ self.assertFalse(Bold('Hello') != Bold('Hello'))
+
+class RichStringTests(unittest2.TestCase):
+
+ def test_to_html(self):
+ s = RichString(
+ Bold('bold'),
+ ' normal ',
+ Italic('italic'),
+ Underline('wonderline')
+ )
+ self.assertEquals(
+ '<strong>bold</strong> normal <em>italic</em><u>wonderline</u>',
+ s.to_html()
+ )
+
+class ParseEmphasisTests(unittest2.TestCase):
+
+ def test_parse_without_emphasis(self):
+ self.assertEquals(RichString('Hello'), parse_emphasis('Hello'),
+ 'Expected parse_emphasis to return a string')
+
+ def test_parse_bold(self):
+ self.assertEquals(
+ parse_emphasis('**Hello**'),
+ Bold('Hello')
+ )
+
+ def test_parse_pre_and_postfix_and_bold(self):
+ self.assertEquals(
+ parse_emphasis('pre**Hello**post'),
+ RichString('pre', Bold('Hello'), 'post'),
+ )
+
+ def test_parse_multiple_bold(self):
+ self.assertEquals(
+ parse_emphasis('x**Hello** **there**'),
+ RichString('x', Bold('Hello'), ' ', Bold('there'))
+ )
+
+ def test_parse_adjacent_bold(self):
+ self.assertEquals(
+ parse_emphasis('**123****456**'),
+ RichString(Bold('123**'), '456**')
+ )
+
+ def test_italic(self):
+ self.assertEquals(
+ parse_emphasis('*Italian style*'),
+ Italic('Italian style')
+ )
+
+ def test_bold_inside_italic(self):
+ self.assertEquals(
+ parse_emphasis('*Swedish **style** rules*'),
+ Italic('Swedish ', Bold('style'), ' rules')
+ )
+
+ def test_italic_inside_bold(self):
+ self.assertEquals(
+ parse_emphasis('**Swedish *style* rules**'),
+ Bold('Swedish ', Italic('style'), ' rules')
+ )
+
+ def test_italic_and_bold(self):
+ self.assertEquals(
+ parse_emphasis('***really strong***'),
+ Bold(Italic('really strong'))
+ )
+
+ @unittest2.expectedFailure
+ def test_additional_star(self):
+ self.assertEquals(
+ parse_emphasis('*foo* bar* baz'),
+ RichString(Italic('foo'), ' bar* baz')
+ )
+
+ def test_underline(self):
+ self.assertEquals(
+ parse_emphasis('_hello_'),
+ Underline('hello')
+ )
+
+ def test_bold_inside_underline(self):
+ self.assertEquals(
+ parse_emphasis('_**hello**_'),
+ Underline(Bold('hello'))
+ )
+
+ def test_overlapping_underscore_and_italic(self):
+ self.assertEquals(
+ parse_emphasis('_*he_llo*'),
+ RichString(Underline('*he'), 'llo*')
+ )
diff --git a/tests/spmd_test.py b/tests/spmd_test.py
index caa5b20..4a04660 100644
--- a/tests/spmd_test.py
+++ b/tests/spmd_test.py
@@ -4,10 +4,6 @@ from screenplain.types import Slug, Action, Dialog, DualDialog, Transition
class ParseTests(unittest2.TestCase):
- # Without this, the @skip decorator gives
- # AttributeError: 'ParseTests' object has no attribute '__name__'
- __name__ = 'ParseTests'
-
# A Scene Heading, or "slugline," is any line that has a blank
# line following it, and either begins with INT or EXT, or has
# two empty lines preceding it. A Scene Heading always has at