Mercurial > jouvence
changeset 2:59fe8cb6190d
Add lots of tests, fix lots of bugs.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Tue, 03 Jan 2017 09:05:28 -0800 |
parents | 74b83e3d921e |
children | 6019eee799bf |
files | fontaine/document.py fontaine/parser.py tests/conftest.py tests/test_action.yaml tests/test_character.yaml tests/test_pagebreak.yaml tests/test_sceneheadings.yaml tests/test_scripts.yaml tests/test_titlepage.yaml |
diffstat | 9 files changed, 196 insertions(+), 98 deletions(-) [+] |
line wrap: on
line diff
--- a/fontaine/document.py Mon Jan 02 21:54:59 2017 -0800 +++ b/fontaine/document.py Tue Jan 03 09:05:28 2017 -0800 @@ -85,6 +85,7 @@ TYPE_TRANSITION = 5 TYPE_LYRICS = 6 TYPE_PAGEBREAK = 7 +TYPE_EMPTYLINES = 8 def _scene_element_type_str(t): @@ -104,6 +105,8 @@ return 'LYRICS' if t == TYPE_PAGEBREAK: return 'PAGEBREAK' + if t == TYPE_EMPTYLINES: + return 'EMPTYLINES' raise NotImplementedError()
--- a/fontaine/parser.py Mon Jan 02 21:54:59 2017 -0800 +++ b/fontaine/parser.py Tue Jan 03 09:05:28 2017 -0800 @@ -1,16 +1,14 @@ import re import logging +from .document import TYPE_ACTION logger = logging.getLogger(__name__) class FontaineState: - can_merge = False - needs_pending_empty_lines = True - def __init__(self): - self.has_pending_empty_line = False + pass def match(self, fp, ctx): return False @@ -18,10 +16,7 @@ def consume(self, fp, ctx): raise NotImplementedError() - def merge(self): - pass - - def exit(self, ctx): + def exit(self, ctx, next_state): pass @@ -42,7 +37,7 @@ 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*:") +RE_TITLE_KEY_VALUE = re.compile(r"^(?P<key>[\w\s\-]+)\s*:\s*") class _TitlePageState(FontaineState): @@ -65,27 +60,27 @@ if m: # Commit current value, start new one. self._commit(ctx) - self._cur_key = m.group('key') - self._cur_val = line[m.end():].strip() + 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.strip() + 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. - self.has_pending_empty_line = True break return ANY_STATE - def exit(self, ctx): + def exit(self, ctx, next_state): self._commit(ctx) def _commit(self, ctx): if self._cur_key is not None: - ctx.document.title_values[self._cur_key] = self._cur_val + val = self._cur_val.rstrip('\r\n') + ctx.document.title_values[self._cur_key] = val self._cur_key = None self._cur_val = None @@ -107,19 +102,13 @@ line = fp.readline().rstrip('\r\n') line = line.lstrip('.') # In case it was forced. ctx.document.addScene(line) - self.has_pending_empty_line = True return ANY_STATE class _ActionState(FontaineState): - can_merge = True - needs_pending_empty_lines = False - def __init__(self): super().__init__() self.text = '' - self._to_merge = None - self._was_merged = False def match(self, fp, ctx): return True @@ -132,6 +121,10 @@ 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 @@ -139,23 +132,19 @@ # the line we just got because it's probably gonna be the # last one. if RE_EMPTY_LINE.match(fp.peekline()): - stripped_line = line.rstrip("\r\n") - self.text += stripped_line - self._to_merge = line[len(stripped_line):] + self.text += line.rstrip("\r\n") break # ...otherwise, add the line with in full. self.text += line return ANY_STATE - def merge(self): - # Put back the stuff we stripped from what we thought was the - # last line. - self.text += self._to_merge - self._was_merged = True - - def exit(self, ctx): - ctx.document.lastScene().addAction(self.text) + 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) @@ -190,7 +179,6 @@ # if we detect a line that doesn't have them, we make this # paragraph be a normal action instead. fp.restore(snapshot) - self.has_pending_empty_line = True self._aborted = True return _ActionState() else: @@ -199,18 +187,17 @@ if RE_EMPTY_LINE.match(fp.peekline()): self.text += clean_line - self.has_pending_empty_line = True break self.text += clean_line + eol return ANY_STATE - def exit(self, ctx): + 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\-]+\s*(\(.*\))?$", re.M) +RE_CHARACTER_LINE = re.compile(r"^\s*[A-Z][A-Z\-\._\s]+\s*(\(.*\))?$", re.M) class _CharacterState(FontaineState): @@ -247,7 +234,6 @@ if not RE_EMPTY_LINE.match(next_line): return _DialogState() - self.has_pending_empty_line = True return ANY_STATE @@ -280,13 +266,12 @@ if RE_EMPTY_LINE.match(next_line): self.text += line.rstrip('\r\n') - self.has_pending_empty_line = True break self.text += line return ANY_STATE - def exit(self, ctx): + def exit(self, ctx, next_state): ctx.document.lastScene().addDialog(self.text.rstrip('\r\n')) @@ -312,19 +297,17 @@ else: logger.debug("Rolling back lyrics into action paragraph.") fp.restore(snapshot) - self.has_pending_empty_line = True self._aborted = True return _ActionState() if RE_EMPTY_LINE.match(fp.peekline()): self.text += line.rstrip('\r\n') - self.has_pending_empty_line = True break self.text += line return ANY_STATE - def exit(self, ctx): + def exit(self, ctx, next_state): if not self._aborted: ctx.document.lastScene().addLyrics(self.text) @@ -345,7 +328,7 @@ line = fp.readline().lstrip().rstrip('\r\n') line = line.lstrip('>') # In case it was forced. ctx.document.lastScene().addTransition(line) - self.has_pending_empty_line = True + return ANY_STATE RE_PAGE_BREAK_LINE = re.compile(r"^\=\=\=+$", re.M) @@ -363,7 +346,6 @@ fp.readline() fp.readline() ctx.document.lastScene().addPageBreak() - self.has_pending_empty_line = True return ANY_STATE @@ -407,6 +389,30 @@ 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, @@ -414,6 +420,7 @@ _TransitionState, _PageBreakState, _CenteredActionState, + _EmptyLineState, # Must be second to last. _ActionState, # Must be last. ] @@ -424,12 +431,13 @@ self._fp = fp self._blankAt0 = False - def readline(self, size=-1): + def readline(self): if self._blankAt0: self._blankAt0 = False + self.line_no = 0 return '\n' - data = self._fp.readline(size) + data = self._fp.readline() self.line_no += 1 return data @@ -466,12 +474,7 @@ raise Exception( "Can't add blank line at 0 if reading has started.") self._blankAt0 = True - - def _read(self, size, advance_line_no): - data = self._fp.read(size) - if advance_line_no: - self.line_no += data.count('\n') - return data + self.line_no = 0 class _FontaineStateMachine: @@ -497,9 +500,6 @@ # 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() - # Make this added empty line "pending" so if the first line - # is an action paragraph, it doesn't include it. - self.state.has_pending_empty_line = True # Start parsing! Here we try to do a mostly-forward-only parser with # non overlapping regexes to make it decently fast. @@ -516,10 +516,12 @@ # Figure out what to do next... if res is None: - raise Exception( + 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.") + "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 @@ -544,36 +546,19 @@ # Handle the current state before we move on to the new one. if self.state: - if type(self.state) == type(res) and self.state.can_merge: - # Don't switch states if the next state is the same - # type and that type supports merging. - self.state.merge() - continue - - self.state.exit(self) - if (self.state.has_pending_empty_line and - not res.needs_pending_empty_lines): - logger.debug("Skipping pending blank line from %s" % - self.state.__class__.__name__) - self.fp.readline() - + 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) - if (self.state.has_pending_empty_line and - not res.needs_pending_empty_lines): - logger.debug("Skipping pending blank line from %s" % - self.state.__class__.__name__) - self.fp.readline() + self.state.exit(self, res) self.state = res elif res is EOF_STATE: # Reached end of file. if self.state: - self.state.exit(self) + self.state.exit(self, res) break else:
--- a/tests/conftest.py Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/conftest.py Tue Jan 03 09:05:28 2017 -0800 @@ -1,3 +1,4 @@ +import re import sys import logging import yaml @@ -6,9 +7,9 @@ FontaineSceneElement, 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 fontaine.renderer import BaseRenderer def pytest_addoption(parser): @@ -153,6 +154,9 @@ p.text) +RE_BLANK_LINE = re.compile(r"^\s+$") + + def make_scenes(spec): if not isinstance(spec, list): raise Exception("Script specs must be lists.") @@ -166,6 +170,11 @@ cur_paras.append(FontaineSceneElement(TYPE_PAGEBREAK, None)) continue + if RE_BLANK_LINE.match(item): + text = len(item) * '\n' + cur_paras.append(FontaineSceneElement(TYPE_EMPTYLINES, text)) + continue + token = item[:1] if token == '.': if cur_header or cur_paras: @@ -193,14 +202,3 @@ if cur_header or cur_paras: out.append([cur_header] + cur_paras) return out - - -class TestRenderer(BaseRenderer): - def write_bold(self, text): - pass - - def write_italics(self, text): - pass - - def write_underline(self, text): - pass
--- a/tests/test_action.yaml Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/test_action.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -14,3 +14,44 @@ in: "!EXT. ACTION. FORCED." out: - "!EXT. ACTION. FORCED." +--- +in: | + JOHN + This is my dialogue. + + JAKE + There were blanks before mine. +out: + - "@JOHN" + - "=This is my dialogue." + - "@JAKE" + - "=There were blanks before mine." +--- +in: | + JOHN + This is my dialogue. + + + JAKE + There were blanks before mine. +out: + - "@JOHN" + - "=This is my dialogue." + - "!" + - "@JAKE" + - "=There were blanks before mine." +--- +in: | + JOHN + This is my dialogue. + + + + JAKE + There were blanks before mine. +out: + - "@JOHN" + - "=This is my dialogue." + - "!\n" + - "@JAKE" + - "=There were blanks before mine."
--- a/tests/test_character.yaml Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/test_character.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -18,3 +18,13 @@ out: - '@McCLANE' - "=Yippie ki-yay!" +--- +in: "DR. BENNET\nHere's your medication." +out: + - '@DR. BENNET' + - "=Here's your medication." +--- +in: "CROWD (VARIOUS)\nWe want free t-shirts!" +out: + - "@CROWD (VARIOUS)" + - "=We want free t-shirts!"
--- a/tests/test_pagebreak.yaml Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/test_pagebreak.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -13,4 +13,4 @@ - "@NARRATOR" - "=A page break is coming." - "<pagebreak>" - - "!There it was." + - "!\nThere it was."
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_sceneheadings.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -0,0 +1,36 @@ +--- +in: | + EXT. ANOTHER PLACE + + This is an action I think. +out: + - ".EXT. ANOTHER PLACE" + - "!\nThis is an action I think." +--- +in: | + An action. + + + EXT. ANOTHER PLACE + + This is an action I think. +out: + - "!An action.\n" + - ".EXT. ANOTHER PLACE" + - "!\nThis is an action I think." +--- +in: | + title: Something for Testing + author: Moi + + + EXT. ANOTHER PLACE + + This is an action I think. +title: + title: Something for Testing + author: Moi +out: + - "!" + - ".EXT. ANOTHER PLACE" + - "!\nThis is an action I think."
--- a/tests/test_scripts.yaml Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/test_scripts.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -19,7 +19,7 @@ - "!><**End of Act One**" - "<pagebreak>" - "!><**Act Two**" - - "!The General Lee hangs in the air, right where we left it." + - "!\nThe General Lee hangs in the air, right where we left it." --- in: | EXT. BRICK'S POOL - DAY @@ -38,7 +38,7 @@ From what seems like only INCHES AWAY. _Steel's face FILLS the *Leupold Mark 4* scope_. out: - ".EXT. BRICK'S POOL - DAY" - - "!Steel, in the middle of a heated phone call:" + - "!\nSteel, in the middle of a heated phone call:" - "@STEEL" - "=They're coming out of the woodwork!" - "_(pause)" @@ -46,4 +46,18 @@ - "_(pause)" - "=Point Blank Sniper?" - ".SNIPER SCOPE POV" - - "!From what seems like only INCHES AWAY. _Steel's face FILLS the *Leupold Mark 4* scope_." + - "!\nFrom what seems like only INCHES AWAY. _Steel's face FILLS the *Leupold Mark 4* scope_." +--- +in: | + Title: Something for Testing + Author: Moi + + EXT. ANOTHER PLACE + + This is an action I think. +title: + title: Something for Testing + author: Moi +out: + - ".EXT. ANOTHER PLACE" + - "!\nThis is an action I think."
--- a/tests/test_titlepage.yaml Mon Jan 02 21:54:59 2017 -0800 +++ b/tests/test_titlepage.yaml Tue Jan 03 09:05:28 2017 -0800 @@ -22,8 +22,8 @@ Title: This simple test Author: Ludovic title: - Title: "This simple test" - Author: "Ludovic" + title: "This simple test" + author: "Ludovic" out: [] --- in: | @@ -31,7 +31,18 @@ It doesn't have much. title: - Title: "This simple test" + title: "This simple test" out: - - "!It doesn't have much." - + - "!\nIt doesn't have much." +--- +in: | + Title: + _**BRICK & STEEL**_ + _**FULL RETIRED**_ + Credit: Written by + Authors: Stu Maschwitz +title: + title: "_**BRICK & STEEL**_\n_**FULL RETIRED**_" + credit: Written by + authors: Stu Maschwitz +out: []