changeset 24:2ef526c301cc 0.3.0

Add support for sections and synopsis.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 05 Jan 2017 09:27:11 -0800
parents 36424a1081ff
children cd284808d139
files README.rst jouvence/console.py jouvence/document.py jouvence/html.py jouvence/parser.py jouvence/renderer.py jouvence/resources/html_header.html jouvence/resources/html_styles.css jouvence/resources/html_styles.scss tests/conftest.py tests/test_scripts.yaml tests/test_synopsis.yaml
diffstat 12 files changed, 177 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/README.rst	Wed Jan 04 23:39:13 2017 -0800
+++ b/README.rst	Thu Jan 05 09:27:11 2017 -0800
@@ -59,7 +59,6 @@
 are not implemented yet:
 
 * Dual dialogue
-* Sections and synopses
 * Proper Unicode support (although Fountain's spec greatly assumes English screenplays, sadly).
 
 
--- a/jouvence/console.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/console.py	Thu Jan 05 09:27:11 2017 -0800
@@ -78,6 +78,14 @@
         print("", file=out)
         _w(out, colorama.Style.DIM, 80 * '=')
 
+    def write_section(self, depth, text, out):
+        print("", file=out)
+        _w(out, colorama.Fore.CYAN, '#' * depth + ' ' + text, True)
+
+    def write_synopsis(self, text, out):
+        print("", file=out)
+        _w(out, colorama.Fore.GREEN, '= ' + text, True)
+
 
 class ConsoleTextRenderer(BaseTextRenderer):
     def _writeStyled(self, style, text):
--- a/jouvence/document.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/document.py	Thu Jan 05 09:27:11 2017 -0800
@@ -59,6 +59,9 @@
     def addPageBreak(self):
         self.paragraphs.append(JouvenceSceneElement(TYPE_PAGEBREAK, None))
 
+    def addSection(self, depth, text):
+        self.paragraphs.append(JouvenceSceneSection(depth, text))
+
     def lastParagraph(self):
         try:
             return self.paragraphs[-1]
@@ -77,6 +80,12 @@
             _ellipsis(self.text, 15))
 
 
+class JouvenceSceneSection(JouvenceSceneElement):
+    def __init__(self, depth, text):
+        super().__init__(TYPE_SECTION, text)
+        self.depth = depth
+
+
 TYPE_ACTION = 0
 TYPE_CENTEREDACTION = 1
 TYPE_CHARACTER = 2
@@ -85,7 +94,8 @@
 TYPE_TRANSITION = 5
 TYPE_LYRICS = 6
 TYPE_PAGEBREAK = 7
-TYPE_EMPTYLINES = 8
+TYPE_SECTION = 8
+TYPE_SYNOPSIS = 9
 
 
 def _scene_element_type_str(t):
@@ -105,8 +115,10 @@
         return 'LYRICS'
     if t == TYPE_PAGEBREAK:
         return 'PAGEBREAK'
-    if t == TYPE_EMPTYLINES:
-        return 'EMPTYLINES'
+    if t == TYPE_SECTION:
+        return 'SECTION'
+    if t == TYPE_SYNOPSIS:
+        return 'SYNOPSIS'
     raise NotImplementedError()
 
 
--- a/jouvence/html.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/html.py	Thu Jan 05 09:27:11 2017 -0800
@@ -103,6 +103,12 @@
     def write_pagebreak(self, out):
         out.write('<hr/>\n')
 
+    def write_section(self, depth, text, out):
+        _elem(out, 'p', 'section', '%s %s' % ('#' * depth, text))
+
+    def write_synopsis(self, text, out):
+        _elem(out, 'p', 'synopsis', text)
+
     def _render_footnotes(self, out):
         for i, n in enumerate(self.text_renderer.notes):
             note_id = i + 1
--- a/jouvence/parser.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/parser.py	Thu Jan 05 09:27:11 2017 -0800
@@ -163,7 +163,7 @@
             ctx.document.lastScene().addAction(self.text)
 
 
