Mercurial > jouvence
changeset 13:ee741bbe96a8
Rename to 'Jouvence'.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 04 Jan 2017 09:02:29 -0800 |
parents | eea60b93da2c |
children | 1eb3c8ac8940 |
files | README.rst fontaine/__init__.py fontaine/cli.py fontaine/console.py fontaine/document.py fontaine/html.py fontaine/parser.py fontaine/renderer.py fontaine/resources/html_footer.html fontaine/resources/html_header.html fontaine/resources/html_styles.css fontaine/resources/html_styles.scss jouvence/__init__.py jouvence/cli.py jouvence/console.py jouvence/document.py jouvence/html.py jouvence/parser.py jouvence/renderer.py jouvence/resources/html_footer.html jouvence/resources/html_header.html jouvence/resources/html_styles.css jouvence/resources/html_styles.scss jouvence/version.py scripts/fontaine scripts/jouvence setup.py tests/conftest.py tests/test_renderer.py |
diffstat | 29 files changed, 1227 insertions(+), 1223 deletions(-) [+] |
line wrap: on
line diff
--- a/README.rst Wed Jan 04 08:51:32 2017 -0800 +++ b/README.rst Wed Jan 04 09:02:29 2017 -0800 @@ -1,13 +1,13 @@ ######## -FONTAINE +JOUVENCE ######## -`Fountain`_ is a plain text markup language for screenwriting. Fontaine +`Fountain`_ is a plain text markup language for screenwriting. Jouvence is a Python package for parsing and rendering Fountain documents. -Fontaine supports: +Jouvence supports: * Most of the Fountain specification (see limitations below). * Rendering to HTML and terminals. @@ -19,16 +19,16 @@ ============ As with many Python packages, it's recommended that you use `virtualenv`_, -but since Fontaine doesn't have many dependencies, you should be fine. +but since Jouvence doesn't have many dependencies, you should be fine. -You can install Fontaine the usual way:: +You can install Jouvence the usual way:: - pip install fontaine + pip install jouvence If you want to test that it works, you can feed it a Fountain screenplay and see if it prints it nicely in your terminal:: - fontaine <path-to-fountain-file> + jouvence <path-to-fountain-file> You should then see the Fountain file rendered with colored and indented styles. @@ -39,12 +39,12 @@ Usage ===== -The Fontaine API goes pretty much like this:: +The Jouvence API goes pretty much like this:: - from fontaine.parser import FontaineParser - from fontaine.html import HtmlDocumentRenderer + from jouvence.parser import JouvenceParser + from jouvence.html import HtmlDocumentRenderer - parser = FontaineParser() + parser = JouvenceParser() document = parser.parse(path_to_file) renderer = HtmlDocumentRenderer() markup = renderer.render_doc(document) @@ -54,7 +54,7 @@ Limitations =========== -Fontaine doesn't support the complete Fountain syntax yet. The following things +Jouvence doesn't support the complete Fountain syntax yet. The following things are not implemented yet: * Dual dialogue
--- a/fontaine/__init__.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -try: - from .version import version -except ImportError: - version = '<unknown>'
--- a/fontaine/cli.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -import sys -import argparse - - -def main(): - parser = argparse.ArgumentParser( - description='Fontaine command line utility') - parser.add_argument('script') - parser.add_argument('out_file', nargs='?') - args = parser.parse_args() - - from fontaine.parser import FontaineParser - p = FontaineParser() - doc = p.parse(args.script) - - if not args.out_file: - from fontaine.console import ConsoleDocumentRenderer - rdr = ConsoleDocumentRenderer() - rdr.render_doc(doc, sys.stdout) - else: - from fontaine.html import HtmlDocumentRenderer - rdr = HtmlDocumentRenderer() - with open(args.out_file, 'w') as fp: - rdr.render_doc(doc, fp)
--- a/fontaine/console.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,93 +0,0 @@ -import os -import colorama -from .renderer import BaseDocumentRenderer, BaseTextRenderer - - -def _w(out, style, text, reset_all=False): - f = out.write - f(style) - f(text) - if not reset_all: - f(colorama.Style.NORMAL) - else: - f(colorama.Style.RESET_ALL) - f(os.linesep) - - -class ConsoleDocumentRenderer(BaseDocumentRenderer): - def __init__(self, width=80): - super().__init__(ConsoleTextRenderer()) - self.width = width - colorama.init() - - def write_title_page(self, values, out): - known = ['title', 'credit', 'author', 'source'] - center_values = [values.get(i) for i in known - if i is not None] - - print("", file=out) - for val in center_values: - for l in val.split('\n'): - print(l.center(self.width), file=out) - print("", file=out) - print("", file=out) - print("", file=out) - - ddate = values.get('date') or values.get('draft date') - contact = values.get('contact') - bottom_lines = [i for i in [ddate, contact] - if i is not None] - - _w(out, colorama.Style.DIM, '\n\n'.join(bottom_lines)) - print("", file=out) - _w(out, colorama.Style.DIM, 80 * '=') - - def write_scene_heading(self, text, out): - print("", file=out) - _w(out, colorama.Fore.WHITE + colorama.Style.BRIGHT, text, True) - - def write_action(self, text, out): - print(text, file=out) - - def write_centeredaction(self, text, out): - print("", file=out) - for line in text.split('\n'): - print(line.center(self.width), file=out) - - def write_character(self, text, out): - print("", file=out) - _w(out, colorama.Fore.WHITE, "\t\t\t" + text, True) - - def write_dialog(self, text, out): - for line in text.split('\n'): - print("\t" + line, file=out) - - def write_parenthetical(self, text, out): - for line in text.split('\n'): - print("\t\t" + line, file=out) - - def write_transition(self, text, out): - print("", file=out) - print("\t\t\t\t" + text, file=out) - - def write_lyrics(self, text, out): - print("", file=out) - _w(out, colorama.Fore.MAGENTA, text, True) - - def write_pagebreak(self, out): - print("", file=out) - _w(out, colorama.Style.DIM, 80 * '=') - - -class ConsoleTextRenderer(BaseTextRenderer): - def _writeStyled(self, style, text): - return style + text + colorama.Style.NORMAL - - def make_italics(self, text): - return self._writeStyled(colorama.Style.BRIGHT, text) - - def make_bold(self, text): - return self._writeStyled(colorama.Style.BRIGHT, text) - - def make_underline(self, text): - return self._writeStyled(colorama.Style.BRIGHT, text)
--- a/fontaine/document.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -import sys - - -class FontaineDocument: - def __init__(self): - self.title_values = {} - self.scenes = [] - - def addScene(self, header=None): - s = FontaineScene() - if header: - s.header = header - self.scenes.append(s) - return s - - def lastScene(self, auto_create=True): - try: - return self.scenes[-1] - except IndexError: - if auto_create: - s = self.addScene() - return s - return None - - def lastParagraph(self): - s = self.lastScene(False) - if s: - return s.lastParagraph() - return None - - -class FontaineScene: - def __init__(self): - self.header = None - self.paragraphs = [] - self._adders = {} - - def __getattr__(self, name): - if name.startswith('add'): - add_type_name = name[3:] - try: - adder = self._adders[add_type_name] - except KeyError: - module = sys.modules[__name__] - add_type = getattr(module, - 'TYPE_%s' % add_type_name.upper()) - - def _type_adder(_text): - new_p = FontaineSceneElement(add_type, _text) - self.paragraphs.append(new_p) - return new_p - - adder = _type_adder - self._adders[add_type_name] = adder - return adder - else: - raise AttributeError - - def addPageBreak(self): - self.paragraphs.append(FontaineSceneElement(TYPE_PAGEBREAK, None)) - - def lastParagraph(self): - try: - return self.paragraphs[-1] - except IndexError: - return None - - -class FontaineSceneElement: - def __init__(self, el_type, text): - self.type = el_type - self.text = text - - def __str__(self): - return '%s: %s' % ( - _scene_element_type_str(self.type), - _ellipsis(self.text, 15)) - - -TYPE_ACTION = 0 -TYPE_CENTEREDACTION = 1 -TYPE_CHARACTER = 2 -TYPE_DIALOG = 3 -TYPE_PARENTHETICAL = 4 -TYPE_TRANSITION = 5 -TYPE_LYRICS = 6 -TYPE_PAGEBREAK = 7 -TYPE_EMPTYLINES = 8 - - -def _scene_element_type_str(t): - if t == TYPE_ACTION: - return 'ACTION' - if t == TYPE_CENTEREDACTION: - return 'CENTEREDACTION' - if t == TYPE_CHARACTER: - return 'CHARACTER' - if t == TYPE_DIALOG: - return 'DIALOG' - if t == TYPE_PARENTHETICAL: - return 'PARENTHETICAL' - if t == TYPE_TRANSITION: - return 'TRANSITION' - if t == TYPE_LYRICS: - return 'LYRICS' - if t == TYPE_PAGEBREAK: - return 'PAGEBREAK' - if t == TYPE_EMPTYLINES: - return 'EMPTYLINES' - raise NotImplementedError() - - -def _ellipsis(text, length): - if len(text) > length: - return text[:length - 3] + '...' - return text
--- a/fontaine/html.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -import os.path -from markupsafe import escape -from .renderer import BaseDocumentRenderer, BaseTextRenderer - - -def _elem(out, elem_name, class_name, contents): - f = out.write - f('<%s' % elem_name) - if class_name: - f(' class="fontaine-%s"' % class_name) - f('>') - f(contents) - f('</%s>\n' % elem_name) - - -def _br(text, strip_first=False): - lines = text.split('\n') - if strip_first and lines[0].strip() == '': - lines = lines[1:] - return '<br/>\n'.join(lines) - - -def _res(filename): - path = os.path.join(os.path.dirname(__file__), 'resources', filename) - with open(path, 'r') as fp: - return fp.read() - - -class HtmlDocumentRenderer(BaseDocumentRenderer): - def __init__(self, standalone=True): - super().__init__(HtmlTextRenderer()) - self.standalone = standalone - - def get_css(self): - return _res('html_styles.css') - - def write_header(self, doc, out): - if self.standalone: - meta = doc.title_values.get - data = { - # TODO: need a "strip formatting" to have a clean title. - 'title': meta('title', "Fountain Screenplay"), - 'description': meta('description', ''), - 'css': self.get_css() - } - out.write(_res('html_header.html') % data) - out.write('<div class="fontaine-doc">\n') - - def write_footer(self, doc, out): - out.write('</div>\n') - if self.standalone: - out.write(_res('html_footer.html')) - - def write_title_page(self, values, out): - out.write('<div class="fontaine-title-page">\n') - - _elem(out, 'h1', None, _br(values['title'])) - _elem(out, 'p', 'title-page-heading', _br(values['credit'])) - _elem(out, 'p', 'title-page-heading', _br(values['author'])) - - ddate = values.get('date') or values.get('draft date') - if ddate: - _elem(out, 'p', 'title-page-footer', _br(ddate)) - contact = values.get('contact') - if contact: - _elem(out, 'p', 'title-page-footer', _br(contact)) - - out.write('</div>\n') - self.write_pagebreak(out) - - def write_scene_heading(self, text, out): - _elem(out, 'p', 'scene-heading', text) - - def write_action(self, text, out): - _elem(out, 'p', 'action', _br(text, True)) - - def write_centeredaction(self, text, out): - _elem(out, 'p', 'action-centered', _br(text, True)) - - def write_character(self, text, out): - _elem(out, 'p', 'character', text) - - def write_dialog(self, text, out): - _elem(out, 'p', 'dialog', _br(text)) - - def write_parenthetical(self, text, out): - _elem(out, 'p', 'parenthetical', text) - - def write_transition(self, text, out): - _elem(out, 'p', 'transition', text) - - def write_lyrics(self, text, out): - _elem(out, 'p', 'lyrics', _br(text, True)) - - def write_pagebreak(self, out): - out.write('<hr/>\n') - - -class HtmlTextRenderer(BaseTextRenderer): - def render_text(self, text): - return super().render_text(escape(text)) - - def make_italics(self, text): - return '<em>%s</em>' % text - - def make_bold(self, text): - return '<strong>%s</strong>' % text - - def make_underline(self, text): - return '<u>%s</u>' % text
--- a/fontaine/parser.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,589 +0,0 @@ -import re -import logging -from .document import TYPE_ACTION - - -logger = logging.getLogger(__name__) - - -class FontaineState: - def __init__(self): - pass - - def match(self, fp, ctx): - return False - - def consume(self, fp, ctx): - raise NotImplementedError() - - def exit(self, ctx, next_state): - pass - - -class _PassThroughState(FontaineState): - def consume(self, fp, ctx): - return ANY_STATE - - -class FontaineParserError(Exception): - def __init__(self, line_no, message): - super().__init__("Error line %d: %s" % (line_no, message)) - - -ANY_STATE = object() -EOF_STATE = object() - - -RE_EMPTY_LINE = re.compile(r"^$", re.M) -RE_BLANK_LINE = re.compile(r"^\s*$", re.M) - -RE_TITLE_KEY_VALUE = re.compile(r"^(?P<key>[\w\s\-]+)\s*:\s*") - - -class _TitlePageState(FontaineState): - def __init__(self): - super().__init__() - self._cur_key = None - self._cur_val = None - - def match(self, fp, ctx): - line = fp.peekline() - return RE_TITLE_KEY_VALUE.match(line) - - def consume(self, fp, ctx): - while True: - line = fp.readline() - if not line: - return EOF_STATE - - m = RE_TITLE_KEY_VALUE.match(line) - if m: - # Commit current value, start new one. - self._commit(ctx) - self._cur_key = m.group('key').lower() - self._cur_val = line[m.end():] - else: - # Keep accumulating the value of one of the title page's - # values. - self._cur_val += line.lstrip() - - if RE_EMPTY_LINE.match(fp.peekline()): - self._commit(ctx) - # Finished with the page title, now move on to the first scene. - break - - return ANY_STATE - - def exit(self, ctx, next_state): - self._commit(ctx) - - def _commit(self, ctx): - if self._cur_key is not None: - val = self._cur_val.rstrip('\r\n') - ctx.document.title_values[self._cur_key] = val - self._cur_key = None - self._cur_val = None - - -RE_SCENE_HEADER_PATTERN = re.compile( - r"^(int|ext|est|int/ext|int./ext|i/e)[\s\.]", re.I) - - -class _SceneHeaderState(FontaineState): - def match(self, fp, ctx): - lines = fp.peeklines(3) - return ( - RE_EMPTY_LINE.match(lines[0]) and - RE_SCENE_HEADER_PATTERN.match(lines[1]) and - RE_EMPTY_LINE.match(lines[2])) - - def consume(self, fp, ctx): - fp.readline() # Get past the blank line. - line = fp.readline().rstrip('\r\n') - line = line.lstrip('.') # In case it was forced. - ctx.document.addScene(line) - return ANY_STATE - - -class _ActionState(FontaineState): - def __init__(self): - super().__init__() - self.text = '' - - def match(self, fp, ctx): - return True - - def consume(self, fp, ctx): - is_first_line = True - while True: - line = fp.readline() - if not line: - return EOF_STATE - - if is_first_line: - # Ignore the fake blank line at 0 if it's threre. - if fp.line_no == 0: - continue - - line = line.lstrip('!') # In case it was forced. - is_first_line = False - - # If the next line is empty, strip the carriage return from - # the line we just got because it's probably gonna be the - # last one. - if RE_EMPTY_LINE.match(fp.peekline()): - self.text += line.rstrip("\r\n") - break - # ...otherwise, add the line with in full. - self.text += line - - return ANY_STATE - - def exit(self, ctx, next_state): - last_para = ctx.document.lastParagraph() - if last_para and last_para.type == TYPE_ACTION: - last_para.text += '\n' + self.text - else: - ctx.document.lastScene().addAction(self.text) - - -RE_CENTERED_LINE = re.compile(r"^\s*>\s*.*\s*<\s*$", re.M) - - -class _CenteredActionState(FontaineState): - def __init__(self): - super().__init__() - self.text = '' - self._aborted = False - - def match(self, fp, ctx): - lines = fp.peeklines(2) - return ( - RE_EMPTY_LINE.match(lines[0]) and - RE_CENTERED_LINE.match(lines[1])) - - def consume(self, fp, ctx): - snapshot = fp.snapshot() - fp.readline() # Get past the empty line. - while True: - line = fp.readline() - if not line: - return EOF_STATE - - clean_line = line.rstrip('\r\n') - eol = line[len(clean_line):] - - clean_line = clean_line.strip() - if clean_line[0] != '>' or clean_line[-1] != '<': - # The whole paragraph must have `>` and `<` wrappers, so - # if we detect a line that doesn't have them, we make this - # paragraph be a normal action instead. - fp.restore(snapshot) - self._aborted = True - return _ActionState() - else: - # Remove wrapping `>`/`<`, and spaces. - clean_line = clean_line[1:-1].strip() - - if RE_EMPTY_LINE.match(fp.peekline()): - self.text += clean_line - break - self.text += clean_line + eol - - return ANY_STATE - - def exit(self, ctx, next_state): - if not self._aborted: - ctx.document.lastScene().addCenteredAction(self.text) - - -RE_CHARACTER_LINE = re.compile(r"^\s*[A-Z][A-Z\-\._\s]+\s*(\(.*\))?$", re.M) - - -class _CharacterState(FontaineState): - def match(self, fp, ctx): - lines = fp.peeklines(3) - return (RE_EMPTY_LINE.match(lines[0]) and - RE_CHARACTER_LINE.match(lines[1]) and - not RE_EMPTY_LINE.match(lines[2])) - - def consume(self, fp, ctx): - fp.readline() # Get past the empty line. - line = fp.readline().rstrip('\r\n') - line = line.lstrip() # Remove indenting. - line = line.lstrip('@') # In case it was forced. - ctx.document.lastScene().addCharacter(line) - return [_ParentheticalState, _DialogState] - - -RE_PARENTHETICAL_LINE = re.compile(r"^\s*\(.*\)\s*$", re.M) - - -class _ParentheticalState(FontaineState): - def match(self, fp, ctx): - # We only get here from a `_CharacterState` so we know the previous - # one is already that. - line = fp.peekline() - return RE_PARENTHETICAL_LINE.match(line) - - def consume(self, fp, ctx): - line = fp.readline().lstrip().rstrip('\r\n') - ctx.document.lastScene().addParenthetical(line) - - next_line = fp.peekline() - if not RE_EMPTY_LINE.match(next_line): - return _DialogState() - - return ANY_STATE - - -class _DialogState(FontaineState): - def __init__(self): - super().__init__() - self.text = '' - - def match(self, fp, ctx): - # We only get here from a `_CharacterState` or `_ParentheticalState` - # so we just need to check there's some text. - line = fp.peekline() - return not RE_EMPTY_LINE.match(line) - - def consume(self, fp, ctx): - while True: - line = fp.readline() - if not line: - return EOF_STATE - - line = line.lstrip() # Remove indenting. - - # Next we could be either continuing the dialog line, going to - # a parenthetical, or exiting dialog altogether. - next_line = fp.peekline() - - if RE_PARENTHETICAL_LINE.match(next_line): - self.text += line.rstrip('\r\n') - return _ParentheticalState() - - if RE_EMPTY_LINE.match(next_line): - self.text += line.rstrip('\r\n') - break - self.text += line - - return ANY_STATE - - def exit(self, ctx, next_state): - ctx.document.lastScene().addDialog(self.text.rstrip('\r\n')) - - -class _LyricsState(FontaineState): - def __init__(self): - super().__init__() - self.text = '' - self._aborted = False - - # No `match` method, this can only be forced. - # (see `_ForcedParagraphStates`) - - def consume(self, fp, ctx): - snapshot = fp.snapshot() - fp.readline() # Get past the empty line. - while True: - line = fp.readline() - if not line: - return EOF_STATE - - if line.startswith('~'): - line = line.lstrip('~') - else: - logger.debug("Rolling back lyrics into action paragraph.") - fp.restore(snapshot) - self._aborted = True - return _ActionState() - - if RE_EMPTY_LINE.match(fp.peekline()): - self.text += line.rstrip('\r\n') - break - self.text += line - - return ANY_STATE - - def exit(self, ctx, next_state): - if not self._aborted: - ctx.document.lastScene().addLyrics(self.text) - - -RE_TRANSITION_LINE = re.compile(r"^\s*[^a-z]+TO\:$", re.M) - - -class _TransitionState(FontaineState): - def match(self, fp, ctx): - lines = fp.peeklines(3) - return ( - RE_EMPTY_LINE.match(lines[0]) and - RE_TRANSITION_LINE.match(lines[1]) and - RE_EMPTY_LINE.match(lines[2])) - - def consume(self, fp, ctx): - fp.readline() # Get past the empty line. - line = fp.readline().lstrip().rstrip('\r\n') - line = line.lstrip('>') # In case it was forced. - ctx.document.lastScene().addTransition(line) - return ANY_STATE - - -RE_PAGE_BREAK_LINE = re.compile(r"^\=\=\=+$", re.M) - - -class _PageBreakState(FontaineState): - def match(self, fp, ctx): - lines = fp.peeklines(3) - return ( - RE_EMPTY_LINE.match(lines[0]) and - RE_PAGE_BREAK_LINE.match(lines[1]) and - RE_EMPTY_LINE.match(lines[2])) - - def consume(self, fp, ctx): - fp.readline() - fp.readline() - ctx.document.lastScene().addPageBreak() - return ANY_STATE - - -class _ForcedParagraphStates(FontaineState): - STATE_SYMBOLS = { - '.': _SceneHeaderState, - '!': _ActionState, - '@': _CharacterState, - '~': _LyricsState, - '>': _TransitionState - } - - def __init__(self): - super().__init__() - self._state_cls = None - self._consume_empty_line = False - - def match(self, fp, ctx): - lines = fp.peeklines(2) - symbol = lines[1][:1] - if (RE_EMPTY_LINE.match(lines[0]) and - symbol in self.STATE_SYMBOLS): - # Special case: don't force a transition state if it's - # really some centered text. - if symbol == '>' and RE_CENTERED_LINE.match(lines[1]): - return False - - self._state_cls = self.STATE_SYMBOLS[symbol] - - # Special case: for forced action paragraphs, don't leave - # the blank line there. - if symbol == '!': - self._consume_empty_line = True - - return True - return False - - def consume(self, fp, ctx): - if self._consume_empty_line: - fp.readline() - return self._state_cls() - - -class _EmptyLineState(FontaineState): - def __init__(self): - super().__init__() - self.line_count = 0 - - def match(self, fp, ctx): - return RE_EMPTY_LINE.match(fp.peekline()) - - def consume(self, fp, ctx): - fp.readline() - if fp.line_no > 1: # Don't take into account the fake blank at 0 - self.line_count += 1 - return ANY_STATE - - def exit(self, ctx, next_state): - if self.line_count > 0: - text = self.line_count * '\n' - last_para = ctx.document.lastParagraph() - if last_para and last_para.type == TYPE_ACTION: - last_para.text += text - else: - ctx.document.lastScene().addAction(text[1:]) - - -ROOT_STATES = [ - _ForcedParagraphStates, # Must be first. - _SceneHeaderState, - _CharacterState, - _TransitionState, - _PageBreakState, - _CenteredActionState, - _EmptyLineState, # Must be second to last. - _ActionState, # Must be last. -] - - -class _PeekableFile: - def __init__(self, fp): - self.line_no = 1 - self._fp = fp - self._blankAt0 = False - - def readline(self): - if self._blankAt0: - self._blankAt0 = False - self.line_no = 0 - return '\n' - - data = self._fp.readline() - self.line_no += 1 - return data - - def peekline(self): - if self._blankAt0: - return '\n' - - pos = self._fp.tell() - line = self._fp.readline() - self._fp.seek(pos) - return line - - def peeklines(self, count): - pos = self._fp.tell() - lines = [] - if self._blankAt0: - lines.append('\n') - count -= 1 - for i in range(count): - lines.append(self._fp.readline()) - self._fp.seek(pos) - return lines - - def snapshot(self): - return (self._fp.tell(), self._blankAt0, self.line_no) - - def restore(self, snapshot): - self._fp.seek(snapshot[0]) - self._blankAt0 = snapshot[1] - self.line_no = snapshot[2] - - def _addBlankAt0(self): - if self._fp.tell() != 0: - raise Exception( - "Can't add blank line at 0 if reading has started.") - self._blankAt0 = True - self.line_no = 0 - - -class _FontaineStateMachine: - def __init__(self, fp, doc): - self.fp = _PeekableFile(fp) - self.state = None - self.document = doc - - @property - def line_no(self): - return self.fp.line_no - - def run(self): - # Start with the page title... unless it doesn't match, in which - # case we start with a "pass through" state that will just return - # `ANY_STATE` so we can start matching stuff. - self.state = _TitlePageState() - if not self.state.match(self.fp, self): - logger.debug("No title page value found on line 1, " - "using pass-through state with added blank line.") - self.state = _PassThroughState() - if not RE_EMPTY_LINE.match(self.fp.peekline()): - # Add a fake empty line at the beginning of the text if - # there's not one already. This makes state matching easier. - self.fp._addBlankAt0() - - # Start parsing! Here we try to do a mostly-forward-only parser with - # non overlapping regexes to make it decently fast. - while True: - logger.debug("State '%s' consuming from '%s'..." % - (self.state.__class__.__name__, self.fp.peekline())) - res = self.state.consume(self.fp, self) - - # See if we reached the end of the file. - if not self.fp.peekline(): - logger.debug("Reached end of line... ending parsing.") - res = EOF_STATE - - # Figure out what to do next... - - if res is None: - raise FontaineParserError( - self.line_no, - "State '%s' returned a `None` result. " - "States need to return `ANY_STATE`, one or more specific " - "states, or `EOF_STATE` if they reached the end of the " - "file." % self.state.__class__.__name__) - - elif res is ANY_STATE or isinstance(res, list): - # State wants to exit, we need to figure out what is the - # next state. - pos = self.fp._fp.tell() - next_states = res - if next_states is ANY_STATE: - next_states = ROOT_STATES - logger.debug("Trying to match next state from: %s" % - [t.__name__ for t in next_states]) - for sc in next_states: - s = sc() - if s.match(self.fp, self): - logger.debug("Matched state %s" % - s.__class__.__name__) - self.fp._fp.seek(pos) - res = s - break - else: - raise Exception("Can't match following state after: %s" % - self.state) - - # Handle the current state before we move on to the new one. - if self.state: - self.state.exit(self, res) - self.state = res - - elif isinstance(res, FontaineState): - # State wants to exit, wants a specific state to be next. - if self.state: - self.state.exit(self, res) - self.state = res - - elif res is EOF_STATE: - # Reached end of file. - if self.state: - self.state.exit(self, res) - break - - else: - raise Exception("Unsupported state result: %s" % res) - - -class FontaineParser: - def __init__(self): - pass - - def parse(self, filein): - if isinstance(filein, str): - with open(filein, 'r') as fp: - return self._doParse(fp) - else: - return self._doParse(fp) - - def parseString(self, text): - import io - with io.StringIO(text) as fp: - return self._doParse(fp) - - def _doParse(self, fp): - from .document import FontaineDocument - doc = FontaineDocument() - machine = _FontaineStateMachine(fp, doc) - machine.run() - return doc
--- a/fontaine/renderer.py Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,146 +0,0 @@ -import re -from fontaine.document import ( - TYPE_ACTION, TYPE_CENTEREDACTION, TYPE_CHARACTER, TYPE_DIALOG, - TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK) - - -class BaseDocumentRenderer: - def __init__(self, text_renderer=None): - self.text_renderer = text_renderer - if not text_renderer: - self.text_renderer = NullTextRenderer() - - self._para_rdrs = { - TYPE_ACTION: self.write_action, - TYPE_CENTEREDACTION: self.write_centeredaction, - TYPE_CHARACTER: self.write_character, - TYPE_DIALOG: self.write_dialog, - TYPE_PARENTHETICAL: self.write_parenthetical, - TYPE_TRANSITION: self.write_transition, - TYPE_LYRICS: self.write_lyrics, - TYPE_PAGEBREAK: self.write_pagebreak, - } - - def _tr(self, text): - return self.text_renderer.render_text(text) - - def render_doc(self, doc, out): - self.write_header(doc, out) - self.render_title_page(doc.title_values, out) - for s in doc.scenes: - self.render_scene(s, out) - self.write_footer(doc, out) - - def render_title_page(self, values, out): - clean_values = values.copy() - clean_values.setdefault('title', 'Untitled Screenplay') - clean_values.setdefault('credit', 'Written by') - clean_values.setdefault('author', 'Unknown') - for k in clean_values: - clean_values[k] = self._tr(clean_values[k]) - self.write_title_page(clean_values, out) - - def render_scene(self, scene, out): - if scene.header is not None: - self.write_scene_heading(scene.header, out) - for p in scene.paragraphs: - rdr_func = self._para_rdrs[p.type] - if p.type != TYPE_PAGEBREAK: - rdr_func(self._tr(p.text), out) - else: - rdr_func(out) - - def write_header(self, doc, out): - pass - - def write_footer(self, doc, out): - pass - - def write_title_page(self, values, out): - raise NotImplementedError() - - def write_scene_heading(self, text, out): - raise NotImplementedError() - - def write_action(self, text, out): - raise NotImplementedError() - - def write_centeredaction(self, text, out): - raise NotImplementedError() - - def write_character(self, text, out): - raise NotImplementedError() - - def write_dialog(self, text, out): - raise NotImplementedError() - - def write_parenthetical(self, text, out): - raise NotImplementedError() - - def write_transition(self, text, out): - raise NotImplementedError() - - def write_lyrics(self, text, out): - raise NotImplementedError() - - def write_pagebreak(self, out): - raise NotImplementedError() - - -RE_ITALICS = re.compile( - r"(?P<before>^|\s)(?P<esc>\\)?\*(?P<text>.*[^\s\*])\*(?=[^a-zA-Z0-9\*]|$)") -RE_BOLD = re.compile( - r"(?P<before>^|\s)(?P<esc>\\)?\*\*(?P<text>.*[^\s])\*\*(?=[^a-zA-Z0-9]|$)") -RE_UNDERLINE = re.compile( - r"(?P<before>^|\s)(?P<esc>\\)?_(?P<text>.*[^\s])_(?=[^a-zA-Z0-9]|$)") - - -class BaseTextRenderer: - def render_text(self, text): - # Replace bold stuff to catch double asterisks. - text = RE_BOLD.sub(self._do_make_bold, text) - text = RE_ITALICS.sub(self._do_make_italics, text) - text = RE_UNDERLINE.sub(self._do_make_underline, text) - - return text - - def _do_make_italics(self, m): - if m.group('esc'): - return m.group('before') + '*' + m.group('text') + '*' - return ( - m.group('before') + - self.make_italics(m.group('text'))) - - def _do_make_bold(self, m): - if m.group('esc'): - return m.group('before') + '**' + m.group('text') + '**' - return ( - m.group('before') + - self.make_bold(m.group('text'))) - - def _do_make_underline(self, m): - if m.group('esc'): - return m.group('before') + '_' + m.group('text') + '_' - return ( - m.group('before') + - self.make_underline(m.group('text'))) - - def make_italics(self, text): - raise NotImplementedError() - - def make_bold(self, text): - raise NotImplementedError() - - def make_underline(self, text): - raise NotImplementedError() - - -class NullTextRenderer(BaseTextRenderer): - def make_bold(self, text): - return text - - def make_italics(self, text): - return text - - def make_underline(self, text): - return text
--- a/fontaine/resources/html_footer.html Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ - <p class="footer">Generated with <a href="https://bolt80.com/fontaine">Fontaine</a></p> - </body> -</html>
--- a/fontaine/resources/html_header.html Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -<!doctype html> -<html> - <head> - <meta charset="utf-8"> - <meta http-equiv="x-ua-compatible" content="ie=edge"> - <title>%(title)s</title> - <meta name="description" content="%(description)s"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - - <link rel="apple-touch-icon" href="apple-touch-icon.png"> - <!-- Place favicon.ico in the root directory --> - - <style> - body { - margin: 1em 3em; - background-color: #666; - } - .fontaine-doc { - background-color: #fff; - padding: 2em; - box-shadow: #111 0px 0.5em 2em; - } - p.footer { - text-align: center; - text-transform: uppercase; - font-size: 0.9em; - font-family: "Times New Roman", "Times", serif; - color: #333; - padding: 1em; - } - p.footer a:link, p.footer a:visited, p.footer a:active { - color: #222; - } - p.footer a:hover { - color: #227; - } - </style> - <style>%(css)s</style> - </head> - <body>
--- a/fontaine/resources/html_styles.css Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -.fontaine-doc { - font-family: "Courier Prime", "Courier New", Courier, sans-serif; } - .fontaine-doc h1, .fontaine-doc .fontaine-title-page-heading { - text-align: center; } - .fontaine-doc .fontaine-scene-heading { - font-weight: bold; } - .fontaine-doc .fontaine-character { - margin: auto 12rem 0 12rem; } - .fontaine-doc .fontaine-parenthetical { - margin: 0 8rem 0 8rem; } - .fontaine-doc .fontaine-dialog { - margin: 0 4rem 0 4rem; } - .fontaine-doc .fontaine-action-centered { - text-align: center; } - -/*# sourceMappingURL=html_styles.css.map */
--- a/fontaine/resources/html_styles.scss Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +0,0 @@ -.fontaine-doc { - font-family: "Courier Prime", "Courier New", Courier, sans-serif; - - h1, .fontaine-title-page-heading { - text-align: center; - } - - .fontaine-scene-heading { - font-weight: bold; - } - - .fontaine-transition { - } - - .fontaine-character { - margin: auto 3 * 4rem 0 3 * 4rem; - } - - .fontaine-parenthetical { - margin: 0 2 * 4rem 0 2 * 4rem; - } - - .fontaine-dialog { - margin: 0 4rem 0 4rem; - } - - .fontaine-action-centered { - text-align: center; - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/__init__.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,4 @@ +try: + from .version import version +except ImportError: + version = '<unknown>'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/cli.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,24 @@ +import sys +import argparse + + +def main(): + parser = argparse.ArgumentParser( + description='Jouvence command line utility') + parser.add_argument('script') + parser.add_argument('out_file', nargs='?') + args = parser.parse_args() + + from jouvence.parser import JouvenceParser + p = JouvenceParser() + doc = p.parse(args.script) + + if not args.out_file: + from jouvence.console import ConsoleDocumentRenderer + rdr = ConsoleDocumentRenderer() + rdr.render_doc(doc, sys.stdout) + else: + from jouvence.html import HtmlDocumentRenderer + rdr = HtmlDocumentRenderer() + with open(args.out_file, 'w') as fp: + rdr.render_doc(doc, fp)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/console.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,93 @@ +import os +import colorama +from .renderer import BaseDocumentRenderer, BaseTextRenderer + + +def _w(out, style, text, reset_all=False): + f = out.write + f(style) + f(text) + if not reset_all: + f(colorama.Style.NORMAL) + else: + f(colorama.Style.RESET_ALL) + f(os.linesep) + + +class ConsoleDocumentRenderer(BaseDocumentRenderer): + def __init__(self, width=80): + super().__init__(ConsoleTextRenderer()) + self.width = width + colorama.init() + + def write_title_page(self, values, out): + known = ['title', 'credit', 'author', 'source'] + center_values = [values.get(i) for i in known + if i is not None] + + print("", file=out) + for val in center_values: + for l in val.split('\n'): + print(l.center(self.width), file=out) + print("", file=out) + print("", file=out) + print("", file=out) + + ddate = values.get('date') or values.get('draft date') + contact = values.get('contact') + bottom_lines = [i for i in [ddate, contact] + if i is not None] + + _w(out, colorama.Style.DIM, '\n\n'.join(bottom_lines)) + print("", file=out) + _w(out, colorama.Style.DIM, 80 * '=') + + def write_scene_heading(self, text, out): + print("", file=out) + _w(out, colorama.Fore.WHITE + colorama.Style.BRIGHT, text, True) + + def write_action(self, text, out): + print(text, file=out) + + def write_centeredaction(self, text, out): + print("", file=out) + for line in text.split('\n'): + print(line.center(self.width), file=out) + + def write_character(self, text, out): + print("", file=out) + _w(out, colorama.Fore.WHITE, "\t\t\t" + text, True) + + def write_dialog(self, text, out): + for line in text.split('\n'): + print("\t" + line, file=out) + + def write_parenthetical(self, text, out): + for line in text.split('\n'): + print("\t\t" + line, file=out) + + def write_transition(self, text, out): + print("", file=out) + print("\t\t\t\t" + text, file=out) + + def write_lyrics(self, text, out): + print("", file=out) + _w(out, colorama.Fore.MAGENTA, text, True) + + def write_pagebreak(self, out): + print("", file=out) + _w(out, colorama.Style.DIM, 80 * '=') + + +class ConsoleTextRenderer(BaseTextRenderer): + def _writeStyled(self, style, text): + return style + text + colorama.Style.NORMAL + + def make_italics(self, text): + return self._writeStyled(colorama.Style.BRIGHT, text) + + def make_bold(self, text): + return self._writeStyled(colorama.Style.BRIGHT, text) + + def make_underline(self, text): + return self._writeStyled(colorama.Style.BRIGHT, text)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/document.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,116 @@ +import sys + + +class JouvenceDocument: + def __init__(self): + self.title_values = {} + self.scenes = [] + + def addScene(self, header=None): + s = JouvenceScene() + if header: + s.header = header + self.scenes.append(s) + return s + + def lastScene(self, auto_create=True): + try: + return self.scenes[-1] + except IndexError: + if auto_create: + s = self.addScene() + return s + return None + + def lastParagraph(self): + s = self.lastScene(False) + if s: + return s.lastParagraph() + return None + + +class JouvenceScene: + def __init__(self): + self.header = None + self.paragraphs = [] + self._adders = {} + + def __getattr__(self, name): + if name.startswith('add'): + add_type_name = name[3:] + try: + adder = self._adders[add_type_name] + except KeyError: + module = sys.modules[__name__] + add_type = getattr(module, + 'TYPE_%s' % add_type_name.upper()) + + def _type_adder(_text): + new_p = JouvenceSceneElement(add_type, _text) + self.paragraphs.append(new_p) + return new_p + + adder = _type_adder + self._adders[add_type_name] = adder + return adder + else: + raise AttributeError + + def addPageBreak(self): + self.paragraphs.append(JouvenceSceneElement(TYPE_PAGEBREAK, None)) + + def lastParagraph(self): + try: + return self.paragraphs[-1] + except IndexError: + return None + + +class JouvenceSceneElement: + def __init__(self, el_type, text): + self.type = el_type + self.text = text + + def __str__(self): + return '%s: %s' % ( + _scene_element_type_str(self.type), + _ellipsis(self.text, 15)) + + +TYPE_ACTION = 0 +TYPE_CENTEREDACTION = 1 +TYPE_CHARACTER = 2 +TYPE_DIALOG = 3 +TYPE_PARENTHETICAL = 4 +TYPE_TRANSITION = 5 +TYPE_LYRICS = 6 +TYPE_PAGEBREAK = 7 +TYPE_EMPTYLINES = 8 + + +def _scene_element_type_str(t): + if t == TYPE_ACTION: + return 'ACTION' + if t == TYPE_CENTEREDACTION: + return 'CENTEREDACTION' + if t == TYPE_CHARACTER: + return 'CHARACTER' + if t == TYPE_DIALOG: + return 'DIALOG' + if t == TYPE_PARENTHETICAL: + return 'PARENTHETICAL' + if t == TYPE_TRANSITION: + return 'TRANSITION' + if t == TYPE_LYRICS: + return 'LYRICS' + if t == TYPE_PAGEBREAK: + return 'PAGEBREAK' + if t == TYPE_EMPTYLINES: + return 'EMPTYLINES' + raise NotImplementedError() + + +def _ellipsis(text, length): + if len(text) > length: + return text[:length - 3] + '...' + return text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/html.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,110 @@ +import os.path +from markupsafe import escape +from .renderer import BaseDocumentRenderer, BaseTextRenderer + + +def _elem(out, elem_name, class_name, contents): + f = out.write + f('<%s' % elem_name) + if class_name: + f(' class="jouvence-%s"' % class_name) + f('>') + f(contents) + f('</%s>\n' % elem_name) + + +def _br(text, strip_first=False): + lines = text.split('\n') + if strip_first and lines[0].strip() == '': + lines = lines[1:] + return '<br/>\n'.join(lines) + + +def _res(filename): + path = os.path.join(os.path.dirname(__file__), 'resources', filename) + with open(path, 'r') as fp: + return fp.read() + + +class HtmlDocumentRenderer(BaseDocumentRenderer): + def __init__(self, standalone=True): + super().__init__(HtmlTextRenderer()) + self.standalone = standalone + + def get_css(self): + return _res('html_styles.css') + + def write_header(self, doc, out): + if self.standalone: + meta = doc.title_values.get + data = { + # TODO: need a "strip formatting" to have a clean title. + 'title': meta('title', "Fountain Screenplay"), + 'description': meta('description', ''), + 'css': self.get_css() + } + out.write(_res('html_header.html') % data) + out.write('<div class="jouvence-doc">\n') + + def write_footer(self, doc, out): + out.write('</div>\n') + if self.standalone: + out.write(_res('html_footer.html')) + + def write_title_page(self, values, out): + out.write('<div class="jouvence-title-page">\n') + + _elem(out, 'h1', None, _br(values['title'])) + _elem(out, 'p', 'title-page-heading', _br(values['credit'])) + _elem(out, 'p', 'title-page-heading', _br(values['author'])) + + ddate = values.get('date') or values.get('draft date') + if ddate: + _elem(out, 'p', 'title-page-footer', _br(ddate)) + contact = values.get('contact') + if contact: + _elem(out, 'p', 'title-page-footer', _br(contact)) + + out.write('</div>\n') + self.write_pagebreak(out) + + def write_scene_heading(self, text, out): + _elem(out, 'p', 'scene-heading', text) + + def write_action(self, text, out): + _elem(out, 'p', 'action', _br(text, True)) + + def write_centeredaction(self, text, out): + _elem(out, 'p', 'action-centered', _br(text, True)) + + def write_character(self, text, out): + _elem(out, 'p', 'character', text) + + def write_dialog(self, text, out): + _elem(out, 'p', 'dialog', _br(text)) + + def write_parenthetical(self, text, out): + _elem(out, 'p', 'parenthetical', text) + + def write_transition(self, text, out): + _elem(out, 'p', 'transition', text) + + def write_lyrics(self, text, out): + _elem(out, 'p', 'lyrics', _br(text, True)) + + def write_pagebreak(self, out): + out.write('<hr/>\n') + + +class HtmlTextRenderer(BaseTextRenderer): + def render_text(self, text): + return super().render_text(escape(text)) + + def make_italics(self, text): + return '<em>%s</em>' % text + + def make_bold(self, text): + return '<strong>%s</strong>' % text + + def make_underline(self, text): + return '<u>%s</u>' % text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/parser.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,589 @@ +import re +import logging +from .document import TYPE_ACTION + + +logger = logging.getLogger(__name__) + + +class JouvenceState: + def __init__(self): + pass + + def match(self, fp, ctx): + return False + + def consume(self, fp, ctx): + raise NotImplementedError() + + def exit(self, ctx, next_state): + pass + + +class _PassThroughState(JouvenceState): + def consume(self, fp, ctx): + return ANY_STATE + + +class JouvenceParserError(Exception): + def __init__(self, line_no, message): + super().__init__("Error line %d: %s" % (line_no, message)) + + +ANY_STATE = object() +EOF_STATE = object() + + +RE_EMPTY_LINE = re.compile(r"^$", re.M) +RE_BLANK_LINE = re.compile(r"^\s*$", re.M) + +RE_TITLE_KEY_VALUE = re.compile(r"^(?P<key>[\w\s\-]+)\s*:\s*") + + +class _TitlePageState(JouvenceState): + def __init__(self): + super().__init__() + self._cur_key = None + self._cur_val = None + + def match(self, fp, ctx): + line = fp.peekline() + return RE_TITLE_KEY_VALUE.match(line) + + def consume(self, fp, ctx): + while True: + line = fp.readline() + if not line: + return EOF_STATE + + m = RE_TITLE_KEY_VALUE.match(line) + if m: + # Commit current value, start new one. + self._commit(ctx) + self._cur_key = m.group('key').lower() + self._cur_val = line[m.end():] + else: + # Keep accumulating the value of one of the title page's + # values. + self._cur_val += line.lstrip() + + if RE_EMPTY_LINE.match(fp.peekline()): + self._commit(ctx) + # Finished with the page title, now move on to the first scene. + break + + return ANY_STATE + + def exit(self, ctx, next_state): + self._commit(ctx) + + def _commit(self, ctx): + if self._cur_key is not None: + val = self._cur_val.rstrip('\r\n') + ctx.document.title_values[self._cur_key] = val + self._cur_key = None + self._cur_val = None + + +RE_SCENE_HEADER_PATTERN = re.compile( + r"^(int|ext|est|int/ext|int./ext|i/e)[\s\.]", re.I) + + +class _SceneHeaderState(JouvenceState): + def match(self, fp, ctx): + lines = fp.peeklines(3) + return ( + RE_EMPTY_LINE.match(lines[0]) and + RE_SCENE_HEADER_PATTERN.match(lines[1]) and + RE_EMPTY_LINE.match(lines[2])) + + def consume(self, fp, ctx): + fp.readline() # Get past the blank line. + line = fp.readline().rstrip('\r\n') + line = line.lstrip('.') # In case it was forced. + ctx.document.addScene(line) + return ANY_STATE + + +class _ActionState(JouvenceState): + def __init__(self): + super().__init__() + self.text = '' + + def match(self, fp, ctx): + return True + + def consume(self, fp, ctx): + is_first_line = True + while True: + line = fp.readline() + if not line: + return EOF_STATE + + if is_first_line: + # Ignore the fake blank line at 0 if it's threre. + if fp.line_no == 0: + continue + + line = line.lstrip('!') # In case it was forced. + is_first_line = False + + # If the next line is empty, strip the carriage return from + # the line we just got because it's probably gonna be the + # last one. + if RE_EMPTY_LINE.match(fp.peekline()): + self.text += line.rstrip("\r\n") + break + # ...otherwise, add the line with in full. + self.text += line + + return ANY_STATE + + def exit(self, ctx, next_state): + last_para = ctx.document.lastParagraph() + if last_para and last_para.type == TYPE_ACTION: + last_para.text += '\n' + self.text + else: + ctx.document.lastScene().addAction(self.text) + + +RE_CENTERED_LINE = re.compile(r"^\s*>\s*.*\s*<\s*$", re.M) + + +class _CenteredActionState(JouvenceState): + def __init__(self): + super().__init__() + self.text = '' + self._aborted = False + + def match(self, fp, ctx): + lines = fp.peeklines(2) + return ( + RE_EMPTY_LINE.match(lines[0]) and + RE_CENTERED_LINE.match(lines[1])) + + def consume(self, fp, ctx): + snapshot = fp.snapshot() + fp.readline() # Get past the empty line. + while True: + line = fp.readline() + if not line: + return EOF_STATE + + clean_line = line.rstrip('\r\n') + eol = line[len(clean_line):] + + clean_line = clean_line.strip() + if clean_line[0] != '>' or clean_line[-1] != '<': + # The whole paragraph must have `>` and `<` wrappers, so + # if we detect a line that doesn't have them, we make this + # paragraph be a normal action instead. + fp.restore(snapshot) + self._aborted = True + return _ActionState() + else: + # Remove wrapping `>`/`<`, and spaces. + clean_line = clean_line[1:-1].strip() + + if RE_EMPTY_LINE.match(fp.peekline()): + self.text += clean_line + break + self.text += clean_line + eol + + return ANY_STATE + + def exit(self, ctx, next_state): + if not self._aborted: + ctx.document.lastScene().addCenteredAction(self.text) + + +RE_CHARACTER_LINE = re.compile(r"^\s*[A-Z][A-Z\-\._\s]+\s*(\(.*\))?$", re.M) + + +class _CharacterState(JouvenceState): + def match(self, fp, ctx): + lines = fp.peeklines(3) + return (RE_EMPTY_LINE.match(lines[0]) and + RE_CHARACTER_LINE.match(lines[1]) and + not RE_EMPTY_LINE.match(lines[2])) + + def consume(self, fp, ctx): + fp.readline() # Get past the empty line. + line = fp.readline().rstrip('\r\n') + line = line.lstrip() # Remove indenting. + line = line.lstrip('@') # In case it was forced. + ctx.document.lastScene().addCharacter(line) + return [_ParentheticalState, _DialogState] + + +RE_PARENTHETICAL_LINE = re.compile(r"^\s*\(.*\)\s*$", re.M) + + +class _ParentheticalState(JouvenceState): + def match(self, fp, ctx): + # We only get here from a `_CharacterState` so we know the previous + # one is already that. + line = fp.peekline() + return RE_PARENTHETICAL_LINE.match(line) + + def consume(self, fp, ctx): + line = fp.readline().lstrip().rstrip('\r\n') + ctx.document.lastScene().addParenthetical(line) + + next_line = fp.peekline() + if not RE_EMPTY_LINE.match(next_line): + return _DialogState() + + return ANY_STATE + + +class _DialogState(JouvenceState): + def __init__(self): + super().__init__() + self.text = '' + + def match(self, fp, ctx): + # We only get here from a `_CharacterState` or `_ParentheticalState` + # so we just need to check there's some text. + line = fp.peekline() + return not RE_EMPTY_LINE.match(line) + + def consume(self, fp, ctx): + while True: + line = fp.readline() + if not line: + return EOF_STATE + + line = line.lstrip() # Remove indenting. + + # Next we could be either continuing the dialog line, going to + # a parenthetical, or exiting dialog altogether. + next_line = fp.peekline() + + if RE_PARENTHETICAL_LINE.match(next_line): + self.text += line.rstrip('\r\n') + return _ParentheticalState() + + if RE_EMPTY_LINE.match(next_line): + self.text += line.rstrip('\r\n') + break + self.text += line + + return ANY_STATE + + def exit(self, ctx, next_state): + ctx.document.lastScene().addDialog(self.text.rstrip('\r\n')) + + +class _LyricsState(JouvenceState): + def __init__(self): + super().__init__() + self.text = '' + self._aborted = False + + # No `match` method, this can only be forced. + # (see `_ForcedParagraphStates`) + + def consume(self, fp, ctx): + snapshot = fp.snapshot() + fp.readline() # Get past the empty line. + while True: + line = fp.readline() + if not line: + return EOF_STATE + + if line.startswith('~'): + line = line.lstrip('~') + else: + logger.debug("Rolling back lyrics into action paragraph.") + fp.restore(snapshot) + self._aborted = True + return _ActionState() + + if RE_EMPTY_LINE.match(fp.peekline()): + self.text += line.rstrip('\r\n') + break + self.text += line + + return ANY_STATE + + def exit(self, ctx, next_state): + if not self._aborted: + ctx.document.lastScene().addLyrics(self.text) + + +RE_TRANSITION_LINE = re.compile(r"^\s*[^a-z]+TO\:$", re.M) + + +class _TransitionState(JouvenceState): + def match(self, fp, ctx): + lines = fp.peeklines(3) + return ( + RE_EMPTY_LINE.match(lines[0]) and + RE_TRANSITION_LINE.match(lines[1]) and + RE_EMPTY_LINE.match(lines[2])) + + def consume(self, fp, ctx): + fp.readline() # Get past the empty line. + line = fp.readline().lstrip().rstrip('\r\n') + line = line.lstrip('>') # In case it was forced. + ctx.document.lastScene().addTransition(line) + return ANY_STATE + + +RE_PAGE_BREAK_LINE = re.compile(r"^\=\=\=+$", re.M) + + +class _PageBreakState(JouvenceState): + def match(self, fp, ctx): + lines = fp.peeklines(3) + return ( + RE_EMPTY_LINE.match(lines[0]) and + RE_PAGE_BREAK_LINE.match(lines[1]) and + RE_EMPTY_LINE.match(lines[2])) + + def consume(self, fp, ctx): + fp.readline() + fp.readline() + ctx.document.lastScene().addPageBreak() + return ANY_STATE + + +class _ForcedParagraphStates(JouvenceState): + STATE_SYMBOLS = { + '.': _SceneHeaderState, + '!': _ActionState, + '@': _CharacterState, + '~': _LyricsState, + '>': _TransitionState + } + + def __init__(self): + super().__init__() + self._state_cls = None + self._consume_empty_line = False + + def match(self, fp, ctx): + lines = fp.peeklines(2) + symbol = lines[1][:1] + if (RE_EMPTY_LINE.match(lines[0]) and + symbol in self.STATE_SYMBOLS): + # Special case: don't force a transition state if it's + # really some centered text. + if symbol == '>' and RE_CENTERED_LINE.match(lines[1]): + return False + + self._state_cls = self.STATE_SYMBOLS[symbol] + + # Special case: for forced action paragraphs, don't leave + # the blank line there. + if symbol == '!': + self._consume_empty_line = True + + return True + return False + + def consume(self, fp, ctx): + if self._consume_empty_line: + fp.readline() + return self._state_cls() + + +class _EmptyLineState(JouvenceState): + def __init__(self): + super().__init__() + self.line_count = 0 + + def match(self, fp, ctx): + return RE_EMPTY_LINE.match(fp.peekline()) + + def consume(self, fp, ctx): + fp.readline() + if fp.line_no > 1: # Don't take into account the fake blank at 0 + self.line_count += 1 + return ANY_STATE + + def exit(self, ctx, next_state): + if self.line_count > 0: + text = self.line_count * '\n' + last_para = ctx.document.lastParagraph() + if last_para and last_para.type == TYPE_ACTION: + last_para.text += text + else: + ctx.document.lastScene().addAction(text[1:]) + + +ROOT_STATES = [ + _ForcedParagraphStates, # Must be first. + _SceneHeaderState, + _CharacterState, + _TransitionState, + _PageBreakState, + _CenteredActionState, + _EmptyLineState, # Must be second to last. + _ActionState, # Must be last. +] + + +class _PeekableFile: + def __init__(self, fp): + self.line_no = 1 + self._fp = fp + self._blankAt0 = False + + def readline(self): + if self._blankAt0: + self._blankAt0 = False + self.line_no = 0 + return '\n' + + data = self._fp.readline() + self.line_no += 1 + return data + + def peekline(self): + if self._blankAt0: + return '\n' + + pos = self._fp.tell() + line = self._fp.readline() + self._fp.seek(pos) + return line + + def peeklines(self, count): + pos = self._fp.tell() + lines = [] + if self._blankAt0: + lines.append('\n') + count -= 1 + for i in range(count): + lines.append(self._fp.readline()) + self._fp.seek(pos) + return lines + + def snapshot(self): + return (self._fp.tell(), self._blankAt0, self.line_no) + + def restore(self, snapshot): + self._fp.seek(snapshot[0]) + self._blankAt0 = snapshot[1] + self.line_no = snapshot[2] + + def _addBlankAt0(self): + if self._fp.tell() != 0: + raise Exception( + "Can't add blank line at 0 if reading has started.") + self._blankAt0 = True + self.line_no = 0 + + +class _JouvenceStateMachine: + def __init__(self, fp, doc): + self.fp = _PeekableFile(fp) + self.state = None + self.document = doc + + @property + def line_no(self): + return self.fp.line_no + + def run(self): + # Start with the page title... unless it doesn't match, in which + # case we start with a "pass through" state that will just return + # `ANY_STATE` so we can start matching stuff. + self.state = _TitlePageState() + if not self.state.match(self.fp, self): + logger.debug("No title page value found on line 1, " + "using pass-through state with added blank line.") + self.state = _PassThroughState() + if not RE_EMPTY_LINE.match(self.fp.peekline()): + # Add a fake empty line at the beginning of the text if + # there's not one already. This makes state matching easier. + self.fp._addBlankAt0() + + # Start parsing! Here we try to do a mostly-forward-only parser with + # non overlapping regexes to make it decently fast. + while True: + logger.debug("State '%s' consuming from '%s'..." % + (self.state.__class__.__name__, self.fp.peekline())) + res = self.state.consume(self.fp, self) + + # See if we reached the end of the file. + if not self.fp.peekline(): + logger.debug("Reached end of line... ending parsing.") + res = EOF_STATE + + # Figure out what to do next... + + if res is None: + raise JouvenceParserError( + self.line_no, + "State '%s' returned a `None` result. " + "States need to return `ANY_STATE`, one or more specific " + "states, or `EOF_STATE` if they reached the end of the " + "file." % self.state.__class__.__name__) + + elif res is ANY_STATE or isinstance(res, list): + # State wants to exit, we need to figure out what is the + # next state. + pos = self.fp._fp.tell() + next_states = res + if next_states is ANY_STATE: + next_states = ROOT_STATES + logger.debug("Trying to match next state from: %s" % + [t.__name__ for t in next_states]) + for sc in next_states: + s = sc() + if s.match(self.fp, self): + logger.debug("Matched state %s" % + s.__class__.__name__) + self.fp._fp.seek(pos) + res = s + break + else: + raise Exception("Can't match following state after: %s" % + self.state) + + # Handle the current state before we move on to the new one. + if self.state: + self.state.exit(self, res) + self.state = res + + elif isinstance(res, JouvenceState): + # State wants to exit, wants a specific state to be next. + if self.state: + self.state.exit(self, res) + self.state = res + + elif res is EOF_STATE: + # Reached end of file. + if self.state: + self.state.exit(self, res) + break + + else: + raise Exception("Unsupported state result: %s" % res) + + +class JouvenceParser: + def __init__(self): + pass + + def parse(self, filein): + if isinstance(filein, str): + with open(filein, 'r') as fp: + return self._doParse(fp) + else: + return self._doParse(fp) + + def parseString(self, text): + import io + with io.StringIO(text) as fp: + return self._doParse(fp) + + def _doParse(self, fp): + from .document import JouvenceDocument + doc = JouvenceDocument() + machine = _JouvenceStateMachine(fp, doc) + machine.run() + return doc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/renderer.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,146 @@ +import re +from jouvence.document import ( + TYPE_ACTION, TYPE_CENTEREDACTION, TYPE_CHARACTER, TYPE_DIALOG, + TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK) + + +class BaseDocumentRenderer: + def __init__(self, text_renderer=None): + self.text_renderer = text_renderer + if not text_renderer: + self.text_renderer = NullTextRenderer() + + self._para_rdrs = { + TYPE_ACTION: self.write_action, + TYPE_CENTEREDACTION: self.write_centeredaction, + TYPE_CHARACTER: self.write_character, + TYPE_DIALOG: self.write_dialog, + TYPE_PARENTHETICAL: self.write_parenthetical, + TYPE_TRANSITION: self.write_transition, + TYPE_LYRICS: self.write_lyrics, + TYPE_PAGEBREAK: self.write_pagebreak, + } + + def _tr(self, text): + return self.text_renderer.render_text(text) + + def render_doc(self, doc, out): + self.write_header(doc, out) + self.render_title_page(doc.title_values, out) + for s in doc.scenes: + self.render_scene(s, out) + self.write_footer(doc, out) + + def render_title_page(self, values, out): + clean_values = values.copy() + clean_values.setdefault('title', 'Untitled Screenplay') + clean_values.setdefault('credit', 'Written by') + clean_values.setdefault('author', 'Unknown') + for k in clean_values: + clean_values[k] = self._tr(clean_values[k]) + self.write_title_page(clean_values, out) + + def render_scene(self, scene, out): + if scene.header is not None: + self.write_scene_heading(scene.header, out) + for p in scene.paragraphs: + rdr_func = self._para_rdrs[p.type] + if p.type != TYPE_PAGEBREAK: + rdr_func(self._tr(p.text), out) + else: + rdr_func(out) + + def write_header(self, doc, out): + pass + + def write_footer(self, doc, out): + pass + + def write_title_page(self, values, out): + raise NotImplementedError() + + def write_scene_heading(self, text, out): + raise NotImplementedError() + + def write_action(self, text, out): + raise NotImplementedError() + + def write_centeredaction(self, text, out): + raise NotImplementedError() + + def write_character(self, text, out): + raise NotImplementedError() + + def write_dialog(self, text, out): + raise NotImplementedError() + + def write_parenthetical(self, text, out): + raise NotImplementedError() + + def write_transition(self, text, out): + raise NotImplementedError() + + def write_lyrics(self, text, out): + raise NotImplementedError() + + def write_pagebreak(self, out): + raise NotImplementedError() + + +RE_ITALICS = re.compile( + r"(?P<before>^|\s)(?P<esc>\\)?\*(?P<text>.*[^\s\*])\*(?=[^a-zA-Z0-9\*]|$)") +RE_BOLD = re.compile( + r"(?P<before>^|\s)(?P<esc>\\)?\*\*(?P<text>.*[^\s])\*\*(?=[^a-zA-Z0-9]|$)") +RE_UNDERLINE = re.compile( + r"(?P<before>^|\s)(?P<esc>\\)?_(?P<text>.*[^\s])_(?=[^a-zA-Z0-9]|$)") + + +class BaseTextRenderer: + def render_text(self, text): + # Replace bold stuff to catch double asterisks. + text = RE_BOLD.sub(self._do_make_bold, text) + text = RE_ITALICS.sub(self._do_make_italics, text) + text = RE_UNDERLINE.sub(self._do_make_underline, text) + + return text + + def _do_make_italics(self, m): + if m.group('esc'): + return m.group('before') + '*' + m.group('text') + '*' + return ( + m.group('before') + + self.make_italics(m.group('text'))) + + def _do_make_bold(self, m): + if m.group('esc'): + return m.group('before') + '**' + m.group('text') + '**' + return ( + m.group('before') + + self.make_bold(m.group('text'))) + + def _do_make_underline(self, m): + if m.group('esc'): + return m.group('before') + '_' + m.group('text') + '_' + return ( + m.group('before') + + self.make_underline(m.group('text'))) + + def make_italics(self, text): + raise NotImplementedError() + + def make_bold(self, text): + raise NotImplementedError() + + def make_underline(self, text): + raise NotImplementedError() + + +class NullTextRenderer(BaseTextRenderer): + def make_bold(self, text): + return text + + def make_italics(self, text): + return text + + def make_underline(self, text): + return text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/resources/html_footer.html Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,3 @@ + <p class="footer">Generated with <a href="https://bolt80.com/jouvence">Jouvence</a></p> + </body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/resources/html_header.html Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,40 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="x-ua-compatible" content="ie=edge"> + <title>%(title)s</title> + <meta name="description" content="%(description)s"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <link rel="apple-touch-icon" href="apple-touch-icon.png"> + <!-- Place favicon.ico in the root directory --> + + <style> + body { + margin: 1em 3em; + background-color: #666; + } + .jouvence-doc { + background-color: #fff; + padding: 2em; + box-shadow: #111 0px 0.5em 2em; + } + p.footer { + text-align: center; + text-transform: uppercase; + font-size: 0.9em; + font-family: "Times New Roman", "Times", serif; + color: #333; + padding: 1em; + } + p.footer a:link, p.footer a:visited, p.footer a:active { + color: #222; + } + p.footer a:hover { + color: #227; + } + </style> + <style>%(css)s</style> + </head> + <body>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/resources/html_styles.css Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,16 @@ +.jouvence-doc { + font-family: "Courier Prime", "Courier New", Courier, sans-serif; } + .jouvence-doc h1, .jouvence-doc .jouvence-title-page-heading { + text-align: center; } + .jouvence-doc .jouvence-scene-heading { + font-weight: bold; } + .jouvence-doc .jouvence-character { + margin: auto 12rem 0 12rem; } + .jouvence-doc .jouvence-parenthetical { + margin: 0 8rem 0 8rem; } + .jouvence-doc .jouvence-dialog { + margin: 0 4rem 0 4rem; } + .jouvence-doc .jouvence-action-centered { + text-align: center; } + +/*# sourceMappingURL=html_styles.css.map */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/resources/html_styles.scss Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,30 @@ +.jouvence-doc { + font-family: "Courier Prime", "Courier New", Courier, sans-serif; + + h1, .jouvence-title-page-heading { + text-align: center; + } + + .jouvence-scene-heading { + font-weight: bold; + } + + .jouvence-transition { + } + + .jouvence-character { + margin: auto 3 * 4rem 0 3 * 4rem; + } + + .jouvence-parenthetical { + margin: 0 2 * 4rem 0 2 * 4rem; + } + + .jouvence-dialog { + margin: 0 4rem 0 4rem; + } + + .jouvence-action-centered { + text-align: center; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jouvence/version.py Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,4 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '0.1.0'
--- a/scripts/fontaine Wed Jan 04 08:51:32 2017 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -#!/usr/bin/env python -import os.path -import sys - - -sys.path.append(os.path.dirname(os.path.dirname(__file__))) - - -if __name__ == '__main__': - from fontaine.cli import main - main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/jouvence Wed Jan 04 09:02:29 2017 -0800 @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import os.path +import sys + + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + + +if __name__ == '__main__': + from jouvence.cli import main + main()
--- a/setup.py Wed Jan 04 08:51:32 2017 -0800 +++ b/setup.py Wed Jan 04 09:02:29 2017 -0800 @@ -20,14 +20,14 @@ setup( - name="Fontaine", - use_scm_version={'write_to': 'fontaine/version.py'}, + name="Jouvence", + use_scm_version={'write_to': 'jouvence/version.py'}, description="A library for parsing and rendering Fountain screenplays.", long_description=long_description, author="Ludovic Chabant", author_email="ludovic@chabant.com", license="Apache License 2.0", - url="https://bolt80.com/fontaine", + url="https://bolt80.com/jouvence", keywords='fountain screenplay screenwriting screenwriter', packages=find_packages(), include_package_data=True, @@ -37,6 +37,6 @@ install_requires=install_requires, entry_points={ 'console_scripts': [ - 'fontaine = fontaine.cli.main' + 'jouvence = jouvence.cli.main' ]} )
--- a/tests/conftest.py Wed Jan 04 08:51:32 2017 -0800 +++ b/tests/conftest.py Wed Jan 04 09:02:29 2017 -0800 @@ -3,32 +3,32 @@ import logging import yaml import pytest -from fontaine.document import ( - FontaineSceneElement, +from jouvence.document import ( + JouvenceSceneElement, TYPE_ACTION, TYPE_CENTEREDACTION, TYPE_CHARACTER, TYPE_DIALOG, TYPE_PARENTHETICAL, TYPE_TRANSITION, TYPE_LYRICS, TYPE_PAGEBREAK, TYPE_EMPTYLINES, _scene_element_type_str) -from fontaine.parser import FontaineParser, FontaineParserError +from jouvence.parser import JouvenceParser, JouvenceParserError def pytest_addoption(parser): parser.addoption( '--log-debug', action='store_true', - help="Sets the Fontaine logger to output debug info to stdout.") + help="Sets the Jouvence logger to output debug info to stdout.") def pytest_configure(config): if config.getoption('--log-debug'): hdl = logging.StreamHandler(stream=sys.stdout) - logging.getLogger('fontaine').addHandler(hdl) - logging.getLogger('fontaine').setLevel(logging.DEBUG) + logging.getLogger('jouvence').addHandler(hdl) + logging.getLogger('jouvence').setLevel(logging.DEBUG) def pytest_collect_file(parent, path): if path.ext == '.yaml' and path.basename.startswith("test"): - return FontaineScriptTestFile(path, parent) + return JouvenceScriptTestFile(path, parent) return None @@ -48,11 +48,11 @@ def assert_paragraph(actual, expected): if isinstance(expected, str): - assert isinstance(actual, FontaineSceneElement) + assert isinstance(actual, JouvenceSceneElement) assert actual.type == TYPE_ACTION assert actual.text == expected - elif isinstance(expected, FontaineSceneElement): - assert isinstance(actual, FontaineSceneElement) + elif isinstance(expected, JouvenceSceneElement): + assert isinstance(actual, JouvenceSceneElement) assert actual.type == expected.type assert actual.text == expected.text else: @@ -60,23 +60,23 @@ def _c(name): - return FontaineSceneElement(TYPE_CHARACTER, name) + return JouvenceSceneElement(TYPE_CHARACTER, name) def _p(text): - return FontaineSceneElement(TYPE_PARENTHETICAL, text) + return JouvenceSceneElement(TYPE_PARENTHETICAL, text) def _d(text): - return FontaineSceneElement(TYPE_DIALOG, text) + return JouvenceSceneElement(TYPE_DIALOG, text) def _t(text): - return FontaineSceneElement(TYPE_TRANSITION, text) + return JouvenceSceneElement(TYPE_TRANSITION, text) def _l(text): - return FontaineSceneElement(TYPE_LYRICS, text) + return JouvenceSceneElement(TYPE_LYRICS, text) class UnexpectedScriptOutput(Exception): @@ -85,23 +85,23 @@ self.expected = expected -class FontaineScriptTestFile(pytest.File): +class JouvenceScriptTestFile(pytest.File): def collect(self): spec = yaml.load_all(self.fspath.open(encoding='utf8')) for i, item in enumerate(spec): name = '%s_%d' % (self.fspath.basename, i) if 'test_name' in item: name += '_%s' % item['test_name'] - yield FontaineScriptTestItem(name, self, item) + yield JouvenceScriptTestItem(name, self, item) -class FontaineScriptTestItem(pytest.Item): +class JouvenceScriptTestItem(pytest.Item): def __init__(self, name, parent, spec): super().__init__(name, parent) self.spec = spec def reportinfo(self): - return self.fspath, 0, "fontaine script test: %s" % self.name + return self.fspath, 0, "jouvence script test: %s" % self.name def runtest(self): intext = self.spec.get('in') @@ -110,7 +110,7 @@ if intext is None or expected is None: raise Exception("No 'in' or 'out' specified.") - parser = FontaineParser() + parser = JouvenceParser() doc = parser.parseString(intext) if title is not None: assert title == doc.title_values @@ -122,7 +122,7 @@ raise UnexpectedScriptOutput(doc.scenes, exp_scenes) def repr_failure(self, excinfo): - if isinstance(excinfo.value, FontaineParserError): + if isinstance(excinfo.value, JouvenceParserError): return ('\n'.join( ['Parser error:', str(excinfo.value)])) if isinstance(excinfo.value, UnexpectedScriptOutput): @@ -167,12 +167,12 @@ for item in spec: if item == '<pagebreak>': - cur_paras.append(FontaineSceneElement(TYPE_PAGEBREAK, None)) + cur_paras.append(JouvenceSceneElement(TYPE_PAGEBREAK, None)) continue if RE_BLANK_LINE.match(item): text = len(item) * '\n' - cur_paras.append(FontaineSceneElement(TYPE_EMPTYLINES, text)) + cur_paras.append(JouvenceSceneElement(TYPE_EMPTYLINES, text)) continue token = item[:1] @@ -184,7 +184,7 @@ elif token == '!': if item[1:3] == '><': cur_paras.append( - FontaineSceneElement(TYPE_CENTEREDACTION, item[3:])) + JouvenceSceneElement(TYPE_CENTEREDACTION, item[3:])) else: cur_paras.append(item[1:]) elif token == '@':