diff options
-rw-r--r-- | screenplain/parse.py | 49 | ||||
-rw-r--r-- | tests/parse_test.py | 111 |
2 files changed, 133 insertions, 27 deletions
diff --git a/screenplain/parse.py b/screenplain/parse.py index 3f19662..336b4eb 100644 --- a/screenplain/parse.py +++ b/screenplain/parse.py @@ -42,36 +42,17 @@ class Dialog(object): def __init__(self, lines): self.character = lines[0] - self.secondary_character = None - self.dual = False self.blocks = [] # list of tuples of (is_parenthetical, text) - self.secondary_blocks = [] # for the second speaker if dual self._parse(lines[1:]) def _parse(self, lines): - append = self.blocks.append inside_parenthesis = False - it = iter(lines) - while True: - try: - line = it.next() - except StopIteration: - return - if line == '||' and not self.dual: - self.dual = True - append = self.secondary_blocks.append - try: - self.secondary_character = it.next() - # what if this doesn't look like a character name? - except StopIteration: - append(line) # keep trailing '||' - return - else: - if line.startswith('('): - inside_parenthesis = True - append((inside_parenthesis, line)) - if line.endswith(')'): - inside_parenthesis = False + for line in lines: + if line.startswith('('): + inside_parenthesis = True + self.blocks.append((inside_parenthesis, line)) + if line.endswith(')'): + inside_parenthesis = False def format(self): yield self.indent_character + self.character @@ -94,6 +75,11 @@ class Dialog(object): for line in lines: yield line +class DualDialog(object): + def __init__(self, left_lines, right_lines): + self.left = Dialog(left_lines) + self.right = Dialog(right_lines) + class Action(object): indent = '' fill = 68 @@ -127,14 +113,23 @@ def is_slug(blanks_before, string): upper = string.upper() return any(upper.startswith(s) for s in slug_prefixes) +def _create_dialog(line_list): + try: + dual_index = line_list.index('||') + except ValueError: + return Dialog(line_list) + else: + return DualDialog(line_list[:dual_index], line_list[dual_index + 1:]) + def create_paragraph(blanks_before, line_list): if is_slug(blanks_before, line_list[0]): return Slug(line_list) if ( len(line_list) > 1 and line_list[0].isupper() and - not line_list[0].endswith(TWOSPACE)): - return Dialog(line_list) + not line_list[0].endswith(TWOSPACE) + ): + return _create_dialog(line_list) elif len(line_list) == 1 and line_list[0].endswith(':'): # Not according to spec, see my comment #http://prolost.com/blog/2011/8/9/screenplay-markdown.html?lastPage=true#comment14865294 diff --git a/tests/parse_test.py b/tests/parse_test.py new file mode 100644 index 0000000..75d762a --- /dev/null +++ b/tests/parse_test.py @@ -0,0 +1,111 @@ +import unittest2 +from screenplain import parse +from screenplain.parse 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 + # least one blank line preceding it. + # NOTE: Actually the list used in Appendix 1 + def test_slug_with_prefix(self): + paras = list(parse.parse([ + 'INT. SOMEWHERE - DAY', + '', + 'THIS IS JUST ACTION', + ])) + self.assertEquals([Slug, Action], [type(p) for p in paras]) + + def test_action_is_not_a_slug(self): + paras = list(parse.parse([ + '', + 'THIS IS JUST ACTION', + ])) + self.assertEquals([Action], [type(p) for p in paras]) + + def test_two_lines_creates_a_slug(self): + types = [type(p) for p in parse.parse([ + '', + '', + 'This is a slug', + '', + ])] + self.assertEquals([Slug], types) + + # A Character element is any line entirely in caps, with one empty + # line before it and without an empty line after it. + def test_all_caps_is_character(self): + paras = [p for p in parse.parse([ + 'SOME GUY', + 'Hello', + ])] + self.assertEquals(1, len(paras)) + dialog = paras[0] + self.assertEquals(Dialog, type(dialog)) + self.assertEquals('SOME GUY', dialog.character) + + # SPMD would not be able to support a character named "23". We + # might need a syntax to force a character element. + def test_nonalpha_character(self): + paras = list(parse.parse([ + '23', + 'Hello', + ])) + self.assertEquals([Action], [type(p) for p in paras]) + + # See + # http://prolost.com/storage/downloads/spmd/SPMD_proposal.html#section-br + def test_twospaced_line_is_not_character(self): + paras = list(parse.parse([ + 'SCANNING THE AISLES... ', + 'Where is that pit boss?', + ])) + self.assertEquals([Action], [type(p) for p in paras]) + + def test_simple_parenthetical(self): + paras = list(parse.parse([ + 'STEEL', + '(starting the engine)', + 'So much for retirement!', + ])) + self.assertEquals(1, len(paras)) + dialog = paras[0] + self.assertEqual(2, len(dialog.blocks)) + self.assertEqual((True, '(starting the engine)'), dialog.blocks[0]) + self.assertEqual((False, 'So much for retirement!'), dialog.blocks[1]) + + def test_dual_dialog(self): + paras = list(parse.parse([ + 'BRICK', + 'Fuck retirement.', + '||', + 'STEEL', + 'Fuck retirement!', + ])) + self.assertEquals([DualDialog], [type(p) for p in paras]) + dual = paras[0] + self.assertEquals('BRICK', dual.left.character) + self.assertEquals([(False, 'Fuck retirement.')], dual.left.blocks) + self.assertEquals('STEEL', dual.right.character) + self.assertEquals([(False, 'Fuck retirement!')], dual.right.blocks) + + def test_standard_transition(self): + # Need clarification of the spec here, see + # http://prolost.com/blog/2011/8/9/screenplay-markdown.html?lastPage=true#comment14865294 + + paras = list(parse.parse([ + 'Jack begins to argue vociferously in Vietnamese (?)', + '', + 'CUT TO:', + '', + "EXT. BRICK'S POOL - DAY", + ])) + self.assertEquals([Action, Transition, Slug], [type(p) for p in paras]) + +if __name__ == '__main__': + unittest2.main() |