-RE_CENTERED_LINE = re.compile(r"^\s*>\s*.*\s*<\s*$", re.M)
+RE_CENTERED_LINE = re.compile(r"^\s*>\s*.*\s*<\s*$")
 
 
 class _CenteredActionState(JouvenceState):
@@ -213,7 +213,7 @@
             ctx.document.lastScene().addCenteredAction(self.text)
 
 
-RE_CHARACTER_LINE = re.compile(r"^\s*[A-Z][A-Z\-\._\s]+\s*(\(.*\))?$", re.M)
+RE_CHARACTER_LINE = re.compile(r"^\s*[A-Z][A-Z\-\._\s]+\s*(\(.*\))?$")
 
 
 class _CharacterState(JouvenceState):
@@ -232,7 +232,7 @@
         return [_ParentheticalState, _DialogState]
 
 
-RE_PARENTHETICAL_LINE = re.compile(r"^\s*\(.*\)\s*$", re.M)
+RE_PARENTHETICAL_LINE = re.compile(r"^\s*\(.*\)\s*$")
 
 
 class _ParentheticalState(JouvenceState):
@@ -328,7 +328,7 @@
             ctx.document.lastScene().addLyrics(self.text)
 
 
-RE_TRANSITION_LINE = re.compile(r"^\s*[^a-z]+TO\:$", re.M)
+RE_TRANSITION_LINE = re.compile(r"^\s*[^a-z]+TO\:$")
 
 
 class _TransitionState(JouvenceState):
@@ -347,7 +347,7 @@
         return ANY_STATE
 
 
-RE_PAGE_BREAK_LINE = re.compile(r"^\=\=\=+$", re.M)
+RE_PAGE_BREAK_LINE = re.compile(r"^\=\=\=+$")
 
 
 class _PageBreakState(JouvenceState):
@@ -405,8 +405,8 @@
         return self._state_cls()
 
 
-RE_BONEYARD_START = re.compile(r"^/\*", re.M)
-RE_BONEYARD_END = re.compile(r"\*/\s*$", re.M)
+RE_BONEYARD_START = re.compile(r"^/\*")
+RE_BONEYARD_END = re.compile(r"\*/\s*$")
 
 
 class _BoneyardState(JouvenceState):
@@ -421,6 +421,45 @@
         return ANY_STATE
 
 
+RE_SECTION_LINE = re.compile(r"^(?P<depth>#+)\s*")
+
+
+class _SectionState(JouvenceState):
+    def match(self, fp, ctx):
+        lines = fp.peeklines(3)
+        return (RE_EMPTY_LINE.match(lines[0]) and
+                RE_SECTION_LINE.match(lines[1]) and
+                RE_EMPTY_LINE.match(lines[2]))
+
+    def consume(self, fp, ctx):
+        fp.readline()
+        line = fp.readline()
+        m = RE_SECTION_LINE.match(line)
+        depth = len(m.group('depth'))
+        line = line[m.end():].rstrip('\r\n')
+        ctx.document.lastScene().addSection(depth, line)
+        return ANY_STATE
+
+
+RE_SYNOPSIS_LINE = re.compile(r"^=[^=]")
+
+
+class _SynopsisState(JouvenceState):
+    def match(self, fp, ctx):
+        lines = fp.peeklines(3)
+        return (RE_EMPTY_LINE.match(lines[0]) and
+                RE_SYNOPSIS_LINE.match(lines[1]) and
+                RE_EMPTY_LINE.match(lines[2]))
+
+    def consume(self, fp, ctx):
+        fp.readline()
+        line = fp.readline()
+        line = line[1:].lstrip()  # Remove the `#`, and the following spaces.
+        line = line.rstrip('\r\n')
+        ctx.document.lastScene().addSynopsis(line)
+        return ANY_STATE
+
+
 class _EmptyLineState(JouvenceState):
     def __init__(self):
         super().__init__()
@@ -457,6 +496,8 @@
     _TransitionState,
     _PageBreakState,
     _CenteredActionState,
