github / src / sharp / engine.py @ a5c338e8
1 |
# 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 |
def __init__(self, engine, client, world): |
46 |
self.engine = engine
|
47 |
self.client = client
|
48 |
self.world = world
|
49 |
self.locals = {}
|
50 |
self.globals = globals() |
51 |
|
52 |
# Adding the functions
|
53 |
for name, function in FUNCTIONS.items(): |
54 |
function = function(engine, client, self, world)
|
55 |
self.globals[name] = function.run
|
56 |
|
57 |
def execute(self, code): |
58 |
"""Execute the SharpScript code given as an argument."""
|
59 |
if isinstance(code, str): |
60 |
instructions = self.feed(code)
|
61 |
else:
|
62 |
instructions = [code] |
63 |
|
64 |
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 |
# Execute Python code if necessary
|
79 |
codes = [] |
80 |
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 |
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 |
kwargs = {} |
108 |
for argument in statement[1:]: |
109 |
if argument.startswith("{+"): |
110 |
argument = argument[2:-1].lstrip("\n").rstrip("\n ") |
111 |
argument = repr(dedent(argument)).replace("\\n", "\n") |
112 |
argument = "compile(" + argument + ", 'SharpScript', 'exec')" |
113 |
elif argument.startswith("{"): |
114 |
argument = repr(argument[1:-1]) |
115 |
argument = self.replace_semicolons(argument)
|
116 |
elif argument[0] in "-+": |
117 |
kwargs[argument[1:]] = True if argument[0] == "+" else False |
118 |
continue
|
119 |
else:
|
120 |
argument = repr(argument)
|
121 |
argument = self.replace_semicolons(argument)
|
122 |
|
123 |
arguments.append(argument) |
124 |
|
125 |
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 |
|
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 |
if remaining.startswith("#") and not remaining.startswith( |
174 |
"##"):
|
175 |
# 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 |
|
184 |
if argument.startswith("##"): |
185 |
argument = argument[1:]
|
186 |
|
187 |
arguments = [argument] |
188 |
elif remaining[0] == "{": |
189 |
end = self.find_right_brace(remaining)
|
190 |
argument = remaining[:end + 1]
|
191 |
i += end + 1
|
192 |
if argument.startswith("##"): |
193 |
argument = argument[1:]
|
194 |
|
195 |
arguments.append(argument) |
196 |
else:
|
197 |
argument = remaining.splitlines()[0].split(" ")[0] |
198 |
i += len(argument)
|
199 |
if argument.startswith("##"): |
200 |
argument = argument[1:]
|
201 |
|
202 |
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 |
|
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 |
|
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
|