From 579fa278aeaa26dccbdfe28b4b5ce0db3390c0b8 Mon Sep 17 00:00:00 2001 From: Martin Vilcans Date: Thu, 8 Sep 2011 23:03:13 +0200 Subject: Split code into more modules. --- screenplain/export/pdf.py | 2 +- screenplain/export/text.py | 2 +- screenplain/format.py | 19 +++++ screenplain/main.py | 2 +- screenplain/parse.py | 182 ---------------------------------------- screenplain/parsers/__init__.py | 0 screenplain/parsers/spmd.py | 66 +++++++++++++++ screenplain/types.py | 98 ++++++++++++++++++++++ tests/parse_test.py | 152 --------------------------------- tests/spmd_test.py | 152 +++++++++++++++++++++++++++++++++ 10 files changed, 338 insertions(+), 337 deletions(-) create mode 100644 screenplain/format.py delete mode 100644 screenplain/parse.py create mode 100644 screenplain/parsers/__init__.py create mode 100644 screenplain/parsers/spmd.py create mode 100644 screenplain/types.py delete mode 100644 tests/parse_test.py create mode 100644 tests/spmd_test.py diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 48eeab8..f7c9f24 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -3,7 +3,7 @@ import fileinput from reportlab.pdfgen import canvas from reportlab.lib import pagesizes -from screenplain.parse import parse, get_pages +from screenplain.format import get_pages def to_pdf(screenplay, output_file): # pagesizes.letter, pagesizes.A4 diff --git a/screenplain/export/text.py b/screenplain/export/text.py index 8f7cb3d..12b8723 100644 --- a/screenplain/export/text.py +++ b/screenplain/export/text.py @@ -1,6 +1,6 @@ import sys import codecs -from screenplain.parse import parse, get_pages +from screenplain.format import get_pages def to_text(screenplay, output_file): out = codecs.open(output_file, 'w', 'utf-8') diff --git a/screenplain/format.py b/screenplain/format.py new file mode 100644 index 0000000..be5298b --- /dev/null +++ b/screenplain/format.py @@ -0,0 +1,19 @@ +# Numbers from http://www.emacswiki.org/emacs/ScreenPlay +# According to http://johnaugust.com/2004/how-many-lines-per-page +lines_per_page = 56 + + +def get_pages(paragraphs): + """Generates one list of lines per page.""" + lines_on_page = [] + for paragraph in paragraphs: + top_margin = paragraph.top_margin if lines_on_page else 0 + para_lines = list(paragraph.format()) + + if len(lines_on_page) + top_margin + len(para_lines) > lines_per_page: + yield lines_on_page + lines_on_page = [] + else: + lines_on_page += [''] * top_margin + lines_on_page += para_lines + yield lines_on_page diff --git a/screenplain/main.py b/screenplain/main.py index 79ff3eb..469da0f 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -8,7 +8,7 @@ import sys import codecs from optparse import OptionParser -from parse import parse +from screenplain.parsers.spmd import parse usage = 'Usage: %prog [options] input-file output-file' diff --git a/screenplain/parse.py b/screenplain/parse.py deleted file mode 100644 index 9e97eed..0000000 --- a/screenplain/parse.py +++ /dev/null @@ -1,182 +0,0 @@ -import itertools -import textwrap -import re - -# Numbers from http://www.emacswiki.org/emacs/ScreenPlay -# According to http://johnaugust.com/2004/how-many-lines-per-page -lines_per_page = 56 - -slug_prefixes = ( - 'INT. ', - 'EXT. ', - 'INT./EXT. ', - 'INT/EXT. ', - 'INT ', - 'EXT ', - 'INT/EXT ', - 'I/E ', -) - -TWOSPACE = ' ' * 2 - -class Slug(object): - indent = '' - top_margin = 1 - - def __init__(self, lines): - self.lines = [self.indent + line.strip() for line in lines] - - def format(self): - return self.lines - -class Dialog(object): - indent_character = ' ' * 22 - indent_dialog = ' ' * 10 - indent_parenthetical_first = ' ' * 16 - indent_parenthetical_subsequent = ' ' * 17 - - fill_parenthetical = 45 - fill_dialog = 45 - - top_margin = 1 - - def __init__(self, lines): - self.character = lines[0] - self.blocks = [] # list of tuples of (is_parenthetical, text) - self._parse(lines[1:]) - - def _parse(self, lines): - 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 - - for parenthetical, text in self.blocks: - if parenthetical: - lines = textwrap.wrap( - text, - width=self.fill_parenthetical, - initial_indent=self.indent_parenthetical_first, - subsequent_indent=self.indent_parenthetical_subsequent - ) - else: - lines = textwrap.wrap( - text, - width=self.fill_dialog, - initial_indent=self.indent_dialog, - subsequent_indent=self.indent_dialog - ) - for line in lines: - yield line - -class DualDialog(object): - top_margin = 1 - - def __init__(self, left_lines, right_lines): - self.left = Dialog(left_lines) - self.right = Dialog(right_lines) - - def format(self): - # FIXME: I haven't checked yet how dual dialog is supposed to look. - llines = list(self.left.format()) - rlines = list(self.right.format()) - llines += [''] * (len(rlines) - len(llines)) - rlines += [''] * (len(llines) - len(rlines)) - for left, right in zip(llines, rlines): - yield '%-34s%s' % (left, right) - -class Action(object): - indent = '' - fill = 68 - top_margin = 1 - - def __init__(self, lines): - self.text = ' '.join(line.strip() for line in lines) - - def format(self): - for line in textwrap.wrap(self.text, width=self.fill): - yield self.indent + line - -class Transition(object): - indent = '' - fill = 68 - top_margin = 1 - - def __init__(self, lines): - self.text = ' '.join(line.strip() for line in lines) - - def format(self): - for line in textwrap.wrap(self.text, width=self.fill): - yield self.indent + line - -def is_blank(string): - return string == '' or string.isspace() and string != ' ' - -def is_slug(blanks_before, string): - if blanks_before >= 2: - return True - 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 _create_dialog(line_list) - elif len(line_list) == 1 and line_list[0].endswith(':') and line_list[0].isupper(): - # TODO: need to check whether the *next* paragraph is a slug - # before assuming this is a transition. - return Transition(line_list) - else: - return Action(line_list) - -def clean_line(line): - """Strips leading whitespace and trailing end of line characters in a string. - - Leading whitespace is insignificant in SPMD, and trailing EOL - appear when reading from a file or HTML form. - """ - return line.lstrip().rstrip('\r\n') - -def parse(source): - """Reads raw text input and generates paragraph objects.""" - blank_count = 0 - source = (clean_line(line) for line in source) - for blank, lines in itertools.groupby(source, is_blank): - if blank: - blank_count = len(list(lines)) - else: - yield create_paragraph(blank_count, list(lines)) - -def get_pages(paragraphs): - """Generates one list of lines per page.""" - lines_on_page = [] - for paragraph in paragraphs: - top_margin = paragraph.top_margin if lines_on_page else 0 - para_lines = list(paragraph.format()) - - if len(lines_on_page) + top_margin + len(para_lines) > lines_per_page: - yield lines_on_page - lines_on_page = [] - else: - lines_on_page += [''] * top_margin - lines_on_page += para_lines - yield lines_on_page diff --git a/screenplain/parsers/__init__.py b/screenplain/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/screenplain/parsers/spmd.py b/screenplain/parsers/spmd.py new file mode 100644 index 0000000..3da8864 --- /dev/null +++ b/screenplain/parsers/spmd.py @@ -0,0 +1,66 @@ +import itertools +from screenplain.types import Slug, Action, Dialog, DualDialog, Transition + +slug_prefixes = ( + 'INT. ', + 'EXT. ', + 'INT./EXT. ', + 'INT/EXT. ', + 'INT ', + 'EXT ', + 'INT/EXT ', + 'I/E ', +) + +TWOSPACE = ' ' * 2 + +def is_blank(string): + return string == '' or string.isspace() and string != ' ' + +def is_slug(blanks_before, string): + if blanks_before >= 2: + return True + 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 _create_dialog(line_list) + elif len(line_list) == 1 and line_list[0].endswith(':') and line_list[0].isupper(): + # TODO: need to check whether the *next* paragraph is a slug + # before assuming this is a transition. + return Transition(line_list) + else: + return Action(line_list) + +def clean_line(line): + """Strips leading whitespace and trailing end of line characters in a string. + + Leading whitespace is insignificant in SPMD, and trailing EOL + appear when reading from a file or HTML form. + """ + return line.lstrip().rstrip('\r\n') + +def parse(source): + """Reads raw text input and generates paragraph objects.""" + blank_count = 0 + source = (clean_line(line) for line in source) + for blank, lines in itertools.groupby(source, is_blank): + if blank: + blank_count = len(list(lines)) + else: + yield create_paragraph(blank_count, list(lines)) diff --git a/screenplain/types.py b/screenplain/types.py new file mode 100644 index 0000000..4b12418 --- /dev/null +++ b/screenplain/types.py @@ -0,0 +1,98 @@ +import textwrap + +class Slug(object): + indent = '' + top_margin = 1 + + def __init__(self, lines): + self.lines = [self.indent + line.strip() for line in lines] + + def format(self): + return self.lines + +class Dialog(object): + indent_character = ' ' * 22 + indent_dialog = ' ' * 10 + indent_parenthetical_first = ' ' * 16 + indent_parenthetical_subsequent = ' ' * 17 + + fill_parenthetical = 45 + fill_dialog = 45 + + top_margin = 1 + + def __init__(self, lines): + self.character = lines[0] + self.blocks = [] # list of tuples of (is_parenthetical, text) + self._parse(lines[1:]) + + def _parse(self, lines): + 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 + + for parenthetical, text in self.blocks: + if parenthetical: + lines = textwrap.wrap( + text, + width=self.fill_parenthetical, + initial_indent=self.indent_parenthetical_first, + subsequent_indent=self.indent_parenthetical_subsequent + ) + else: + lines = textwrap.wrap( + text, + width=self.fill_dialog, + initial_indent=self.indent_dialog, + subsequent_indent=self.indent_dialog + ) + for line in lines: + yield line + +class DualDialog(object): + top_margin = 1 + + def __init__(self, left_lines, right_lines): + self.left = Dialog(left_lines) + self.right = Dialog(right_lines) + + def format(self): + # FIXME: I haven't checked yet how dual dialog is supposed to look. + llines = list(self.left.format()) + rlines = list(self.right.format()) + llines += [''] * (len(rlines) - len(llines)) + rlines += [''] * (len(llines) - len(rlines)) + for left, right in zip(llines, rlines): + yield '%-34s%s' % (left, right) + +class Action(object): + indent = '' + fill = 68 + top_margin = 1 + + def __init__(self, lines): + self.text = ' '.join(line.strip() for line in lines) + + def format(self): + for line in textwrap.wrap(self.text, width=self.fill): + yield self.indent + line + +class Transition(object): + indent = '' + fill = 68 + top_margin = 1 + + def __init__(self, lines): + self.text = ' '.join(line.strip() for line in lines) + + def format(self): + for line in textwrap.wrap(self.text, width=self.fill): + yield self.indent + line + diff --git a/tests/parse_test.py b/tests/parse_test.py deleted file mode 100644 index 1eabdb2..0000000 --- a/tests/parse_test.py +++ /dev/null @@ -1,152 +0,0 @@ -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): - - 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]) - - def test_standard_transition(self): - - 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]) - - def test_transition_needs_to_be_upper_case(self): - paras = list(parse.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.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]) - - # Not implemented yet - @unittest2.expectedFailure - def test_transition_must_be_followed_by_slug(self): - paras = list(parse.parse([ - 'Bill lights a cigarette.', - '', - 'CUT TO:', - '', - 'SOME GUY mowing the lawn.', - ])) - self.assertEquals([Action, Action, Action], [type(p) for p in paras]) - -if __name__ == '__main__': - unittest2.main() diff --git a/tests/spmd_test.py b/tests/spmd_test.py new file mode 100644 index 0000000..cb349a1 --- /dev/null +++ b/tests/spmd_test.py @@ -0,0 +1,152 @@ +import unittest2 +from screenplain.parsers.spmd import parse +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 + # least one blank line preceding it. + # NOTE: Actually the list used in Appendix 1 + 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_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_a_slug(self): + types = [type(p) for p in 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([ + '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([ + '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([ + '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, '(starting the engine)'), dialog.blocks[0]) + self.assertEqual((False, 'So much for retirement!'), dialog.blocks[1]) + + 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('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): + + 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_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]) + + # Not implemented yet + @unittest2.expectedFailure + def test_transition_must_be_followed_by_slug(self): + paras = list(parse([ + 'Bill lights a cigarette.', + '', + 'CUT TO:', + '', + 'SOME GUY mowing the lawn.', + ])) + self.assertEquals([Action, Action, Action], [type(p) for p in paras]) + +if __name__ == '__main__': + unittest2.main() -- cgit v1.2.3