+    _SectionState,
+    _SynopsisState,
     _BoneyardState,
     _EmptyLineState,   # Must be second to last.
     _ActionState,  # Must be last.
--- a/jouvence/renderer.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/renderer.py	Thu Jan 05 09:27:11 2017 -0800
@@ -1,7 +1,8 @@
 import re
 from jouvence.document import (
     TYPE_ACTION, TYPE_CENTEREDACTION, TYPE_CHARACTER, TYPE_DIALOG,
-    TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK)
+    TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK,
+    TYPE_SECTION, TYPE_SYNOPSIS)
 
 
 class BaseDocumentRenderer:
@@ -19,6 +20,8 @@
             TYPE_TRANSITION: self.write_transition,
             TYPE_LYRICS: self.write_lyrics,
             TYPE_PAGEBREAK: self.write_pagebreak,
+            TYPE_SECTION: self.write_section,
+            TYPE_SYNOPSIS: self.write_synopsis,
         }
 
     def _tr(self, text):
@@ -45,10 +48,12 @@
             self.write_scene_heading(scene.header, out)
         for p in scene.paragraphs:
             rdr_func = self._para_rdrs[p.type]
-            if p.type != TYPE_PAGEBREAK:
+            if p.type == TYPE_PAGEBREAK:
+                rdr_func(out)
+            elif p.type == TYPE_SECTION:
+                rdr_func(p.depth, self._tr(p.text), out)
+            else:
                 rdr_func(self._tr(p.text), out)
-            else:
-                rdr_func(out)
 
     def write_header(self, doc, out):
         pass
@@ -86,6 +91,12 @@
     def write_pagebreak(self, out):
         raise NotImplementedError()
 
+    def write_section(self, depth, text, out):
+        raise NotImplementedError()
+
+    def write_synopsis(self, text, out):
+        raise NotImplementedError()
+
 
 RE_ITALICS = re.compile(
     r"(?P<before>^|\s)(?P<esc>\\)?\*(?P<text>.*[^\s\*])\*(?=[^a-zA-Z0-9\*]|$)")
--- a/jouvence/resources/html_header.html	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/resources/html_header.html	Thu Jan 05 09:27:11 2017 -0800
@@ -16,16 +16,8 @@
             background-color: #666;
         }
         .jouvence-doc {
-            background-color: #fff;
             box-shadow: #111 0px 0.5em 2em;
         }
