aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--screenplain/parsers/spmd.py180
-rw-r--r--tests/spmd_test.py30
2 files changed, 110 insertions, 100 deletions
diff --git a/screenplain/parsers/spmd.py b/screenplain/parsers/spmd.py
index c2a757a..8826d9f 100644
--- a/screenplain/parsers/spmd.py
+++ b/screenplain/parsers/spmd.py
@@ -23,98 +23,121 @@ slug_prefixes = (
TWOSPACE = ' ' * 2
-centered_re = re.compile(r'\s*>\s*(.*)\s*<\s*$')
-preprocess_re = re.compile(r'^([ \t]*)(.*?)([ \t]*)[\r\n]*$')
+centered_re = re.compile(r'\s*>\s*(.*?)\s*<\s*$')
dual_dialog_re = re.compile(r'^(.+?)(\s*\^)$')
+slug_re = re.compile(r'(?:(\.)\s*)?(\S.*?)\s*$')
+transition_re = re.compile(r'(>?)\s*(.+?)(:?)$')
-def is_slug(blanks_before, line_list):
- if len(line_list) != 1:
- return False
- upper = line_list[0].upper()
- if upper.startswith('.') and len(upper) > 1:
- return True
- return any(upper.startswith(s) for s in slug_prefixes)
+def _to_rich(line_or_line_list):
+ """Converts a line list into a list of RichString
+ or a single string to a RichString.
+ """
+ if isinstance(line_or_line_list, basestring):
+ return parse_emphasis(line_or_line_list)
+ else:
+ return [parse_emphasis(line) for line in line_or_line_list]
+
+
+class InputParagraph(object):
+ def __init__(self, blanks_before, lines):
+ self.blanks_before = 0
+ self.lines = lines
+
+ def update_list(self, previous_paragraphs):
+ """Inserts this paragraph into a list.
+ Modifies the `previous_paragraphs` list.
+ """
+ previous_paragraphs.append(
+ self.as_slug() or
+ self.as_centered_action() or
+ self.as_dialog(previous_paragraphs) or
+ self.as_transition() or
+ self.as_action()
+ )
+
+ def as_slug(self):
+ if len(self.lines) != 1:
+ return None
+
+ match = slug_re.match(self.lines[0])
+ if not match:
+ return
+
+ period, text = match.groups()
+ if period:
+ return Slug(_to_rich(text.upper()))
+
+ upper = text.upper()
+ if not any(upper.startswith(s) for s in slug_prefixes):
+ return None
+
+ return Slug(_to_rich(upper))
+
+ def as_centered_action(self):
+ if not all(centered_re.match(line) for line in self.lines):
+ return None
+ return Action(_to_rich(
+ centered_re.match(line).group(1) for line in self.lines
+ ), centered=True)
-def _create_slug(line):
- if line.startswith('.') and len(line) > 1:
- line = line[1:]
- return Slug(_to_rich([line])[0])
+ def _create_dialog(self, character):
+ return Dialog(
+ parse_emphasis(character),
+ _to_rich(line.strip() for line in self.lines[1:])
+ )
+ def as_dialog(self, previous_paragraphs):
+ if len(self.lines) < 2:
+ return None
-def _create_dialog(line_list, previous_paragraphs):
+ character = self.lines[0]
+ if not character.isupper() or character.endswith(TWOSPACE):
+ return None
- if previous_paragraphs and isinstance(previous_paragraphs[-1], Dialog):
- dual_match = dual_dialog_re.match(line_list[0])
- if dual_match:
- previous = previous_paragraphs.pop()
- dialog = Dialog(
- parse_emphasis(dual_match.group(1)),
- _to_rich(line_list[1:])
- )
- return DualDialog(previous, dialog)
+ if previous_paragraphs and isinstance(previous_paragraphs[-1], Dialog):
+ dual_match = dual_dialog_re.match(character)
+ if dual_match:
+ previous = previous_paragraphs.pop()
+ dialog = self._create_dialog(dual_match.group(1))
+ return DualDialog(previous, dialog)
- return Dialog(
- parse_emphasis(line_list[0]),
- _to_rich(line_list[1:])
- )
+ return self._create_dialog(character)
+ def as_transition(self):
+ if len(self.lines) != 1:
+ return None
-def _to_rich(line_list):
- """Converts a line list into a list of RichString."""
- return [parse_emphasis(line.strip()) for line in line_list]
+ match = transition_re.match(self.lines[0])
+ if not match:
+ return None
+ greater_than, text, colon = match.groups()
+ if greater_than:
+ return Transition(_to_rich(text.upper() + colon))
-def create_paragraph(blanks_before, line_list, previous_paragraphs):
- first_line = line_list[0]
- if is_slug(blanks_before, line_list):
- return _create_slug(line_list[0])
- elif all(centered_re.match(line) for line in line_list):
- return Action(_to_rich(
- centered_re.match(line).group(1) for line in line_list
- ), centered=True)
- elif (
- len(line_list) > 1 and
- first_line.isupper() and
- not first_line.endswith(TWOSPACE)
- ):
- return _create_dialog(line_list, previous_paragraphs)
- elif (
- len(line_list) == 1 and first_line.isupper()
- and (first_line.endswith(':') or first_line.startswith('>'))
- ):
- if first_line.startswith('>'):
- return Transition(_to_rich([first_line[1:]])[0])
- else:
- return Transition(_to_rich([first_line])[0])
- else:
- return Action(_to_rich(line_list))
+ if text.isupper() and colon:
+ return Transition(_to_rich(text + colon))
+ return None
-def _preprocess_line(raw_line):
- """Splits a line into leading spaces, text content, and trailing spaces.
+ def as_action(self):
+ return Action(_to_rich(line.rstrip() for line in self.lines))
- >>> _preprocess_line(' foo ')
- (' ', 'foo', ' ')
- For a blank line, the trailing spaces will be returned as trailing
- whitespace:
+def _preprocess_line(raw_line):
+ r"""Replaces tabs with spaces and removes trailing end of line markers.
+
+ >>> _preprocess_line('foo \r\n\n')
+ 'foo '
- >>> _preprocess_line(' ')
- ('', '', ' ')
"""
- line = raw_line.expandtabs(4)
- leading, text, trailing = preprocess_re.match(line).groups()
- if not text:
- trailing = leading
- leading = ''
- return leading, text, trailing
+ return raw_line.expandtabs(4).rstrip('\r\n')
-def _is_blank(preprocessed_line):
- leading, text, trailing = preprocessed_line
- return not text and not trailing
+def _is_blank(line):
+ return line == ''
def parse(source):
@@ -122,18 +145,11 @@ def parse(source):
blank_count = 0
source = (_preprocess_line(line) for line in source)
paragraphs = []
- for blank, preprocessed_lines in itertools.groupby(source, _is_blank):
+ for blank, input_lines in itertools.groupby(source, _is_blank):
if blank:
- blank_count = sum(1 for line in preprocessed_lines)
+ blank_count = sum(1 for line in input_lines)
else:
- paragraph = create_paragraph(
- blank_count,
- [
- text + trailing
- for (leading, text, trailing) in preprocessed_lines
- ],
- paragraphs
- )
- paragraphs.append(paragraph)
+ paragraph = InputParagraph(blank_count, list(input_lines))
+ paragraph.update_list(paragraphs)
return paragraphs
diff --git a/tests/spmd_test.py b/tests/spmd_test.py
index 264995b..9f03076 100644
--- a/tests/spmd_test.py
+++ b/tests/spmd_test.py
@@ -234,28 +234,22 @@ class ParseTests(unittest2.TestCase):
self.assertEquals([Action, Transition], [type(p) for p in paras])
self.assertEquals(plain('FADE OUT.'), paras[1].line)
- def test_multiline_paragraph(self):
- """Check that we don't join lines like Markdown does.
- """
+ def test_action_preserves_leading_whitespace(self):
paras = list(parse([
- 'They drink long and well from the beers.',
+ 'hello',
'',
- "And then there's a long beat.",
- "Longer than is funny. ",
- " Long enough to be depressing.",
- '',
- 'The men look at each other.',
+ ' two spaces',
+ ' three spaces ',
]))
- self.assertEquals([Action, Action, Action], [type(p) for p in paras])
+ self.assertEquals([Action, Action], [type(p) for p in paras])
self.assertEquals(
[
- plain("And then there's a long beat."),
- plain("Longer than is funny."),
- plain("Long enough to be depressing."),
+ plain(u' two spaces'),
+ plain(u' three spaces'),
], paras[1].lines
)
- def test_multiline_dialog(self):
+ def test_leading_and_trailing_spaces_in_dialog(self):
paras = list(parse([
'JULIET',
'O Romeo, Romeo! wherefore art thou Romeo?',
@@ -265,10 +259,10 @@ class ParseTests(unittest2.TestCase):
]))
self.assertEquals([Dialog], [type(p) for p in paras])
self.assertEquals([
- (False, plain('O Romeo, Romeo! wherefore art thou Romeo?')),
- (False, plain('Deny thy father and refuse thy name;')),
- (False, plain('Or, if thou wilt not, be but sworn my love,')),
- (False, plain("And I'll no longer be a Capulet.")),
+ (False, plain(u'O Romeo, Romeo! wherefore art thou Romeo?')),
+ (False, plain(u'Deny thy father and refuse thy name;')),
+ (False, plain(u'Or, if thou wilt not, be but sworn my love,')),
+ (False, plain(u"And I'll no longer be a Capulet.")),
], paras[0].blocks)
def test_single_centered_line(self):