aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Vilcans <martin@librador.com>2014-10-15 17:05:02 +0200
committerMartin Vilcans <martin@librador.com>2014-10-21 11:13:47 +0200
commitbb97fb5f30c41e82b86eb88eecf44a49b2bd9780 (patch)
treed66ab525f7e5bb11f71f6ce6b876628c9fea1d59
parent5bc3859d931ea9d15824ebc9038bca5530c2f48f (diff)
downloadscreenplain-bb97fb5f30c41e82b86eb88eecf44a49b2bd9780.tar.gz
Add Screenplay class
This is to be able to access title page data.
-rw-r--r--screenplain/parsers/fountain.py26
-rw-r--r--screenplain/types.py30
-rw-r--r--tests/fountain_test.py104
3 files changed, 103 insertions, 57 deletions
diff --git a/screenplain/parsers/fountain.py b/screenplain/parsers/fountain.py
index df3d147..9b0c6c7 100644
--- a/screenplain/parsers/fountain.py
+++ b/screenplain/parsers/fountain.py
@@ -7,7 +7,8 @@ from itertools import takewhile
import re
from screenplain.types import (
- Slug, Action, Dialog, DualDialog, Transition, Section, PageBreak
+ Slug, Action, Dialog, DualDialog, Transition, Section, PageBreak,
+ Screenplay
)
from screenplain.richstring import parse_emphasis, plain
@@ -205,6 +206,11 @@ def _is_blank(line):
def parse(stream):
+ """Parses Fountain source.
+
+ Returns a Screenplay object.
+
+ """
content = stream.read()
content = boneyard_re.sub('', content)
lines = linebreak_re.split(content)
@@ -213,8 +219,11 @@ def parse(stream):
def parse_lines(source):
- """Reads raw text input and generates paragraph objects."""
+ """Reads raw text input and generates paragraph objects.
+
+ Returns a Screenplay object.
+ """
source = (_preprocess_line(line) for line in source)
title_page_lines = list(takewhile(lambda line: line != '', source))
@@ -223,12 +232,14 @@ def parse_lines(source):
if title_page:
# The first lines were a title page.
# Parse the rest of the source as screenplay body.
- # TODO: Create a title page from the data in title_page
- return parse_body(source)
+ return Screenplay(title_page, parse_body(source))
else:
# The first lines were not a title page.
# Parse them as part of the screenplay body.
- return parse_body(itertools.chain(title_page_lines, [''], source))
+ return Screenplay(
+ {},
+ parse_body(itertools.chain(title_page_lines, [''], source))
+ )
def parse_body(source):
@@ -244,7 +255,12 @@ def parse_body(source):
def parse_title_page(lines):
+ """Parse the title page.
+ Spec: http://fountain.io/syntax#section-titlepage
+ Returns None if the document does not have a title page section,
+ otherwise a dictionary with the data.
+ """
result = {}
it = iter(lines)
diff --git a/screenplain/types.py b/screenplain/types.py
index 8464ea5..6acdce6 100644
--- a/screenplain/types.py
+++ b/screenplain/types.py
@@ -3,6 +3,36 @@
# http://www.opensource.org/licenses/mit-license.php
+class Screenplay(object):
+ def __init__(self, title_page=None, paragraphs=None):
+ """
+ Create a Screenplay object.
+
+ `title_page` is a dictionary mapping string keys to strings.
+ `paragraphs` is a sequence of paragraph objects.
+ """
+
+ # Key/value pairs for title page
+ if title_page is None:
+ self.title_page = {}
+ else:
+ self.title_page = title_page
+
+ # The paragraphs of the actual script
+ if paragraphs is None:
+ self.paragraphs = []
+ else:
+ self.paragraphs = paragraphs
+
+ def append(self, paragraph):
+ """Append a paragraph to this screenplay."""
+ self.paragraphs.append(paragraph)
+
+ def __iter__(self):
+ """Get an iterator over the paragraphs of this screenplay."""
+ return iter(self.paragraphs)
+
+
class Slug(object):
def __init__(self, line, scene_number=None):
diff --git a/tests/fountain_test.py b/tests/fountain_test.py
index e265da9..9a5d308 100644
--- a/tests/fountain_test.py
+++ b/tests/fountain_test.py
@@ -14,25 +14,25 @@ from StringIO import StringIO
def parse(lines):
content = '\n'.join(lines)
- return fountain.parse(StringIO(content))
+ return list(fountain.parse(StringIO(content)))
class SlugTests(TestCase):
def test_slug_with_prefix(self):
- paras = list(parse([
+ paras = 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([
+ paras = 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.
@@ -40,10 +40,10 @@ class SlugTests(TestCase):
self.assertEquals([plain('Some action')], paras[1].lines)
def test_action_is_not_a_slug(self):
- paras = list(parse([
+ paras = parse([
'',
'THIS IS JUST ACTION',
- ]))
+ ])
self.assertEquals([Action], [type(p) for p in paras])
def test_two_lines_creates_no_slug(self):
@@ -151,25 +151,25 @@ class DialogTests(TestCase):
# 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([
+ paras = parse([
'23',
'Hello',
- ]))
+ ])
self.assertEquals([Action], [type(p) for p in paras])
def test_twospaced_line_is_not_character(self):
- paras = list(parse([
+ paras = 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([
+ paras = parse([
'STEEL',
'(starting the engine)',
'So much for retirement!',
- ]))
+ ])
self.assertEquals(1, len(paras))
dialog = paras[0]
self.assertEqual(2, len(dialog.blocks))
@@ -183,12 +183,12 @@ class DialogTests(TestCase):
)
def test_twospace_keeps_dialog_together(self):
- paras = list(parse([
+ paras = parse([
'SOMEONE',
'One',
' ',
'Two',
- ]))
+ ])
self.assertEquals([Dialog], [type(p) for p in paras])
self.assertEquals([
(False, plain('One')),
@@ -197,13 +197,13 @@ class DialogTests(TestCase):
], paras[0].blocks)
def test_dual_dialog(self):
- paras = list(parse([
+ paras = 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)
@@ -218,12 +218,12 @@ class DialogTests(TestCase):
)
def test_dual_dialog_without_previous_dialog_is_ignored(self):
- paras = list(parse([
+ paras = 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)
@@ -232,13 +232,13 @@ class DialogTests(TestCase):
], dialog.blocks)
def test_leading_and_trailing_spaces_in_dialog(self):
- paras = list(parse([
+ paras = 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?')),
@@ -251,84 +251,84 @@ class DialogTests(TestCase):
class TransitionTests(TestCase):
def test_standard_transition(self):
- paras = list(parse([
+ paras = 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_must_end_with_to(self):
- paras = list(parse([
+ paras = parse([
'CUT TOO:',
'',
"EXT. BRICK'S POOL - DAY",
- ]))
+ ])
self.assertEquals([Action, Slug], [type(p) for p in paras])
def test_transition_needs_to_be_upper_case(self):
- paras = list(parse([
+ paras = 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([
+ paras = 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([
+ paras = 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([
+ paras = 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([
+ paras = 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([
+ paras = 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)
@@ -336,12 +336,12 @@ class TransitionTests(TestCase):
class ActionTests(TestCase):
def test_action_preserves_leading_whitespace(self):
- paras = list(parse([
+ paras = parse([
'hello',
'',
' two spaces',
' three spaces ',
- ]))
+ ])
self.assertEquals([Action, Action], [type(p) for p in paras])
self.assertEquals(
[
@@ -351,7 +351,7 @@ class ActionTests(TestCase):
)
def test_single_centered_line(self):
- paras = list(parse(['> center me! <']))
+ paras = parse(['> center me! <'])
self.assertEquals([Action], [type(p) for p in paras])
self.assertTrue(paras[0].centered)
@@ -361,7 +361,7 @@ class ActionTests(TestCase):
' > second! <',
'> third!< ',
]
- paras = list(parse(lines))
+ paras = parse(lines)
self.assertEquals([Action], [type(p) for p in paras])
self.assertTrue(paras[0].centered)
self.assertEquals([
@@ -371,11 +371,11 @@ class ActionTests(TestCase):
], paras[0].lines)
def test_upper_case_centered_not_parsed_as_dialog(self):
- paras = list(parse([
+ paras = parse([
'> FIRST! <',
' > SECOND! <',
'> THIRD! <',
- ]))
+ ])
self.assertEquals([Action], [type(p) for p in paras])
self.assertTrue(paras[0].centered)
@@ -385,7 +385,7 @@ class ActionTests(TestCase):
'> second! <',
'third!',
]
- paras = list(parse(lines))
+ paras = 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)
@@ -393,12 +393,12 @@ class ActionTests(TestCase):
class SynopsisTests(TestCase):
def test_synopsis_after_slug_adds_synopsis_to_scene(self):
- paras = list(parse([
+ paras = 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.",
@@ -406,11 +406,11 @@ class SynopsisTests(TestCase):
)
def test_synopsis_in_section(self):
- paras = list(parse([
+ paras = 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',
@@ -418,11 +418,11 @@ class SynopsisTests(TestCase):
)
def test_synopsis_syntax_parsed_as_literal(self):
- paras = list(parse([
+ paras = 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')],
@@ -485,9 +485,9 @@ class TitlePageTests(TestCase):
class PageBreakTests(TestCase):
def test_page_break_is_parsed(self):
- paras = list(parse([
+ paras = parse([
'====',
'',
'So here we go'
- ]))
+ ])
self.assertEquals([PageBreak, Action], [type(p) for p in paras])