-        .jouvence-notes {
-            background-color: #593;
-            color: #fff;
-        }
-        .jouvence-main, .jouvence-notes {
-            padding: 2em;
-        }
         p.footer {
             text-align: center;
             text-transform: uppercase;
--- a/jouvence/resources/html_styles.css	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/resources/html_styles.css	Thu Jan 05 09:27:11 2017 -0800
@@ -1,5 +1,15 @@
 .jouvence-doc {
-  font-family: "Courier Prime", "Courier New", Courier, sans-serif; }
+  font-family: "Courier Prime", "Courier New", Courier, sans-serif;
+  background-color: #fff;
+  color: #111; }
+  .jouvence-doc hr {
+    border: none;
+    height: 1px;
+    background: #aaa; }
+  .jouvence-doc .jouvence-main {
+    padding: 2em 0; }
+    .jouvence-doc .jouvence-main p {
+      padding: 0 2em; }
   .jouvence-doc h1, .jouvence-doc .jouvence-title-page-heading {
     text-align: center; }
   .jouvence-doc .jouvence-scene-heading {
@@ -12,5 +22,17 @@
     margin: 0 4rem 0 4rem; }
   .jouvence-doc .jouvence-action-centered {
     text-align: center; }
+  .jouvence-doc p.jouvence-section {
+    background-color: #ccc;
+    padding: 1rem; }
+  .jouvence-doc p.jouvence-synopsis {
+    background-color: #ddd;
+    padding: 1rem; }
+  .jouvence-doc .jouvence-notes {
+    background-color: #593;
+    color: #fff;
+    padding: 1em 0; }
+    .jouvence-doc .jouvence-notes p {
+      padding: 0 2em; }
 
 /*# sourceMappingURL=html_styles.css.map */
--- a/jouvence/resources/html_styles.scss	Wed Jan 04 23:39:13 2017 -0800
+++ b/jouvence/resources/html_styles.scss	Thu Jan 05 09:27:11 2017 -0800
@@ -1,5 +1,19 @@
 .jouvence-doc {
     font-family: "Courier Prime", "Courier New", Courier, sans-serif;
+    background-color: #fff;
+    color: #111;
+
+    hr {
+        border: none;
+        height: 1px;
+        background: #aaa;
+    }
+
+    .jouvence-main {
+        padding: 2em 0;
+
+        p { padding: 0 2em; }
+    }
 
     h1, .jouvence-title-page-heading {
         text-align: center;
@@ -27,4 +41,22 @@
     .jouvence-action-centered {
         text-align: center;
     }
+
+    p.jouvence-section {
+        background-color: #ccc;
+        padding: 1rem;
+    }
+
+    p.jouvence-synopsis {
+        background-color: #ddd;
+        padding: 1rem;
+    }
+
+    .jouvence-notes {
+        background-color: #593;
+        color: #fff;
+        padding: 1em 0;
+
+        p { padding: 0 2em; }
+    }
 }
--- a/tests/conftest.py	Wed Jan 04 23:39:13 2017 -0800
+++ b/tests/conftest.py	Thu Jan 05 09:27:11 2017 -0800
@@ -4,10 +4,10 @@
 import yaml
 import pytest
 from jouvence.document import (
-    JouvenceSceneElement,
+    JouvenceSceneElement, JouvenceSceneSection,
     TYPE_ACTION, TYPE_CENTEREDACTION, TYPE_CHARACTER, TYPE_DIALOG,
     TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK,
-    TYPE_EMPTYLINES,
+    TYPE_SYNOPSIS,
     _scene_element_type_str)
 from jouvence.parser import JouvenceParser, JouvenceParserError
 
@@ -170,11 +170,6 @@
             cur_paras.append(JouvenceSceneElement(TYPE_PAGEBREAK, None))
             continue
 
-        if RE_BLANK_LINE.match(item):
-            text = len(item) * '\n'
-            cur_paras.append(JouvenceSceneElement(TYPE_EMPTYLINES, text))
-            continue
-
         token = item[:1]
         if token == '.':
             if cur_header or cur_paras:
@@ -197,6 +192,12 @@
             cur_paras.append(_t(item[1:]))
         elif token == '~':
             cur_paras.append(_l(item[1:]))
+        elif token == '#':
+            cur_paras.append(
+                JouvenceSceneSection(1, item[1:]))
+        elif token == '+':
+            cur_paras.append(
+                JouvenceSceneElement(TYPE_SYNOPSIS, item[1:]))
         else:
             raise Exception("Unknown token: %s" % token)
     if cur_header or cur_paras:
--- a/tests/test_scripts.yaml	Wed Jan 04 23:39:13 2017 -0800
+++ b/tests/test_scripts.yaml	Thu Jan 05 09:27:11 2017 -0800
@@ -61,3 +61,20 @@
 out:
     - ".EXT. ANOTHER PLACE"
     - "!\nThis is an action I think."
+---
+in: |
+    # ACT I
+
+    = Set up the characters and the story.
+
+    EXT. BRICK'S PATIO - DAY
+
+    = This scene sets up Brick & Steel's new life as retirees.
+
+    A gorgeous day.  The sun is shining.
+out:
+    - "#ACT I"
+    - "+Set up the characters and the story."
+    - ".EXT. BRICK'S PATIO - DAY"
+    - "+This scene sets up Brick & Steel's new life as retirees."
+    - "!\nA gorgeous day.  The sun is shining."
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_synopsis.yaml	Thu Jan 05 09:27:11 2017 -0800
@@ -0,0 +1,5 @@
+---
+in: |
+    = This is a synopsis.
+out:
+    - "+This is a synopsis."