aboutsummaryrefslogtreecommitdiffstats
path: root/tests/fountain_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/fountain_test.py')
-rw-r--r--tests/fountain_test.py435
1 files changed, 435 insertions, 0 deletions
diff --git a/tests/fountain_test.py b/tests/fountain_test.py
new file mode 100644
index 0000000..99578cf
--- /dev/null
+++ b/tests/fountain_test.py
@@ -0,0 +1,435 @@
+# Copyright (c) 2011 Martin Vilcans
+# Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+
+import unittest2
+from screenplain.parsers.fountain import parse
+from screenplain.parsers import fountain
+from screenplain.types import (
+ Slug, Action, Dialog, DualDialog, Transition, Section
+)
+from screenplain.richstring import plain, italic, empty_string
+
+
+class SlugTests(unittest2.TestCase):
+ def test_slug_with_prefix(self):
+ paras = list(parse([
+ 'INT. SOMEWHERE - DAY',
+ '',
+ 'THIS IS JUST ACTION',
+ ]))
+ self.assertEquals([Slug, Action], [type(p) for p in paras])
+
+ def test_slug_must_be_single_line(self):
+ paras = list(parse([
+ 'INT. SOMEWHERE - DAY',
+ 'ANOTHER LINE',
+ '',
+ 'Some action',
+ ]))
+ self.assertEquals([Dialog, Action], [type(p) for p in paras])
+ # What looks like a scene headingis parsed as a character name.
+ # Unexpected perhaps, but that's how I interpreted the spec.
+ self.assertEquals(plain('INT. SOMEWHERE - DAY'), paras[0].character)
+ self.assertEquals([plain('Some action')], paras[1].lines)
+
+ def test_action_is_not_a_slug(self):
+ paras = list(parse([
+ '',
+ 'THIS IS JUST ACTION',
+ ]))
+ self.assertEquals([Action], [type(p) for p in paras])
+
+ def test_two_lines_creates_no_slug(self):
+ types = [type(p) for p in parse([
+ '',
+ '',
+ 'This is a slug',
+ '',
+ ])]
+ # This used to be Slug. Changed in the Jan 2012 version of the spec.
+ self.assertEquals([Action], types)
+
+ def test_period_creates_slug(self):
+ paras = parse([
+ '.SNIPER SCOPE POV',
+ '',
+ ])
+ self.assertEquals(1, len(paras))
+ self.assertEquals(Slug, type(paras[0]))
+ self.assertEquals(plain('SNIPER SCOPE POV'), paras[0].line)
+
+ def test_scene_number_is_parsed(self):
+ paras = parse(['EXT SOMEWHERE - DAY #42#'])
+ self.assertEquals(plain('EXT SOMEWHERE - DAY'), paras[0].line)
+ self.assertEquals(plain('42'), paras[0].scene_number)
+
+ def test_only_last_two_hashes_in_slug_used_for_scene_number(self):
+ paras = parse(['INT ROOM #237 #42#'])
+ self.assertEquals(plain('42'), paras[0].scene_number)
+ self.assertEquals(plain('INT ROOM #237'), paras[0].line)
+
+ def test_scene_number_must_be_alphanumeric(self):
+ paras = parse(['.SOMEWHERE #*HELLO*#'])
+ self.assertIsNone(paras[0].scene_number)
+ self.assertEquals(
+ (plain)(u'SOMEWHERE #') + (italic)(u'HELLO') + (plain)(u'#'),
+ paras[0].line
+ )
+
+
+class SectionTests(unittest2.TestCase):
+ def test_section_parsed_correctly(self):
+ paras = parse([
+ '# first level',
+ '',
+ '## second level',
+ ])
+ self.assertEquals([Section, Section], [type(p) for p in paras])
+ self.assertEquals(1, paras[0].level)
+ self.assertEquals(plain('first level'), paras[0].text)
+ self.assertEquals(2, paras[1].level)
+ self.assertEquals(plain('second level'), paras[1].text)
+
+
+class DialogTests(unittest2.TestCase):
+ # 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([
+ 'SOME GUY',
+ 'Hello',
+ ])]
+ self.assertEquals(1, len(paras))
+ dialog = paras[0]
+ self.assertEquals(Dialog, type(dialog))
+ self.assertEquals(plain('SOME GUY'), dialog.character)
+
+ # Fountain 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([
+ '23',
+ 'Hello',
+ ]))
+ self.assertEquals([Action], [type(p) for p in paras])
+
+ def test_twospaced_line_is_not_character(self):
+ paras = list(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([
+ '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, plain('(starting the engine)')),
+ dialog.blocks[0]
+ )
+ self.assertEqual(
+ (False, plain('So much for retirement!')),
+ dialog.blocks[1]
+ )
+
+ def test_twospace_keeps_dialog_together(self):
+ paras = list(parse([
+ 'SOMEONE',
+ 'One',
+ ' ',
+ 'Two',
+ ]))
+ self.assertEquals([Dialog], [type(p) for p in paras])
+ self.assertEquals([
+ (False, plain('One')),
+ (False, empty_string),
+ (False, plain('Two')),
+ ], paras[0].blocks)
+
+ def test_dual_dialog(self):
+ paras = list(parse([
+ 'BRICK',
+ 'Fuck retirement.',
+ '',
+ 'STEEL ^',
+ 'Fuck retirement!',
+ ]))
+ self.assertEquals([DualDialog], [type(p) for p in paras])
+ dual = paras[0]
+ self.assertEquals(plain('BRICK'), dual.left.character)
+ self.assertEquals(
+ [(False, plain('Fuck retirement.'))],
+ dual.left.blocks
+ )
+ self.assertEquals(plain('STEEL'), dual.right.character)
+ self.assertEquals(
+ [(False, plain('Fuck retirement!'))],
+ dual.right.blocks
+ )
+
+ def test_dual_dialog_without_previous_dialog_is_ignored(self):
+ paras = list(parse([
+ 'Brick strolls down the street.',
+ '',
+ 'BRICK ^',
+ 'Nice retirement.',
+ ]))
+ self.assertEquals([Action, Dialog], [type(p) for p in paras])
+ dialog = paras[1]
+ self.assertEqual(plain('BRICK ^'), dialog.character)
+ self.assertEqual([
+ (False, plain('Nice retirement.'))
+ ], dialog.blocks)
+
+ def test_leading_and_trailing_spaces_in_dialog(self):
+ paras = list(parse([
+ 'JULIET',
+ 'O Romeo, Romeo! wherefore art thou Romeo?',
+ ' Deny thy father and refuse thy name; ',
+ 'Or, if thou wilt not, be but sworn my love,',
+ " And I'll no longer be a Capulet.",
+ ]))
+ self.assertEquals([Dialog], [type(p) for p in paras])
+ self.assertEquals([
+ (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)
+
+
+class TransitionTests(unittest2.TestCase):
+
+ def test_standard_transition(self):
+ paras = list(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])
+
+ def test_transition_needs_to_be_upper_case(self):
+ paras = list(parse([
+ 'Jack begins to argue vociferously in Vietnamese (?)',
+ '',
+ 'cut to:',
+ '',
+ "EXT. BRICK'S POOL - DAY",
+ ]))
+ self.assertEquals([Action, Action, Slug], [type(p) for p in paras])
+
+ def test_not_a_transition_on_trailing_whitespace(self):
+ paras = list(parse([
+ 'Jack begins to argue vociferously in Vietnamese (?)',
+ '',
+ 'CUT TO: ',
+ '',
+ "EXT. BRICK'S POOL - DAY",
+ ]))
+ self.assertEquals([Action, Action, Slug], [type(p) for p in paras])
+
+ def test_transition_does_not_have_to_be_followed_by_slug(self):
+ # The "followed by slug" requirement is gone from the Jan 2012 spec
+ paras = list(parse([
+ 'Bill lights a cigarette.',
+ '',
+ 'CUT TO:',
+ '',
+ 'SOME GUY mowing the lawn.',
+ ]))
+ self.assertEquals(
+ [Action, Transition, Action],
+ [type(p) for p in paras]
+ )
+
+ def test_greater_than_sign_means_transition(self):
+ paras = list(parse([
+ 'Bill blows out the match.',
+ '',
+ '> FADE OUT.',
+ '',
+ '.DARKNESS',
+ ]))
+ self.assertEquals([Action, Transition, Slug], [type(p) for p in paras])
+ self.assertEquals(plain('FADE OUT.'), paras[1].line)
+
+ def test_centered_text_is_not_parsed_as_transition(self):
+ paras = list(parse([
+ 'Bill blows out the match.',
+ '',
+ '> THE END. <',
+ '',
+ 'bye!'
+ ]))
+ self.assertEquals([Action, Action, Action], [type(p) for p in paras])
+
+ def test_transition_at_end(self):
+ paras = list(parse([
+ 'They stroll hand in hand down the street.',
+ '',
+ '> FADE OUT.',
+ ]))
+ self.assertEquals([Action, Transition], [type(p) for p in paras])
+ self.assertEquals(plain('FADE OUT.'), paras[1].line)
+
+
+class ActionTests(unittest2.TestCase):
+
+ def test_action_preserves_leading_whitespace(self):
+ paras = list(parse([
+ 'hello',
+ '',
+ ' two spaces',
+ ' three spaces ',
+ ]))
+ self.assertEquals([Action, Action], [type(p) for p in paras])
+ self.assertEquals(
+ [
+ plain(u' two spaces'),
+ plain(u' three spaces'),
+ ], paras[1].lines
+ )
+
+ def test_single_centered_line(self):
+ paras = list(parse(['> center me! <']))
+ self.assertEquals([Action], [type(p) for p in paras])
+ self.assertTrue(paras[0].centered)
+
+ def test_full_centered_paragraph(self):
+ lines = [
+ '> first! <',
+ ' > second! <',
+ '> third!< ',
+ ]
+ paras = list(parse(lines))
+ self.assertEquals([Action], [type(p) for p in paras])
+ self.assertTrue(paras[0].centered)
+ self.assertEquals([
+ plain('first!'),
+ plain('second!'),
+ plain('third!'),
+ ], paras[0].lines)
+
+ def test_upper_case_centered_not_parsed_as_dialog(self):
+ paras = list(parse([
+ '> FIRST! <',
+ ' > SECOND! <',
+ '> THIRD! <',
+ ]))
+ self.assertEquals([Action], [type(p) for p in paras])
+ self.assertTrue(paras[0].centered)
+
+ def test_centering_marks_in_middle_of_paragraphs_are_verbatim(self):
+ lines = [
+ 'first!',
+ '> second! <',
+ 'third!',
+ ]
+ paras = list(parse(lines))
+ self.assertEquals([Action], [type(p) for p in paras])
+ self.assertFalse(paras[0].centered)
+ self.assertEquals([plain(line) for line in lines], paras[0].lines)
+
+
+class SynopsisTests(unittest2.TestCase):
+ def test_synopsis_after_slug_adds_synopsis_to_scene(self):
+ paras = list(parse([
+ "EXT. BRICK'S PATIO - DAY",
+ '',
+ "= Set up Brick & Steel's new life."
+ '',
+ ]))
+ self.assertEquals([Slug], [type(p) for p in paras])
+ self.assertEquals(
+ "Set up Brick & Steel's new life.",
+ paras[0].synopsis
+ )
+
+ def test_synopsis_in_section(self):
+ paras = list(parse([
+ '# section one',
+ '',
+ '= In which we get to know our characters'
+ ]))
+ self.assertEquals([Section], [type(p) for p in paras])
+ self.assertEquals(
+ 'In which we get to know our characters',
+ paras[0].synopsis
+ )
+
+ def test_synopsis_syntax_parsed_as_literal(self):
+ paras = list(parse([
+ 'Some action',
+ '',
+ '= A line that just happens to look like a synopsis'
+ ]))
+ self.assertEquals([Action, Action], [type(p) for p in paras])
+ self.assertEquals(
+ [plain('= A line that just happens to look like a synopsis')],
+ paras[1].lines
+ )
+
+
+class TitlePageTests(unittest2.TestCase):
+
+ def test_basic_title_page(self):
+ lines = [
+ 'Title:',
+ ' _**BRICK & STEEL**_',
+ ' _**FULL RETIRED**_',
+ 'Author: Stu Maschwitz',
+ ]
+ self.assertDictEqual(
+ {
+ 'Title': ['_**BRICK & STEEL**_', '_**FULL RETIRED**_'],
+ 'Author': ['Stu Maschwitz'],
+ },
+ fountain.parse_title_page(lines)
+ )
+
+ def test_multiple_values(self):
+ lines = [
+ 'Title: Death',
+ 'Title: - a love story',
+ 'Title:',
+ ' (which happens to be true)',
+ ]
+ self.assertDictEqual(
+ {
+ 'Title': [
+ 'Death',
+ '- a love story',
+ '(which happens to be true)'
+ ]
+ },
+ fountain.parse_title_page(lines)
+ )
+
+ def test_empty_value_ignored(self):
+ lines = [
+ 'Title:',
+ 'Author: John August',
+ ]
+ self.assertDictEqual(
+ {'Author': ['John August']},
+ fountain.parse_title_page(lines)
+ )
+
+ def test_unparsable_title_page_returns_none(self):
+ lines = [
+ 'Title: Inception',
+ ' additional line',
+ ]
+ self.assertIsNone(fountain.parse_title_page(lines))
+
+if __name__ == '__main__':
+ unittest2.main()