Project

Profile

Help

Download (9.88 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / sharp / engine.py @ a5c338e8

1 fa348e33 Vincent Le Goff
# Copyright (c) 2016, LE GOFF Vincent
2
# All rights reserved.
3
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions are met:
6
7
# * Redistributions of source code must retain the above copyright notice, this
8
#   list of conditions and the following disclaimer.
9
10
# * Redistributions in binary form must reproduce the above copyright notice,
11
#   this list of conditions and the following disclaimer in the documentation
12
#   and/or other materials provided with the distribution.
13
14
# * Neither the name of ytranslate nor the names of its
15
#   contributors may be used to endorse or promote products derived from
16
#   this software without specific prior written permission.
17
18
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29
"""Module containing the SharpEngine class."""
30
31
from textwrap import dedent
32
33
from sharp import FUNCTIONS
34
35
class SharpScript(object):
36
37
    """Class representing a SharpScript engine.
38

39
    An SharpScript engine is often linked with the game's main engine
40
    and an individual client, which is itself optionally linked to
41
    the ui application.
42

43
    """
44
45 a5c338e8 Vincent Le Goff
    def __init__(self, engine, client, world):
46 fa348e33 Vincent Le Goff
        self.engine = engine
47
        self.client = client
48 a5c338e8 Vincent Le Goff
        self.world = world
49 fa348e33 Vincent Le Goff
        self.locals = {}
50
        self.globals = globals()
51
52
        # Adding the functions
53
        for name, function in FUNCTIONS.items():
54 a5c338e8 Vincent Le Goff
            function = function(engine, client, self, world)
55 fa348e33 Vincent Le Goff
            self.globals[name] = function.run
56
57
    def execute(self, code):
58
        """Execute the SharpScript code given as an argument."""
59 ed873743 Vincent Le Goff
        if isinstance(code, str):
60
            instructions = self.feed(code)
61
        else:
62
            instructions = [code]
63
64 fa348e33 Vincent Le Goff
        globals = self.globals
65
        locals = self.locals
66
        for instruction in instructions:
67
            exec(instruction, globals, locals)
68
69
    def feed(self, content):
70
        """Feed the SharpScript engine with a string content.
71

72
        The content is probably a file with several statements in
73
        SharpScript, or a single statement.  In all cases, this function
74
        returns the list of Python codes corresponding with
75
        this suite of statements.
76

77
        """
78 ed873743 Vincent Le Goff
        # Execute Python code if necessary
79 fa348e33 Vincent Le Goff
        codes = []
80 ed873743 Vincent Le Goff
        while content.startswith("{+"):
81
            end = self.find_right_brace(content)
82
            code = content[2:end - 1].lstrip("\n").rstrip("\n ")
83
            code = repr(dedent(code)).replace("\\n", "\n")
84
            code = "compile(" + code + ", 'SharpScript', 'exec')"
85
            codes.append(code)
86
            content = content[end + 1:]
87
88
        # The remaining must be SharpScript, splits into statements
89
        statements = self.split_statements(content)
90 fa348e33 Vincent Le Goff
        for statement in statements:
91
            pycode = self.convert_to_python(statement)
92
            codes.append(pycode)
93
94
        return codes
95
96
    def convert_to_python(self, statement):
97
        """Convert the statement to Python and return the str code.
98

99
        The statement given in argument should be a tuple:  The first
100
        argument of the tuple should be a function (like '#play' or
101
        '#send').  The remaining arguments should be put in a string,
102
        except for other Sharp or Python code.
103

104
        """
105
        function_name = statement[0][1:].lower()
106
        arguments = []
107 95c4bc24 Vincent Le Goff
        kwargs = {}
108 fa348e33 Vincent Le Goff
        for argument in statement[1:]:
109
            if argument.startswith("{+"):
110 ed873743 Vincent Le Goff
                argument = argument[2:-1].lstrip("\n").rstrip("\n ")
111 b57dc8cb Vincent Le Goff
                argument = repr(dedent(argument)).replace("\\n", "\n")
112
                argument = "compile(" + argument + ", 'SharpScript', 'exec')"
113 fa348e33 Vincent Le Goff
            elif argument.startswith("{"):
114
                argument = repr(argument[1:-1])
115 457ab841 Vincent Le Goff
                argument = self.replace_semicolons(argument)
116 95c4bc24 Vincent Le Goff
            elif argument[0] in "-+":
117
                kwargs[argument[1:]] = True if argument[0] == "+" else False
118
                continue
119 fa348e33 Vincent Le Goff
            else:
120
                argument = repr(argument)
121 457ab841 Vincent Le Goff
                argument = self.replace_semicolons(argument)
122 fa348e33 Vincent Le Goff
123
            arguments.append(argument)
124
125 95c4bc24 Vincent Le Goff
        code = function_name + "(" + ", ".join(arguments)
126
        if arguments and kwargs:
127
            code += ", "
128
129
        code += ", ".join([name + "=" + repr(value) for name, value in \
130
                kwargs.items()])
131
132
        return code + ")"
133 fa348e33 Vincent Le Goff
134
    def split_statements(self, content):
135
        """Split the given string content into different statements.
136

137
        A statement is one-line short at the very least.  It can be
138
        longer by that, if it's enclosed into braces.
139

140
        """
141
        statements = []
142
        i = 0
143
        function_name = ""
144
        arguments = []
145
        while True:
146
            remaining = content[i:]
147
148
            # If remaining is empty, saves the statement and exits the loop
149
            if not remaining or remaining.isspace():
150
                if function_name:
151
                    statements.append((function_name, ) + tuple(arguments))
152
153
                break
154
155
            # If remaining begins with a new line
156
            if remaining[0] == "\n":
157
                if function_name:
158
                    statements.append((function_name, ) + tuple(arguments))
159
                    function_name = ""
160
                    arguments = []
161
162
                i += 1
163
                continue
164
165
            # If remaining begins with a space
166
            if remaining[0].isspace():
167
                remaining = remaining[1:]
168
                i += 1
169
                continue
170
171
            # If the function_name is not defined, take the first parameter
172
            if not function_name:
173 c1e9c2e6 Vincent Le Goff
                if remaining.startswith("#") and not remaining.startswith(
174
                        "##"):
175 fa348e33 Vincent Le Goff
                    # This is obviously a function name
176
                    function_name = remaining.splitlines()[0].split(" ")[0]
177
                    arguments = []
178
                    i += len(function_name)
179
                else:
180
                    function_name = "#send"
181
                    argument = remaining.splitlines()[0]
182
                    i += len(argument)
183 c1e9c2e6 Vincent Le Goff
184
                    if argument.startswith("##"):
185
                        argument = argument[1:]
186
187 fa348e33 Vincent Le Goff
                    arguments = [argument]
188
            elif remaining[0] == "{":
189
                end = self.find_right_brace(remaining)
190
                argument = remaining[:end + 1]
191
                i += end + 1
192 c1e9c2e6 Vincent Le Goff
                if argument.startswith("##"):
193
                    argument = argument[1:]
194
195
                arguments.append(argument)
196 fa348e33 Vincent Le Goff
            else:
197
                argument = remaining.splitlines()[0].split(" ")[0]
198
                i += len(argument)
199 c1e9c2e6 Vincent Le Goff
                if argument.startswith("##"):
200
                    argument = argument[1:]
201
202 fa348e33 Vincent Le Goff
                arguments.append(argument)
203
204
        return statements
205
206
    def find_right_brace(self, text):
207
        """Find the right brace matching the opening one.
208

209
        This function doesn't only look for the first right brace (}).
210
        It looks for a brace that would close the text and return the
211
        position of this character.  For instance:
212
            >>> Engine.find_right_brace("{first parameter {with} something} else")
213
            33
214

215
        """
216
        level = 0
217
        i = 0
218
        while i < len(text):
219
            char = text[i]
220
            if char == "{":
221
                level += 1
222
            elif char == "}":
223
                level -= 1
224
225
            if level == 0:
226
                return i
227
228
            i += 1
229
230
        return None
231 457ab841 Vincent Le Goff
232
    @staticmethod
233
    def replace_semicolons(text):
234
        """Replace all not-escaped semi-colons."""
235
        i = 0
236
        while i < len(text):
237
            remaining = text[i:]
238
            if remaining.startswith(";;"):
239
                i += 2
240
                continue
241
            elif remaining.startswith(";"):
242
                text = text[:i] + "\n" + text[i + 1:]
243
            i += 1
244
245
        return text.replace(";;", ";")
246 c89c6b5b Vincent Le Goff
247
    def format(self, content):
248
        """Write SharpScript and return a string.
249

250
        This method takes as argument the SharpScript content and formats it.  It therefore replaces the default formatting.  Arguments are escaped this way:
251

252
        * If the argument contains space, escape it with braces.
253
        * If the argument contains new line, indent it.
254
        * If the argument contains semi colons, keep it on one line.
255

256
        """
257
        instructions = self.split_statements(content)
258
259
        # At this stage, the instructions are formatted in Python
260
        lines = []
261
        for arguments in instructions:
262
            function = arguments[0].lower()
263
            arguments = list(arguments[1:])
264
265
            # Escape the arguments if necessary
266
            for i, argument in enumerate(arguments):
267
                arguments[i] = self.escape_argument(argument)
268
269
            line = function + " " + " ".join(arguments)
270
            lines.append(line.rstrip(" "))
271
272
        return "\n".join(lines)
273
274
    @staticmethod
275
    def escape_argument(argument):
276
        """Escape the argument if needed."""
277
        if argument.startswith("{"):
278
            pass
279
        elif "\n" in argument:
280
            lines = argument.splitlines()
281
            argument = "{" + "\n    ".join(lines) + "\n}"
282
        elif " " in argument:
283
            argument = "{" + argument + "}"
284
285
        return argument