bTagScript 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bTagScript/__init__.py +66 -0
- bTagScript/adapter/__init__.py +16 -0
- bTagScript/adapter/discord_adapters.py +275 -0
- bTagScript/adapter/function_adapter.py +33 -0
- bTagScript/adapter/int_adapter.py +30 -0
- bTagScript/adapter/object_adapter.py +41 -0
- bTagScript/adapter/string_adapter.py +69 -0
- bTagScript/block/__init__.py +67 -0
- bTagScript/block/break_block.py +41 -0
- bTagScript/block/case_block.py +62 -0
- bTagScript/block/comment_block.py +33 -0
- bTagScript/block/control_block.py +161 -0
- bTagScript/block/counting_blocks.py +88 -0
- bTagScript/block/digitshorthand_block.py +38 -0
- bTagScript/block/discord_blocks/__init__.py +20 -0
- bTagScript/block/discord_blocks/command_block.py +52 -0
- bTagScript/block/discord_blocks/cooldown_block.py +101 -0
- bTagScript/block/discord_blocks/delete_block.py +47 -0
- bTagScript/block/discord_blocks/embed_block.py +248 -0
- bTagScript/block/discord_blocks/override_block.py +61 -0
- bTagScript/block/discord_blocks/react_block.py +49 -0
- bTagScript/block/discord_blocks/redirect_block.py +42 -0
- bTagScript/block/discord_blocks/requirement_blocks.py +83 -0
- bTagScript/block/helpers.py +121 -0
- bTagScript/block/math_blocks.py +257 -0
- bTagScript/block/random_block.py +65 -0
- bTagScript/block/range_block.py +54 -0
- bTagScript/block/replace_block.py +110 -0
- bTagScript/block/stop_block.py +38 -0
- bTagScript/block/strf_block.py +70 -0
- bTagScript/block/url_blocks.py +76 -0
- bTagScript/block/util_blocks/__init__.py +3 -0
- bTagScript/block/util_blocks/debug_block.py +107 -0
- bTagScript/block/var_block.py +49 -0
- bTagScript/block/vargetter_blocks.py +91 -0
- bTagScript/exceptions.py +139 -0
- bTagScript/interface/__init__.py +4 -0
- bTagScript/interface/adapter.py +43 -0
- bTagScript/interface/block.py +136 -0
- bTagScript/interpreter.py +614 -0
- bTagScript/utils.py +65 -0
- bTagScript/verb.py +129 -0
- btagscript-5.0.0.dist-info/METADATA +109 -0
- btagscript-5.0.0.dist-info/RECORD +47 -0
- btagscript-5.0.0.dist-info/WHEEL +5 -0
- btagscript-5.0.0.dist-info/licenses/LICENSE +1 -0
- btagscript-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from itertools import islice
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
BlocknameDuplicateError,
|
|
9
|
+
ProcessError,
|
|
10
|
+
StopError,
|
|
11
|
+
TagScriptError,
|
|
12
|
+
WorkloadExceededError,
|
|
13
|
+
)
|
|
14
|
+
from .interface import Adapter, Block
|
|
15
|
+
from .utils import maybe_await
|
|
16
|
+
from .verb import Verb
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
"Interpreter",
|
|
20
|
+
"AsyncInterpreter",
|
|
21
|
+
"Context",
|
|
22
|
+
"Response",
|
|
23
|
+
"Node",
|
|
24
|
+
"build_node_tree",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
AdapterDict = Dict[str, Adapter]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Node:
|
|
33
|
+
"""
|
|
34
|
+
A low-level object representing a bracketed block.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
coordinates: Tuple[int, int]
|
|
39
|
+
The start and end position of the bracketed text block.
|
|
40
|
+
verb: Optional[Verb]
|
|
41
|
+
The determined Verb for this node.
|
|
42
|
+
output:
|
|
43
|
+
The `Block` processed output for this node.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("output", "verb", "coordinates")
|
|
47
|
+
|
|
48
|
+
def __init__(self, coordinates: Tuple[int, int], verb: Optional[Verb] = None) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Constructing the Node
|
|
51
|
+
"""
|
|
52
|
+
self.coordinates = coordinates
|
|
53
|
+
self.verb = verb
|
|
54
|
+
self.output: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def __str__(self) -> str:
|
|
57
|
+
"""
|
|
58
|
+
String function
|
|
59
|
+
"""
|
|
60
|
+
return str(self.verb) + " at " + str(self.coordinates)
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
String repr
|
|
65
|
+
"""
|
|
66
|
+
return f"<Node verb={self.verb!r} coordinates={self.coordinates!r} output={self.output!r}>"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_node_tree(message: str) -> List[Node]:
|
|
70
|
+
"""
|
|
71
|
+
Function that finds all possible nodes in a string.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
message: str
|
|
76
|
+
The string to find nodes in.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
List[Node]
|
|
81
|
+
A list of all possible text bracket blocks.
|
|
82
|
+
"""
|
|
83
|
+
nodes = []
|
|
84
|
+
# previous = r""
|
|
85
|
+
|
|
86
|
+
starts = []
|
|
87
|
+
for i, ch in enumerate(message):
|
|
88
|
+
if ch == "{": # and previous[1:] != "\\":
|
|
89
|
+
starts.append(i)
|
|
90
|
+
if ch == "}": # and previous[1:] != "\\":
|
|
91
|
+
if not starts:
|
|
92
|
+
continue
|
|
93
|
+
coords = (starts.pop(), i)
|
|
94
|
+
n = Node(coords)
|
|
95
|
+
nodes.append(n)
|
|
96
|
+
|
|
97
|
+
# previous = previous[:1] + ch
|
|
98
|
+
return nodes
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Response:
|
|
102
|
+
"""
|
|
103
|
+
An object containing information on a completed TagScript process.
|
|
104
|
+
|
|
105
|
+
Attributes
|
|
106
|
+
----------
|
|
107
|
+
body: str
|
|
108
|
+
The cleaned message with all verbs interpreted.
|
|
109
|
+
actions: Dict[str, Any]
|
|
110
|
+
A dictionary that blocks can access and modify to define post-processing actions.
|
|
111
|
+
variables: Dict[str, Adapter]
|
|
112
|
+
A dictionary of variables that blocks such as the `LooseVariableGetterBlock` can access.
|
|
113
|
+
extras: Dict[str, Any]
|
|
114
|
+
A dictionary of extra keyword arguments that blocks can use to define their own behavior.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
__slots__ = ("body", "actions", "variables", "extras")
|
|
118
|
+
|
|
119
|
+
def __init__(self, *, variables: AdapterDict = None, extras: Dict[str, Any] = None) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Construct the response
|
|
122
|
+
"""
|
|
123
|
+
self.body: str = None
|
|
124
|
+
self.actions: Dict[str, Any] = {}
|
|
125
|
+
self.variables: AdapterDict = variables if variables is not None else {}
|
|
126
|
+
self.extras: Dict[str, Any] = extras if extras is not None else {}
|
|
127
|
+
|
|
128
|
+
def __repr__(self) -> str:
|
|
129
|
+
"""
|
|
130
|
+
String repr
|
|
131
|
+
"""
|
|
132
|
+
return f"<Response body={self.body!r} actions={self.actions!r} variables={self.variables!r} extras={self.extras!r}>"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Context:
|
|
136
|
+
"""
|
|
137
|
+
An object containing data on the TagScript block processed by the interpreter.
|
|
138
|
+
This class is passed to adapters and blocks during processing.
|
|
139
|
+
|
|
140
|
+
Attributes
|
|
141
|
+
----------
|
|
142
|
+
verb: Verb
|
|
143
|
+
The Verb object representing a TagScript block.
|
|
144
|
+
original_message: str
|
|
145
|
+
The original message passed to the interpreter.
|
|
146
|
+
interpreter: Interpreter
|
|
147
|
+
The interpreter processing the TagScript.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
__slots__ = ("verb", "original_message", "interpreter", "response")
|
|
151
|
+
|
|
152
|
+
def __init__(self, verb: Verb, res: Response, interpreter: Interpreter, og: str) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Construct the context
|
|
155
|
+
"""
|
|
156
|
+
self.verb: Verb = verb
|
|
157
|
+
self.original_message: str = og
|
|
158
|
+
self.interpreter: Interpreter = interpreter
|
|
159
|
+
self.response: Response = res
|
|
160
|
+
|
|
161
|
+
def __repr__(self) -> str:
|
|
162
|
+
"""
|
|
163
|
+
String repr
|
|
164
|
+
"""
|
|
165
|
+
return f"<Context verb={self.verb!r}>"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Interpreter:
|
|
169
|
+
"""
|
|
170
|
+
The TagScript interpreter.
|
|
171
|
+
|
|
172
|
+
Attributes
|
|
173
|
+
----------
|
|
174
|
+
blocks: UnionList[Block]
|
|
175
|
+
A list or tuple of blocks to be used for TagScript processing.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
__slots__ = ("blocks", "_blocknames")
|
|
179
|
+
|
|
180
|
+
def __init__(self, blocks: Union[List[Block], Tuple[Block]]) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Creates a list of blocks, and also gets all acceptable names for processing
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
BlocknameDuplicateError
|
|
187
|
+
If there are duplicate blocknames.
|
|
188
|
+
"""
|
|
189
|
+
self.blocks: Union[List[Block], Tuple[Block]] = blocks
|
|
190
|
+
self._blocknames = set()
|
|
191
|
+
for block in blocks:
|
|
192
|
+
for name in block.ACCEPTED_NAMES:
|
|
193
|
+
if name in self._blocknames:
|
|
194
|
+
raise BlocknameDuplicateError(name)
|
|
195
|
+
self._blocknames.add(name)
|
|
196
|
+
|
|
197
|
+
def __repr__(self) -> str:
|
|
198
|
+
"""
|
|
199
|
+
String repr
|
|
200
|
+
"""
|
|
201
|
+
return f"<{type(self).__name__} blocks={self.blocks!r}>"
|
|
202
|
+
|
|
203
|
+
def _get_context(
|
|
204
|
+
self,
|
|
205
|
+
node: Node,
|
|
206
|
+
final: str,
|
|
207
|
+
*,
|
|
208
|
+
response: Response,
|
|
209
|
+
original_message: str,
|
|
210
|
+
verb_limit: int,
|
|
211
|
+
) -> Context:
|
|
212
|
+
"""
|
|
213
|
+
Construct a context object for a node.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
node: Node
|
|
218
|
+
The node to construct the context for.
|
|
219
|
+
final: str
|
|
220
|
+
The final message to be processed.
|
|
221
|
+
response: Response
|
|
222
|
+
The response object to be passed to the context.
|
|
223
|
+
original_message: str
|
|
224
|
+
The original message passed to the interpreter.
|
|
225
|
+
verb_limit: int
|
|
226
|
+
The maximum number of verbs to process.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
Context
|
|
231
|
+
The constructed context.
|
|
232
|
+
"""
|
|
233
|
+
# Get the updated verb string from coordinates and make the context
|
|
234
|
+
start, end = node.coordinates
|
|
235
|
+
node.verb = Verb(final[start : end + 1], limit=verb_limit)
|
|
236
|
+
return Context(node.verb, response, self, original_message)
|
|
237
|
+
|
|
238
|
+
def _get_acceptors(self, ctx: Context) -> Tuple[Block]:
|
|
239
|
+
"""
|
|
240
|
+
Get a list of acceptors
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
ctx: Context
|
|
245
|
+
The context to get the acceptors for.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
Tuple[Block]
|
|
250
|
+
"""
|
|
251
|
+
return (b for b in self.blocks if b.will_accept(ctx))
|
|
252
|
+
|
|
253
|
+
def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]:
|
|
254
|
+
"""
|
|
255
|
+
Process the blocks
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
ctx: Context
|
|
260
|
+
The context to process the blocks from.
|
|
261
|
+
node: Node
|
|
262
|
+
The node to process the blocks from.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
Optional[str]
|
|
267
|
+
The final message
|
|
268
|
+
"""
|
|
269
|
+
acceptors = self._get_acceptors(ctx)
|
|
270
|
+
for b in acceptors:
|
|
271
|
+
value = b.process(ctx)
|
|
272
|
+
if value is not None: # Value found? We're done here.
|
|
273
|
+
value = str(value)
|
|
274
|
+
node.output = value
|
|
275
|
+
return value
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _check_workload(charlimit: int, total_work: int, output: str) -> Optional[int]:
|
|
280
|
+
"""
|
|
281
|
+
Check if the workload has been exceeded.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
charlimit: int
|
|
286
|
+
The maximum number of characters to process.
|
|
287
|
+
total_work: int
|
|
288
|
+
The total number of characters processed.
|
|
289
|
+
output: str
|
|
290
|
+
The output string.
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
Optional[int]
|
|
295
|
+
The total amount of work that has been processed.
|
|
296
|
+
|
|
297
|
+
Raises
|
|
298
|
+
------
|
|
299
|
+
WorkloadExceededError
|
|
300
|
+
If the workload has been exceeded.
|
|
301
|
+
"""
|
|
302
|
+
if not charlimit:
|
|
303
|
+
return None
|
|
304
|
+
total_work += len(output)
|
|
305
|
+
if total_work > charlimit:
|
|
306
|
+
raise WorkloadExceededError(
|
|
307
|
+
"The TSE interpreter had its workload exceeded. The total characters "
|
|
308
|
+
f"attempted were {total_work}/{charlimit}"
|
|
309
|
+
)
|
|
310
|
+
return total_work
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _text_deform(start: int, end: int, final: str, output: str) -> Tuple[str, int]:
|
|
314
|
+
"""
|
|
315
|
+
Deform the text, replacing code with what was outputted.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
start: int
|
|
320
|
+
The start index of the code.
|
|
321
|
+
end: int
|
|
322
|
+
The end index of the code.
|
|
323
|
+
final: str
|
|
324
|
+
The final message.
|
|
325
|
+
output: str
|
|
326
|
+
The output string.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
Tuple[str, int]
|
|
331
|
+
The new final message, and the change in final length after the change has been applied.
|
|
332
|
+
"""
|
|
333
|
+
message_slice_len = (end + 1) - start
|
|
334
|
+
replacement_len = len(output)
|
|
335
|
+
differential = (
|
|
336
|
+
replacement_len - message_slice_len
|
|
337
|
+
) # The change in size of `final` after the change is applied
|
|
338
|
+
final = final[:start] + output + final[end + 1 :]
|
|
339
|
+
return final, differential
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _translate_nodes(
|
|
343
|
+
node_ordered_list: List[Node], index: int, start: int, differential: int
|
|
344
|
+
) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Get the new coordinates for each node.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
node_ordered_list: List[Node]
|
|
351
|
+
The list of nodes to translate.
|
|
352
|
+
index: int
|
|
353
|
+
The index of the node to translate.
|
|
354
|
+
start: int
|
|
355
|
+
The start index of the code.
|
|
356
|
+
differential: int
|
|
357
|
+
The change in final length after the change has been applied.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
None
|
|
362
|
+
"""
|
|
363
|
+
for future_n in islice(node_ordered_list, index + 1, None):
|
|
364
|
+
fut_start, fut_end = future_n.coordinates
|
|
365
|
+
future_n.coordinates = (
|
|
366
|
+
fut_start + differential if fut_start > start else fut_start,
|
|
367
|
+
fut_end + differential if fut_end > start else fut_end,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def _solve(
|
|
371
|
+
self,
|
|
372
|
+
message: str,
|
|
373
|
+
node_ordered_list: List[Node],
|
|
374
|
+
response: Response,
|
|
375
|
+
*,
|
|
376
|
+
charlimit: int,
|
|
377
|
+
verb_limit: int = 2000,
|
|
378
|
+
) -> Optional[str]:
|
|
379
|
+
"""
|
|
380
|
+
Solve the tagscript by proccessing all possible nodes.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
message: str
|
|
385
|
+
The message to process.
|
|
386
|
+
node_ordered_list: List[Node]
|
|
387
|
+
The list of nodes to process.
|
|
388
|
+
response: Response
|
|
389
|
+
The response object to be passed to the context.
|
|
390
|
+
charlimit: int
|
|
391
|
+
The maximum number of characters to process.
|
|
392
|
+
verb_limit: int
|
|
393
|
+
The maximum number of verbs to process.
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
Optional[str]
|
|
398
|
+
The final, completely processed message.
|
|
399
|
+
"""
|
|
400
|
+
final = message
|
|
401
|
+
total_work = 0
|
|
402
|
+
for index, node in enumerate(node_ordered_list):
|
|
403
|
+
start, end = node.coordinates
|
|
404
|
+
ctx = self._get_context(
|
|
405
|
+
node,
|
|
406
|
+
final,
|
|
407
|
+
response=response,
|
|
408
|
+
original_message=message,
|
|
409
|
+
verb_limit=verb_limit,
|
|
410
|
+
)
|
|
411
|
+
log.debug("Processing context %r at (%r, %r)", ctx, start, end)
|
|
412
|
+
try:
|
|
413
|
+
output = self._process_blocks(ctx, node)
|
|
414
|
+
except StopError as exc:
|
|
415
|
+
log.debug("StopError raised on node %r", node, exc_info=exc)
|
|
416
|
+
return final[:start] + exc.message
|
|
417
|
+
if output is None:
|
|
418
|
+
continue # If there was no value output, no need to text deform.
|
|
419
|
+
|
|
420
|
+
total_work = self._check_workload(charlimit, total_work, output)
|
|
421
|
+
final, differential = self._text_deform(start, end, final, output)
|
|
422
|
+
self._translate_nodes(node_ordered_list, index, start, differential)
|
|
423
|
+
return final
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _return_response(response: Response, output: str) -> Response:
|
|
427
|
+
"""
|
|
428
|
+
Return the response object.
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
response: Response
|
|
433
|
+
The response object to be returned.
|
|
434
|
+
output: str
|
|
435
|
+
The output string.
|
|
436
|
+
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
Response
|
|
440
|
+
The response object.
|
|
441
|
+
"""
|
|
442
|
+
if response.body is None:
|
|
443
|
+
response.body = output.strip()
|
|
444
|
+
else:
|
|
445
|
+
# Dont override an overridden response.
|
|
446
|
+
response.body = response.body.strip()
|
|
447
|
+
return response
|
|
448
|
+
|
|
449
|
+
def process(
|
|
450
|
+
self,
|
|
451
|
+
message: str,
|
|
452
|
+
seed_variables: AdapterDict = None,
|
|
453
|
+
*,
|
|
454
|
+
charlimit: Optional[int] = None,
|
|
455
|
+
**kwargs,
|
|
456
|
+
) -> Response:
|
|
457
|
+
"""
|
|
458
|
+
Processes a given TagScript string.
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
message: str
|
|
463
|
+
A TagScript string to be processed.
|
|
464
|
+
seed_variables: Dict[str, Adapter]
|
|
465
|
+
A dictionary containing strings to adapters to provide context variables for processing.
|
|
466
|
+
charlimit: int
|
|
467
|
+
The maximum characters to process.
|
|
468
|
+
kwargs: Dict[str, Any]
|
|
469
|
+
Additional keyword arguments that may be used by blocks during processing.
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
Response
|
|
474
|
+
A response object containing the processed body, actions and variables.
|
|
475
|
+
|
|
476
|
+
Raises
|
|
477
|
+
------
|
|
478
|
+
TagScriptError
|
|
479
|
+
A block intentionally raised an exception, most likely due to invalid user input.
|
|
480
|
+
WorkloadExceededError
|
|
481
|
+
Signifies the interpreter reached the character limit, if one was provided.
|
|
482
|
+
ProcessError
|
|
483
|
+
An unexpected error occurred while processing blocks.
|
|
484
|
+
"""
|
|
485
|
+
response = Response(variables=seed_variables, extras=kwargs)
|
|
486
|
+
node_ordered_list = build_node_tree(message)
|
|
487
|
+
try:
|
|
488
|
+
output = self._solve(
|
|
489
|
+
message,
|
|
490
|
+
node_ordered_list,
|
|
491
|
+
response,
|
|
492
|
+
charlimit=charlimit,
|
|
493
|
+
)
|
|
494
|
+
except TagScriptError:
|
|
495
|
+
raise
|
|
496
|
+
except Exception as error:
|
|
497
|
+
raise ProcessError(error, response, self) from error
|
|
498
|
+
return self._return_response(response, output)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class AsyncInterpreter(Interpreter):
|
|
502
|
+
"""
|
|
503
|
+
An asynchronous subclass of `Interpreter` that allows blocks to implement asynchronous methods.
|
|
504
|
+
Synchronous blocks are still supported.
|
|
505
|
+
This subclass has no additional attributes from the `Interpreter` class.
|
|
506
|
+
See `Interpreter` for full documentation.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
async def _process_blocks(self, ctx: Context, node: Node) -> Optional[str]:
|
|
510
|
+
"""
|
|
511
|
+
Process the blocks
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
ctx: Context
|
|
516
|
+
The context to process the blocks from.
|
|
517
|
+
node: Node
|
|
518
|
+
The node to process the blocks from.
|
|
519
|
+
|
|
520
|
+
Returns
|
|
521
|
+
-------
|
|
522
|
+
Optional[str]
|
|
523
|
+
The final message
|
|
524
|
+
"""
|
|
525
|
+
for b in self.blocks:
|
|
526
|
+
if not await maybe_await(b.will_accept, ctx):
|
|
527
|
+
continue
|
|
528
|
+
value = await maybe_await(b.process, ctx)
|
|
529
|
+
if value is not None: # Value found? We're done here.
|
|
530
|
+
value = str(value)
|
|
531
|
+
node.output = value
|
|
532
|
+
return value
|
|
533
|
+
|
|
534
|
+
async def _solve(
|
|
535
|
+
self,
|
|
536
|
+
message: str,
|
|
537
|
+
node_ordered_list: List[Node],
|
|
538
|
+
response: Response,
|
|
539
|
+
*,
|
|
540
|
+
charlimit: int,
|
|
541
|
+
verb_limit: int = 2000,
|
|
542
|
+
) -> Optional[str]:
|
|
543
|
+
"""
|
|
544
|
+
Solve the tagscript by proccessing all possible nodes.
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
message: str
|
|
549
|
+
The message to process.
|
|
550
|
+
node_ordered_list: List[Node]
|
|
551
|
+
The list of nodes to process.
|
|
552
|
+
response: Response
|
|
553
|
+
The response object to be passed to the context.
|
|
554
|
+
charlimit: int
|
|
555
|
+
The maximum number of characters to process.
|
|
556
|
+
verb_limit: int
|
|
557
|
+
The maximum number of verbs to process.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
Optional[str]
|
|
562
|
+
The final, completely processed message.
|
|
563
|
+
"""
|
|
564
|
+
final = message
|
|
565
|
+
total_work = 0
|
|
566
|
+
|
|
567
|
+
for index, node in enumerate(node_ordered_list):
|
|
568
|
+
start, end = node.coordinates
|
|
569
|
+
ctx = self._get_context(
|
|
570
|
+
node,
|
|
571
|
+
final,
|
|
572
|
+
response=response,
|
|
573
|
+
original_message=message,
|
|
574
|
+
verb_limit=verb_limit,
|
|
575
|
+
)
|
|
576
|
+
try:
|
|
577
|
+
output = await self._process_blocks(ctx, node)
|
|
578
|
+
except StopError as exc:
|
|
579
|
+
return final[:start] + exc.message
|
|
580
|
+
if output is None:
|
|
581
|
+
continue # If there was no value output, no need to text deform.
|
|
582
|
+
|
|
583
|
+
total_work = self._check_workload(charlimit, total_work, output)
|
|
584
|
+
final, differential = self._text_deform(start, end, final, output)
|
|
585
|
+
self._translate_nodes(node_ordered_list, index, start, differential)
|
|
586
|
+
return final
|
|
587
|
+
|
|
588
|
+
async def process(
|
|
589
|
+
self,
|
|
590
|
+
message: str,
|
|
591
|
+
seed_variables: AdapterDict = None,
|
|
592
|
+
*,
|
|
593
|
+
charlimit: Optional[int] = None,
|
|
594
|
+
**kwargs,
|
|
595
|
+
) -> Response:
|
|
596
|
+
"""
|
|
597
|
+
Asynchronously process a given TagScript string.
|
|
598
|
+
This method has no additional attributes from the `Interpreter` class.
|
|
599
|
+
See `Interpreter.process` for full documentation.
|
|
600
|
+
"""
|
|
601
|
+
response = Response(variables=seed_variables, extras=kwargs)
|
|
602
|
+
node_ordered_list = build_node_tree(message)
|
|
603
|
+
try:
|
|
604
|
+
output = await self._solve(
|
|
605
|
+
message,
|
|
606
|
+
node_ordered_list,
|
|
607
|
+
response,
|
|
608
|
+
charlimit=charlimit,
|
|
609
|
+
)
|
|
610
|
+
except TagScriptError:
|
|
611
|
+
raise
|
|
612
|
+
except Exception as error:
|
|
613
|
+
raise ProcessError(error, response, self) from error
|
|
614
|
+
return self._return_response(response, output)
|
bTagScript/utils.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from inspect import isawaitable
|
|
3
|
+
from typing import Any, Awaitable, Callable, Union
|
|
4
|
+
|
|
5
|
+
__all__ = ("escape_content", "maybe_await")
|
|
6
|
+
|
|
7
|
+
pattern = re.compile(r"(?<!\\)([{():|}])")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _sub_match(match: re.Match) -> str:
|
|
11
|
+
r"""
|
|
12
|
+
Check if the character has a \ in front of it
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
match: re.Match
|
|
17
|
+
The match object.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
str
|
|
22
|
+
The escaped character.
|
|
23
|
+
"""
|
|
24
|
+
return "\\" + match.group(1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def escape_content(string: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Escapes given input to avoid tampering with engine/block behavior.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
string: str
|
|
34
|
+
The string to escape.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
str
|
|
39
|
+
The escaped content.
|
|
40
|
+
"""
|
|
41
|
+
if string is None:
|
|
42
|
+
return
|
|
43
|
+
return pattern.sub(_sub_match, string)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def maybe_await(func: Union[Callable[..., Any], Awaitable[Any]], *args, **kwargs) -> Any:
|
|
47
|
+
"""
|
|
48
|
+
Await the given function if it is awaitable or call it synchronously.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
func: Union[Callable[..., Any], Awaitable[Any]]
|
|
53
|
+
The function callable to call.
|
|
54
|
+
*args: Any
|
|
55
|
+
The arguments to pass to the function.
|
|
56
|
+
**kwargs: Any
|
|
57
|
+
The keyword arguments to pass to the function.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Any
|
|
62
|
+
The result of the awaitable function.
|
|
63
|
+
"""
|
|
64
|
+
value = func(*args, **kwargs)
|
|
65
|
+
return await value if isawaitable(value) else value
|