summaryrefslogtreecommitdiff
path: root/ast2json/renpy/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'ast2json/renpy/parser.py')
-rw-r--r--ast2json/renpy/parser.py1842
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
+
+