diff options
Diffstat (limited to 'ast2json/renpy/parser.py')
-rw-r--r-- | ast2json/renpy/parser.py | 1842 |
1 files changed, 1842 insertions, 0 deletions
diff --git a/ast2json/renpy/parser.py b/ast2json/renpy/parser.py new file mode 100644 index 0000000..c0d9164 --- /dev/null +++ b/ast2json/renpy/parser.py @@ -0,0 +1,1842 @@ +# Copyright 2004-2010 PyTom <pytom@bishoujo.us> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This module contains the parser for the Ren'Py script language. It's +# called when parsing is necessary, and creats an AST from the script. + +import codecs +import re +import os +import os.path + +import renpy +import renpy.ast as ast + +# A list of parse error messages. +parse_errors = [ ] + +class ParseError(Exception): + + def __init__(self, filename, number, msg, line=None, pos=None, first=False): + message = "On line %d of %s: %s" % (number, unicode_filename(filename), msg) + + if line: + lines = line.split('\n') + + if len(lines) > 1: + open_string = None + i = 0 + + while i < len(lines[0]): + c = lines[0][i] + + if c == "\\": + i += 1 + elif c == open_string: + open_string = None + elif open_string: + pass + elif c == '`' or c == '\'' or c == '"': + open_string = c + + i += 1 + + if open_string: + message += "\n(Perhaps you left out a %s at the end of the first line.)" % open_string + + for l in lines: + message += "\n" + l + + if pos is not None: + if pos <= len(l): + message += "\n" + " " * pos + "^" + pos = None + else: + pos -= len(l) + + if first: + break + + self.message = message + + Exception.__init__(self, message) + + def __unicode__(self): + return self.message + +# Something to hold the expected line number. +class LineNumberHolder(object): + """ + Holds the expected line number. + """ + + def __init__(self): + self.line = 0 + +def unicode_filename(fn): + """ + Converts the supplied filename to unicode. + """ + + if isinstance(fn, str): + return fn + + # Windows. + try: + return fn.decode("mbcs") + except: + pass + + # Mac and (sane) Unix + try: + return fn.decode("utf-8") + except: + pass + + # Insane systems, mojibake. + return fn.decode("latin-1") + +# Matches either a word, or something else. Most magic is taken care of +# before this. +lllword = re.compile(r'__(\w+)|\w+| +|.', re.S) + +def munge_filename(fn): + # The prefix that's used when __ is found in the file. + rv = os.path.basename(fn) + rv = os.path.splitext(rv)[0] + rv = rv.replace(" ", "_") + + def munge_char(m): + return hex(ord(m.group(0))) + + rv = re.sub(r'[^a-zA-Z0-9_]', munge_char, rv) + + return "_m1_" + rv + "__" + +def list_logical_lines(filename): + """ + This reads the specified filename, and divides it into logical + line. The return value of this function is a list of (filename, + line number, line text) triples. + """ + + f = codecs.open(filename, "r", "utf-8") + data = f.read() + f.close() + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + + if "RENPY_PATH_ELIDE" in os.environ: + old, new = os.environ["RENPY_PATH_ELIDE"].split(':') + filename = filename.replace(old, new) + + prefix = munge_filename(filename) + + # Add some newlines, to fix lousy editors. + data += "\n\n" + + # The result. + rv = [] + + # The line number in the physical file. + number = 1 + + # The current position we're looking at in the buffer. + pos = 0 + + # Skip the BOM, if any. + if len(data) and data[0] == '\ufeff': + pos += 1 + + # Looping over the lines in the file. + while pos < len(data): + + # The line number of the start of this logical line. + start_number = number + + # The line that we're building up. + line = "" + + # The number of open parenthesis there are right now. + parendepth = 0 + + # Looping over the characters in a single logical line. + while pos < len(data): + + c = data[pos] + + if c == '\t': + raise Exception("%s contains a tab character on line %d. Tab characters are not allowed in Ren'Py scripts." % (filename, number)) + + if c == '\n': + number += 1 + + if c == '\n' and not parendepth: + # If not blank... + if not re.match("^\s*$", line): + + # Add to the results. + rv.append((filename, start_number, line)) + + pos += 1 + # This helps out error checking. + line = "" + break + + # Backslash/newline. + if c == "\\" and data[pos+1] == "\n": + pos += 2 + number += 1 + line += "\\\n" + continue + + # Parenthesis. + if c in ('(', '[', '{'): + parendepth += 1 + + if c in ('}', ']', ')') and parendepth: + parendepth -= 1 + + # Comments. + if c == '#': + while data[pos] != '\n': + pos += 1 + + continue + + # Strings. + if c in ('"', "'", "`"): + delim = c + line += c + pos += 1 + + escape = False + + while pos < len(data): + + c = data[pos] + + if c == '\n': + number += 1 + + if escape: + escape = False + pos += 1 + line += c + continue + + if c == delim: + pos += 1 + line += c + break + + if c == '\\': + escape = True + + line += c + pos += 1 + + continue + + continue + + m = lllword.match(data, pos) + + word = m.group(0) + rest = m.group(1) + + if rest and "__" not in rest: + word = prefix + rest + + line += word + pos = m.end(0) + + # print repr(data[pos:]) + + + if not line == "": + raise ParseError(filename, start_number, "is not terminated with a newline. (Check strings and parenthesis.)", line=line, first=True) + + return rv + + + +def group_logical_lines(lines): + """ + This takes as input the list of logical line triples output from + list_logical_lines, and breaks the lines into blocks. Each block + is represented as a list of (filename, line number, line text, + block) triples, where block is a block list (which may be empty if + no block is associated with this line.) + """ + + # Returns the depth of a line, and the rest of the line. + def depth_split(l): + + depth = 0 + index = 0 + + while True: + if l[index] == ' ': + depth += 1 + index += 1 + continue + + # if l[index] == '\t': + # index += 1 + # depth = depth + 8 - (depth % 8) + # continue + + break + + return depth, l[index:] + + # i, min_depth -> block, new_i + def gll_core(i, min_depth): + + rv = [] + depth = None + + while i < len(lines): + + filename, number, text = lines[i] + + line_depth, rest = depth_split(text) + + # This catches a block exit. + if line_depth < min_depth: + break + + if depth is None: + depth = line_depth + + if depth != line_depth: + raise ParseError(filename, number, "indentation mismatch.") + + # Advance to the next line. + i += 1 + + # Try parsing a block associated with this line. + block, i = gll_core(i, depth + 1) + + rv.append((filename, number, rest, block)) + + return rv, i + + return gll_core(0, 0)[0] + +class Lexer(object): + """ + The lexer that is used to lex script files. This works on the idea + that we want to lex each line in a block individually, and use + sub-lexers to lex sub-blocks. + """ + + # A list of keywords which should not be parsed as names, because + # there is a huge chance of confusion. + keywords = set([ + 'as', + 'at', + 'behind', + 'call', + 'expression', + 'hide', + 'if', + 'image', + 'init', + 'jump', + 'menu', + 'onlayer', + 'python', + 'return', + 'scene', + 'set', + 'show', + 'with', + 'while', + 'zorder', + 'transform', + ]) + + + def __init__(self, block, init=False): + + # Are we underneath an init block? + self.init = init + + self.block = block + self.eob = False + + self.line = -1 + + # These are set by advance. + self.filename = "" + self.text = "" + self.number = 0 + self.subblock = [ ] + self.pos = 0 + self.word_cache_pos = -1 + self.word_cache_newpos = -1 + self.word_cache = "" + + def advance(self): + """ + Advances this lexer to the next line in the block. The lexer + starts off before the first line, so advance must be called + before any matching can be done. Returns True if we've + successfully advanced to a line in the block, or False if we + have advanced beyond all lines in the block. In general, once + this method has returned False, the lexer is in an undefined + state, and it doesn't make sense to call any method other than + advance (which will always return False) on the lexer. + """ + + self.line += 1 + + if self.line >= len(self.block): + self.eob = True + return + + self.filename, self.number, self.text, self.subblock = self.block[self.line] + self.pos = 0 + self.word_cache_pos = -1 + + return True + + def match_regexp(self, regexp): + """ + Tries to match the given regexp at the current location on the + current line. If it succeds, it returns the matched text (if + any), and updates the current position to be after the + match. Otherwise, returns None and the position is unchanged. + """ + + if self.eob: + return None + + if self.pos == len(self.text): + return None + + m = re.compile(regexp, re.DOTALL).match(self.text, self.pos) + + if not m: + return None + + self.pos = m.end() + + return m.group(0) + + def skip_whitespace(self): + """ + Advances the current position beyond any contiguous whitespace. + """ + + # print self.text[self.pos].encode('unicode_escape') + + self.match_regexp(r"(\s+|\\\n)+") + + def match(self, regexp): + """ + Matches something at the current position, skipping past + whitespace. Even if we can't match, the current position is + still skipped past the leading whitespace. + """ + + self.skip_whitespace() + return self.match_regexp(regexp) + + + def keyword(self, word): + """ + Matches a keyword at the current position. A keyword is a word + that is surrounded by things that aren't words, like + whitespace. (This prevents a keyword from matching a prefix.) + """ + + oldpos = self.pos + if self.word() == word: + return word + + self.pos = oldpos + return '' + + + def error(self, msg): + """ + Convenience function for reporting a parse error at the current + location. + """ + + raise ParseError(self.filename, self.number, msg, self.text, self.pos) + + def eol(self): + """ + Returns True if, after skipping whitespace, the current + position is at the end of the end of the current line, or + False otherwise. + """ + + self.skip_whitespace() + return self.pos >= len(self.text) + + def expect_eol(self): + """ + If we are not at the end of the line, raise an error. + """ + + if not self.eol(): + self.error('end of line expected.') + + def expect_noblock(self, stmt): + """ + Called to indicate this statement does not expect a block. + If a block is found, raises an error. + """ + + if self.subblock: + self.error('%s does not expect a block. Please check the indentation of the line after this one.' % stmt) + + def expect_block(self, stmt): + """ + Called to indicate that the statement requires that a non-empty + block is present. + """ + + if not self.subblock: + self.error('%s expects a non-empty block.' % stmt) + + + def subblock_lexer(self, init=False): + """ + Returns a new lexer object, equiped to parse the block + associated with this line. + """ + + init = self.init or init + + return Lexer(self.subblock, init=init) + + def string(self): + """ + Lexes a string, and returns the string to the user, or none if + no string could be found. This also takes care of expanding + escapes and collapsing whitespace. + + Be a little careful, as this can return an empty string, which is + different than None. + """ + + s = self.match(r'r?"([^\\"]|\\.)*"') + + if s is None: + s = self.match(r"r?'([^\\']|\\.)*'") + + if s is None: + s = self.match(r"r?`([^\\`]|\\.)*`") + + if s is None: + return None + + if s[0] == 'r': + raw = True + s = s[1:] + else: + raw = False + + # Strip off delimiters. + s = s[1:-1] + + if not raw: + + # Collapse runs of whitespace into single spaces. + s = re.sub(r'\s+', ' ', s) + + s = s.replace("\\n", "\n") + s = s.replace("\\{", "{{") + s = s.replace("\\%", "%%") + s = re.sub(r'\\u([0-9a-fA-F]{1,4})', + lambda m : chr(int(m.group(1), 16)), s) + s = re.sub(r'\\(.)', r'\1', s) + + return s + + def integer(self): + """ + Tries to parse an integer. Returns a string containing the + integer, or None. + """ + + return self.match(r'(\+|\-)?\d+') + + def float(self): + """ + Tries to parse a number (float). Returns a string containing the + number, or None. + """ + + return self.match(r'(\+|\-)?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?') + + def word(self): + """ + Parses a name, which may be a keyword or not. + """ + + if self.pos == self.word_cache_pos: + self.pos = self.word_cache_newpos + return self.word_cache + + self.word_cache_pos = self.pos + rv = self.match(r'[a-zA-Z_\u00a0-\ufffd][0-9a-zA-Z_\u00a0-\ufffd]*') + self.word_cache = rv + self.word_cache_newpos = self.pos + + return rv + + + def name(self): + """ + This tries to parse a name. Returns the name or None. + """ + + oldpos = self.pos + rv = self.word() + + if rv in self.keywords: + self.pos = oldpos + return None + + return rv + + def python_string(self): + """ + This tries to match a python string at the current + location. If it matches, it returns True, and the current + position is updated to the end of the string. Otherwise, + returns False. + """ + + if self.eol(): + return False + + c = self.text[self.pos] + + # Allow unicode strings. + if c == 'u': + self.pos += 1 + + if self.pos == len(self.text): + self.pos -= 1 + return False + + c = self.text[self.pos] + + if c not in ('"', "'"): + self.pos -= 1 + return False + + elif c not in ('"', "'"): + return False + + delim = c + + while True: + self.pos += 1 + + if self.eol(): + self.error("end of line reached while parsing string.") + + c = self.text[self.pos] + + if c == delim: + break + + if c == '\\': + self.pos += 1 + + self.pos += 1 + return True + + + def dotted_name(self): + """ + This tries to match a dotted name, which is one or more names, + separated by dots. Returns the dotted name if it can, or None + if it cannot. + + Once this sees the first name, it commits to parsing a + dotted_name. It will report an error if it then sees a dot + without a name behind it. + """ + + rv = self.name() + + if not rv: + return None + + while self.match(r'\.'): + n = self.name() + if not n: + self.error('expecting name.') + + rv += "." + n + + return rv + + def delimited_python(self, delim): + """ + This matches python code up to, but not including, the non-whitespace + delimiter characters. Returns a string containing the matched code, + which may be empty if the first thing is the delimiter. Raises an + error if EOL is reached before the delimiter. + """ + + start = self.pos + + while not self.eol(): + + c = self.text[self.pos] + + if c in delim: + return renpy.ast.PyExpr(self.text[start:self.pos], self.filename, self.number) + + if c == '"' or c == "'": + self.python_string() + continue + + if self.parenthesised_python(): + continue + + self.pos += 1 + + self.error("reached end of line when expecting '%s'." % delim) + + def python_expression(self): + """ + Returns a python expression, which is arbitrary python code + extending to a colon. + """ + + pe = self.delimited_python(':') + + if not pe: + self.error("expected python_expression") + + rv = renpy.ast.PyExpr(pe.strip(), pe.filename, pe.linenumber) # E1101 + + return rv + + def parenthesised_python(self): + """ + Tries to match a parenthesised python expression. If it can, + returns true and updates the current position to be after the + closing parenthesis. Returns False otherewise. + """ + + c = self.text[self.pos] + + if c == '(': + self.pos += 1 + self.delimited_python(')') + self.pos += 1 + return True + + if c == '[': + self.pos += 1 + self.delimited_python(']') + self.pos += 1 + return True + + + if c == '{': + self.pos += 1 + self.delimited_python('}') + self.pos += 1 + return True + + return False + + + def simple_expression(self): + """ + Tries to parse a simple_expression. Returns the text if it can, or + None if it cannot. + """ + + self.skip_whitespace() + if self.eol(): + return None + + start = self.pos + + # We start with either a name, a python_string, or parenthesized + # python + if (not self.python_string() and + not self.name() and + not self.float() and + not self.parenthesised_python()): + + return None + + while not self.eol(): + self.skip_whitespace() + + if self.eol(): + break + + # If we see a dot, expect a dotted name. + if self.match(r'\.'): + n = self.name() + if not n: + self.error("expecting name after dot.") + + continue + + # Otherwise, try matching parenthesised python. + if self.parenthesised_python(): + continue + + break + + return self.text[start:self.pos] + + def checkpoint(self): + """ + Returns an opaque representation of the lexer state. This can be + passed to revert to back the lexer up. + """ + + return self.filename, self.number, self.text, self.subblock, self.pos + + def revert(self, state): + """ + Reverts the lexer to the given state. State must have been returned + by a previous checkpoint operation on this lexer. + """ + + self.filename, self.number, self.text, self.subblock, self.pos = state + self.word_cache_pos = -1 + + def get_location(self): + """ + Returns a (filename, line number) tuple representing the current + physical location of the start of the current logical line. + """ + + return self.filename, self.number + + def require(self, thing, name=None): + """ + Tries to parse thing, and reports an error if it cannot be done. + + If thing is a string, tries to parse it using + self.match(thing). Otherwise, thing must be a method on this lexer + object, which is called directly. + """ + + if isinstance(thing, str): + name = name or thing + rv = self.match(thing) + else: + name = name or thing.__func__.__name__ + rv = thing() + + if rv is None: + self.error("expected '%s' not found." % name) + + return rv + + def rest(self): + """ + Skips whitespace, then returns the rest of the current + line, and advances the current position to the end of + the current line. + """ + + self.skip_whitespace() + + pos = self.pos + self.pos = len(self.text) + return self.text[pos:] + + def python_block(self): + """ + Returns the subblock of this code, and subblocks of that + subblock, as indented python code. This tries to insert + whitespace to ensure line numbers match up. + """ + + rv = [ ] + + o = LineNumberHolder() + o.line = self.number + + def process(block, indent): + + for fn, ln, text, subblock in block: + + if o.line > ln: + assert False + + while o.line < ln: + rv.append(indent + '\n') + o.line += 1 + + linetext = indent + text + '\n' + + rv.append(linetext) + o.line += linetext.count('\n') + + process(subblock, indent + ' ') + + process(self.subblock, '') + return ''.join(rv) + +def parse_image_name(l): + """ + This parses an image name, and returns it as a tuple. It requires + that the image name be present. + """ + + rv = [ l.require(l.name) ] + + while True: + n = l.simple_expression() + if not n: + break + + rv.append(n.strip()) + + return tuple(rv) + +def parse_simple_expression_list(l): + """ + This parses a comma-separated list of simple_expressions, and + returns a list of strings. It requires at least one + simple_expression be present. + """ + + rv = [ l.require(l.simple_expression) ] + + while True: + if not l.match(','): + break + + e = l.simple_expression() + + if not e: + break + + rv.append(e) + + return rv + +def parse_image_specifier(l): + """ + This parses an image specifier. + """ + + tag = None + layer = None + at_list = [ ] + zorder = None + behind = [ ] + + if l.keyword("expression") or l.keyword("image"): + expression = l.require(l.simple_expression) + image_name = ( expression.strip(), ) + else: + image_name = parse_image_name(l) + expression = None + + while True: + + if l.keyword("onlayer"): + if layer: + l.error("multiple onlayer clauses are prohibited.") + else: + layer = l.require(l.name) + + continue + + if l.keyword("at"): + + if at_list: + l.error("multiple at clauses are prohibited.") + else: + at_list = parse_simple_expression_list(l) + + continue + + if l.keyword("as"): + + if tag: + l.error("multiple as clauses are prohibited.") + else: + tag = l.require(l.name) + + continue + + if l.keyword("zorder"): + + if zorder is not None: + l.error("multiple zorder clauses are prohibited.") + else: + zorder = l.require(l.simple_expression) + + continue + + if l.keyword("behind"): + + if behind: + l.error("multiple behind clauses are prohibited.") + + while True: + bhtag = l.require(l.name) + behind.append(bhtag) + if not l.match(','): + break + + continue + + break + + if layer is None: + layer = 'master' + + + + return image_name, expression, tag, at_list, layer, zorder, behind + +def parse_with(l, node): + """ + Tries to parse the with clause associated with this statement. If + one exists, then the node is wrapped in a list with the + appropriate pair of With nodes. Otherwise, just returns the + statement by itself. + """ + + loc = l.get_location() + + if not l.keyword('with'): + return node + + expr = l.require(l.simple_expression) + + return [ ast.With(loc, "None", expr), + node, + ast.With(loc, expr) ] + + + +def parse_menu(stmtl, loc): + + l = stmtl.subblock_lexer() + + has_choice = False + + has_say = False + has_caption = False + + with_ = None + set = None + + say_who = None + say_what = None + + # Tuples of (label, condition, block) + items = [ ] + + l.advance() + + while not l.eob: + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('with clause') + l.advance() + + continue + + if l.keyword('set'): + set = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('set menuitem') + l.advance() + + continue + + # Try to parse a say menuitem. + state = l.checkpoint() + + who = l.simple_expression() + what = l.string() + + if who is not None and what is not None: + + l.expect_eol() + l.expect_noblock("say menuitem") + + if has_caption: + l.error("Say menuitems and captions may not exist in the same menu.") + + if has_say: + l.error("Only one say menuitem may exist per menu.") + + has_say = True + say_who = who + say_what = what + + l.advance() + + continue + + l.revert(state) + + + label = l.string() + + if label is None: + l.error('expected menuitem') + + # A string on a line by itself is a caption. + if l.eol(): + l.expect_noblock('caption menuitem') + + if label and has_say: + l.error("Captions and say menuitems may not exist in the same menu.") + + # Only set this if the caption is not "". + if label: + has_caption = True + + items.append((label, "True", None)) + l.advance() + + continue + + # Otherwise, we have a choice. + has_choice = True + + condition = "True" + + if l.keyword('if'): + condition = l.require(l.python_expression) + + l.require(':') + l.expect_eol() + l.expect_block('choice menuitem') + + block = parse_block(l.subblock_lexer()) + + items.append((label, condition, block)) + l.advance() + + if not has_choice: + stmtl.error("Menu does not contain any choices.") + + rv = [ ] + if has_say: + rv.append(ast.Say(loc, say_who, say_what, None, interact=False)) + + rv.append(ast.Menu(loc, items, set, with_)) + + return rv + +def parse_parameters(l): + + parameters = [ ] + positional = [ ] + extrapos = None + extrakw = None + + add_positional = True + + names = set() + + if not l.match(r'\('): + return None + + while True: + + if l.match('\)'): + break + + if l.match(r'\*\*'): + + if extrakw is not None: + l.error('a label may have only one ** parameter') + + extrakw = l.require(l.name) + + if extrakw in names: + l.error('parameter %s appears twice.' % extrakw) + + names.add(extrakw) + + + elif l.match(r'\*'): + + if not add_positional: + l.error('a label may have only one * parameter') + + add_positional = False + + extrapos = l.name() + + if extrapos is not None: + + if extrapos in names: + l.error('parameter %s appears twice.' % extrapos) + + names.add(extrapos) + + else: + + name = l.require(l.name) + + if name in names: + l.error('parameter %s appears twice.' % name) + + names.add(name) + + if l.match(r'='): + default = l.delimited_python("),") + else: + default = None + + parameters.append((name, default)) + + if add_positional: + positional.append(name) + + if l.match(r'\)'): + break + + l.require(r',') + + return renpy.ast.ParameterInfo(parameters, positional, extrapos, extrakw) + +def parse_arguments(l): + """ + Parse a list of arguments, if one is present. + """ + + arguments = [ ] + extrakw = None + extrapos = None + + if not l.match(r'\('): + return None + + while True: + + if l.match('\)'): + break + + if l.match(r'\*\*'): + + if extrakw is not None: + l.error('a call may have only one ** argument') + + extrakw = l.delimited_python("),") + + + elif l.match(r'\*'): + if extrapos is not None: + l.error('a call may have only one * argument') + + extrapos = l.delimited_python("),") + + else: + + state = l.checkpoint() + + name = l.name() + if not (name and l.match(r'=')): + l.revert(state) + name = None + + arguments.append((name, l.delimited_python("),"))) + + if l.match(r'\)'): + break + + l.require(r',') + + return renpy.ast.ArgumentInfo(arguments, extrapos, extrakw) + + + + +def parse_statement(l): + """ + This parses a Ren'Py statement. l is expected to be a Ren'Py lexer + that has been advanced to a logical line. This function will + advance l beyond the last logical line making up the current + statement, and will return an AST object representing this + statement, or a list of AST objects representing this statement. + """ + + # Store the current location. + loc = l.get_location() + + ### If statement + if l.keyword('if'): + entries = [ ] + + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('if statement') + + block = parse_block(l.subblock_lexer()) + + entries.append((condition, block)) + + l.advance() + + while l.keyword('elif'): + + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('elif clause') + + block = parse_block(l.subblock_lexer()) + + entries.append((condition, block)) + + l.advance() + + if l.keyword('else'): + l.require(':') + l.expect_eol() + l.expect_block('else clause') + + block = parse_block(l.subblock_lexer()) + + entries.append(('True', block)) + + l.advance() + + return ast.If(loc, entries) + + if l.keyword('elif'): + l.error('elif clause must be associated with an if statement.') + + if l.keyword('else'): + l.error('else clause must be associated with an if statement.') + + + ### While statement + if l.keyword('while'): + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('while statement') + block = parse_block(l.subblock_lexer()) + l.advance() + + return ast.While(loc, condition, block) + + + ### Pass statement + if l.keyword('pass'): + l.expect_noblock('pass statement') + l.expect_eol() + l.advance() + + return ast.Pass(loc) + + + ### Menu statement. + if l.keyword('menu'): + l.expect_block('menu statement') + label = l.name() + l.require(':') + l.expect_eol() + + menu = parse_menu(l, loc) + + l.advance() + + rv = [ ] + + if label: + rv.append(ast.Label(loc, label, [], None)) + + rv.extend(menu) + + return rv + + ### Return statement. + if l.keyword('return'): + l.expect_noblock('return statement') + + rest = l.rest() + if not rest: + rest = None + + l.expect_eol() + l.advance() + + return ast.Return(loc, rest) + + ### Jump statement + if l.keyword('jump'): + l.expect_noblock('jump statement') + + if l.keyword('expression'): + expression = True + target = l.require(l.simple_expression) + else: + expression = False + target = l.require(l.name) + + l.expect_eol() + l.advance() + + return ast.Jump(loc, target, expression) + + + ### Call/From statement. + if l.keyword('call'): + l.expect_noblock('call statment') + + if l.keyword('expression'): + expression = True + target = l.require(l.simple_expression) + + else: + expression = False + target = l.require(l.name) + + # Optional pass, to let someone write: + # call expression foo pass (bar, baz) + l.keyword('pass') + + arguments = parse_arguments(l) + + rv = [ ast.Call(loc, target, expression, arguments) ] + + if l.keyword('from'): + name = l.require(l.name) + rv.append(ast.Label(loc, name, [], None)) + else: + rv.append(ast.Pass(loc)) + + l.expect_eol() + l.advance() + + return rv + + ### Scene statement. + if l.keyword('scene'): + + if l.keyword('onlayer'): + layer = l.require(l.name) + else: + layer = "master" + + # Empty. + if l.eol(): + l.advance() + return ast.Scene(loc, None, layer) + + imspec = parse_image_specifier(l) + stmt = ast.Scene(loc, imspec, imspec[4]) + rv = parse_with(l, stmt) + + if l.match(':'): + stmt.atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.expect_noblock('scene statement') + + l.expect_eol() + l.advance() + + return rv + + ### Show statement. + if l.keyword('show'): + imspec = parse_image_specifier(l) + stmt = ast.Show(loc, imspec) + rv = parse_with(l, stmt) + + if l.match(':'): + stmt.atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.expect_noblock('show statement') + + l.expect_eol() + l.advance() + + return rv + + ### Hide statement. + if l.keyword('hide'): + imspec = parse_image_specifier(l) + rv = parse_with(l, ast.Hide(loc, imspec)) + + l.expect_eol() + l.expect_noblock('hide statement') + l.advance() + + return rv + + ### With statement. + if l.keyword('with'): + expr = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('with statement') + l.advance() + + return ast.With(loc, expr) + + ### Image statement. + if l.keyword('image'): + + name = parse_image_name(l) + + if l.match(':'): + l.expect_eol() + expr = None + atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.require('=') + expr = l.rest() + atl = None + l.expect_noblock('image statement') + + rv = ast.Image(loc, name, expr, atl) + + if not l.init: + rv = ast.Init(loc, [ rv ], 990) + + l.advance() + + return rv + + ### Define statement. + if l.keyword('define'): + + priority = l.integer() + if priority: + priority = int(priority) + else: + priority = 0 + + name = l.require(l.name) + l.require('=') + expr = l.rest() + + l.expect_noblock('define statement') + + rv = ast.Define(loc, name, expr) + + if not l.init: + rv = ast.Init(loc, [ rv ], priority) + + l.advance() + + return rv + + ### Transform statement. + if l.keyword('transform'): + + priority = l.integer() + if priority: + priority = int(priority) + else: + priority = 0 + + name = l.require(l.name) + parameters = parse_parameters(l) + + if parameters and (parameters.extrakw or parameters.extrapos): + l.error('transform statement does not take a variable number of parameters') + + l.require(':') + l.expect_eol() + + atl = renpy.atl.parse_atl(l.subblock_lexer()) + + rv = ast.Transform(loc, name, atl, parameters) + + if not l.init: + rv = ast.Init(loc, [ rv ], priority) + + l.advance() + + return rv + + ### One-line python statement. + if l.match(r'\$'): + python_code = l.rest() + l.expect_noblock('one-line python statement') + l.advance() + + return ast.Python(loc, python_code) + + ### Python block. + if l.keyword('python'): + + hide = False + early = False + + if l.keyword('early'): + early = True + + if l.keyword('hide'): + hide = True + + l.require(':') + l.expect_block('python block') + + python_code = l.python_block() + + l.advance() + + if early: + return ast.EarlyPython(loc, python_code, hide) + else: + return ast.Python(loc, python_code, hide) + + ### Label Statement + if l.keyword('label'): + name = l.require(l.name) + + parameters = parse_parameters(l) + + l.require(':') + l.expect_eol() + + # Optional block here. It's empty if no block is associated with + # this statement. + block = parse_block(l.subblock_lexer()) + + l.advance() + return ast.Label(loc, name, block, parameters) + + ### Init Statement + if l.keyword('init'): + + p = l.integer() + + if p: + priority = int(p) + else: + priority = 0 + + if l.keyword('python'): + + hide = False + if l.keyword('hide'): + hide = True + + l.require(':') + l.expect_block('python block') + + python_code = l.python_block() + + l.advance() + block = [ ast.Python(loc, python_code, hide) ] + + else: + l.require(':') + + l.expect_eol() + l.expect_block('init statement') + + block = parse_block(l.subblock_lexer(True)) + + l.advance() + + return ast.Init(loc, block, priority) + + # Try parsing as a user-statement. If that doesn't work, revert and + # try as a say. + + state = l.checkpoint() + + word = l.word() + if (word,) in renpy.statements.registry: + text = l.text + + l.expect_noblock(word + ' statement') + l.advance() + + renpy.exports.push_error_handler(l.error) + try: + rv = ast.UserStatement(loc, text) + finally: + renpy.exports.pop_error_handler() + + return rv + + l.revert(state) + + # Try parsing as the default statement. + if () in renpy.statements.registry: + text = l.text + l.expect_noblock('default statement') + l.advance() + + renpy.exports.push_error_handler(l.error) + try: + rv = ast.UserStatement(loc, text) + finally: + renpy.exports.pop_error_handler() + + return rv + + # The one and two arguement say statements are ambiguous in terms + # of lookahead. So we first try parsing as a one-argument, then a + # two-argument. + + # We're using the checkpoint from above. + + what = l.string() + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + else: + with_ = None + + if what is not None and l.eol(): + # We have a one-argument say statement. + l.expect_noblock('say statement') + l.advance() + return ast.Say(loc, None, what, with_) + + l.revert(state) + + # Try for a two-argument say statement. + who = l.simple_expression() + what = l.string() + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + else: + with_ = None + + if who and what is not None: + l.expect_eol() + l.expect_noblock('say statement') + l.advance() + return ast.Say(loc, who, what, with_) + + l.error('expected statement.') + +def parse_block(l): + """ + This parses a block of Ren'Py statements. It returns a list of the + statements contained within the block. l is a new Lexer object, for + this block. + """ + + l.advance() + rv = [ ] + + while not l.eob: + try: + + stmt = parse_statement(l) + if isinstance(stmt, list): + rv.extend(stmt) + else: + rv.append(stmt) + + except ParseError as e: + parse_errors.append(e.message) + l.advance() + + return rv + +def parse(fn): + """ + Parses a Ren'Py script contained within the file with the given + filename. Returns a list of AST objects representing the + statements that were found at the top level of the file. + """ + + renpy.game.exception_info = 'While parsing ' + fn + '.' + + try: + lines = list_logical_lines(fn) + nested = group_logical_lines(lines) + except ParseError as e: + parse_errors.append(e.message) + return None + + l = Lexer(nested) + + rv = parse_block(l) + + if parse_errors: + return None + + return rv + +def report_parse_errors(): + + if not parse_errors: + return False + + erfile = "errors.txt" + f = file(erfile, "w") + opat_err = os.path.realpath(erfile) + f.write(codecs.BOM_UTF8) + + print("I'm sorry, but errors were detected in your script. Please correct the", file=f) + print("errors listed below, and try again.", file=f) + print(file=f) + + for i in parse_errors: + + try: + i = i.encode("utf-8") + except: + pass + + print() + print(file=f) + print(i) + print(i, file=f) + + print(file=f) + print("Ren'Py Version:", renpy.version, file=f) + + f.close() + + try: + if renpy.config.editor: + renpy.exports.launch_editor([ opat_err ], 1, transient=1) + else: + os.startfile(erfile) # E1101 + except: + pass + + return True + + |