# Copyright 2004-2019 Tom Rothamel # # 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. from __future__ import print_function import collections import renpy.sl2 import renpy.sl2.slast as slast from ast import literal_eval # A tuple of style prefixes that we know of. STYLE_PREFIXES = [ '', 'insensitive_', 'hover_', 'idle_', 'activate_', 'selected_', 'selected_insensitive_', 'selected_hover_', 'selected_idle_', 'selected_activate_', ] ############################################################################## # Parsing. # The parser that things are being added to. parser = None # All statements we know about. all_statements = [ ] # Statements that can contain children. childbearing_statements = set() class Positional(object): """ This represents a positional parameter to a function. """ def __init__(self, name): self.name = name if parser: parser.add(self) # This is a map from (prefix, use_style_prefixes) to a set of property names. properties = collections.defaultdict(set) class Keyword(object): """ This represents an optional keyword parameter to a function. """ def __init__(self, name): self.name = name properties['', False].add(name) if parser: parser.add(self) class Style(object): """ This represents a style parameter to a function. """ def __init__(self, name): self.name = name properties['', True].add(self.name) if parser: parser.add(self) class PrefixStyle(object): """ This represents a prefixed style parameter to a function. """ def __init__(self, prefix, name): self.prefix = prefix self.name = name properties[prefix, True].add(self.name) if parser: parser.add(self) class Parser(object): # The number of children this statement takes, out of 0, 1, or "many". # This defaults to "many" so the has statement errors out when not # inside something that takes a single child. nchildren = "many" def __init__(self, name, statement=True): # The name of this object. self.name = name # The positional arguments, keyword arguments, and child # statements of this statement. self.positional = [ ] self.keyword = { } self.children = { } # True if this parser takes "as". self.variable = False if statement: all_statements.append(self) global parser parser = self def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.name) def add(self, i): """ Adds a clause to this parser. """ if isinstance(i, list): for j in i: self.add(j) return if isinstance(i, Positional): self.positional.append(i) elif isinstance(i, Keyword): self.keyword[i.name] = i elif isinstance(i, Style): for j in STYLE_PREFIXES: self.keyword[j + i.name] = i elif isinstance(i, PrefixStyle): for j in STYLE_PREFIXES: self.keyword[i.prefix + j + i.name] = i elif isinstance(i, Parser): self.children[i.name] = i def parse_statement(self, loc, l, layout_mode=False, keyword=True): word = l.word() or l.match(r'\$') if word and word in self.children: if layout_mode: c = self.children[word].parse_layout(loc, l, self, keyword) else: c = self.children[word].parse(loc, l, self, keyword) return c else: return None def parse_layout(self, loc, l, parent, keyword): l.error("The %s statement cannot be used as a container for the has statement." % self.name) def parse(self, loc, l, parent, keyword): """ This is expected to parse a function statement, and to return a list of python ast statements. `loc` The location of the current statement. `l` The lexer. `parent` The parent Parser of the current statement. """ raise Exception("Not Implemented") def parse_contents(self, l, target, layout_mode=False, can_has=False, can_tag=False, block_only=False, keyword=True): """ Parses the remainder of the current line of `l`, and all of its subblock, looking for keywords and children. `layout_mode` If true, parsing continues to the end of `l`, rather than stopping with the end of the first logical line. `can_has` If true, we should parse layouts. `can_tag` If true, we should parse the ``tag`` keyword, as it's used by screens. `block_only` If true, only parse the block and not the initial properties. """ seen_keywords = set() block = False # Parses a keyword argument from the lexer. def parse_keyword(l, expect, first_line): name = l.word() if name is None: l.error(expect) if can_tag and name == "tag": if target.tag is not None: l.error('keyword argument %r appears more than once in a %s statement.' % (name, self.name)) target.tag = l.require(l.word) l.expect_noblock(name) return True if self.variable: if name == "as": if target.variable is not None: l.error('an as clause may only appear once in a %s statement.' % (self.name,)) target.variable = l.require(l.word) return if name not in self.keyword: l.error('%r is not a keyword argument or valid child for the %s statement.' % (name, self.name)) if name in seen_keywords: l.error('keyword argument %r appears more than once in a %s statement.' % (name, self.name)) seen_keywords.add(name) if name == "at" and block and l.keyword("transform"): l.require(":") l.expect_eol() l.expect_block("ATL block") expr = renpy.atl.parse_atl(l.subblock_lexer()) target.atl_transform = expr return expr = l.comma_expression() if (not keyword) and (not renpy.config.keyword_after_python): try: literal_eval(expr) except: l.error("a non-constant keyword argument like '%s %s' is not allowed after a python block." % (name, expr)) target.keyword.append((name, expr)) if not first_line: l.expect_noblock(name) if block_only: l.expect_eol() l.expect_block(self.name) block = True else: # If not block_only, we allow keyword arguments on the starting # line. while True: if l.match(':'): l.expect_eol() l.expect_block(self.name) block = True break if l.eol(): l.expect_noblock(self.name) block = False break parse_keyword(l, 'expected a keyword argument, colon, or end of line.', True) # A list of lexers we need to parse the contents of. lexers = [ ] if block: lexers.append(l.subblock_lexer()) if layout_mode: lexers.append(l) # If we have a block, parse it. This also takes care of parsing the # block after a has clause. for l in lexers: while l.advance(): state = l.checkpoint() loc = l.get_location() if l.keyword(r'has'): if not can_has: l.error("The has statement is not allowed here.") if target.has_noncondition_child(): l.error("The has statement may not be given after a child has been supplied.") c = self.parse_statement(loc, l, layout_mode=True, keyword=keyword) if c is None: l.error('Has expects a child statement.') target.children.append(c) if c.has_python(): keyword = False continue c = self.parse_statement(loc, l) # Ignore passes. if isinstance(c, slast.SLPass): continue # If not none, add the child to our AST. if c is not None: target.children.append(c) if c.has_python(): keyword = False continue l.revert(state) if not l.eol(): parse_keyword(l, "expected a keyword argument or child statement.", False) while not l.eol(): parse_keyword(l, "expected a keyword argument or end of line.", False) def add_positional(self, name): global parser parser = self Positional(name) return self def add_property(self, name): global parser parser = self Keyword(name) return self def add_style_property(self, name): global parser parser = self Style(name) return self def add_prefix_style_property(self, prefix, name): global parser parser = self PrefixStyle(prefix, name) return self def add_property_group(self, group, prefix=''): global parser parser = self if group not in renpy.sl2.slproperties.property_groups: raise Exception("{!r} is not a known property group.".format(group)) for prop in renpy.sl2.slproperties.property_groups[group]: if isinstance(prop, Keyword): Keyword(prefix + prop.name) else: PrefixStyle(prefix, prop.name) return self def add(thing): parser.add(thing) # A singleton value. many = renpy.object.Sentinel("many") def register_sl_displayable(*args, **kwargs): """ :doc: custom_sl class :args: (name, displayable, style, nchildren=0, scope=False, replaces=False, default_keywords={}, default_properties=True) Registers a screen language statement that creates a displayable. `name` The name of the screen language statement, a string containing a Ren'Py keyword. This keyword is used to introduce the new statement. `displayable` This is a function that, when called, returns a displayable object. All position arguments, properties, and style properties are passed as arguments to this function. Other keyword arguments are also given to this function, a described below. This must return a Displayable. If it returns multiple displayables, the _main attribute of the outermost displayable should be set to the "main" displayable - the one that children should be added to. `style` The base name of the style of this displayable. If the style property is not given, this will have the style prefix added to it. The computed style is passed to the `displayable` function as the ``style`` keyword argument. `nchildren` The number of children of this displayable. One of: 0 The displayable takes no children. 1 The displayable takes 1 child. If more than one child is given, the children are placed in a Fixed. "many" The displayable takes more than one child. The following arguments should be passed in using keyword arguments: `replaces` If true, and the displayable replaces a prior displayable, that displayable is passed as a parameter to the new displayable. `default_keywords` The default set of keyword arguments to supply to the displayable. `default_properties` If true, the ui and position properties are added by default. Returns an object that can have positional arguments and properties added to it by calling the following methods. Each of these methods returns the object it is called on, allowing methods to be chained together. .. method:: add_positional(name) Adds a positional argument with `name` .. method:: add_property(name) Adds a property with `name`. Properties are passed as keyword arguments. .. method:: add_style_property(name) Adds a family of properties, ending with `name` and prefixed with the various style property prefixes. For example, if called with ("size"), this will define size, idle_size, hover_size, etc. .. method:: add_prefix_style_property(prefix, name) Adds a family of properties with names consisting of `prefix`, a style property prefix, and `name`. For example, if called with a prefix of `text_` and a name of `size`, this will create text_size, text_idle_size, text_hover_size, etc. .. method:: add_property_group(group, prefix='') Adds a group of properties, prefixed with `prefix`. `Group` may be one of the strings: * "bar" * "box" * "button" * "position" * "text" * "window" These correspond to groups of :ref:`style-properties`. Group can also be "ui", in which case it adds the :ref:`common ui properties `. """ rv = DisplayableParser(*args, **kwargs) for i in childbearing_statements: i.add(rv) screen_parser.add(rv) if rv.nchildren != 0: childbearing_statements.add(rv) for i in all_statements: rv.add(i) rv.add(if_statement) return rv class DisplayableParser(Parser): def __init__(self, name, displayable, style, nchildren=0, scope=False, pass_context=False, imagemap=False, replaces=False, default_keywords={}, hotspot=False, default_properties=True): """ `scope` If true, the scope is passed into the displayable functionas a keyword argument named "scope". `pass_context` If true, the context is passed as the first positional argument of the displayable. `imagemap` If true, the displayable is treated as defining an imagemap. (The imagemap is added to and removed from renpy.ui.imagemap_stack as appropriate.) `hotspot` If true, the displayable is treated as a hotspot. (It needs to be re-created if the imagemap it belongs to has changed.) `default_properties` If true, the ui and positional properties are added by default. """ super(DisplayableParser, self).__init__(name) # The displayable that is called when this statement runs. self.displayable = displayable if nchildren == "many": nchildren = many # The number of children we have. self.nchildren = nchildren if nchildren != 0: childbearing_statements.add(self) self.style = style self.scope = scope self.pass_context = pass_context self.imagemap = imagemap self.hotspot = hotspot self.replaces = replaces self.default_keywords = default_keywords self.variable = True Keyword("arguments") Keyword("properties") if default_properties: add(renpy.sl2.slproperties.ui_properties) add(renpy.sl2.slproperties.position_properties) def parse_layout(self, loc, l, parent, keyword): return self.parse(loc, l, parent, keyword, layout_mode=True) def parse(self, loc, l, parent, keyword, layout_mode=False): rv = slast.SLDisplayable( loc, self.displayable, scope=self.scope, child_or_fixed=(self.nchildren == 1), style=self.style, pass_context=self.pass_context, imagemap=self.imagemap, replaces=self.replaces, default_keywords=self.default_keywords, hotspot=self.hotspot, ) for _i in self.positional: expr = l.simple_expression() if expr is None: break rv.positional.append(expr) can_has = (self.nchildren == 1) self.parse_contents(l, rv, layout_mode=layout_mode, can_has=can_has, can_tag=False) if len(rv.positional) != len(self.positional): for i in rv.keyword: if i[0] == 'arguments': break else: l.error("{} statement expects {} positional arguments, got {}.".format(self.name, len(self.positional), len(rv.positional))) return rv class IfParser(Parser): def __init__(self, name, node_type, parent_contents): """ `node_type` The type of node to create. `parent_contents` If true, our children must be children of our parent. Otherwise, our children must be children of ourself. """ super(IfParser, self).__init__(name) self.node_type = node_type self.parent_contents = parent_contents if not parent_contents: childbearing_statements.add(self) def parse(self, loc, l, parent, keyword): if self.parent_contents: contents_from = parent else: contents_from = self rv = self.node_type(loc) condition = l.require(l.python_expression) l.require(':') block = slast.SLBlock(loc) contents_from.parse_contents(l, block, block_only=True) rv.entries.append((condition, block)) state = l.checkpoint() while l.advance(): loc = l.get_location() if l.keyword("elif"): condition = l.require(l.python_expression) l.require(':') block = slast.SLBlock(loc) contents_from.parse_contents(l, block, block_only=True, keyword=keyword) rv.entries.append((condition, block)) state = l.checkpoint() elif l.keyword("else"): condition = None l.require(':') block = slast.SLBlock(loc) contents_from.parse_contents(l, block, block_only=True, keyword=keyword) rv.entries.append((condition, block)) state = l.checkpoint() break else: l.revert(state) break return rv if_statement = IfParser("if", slast.SLIf, True) IfParser("showif", slast.SLShowIf, False) class ForParser(Parser): def __init__(self, name): super(ForParser, self).__init__(name) childbearing_statements.add(self) def name_or_tuple_pattern(self, l): """ Matches either a name or a tuple pattern. If a single name is being matched, returns it. Otherwise, returns None. """ name = None pattern = False while True: if l.match(r"\("): name = self.name_or_tuple_pattern(l) l.require(r'\)') pattern = True else: name = l.name() if name is None: break if l.match(r","): pattern = True else: break if pattern: return None if name is not None: return name l.error("expected variable or tuple pattern.") def parse(self, loc, l, parent, keyword): l.skip_whitespace() tuple_start = l.pos name = self.name_or_tuple_pattern(l) if not name: name = "_sl2_i" pattern = l.text[tuple_start:l.pos] stmt = pattern + " = " + name code = renpy.ast.PyCode(stmt, loc) else: code = None if l.match('index'): index_expression = l.require(l.say_expression) else: index_expression = None l.require('in') expression = l.require(l.python_expression) l.require(':') l.expect_eol() rv = slast.SLFor(loc, name, expression, index_expression) if code: rv.children.append(slast.SLPython(loc, code)) self.parse_contents(l, rv, block_only=True) return rv ForParser("for") class OneLinePythonParser(Parser): def parse(self, loc, l, parent, keyword): loc = l.get_location() source = l.require(l.rest_statement) l.expect_eol() l.expect_noblock("one-line python") code = renpy.ast.PyCode(source, loc) return slast.SLPython(loc, code) OneLinePythonParser("$") class MultiLinePythonParser(Parser): def parse(self, loc, l, parent, keyword): loc = l.get_location() l.require(':') l.expect_eol() l.expect_block("python block") source = l.python_block() code = renpy.ast.PyCode(source, loc) return slast.SLPython(loc, code) MultiLinePythonParser("python") class PassParser(Parser): def parse(self, loc, l, parent, keyword): l.expect_eol() return slast.SLPass(loc) PassParser("pass") class DefaultParser(Parser): def parse(self, loc, l, parent, keyword): name = l.require(l.word) l.require(r'=') rest = l.rest() l.expect_eol() l.expect_noblock('default statement') return slast.SLDefault(loc, name, rest) DefaultParser("default") class UseParser(Parser): def __init__(self, name): super(UseParser, self).__init__(name) childbearing_statements.add(self) def parse(self, loc, l, parent, keyword): if l.keyword('expression'): target = l.require(l.simple_expression) l.keyword('pass') else: target = l.require(l.word) args = renpy.parser.parse_arguments(l) if l.keyword('id'): id_expr = l.simple_expression() else: id_expr = None if l.match(':'): l.expect_eol() l.expect_block("use statement") block = slast.SLBlock(loc) self.parse_contents(l, block, can_has=True, block_only=True) else: l.expect_eol() l.expect_noblock("use statement") block = None return slast.SLUse(loc, target, args, id_expr, block) UseParser("use") Keyword("style_prefix") Keyword("style_group") class TranscludeParser(Parser): def parse(self, loc, l, parent, keyword): l.expect_eol() return slast.SLTransclude(loc) TranscludeParser("transclude") class CustomParser(Parser): """ :doc: custom_sl class :name: renpy.register_sl_statement Registers a custom screen language statement with Ren'Py. `name` This must be a word. It's the name of the custom screen language statement. `positional` The number of positional parameters this statement takes. `children` The number of children this custom statement takes. This should be 0, 1, or "many", which means zero or more. `screen` The screen to use. If not given, defaults to `name`. Returns an object that can have positional arguments and properties added to it. This object has the same .add_ methods as the objects returned by :class:`renpy.register_sl_displayable`. """ def __init__(self, name, positional=0, children="many", screen=None): Parser.__init__(self, name) if children == "many": children = many for i in childbearing_statements: i.add(self) screen_parser.add(self) self.nchildren = children if self.nchildren != 0: childbearing_statements.add(self) for i in all_statements: self.add(i) self.add(if_statement) global parser parser = None # The screen to use. if screen is not None: self.screen = screen else: self.screen = name # The number of positional parameters required. self.positional = positional def parse(self, loc, l, parent, keyword): arguments = [ ] # Parse positional arguments. for _i in range(self.positional): expr = l.require(l.simple_expression) arguments.append((None, expr)) # Parser keyword arguments and children. block = slast.SLBlock(loc) can_has = (self.nchildren == 1) self.parse_contents(l, block, can_has=can_has, can_tag=False) # Add the keyword arguments, and create an ArgumentInfo object. arguments.extend(block.keyword) block.keyword = [ ] args = renpy.ast.ArgumentInfo(arguments, None, None) # We only need a SLBlock if we have children. if not block.children: block = None # Create the Use statement. return slast.SLUse(loc, self.screen, args, None, block) class ScreenParser(Parser): def __init__(self): super(ScreenParser, self).__init__("screen", statement=False) def parse(self, loc, l, parent, name="_name", keyword=True): screen = slast.SLScreen(loc) screen.name = l.require(l.word) screen.parameters = renpy.parser.parse_parameters(l) self.parse_contents(l, screen, can_tag=True) keyword = dict(screen.keyword) screen.modal = keyword.get("modal", "False") screen.zorder = keyword.get("zorder", "0") screen.variant = keyword.get("variant", "None") screen.predict = keyword.get("predict", "None") screen.layer = keyword.get("layer", "'screens'") screen.sensitive = keyword.get("sensitive", "True") return screen screen_parser = ScreenParser() Keyword("modal") Keyword("zorder") Keyword("variant") Keyword("predict") Keyword("style_group") Keyword("style_prefix") Keyword("layer") Keyword("sensitive") parser = None def init(): screen_parser.add(all_statements) for i in all_statements: if i in childbearing_statements: i.add(all_statements) else: i.add(if_statement) def parse_screen(l, loc): """ Parses the screen statement. """ return screen_parser.parse(loc, l, None)