]>
Commit | Line | Data |
---|---|---|
19a03940 | 1 | import collections |
2 | import contextlib | |
825abb81 | 3 | import json |
9e3f1991 | 4 | import operator |
2b25cb5d PH |
5 | import re |
6 | ||
a1c5bd82 | 7 | from .utils import ExtractorError, remove_quotes, truncate_string |
2b25cb5d | 8 | |
230d5c82 | 9 | _NAME_RE = r'[a-zA-Z_$][\w$]*' |
10 | _OPERATORS = { | |
11 | '|': operator.or_, | |
12 | '^': operator.xor, | |
13 | '&': operator.and_, | |
14 | '>>': operator.rshift, | |
15 | '<<': operator.lshift, | |
16 | '-': operator.sub, | |
17 | '+': operator.add, | |
18 | '%': operator.mod, | |
19 | '/': operator.truediv, | |
20 | '*': operator.mul, | |
21 | } | |
9e3f1991 | 22 | |
06dfe0a0 | 23 | _MATCHING_PARENS = dict(zip('({[', ')}]')) |
64fa820c | 24 | _QUOTES = '\'"' |
06dfe0a0 | 25 | |
2b25cb5d | 26 | |
404f611f | 27 | class JS_Break(ExtractorError): |
28 | def __init__(self): | |
29 | ExtractorError.__init__(self, 'Invalid break') | |
30 | ||
31 | ||
32 | class JS_Continue(ExtractorError): | |
33 | def __init__(self): | |
34 | ExtractorError.__init__(self, 'Invalid continue') | |
35 | ||
36 | ||
19a03940 | 37 | class LocalNameSpace(collections.ChainMap): |
404f611f | 38 | def __setitem__(self, key, value): |
19a03940 | 39 | for scope in self.maps: |
404f611f | 40 | if key in scope: |
41 | scope[key] = value | |
19a03940 | 42 | return |
43 | self.maps[0][key] = value | |
404f611f | 44 | |
45 | def __delitem__(self, key): | |
46 | raise NotImplementedError('Deleting is not supported') | |
47 | ||
404f611f | 48 | |
86e5f3ed | 49 | class JSInterpreter: |
230d5c82 | 50 | __named_object_counter = 0 |
51 | ||
9e3f1991 | 52 | def __init__(self, code, objects=None): |
230d5c82 | 53 | self.code, self._functions = code, {} |
54 | self._objects = {} if objects is None else objects | |
404f611f | 55 | |
a1c5bd82 | 56 | class Exception(ExtractorError): |
57 | def __init__(self, msg, expr=None, *args, **kwargs): | |
58 | if expr is not None: | |
59 | msg += f' in: {truncate_string(expr, 50, 50)}' | |
60 | super().__init__(msg, *args, **kwargs) | |
61 | ||
404f611f | 62 | def _named_object(self, namespace, obj): |
63 | self.__named_object_counter += 1 | |
64 | name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}' | |
65 | namespace[name] = obj | |
66 | return name | |
67 | ||
68 | @staticmethod | |
e75bb0d6 | 69 | def _separate(expr, delim=',', max_split=None): |
404f611f | 70 | if not expr: |
71 | return | |
06dfe0a0 | 72 | counters = {k: 0 for k in _MATCHING_PARENS.values()} |
73 | start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1 | |
64fa820c | 74 | in_quote, escaping = None, False |
404f611f | 75 | for idx, char in enumerate(expr): |
06dfe0a0 | 76 | if char in _MATCHING_PARENS: |
77 | counters[_MATCHING_PARENS[char]] += 1 | |
78 | elif char in counters: | |
79 | counters[char] -= 1 | |
64fa820c | 80 | elif not escaping and char in _QUOTES and in_quote in (char, None): |
81 | in_quote = None if in_quote else char | |
82 | escaping = not escaping and in_quote and char == '\\' | |
83 | ||
84 | if char != delim[pos] or any(counters.values()) or in_quote: | |
404f611f | 85 | pos = 0 |
06dfe0a0 | 86 | continue |
87 | elif pos != delim_len: | |
88 | pos += 1 | |
89 | continue | |
90 | yield expr[start: idx - delim_len] | |
91 | start, pos = idx + 1, 0 | |
92 | splits += 1 | |
93 | if max_split and splits >= max_split: | |
94 | break | |
404f611f | 95 | yield expr[start:] |
96 | ||
230d5c82 | 97 | @classmethod |
98 | def _separate_at_paren(cls, expr, delim): | |
99 | separated = list(cls._separate(expr, delim, 1)) | |
e75bb0d6 | 100 | if len(separated) < 2: |
a1c5bd82 | 101 | raise cls.Exception(f'No terminating paren {delim}', expr) |
e75bb0d6 | 102 | return separated[0][1:].strip(), separated[1].strip() |
9e3f1991 | 103 | |
9e3f1991 | 104 | def interpret_statement(self, stmt, local_vars, allow_recursion=100): |
2b25cb5d | 105 | if allow_recursion < 0: |
a1c5bd82 | 106 | raise self.Exception('Recursion limit reached') |
2b25cb5d | 107 | |
230d5c82 | 108 | should_abort = False |
109 | sub_statements = list(self._separate(stmt, ';')) or [''] | |
110 | stmt = sub_statements.pop().lstrip() | |
111 | ||
404f611f | 112 | for sub_stmt in sub_statements: |
113 | ret, should_abort = self.interpret_statement(sub_stmt, local_vars, allow_recursion - 1) | |
114 | if should_abort: | |
230d5c82 | 115 | return ret, should_abort |
404f611f | 116 | |
230d5c82 | 117 | m = re.match(r'(?P<var>var\s)|return(?:\s+|$)', stmt) |
118 | if not m: # Try interpreting it as an expression | |
119 | expr = stmt | |
120 | elif m.group('var'): | |
121 | expr = stmt[len(m.group(0)):] | |
2b25cb5d | 122 | else: |
230d5c82 | 123 | expr = stmt[len(m.group(0)):] |
124 | should_abort = True | |
2b25cb5d | 125 | |
230d5c82 | 126 | return self.interpret_expression(expr, local_vars, allow_recursion), should_abort |
2b25cb5d PH |
127 | |
128 | def interpret_expression(self, expr, local_vars, allow_recursion): | |
9e3f1991 | 129 | expr = expr.strip() |
230d5c82 | 130 | if not expr: |
9e3f1991 PH |
131 | return None |
132 | ||
404f611f | 133 | if expr.startswith('{'): |
e75bb0d6 | 134 | inner, outer = self._separate_at_paren(expr, '}') |
404f611f | 135 | inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion - 1) |
136 | if not outer or should_abort: | |
137 | return inner | |
138 | else: | |
139 | expr = json.dumps(inner) + outer | |
140 | ||
9e3f1991 | 141 | if expr.startswith('('): |
e75bb0d6 | 142 | inner, outer = self._separate_at_paren(expr, ')') |
404f611f | 143 | inner = self.interpret_expression(inner, local_vars, allow_recursion) |
144 | if not outer: | |
145 | return inner | |
146 | else: | |
147 | expr = json.dumps(inner) + outer | |
148 | ||
149 | if expr.startswith('['): | |
e75bb0d6 | 150 | inner, outer = self._separate_at_paren(expr, ']') |
404f611f | 151 | name = self._named_object(local_vars, [ |
152 | self.interpret_expression(item, local_vars, allow_recursion) | |
e75bb0d6 | 153 | for item in self._separate(inner)]) |
404f611f | 154 | expr = name + outer |
155 | ||
230d5c82 | 156 | m = re.match(r'(?P<try>try)\s*|(?:(?P<catch>catch)|(?P<for>for)|(?P<switch>switch))\s*\(', expr) |
157 | if m and m.group('try'): | |
404f611f | 158 | if expr[m.end()] == '{': |
e75bb0d6 | 159 | try_expr, expr = self._separate_at_paren(expr[m.end():], '}') |
404f611f | 160 | else: |
161 | try_expr, expr = expr[m.end() - 1:], '' | |
162 | ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion - 1) | |
163 | if should_abort: | |
164 | return ret | |
165 | return self.interpret_statement(expr, local_vars, allow_recursion - 1)[0] | |
166 | ||
230d5c82 | 167 | elif m and m.group('catch'): |
404f611f | 168 | # We ignore the catch block |
e75bb0d6 | 169 | _, expr = self._separate_at_paren(expr, '}') |
404f611f | 170 | return self.interpret_statement(expr, local_vars, allow_recursion - 1)[0] |
171 | ||
230d5c82 | 172 | elif m and m.group('for'): |
e75bb0d6 | 173 | constructor, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') |
404f611f | 174 | if remaining.startswith('{'): |
e75bb0d6 | 175 | body, expr = self._separate_at_paren(remaining, '}') |
404f611f | 176 | else: |
230d5c82 | 177 | switch_m = re.match(r'switch\s*\(', remaining) # FIXME |
178 | if switch_m: | |
179 | switch_val, remaining = self._separate_at_paren(remaining[switch_m.end() - 1:], ')') | |
e75bb0d6 | 180 | body, expr = self._separate_at_paren(remaining, '}') |
404f611f | 181 | body = 'switch(%s){%s}' % (switch_val, body) |
9e3f1991 | 182 | else: |
404f611f | 183 | body, expr = remaining, '' |
e75bb0d6 | 184 | start, cndn, increment = self._separate(constructor, ';') |
404f611f | 185 | if self.interpret_statement(start, local_vars, allow_recursion - 1)[1]: |
a1c5bd82 | 186 | raise self.Exception('Premature return in the initialization of a for loop', constructor) |
404f611f | 187 | while True: |
188 | if not self.interpret_expression(cndn, local_vars, allow_recursion): | |
189 | break | |
190 | try: | |
191 | ret, should_abort = self.interpret_statement(body, local_vars, allow_recursion - 1) | |
192 | if should_abort: | |
193 | return ret | |
194 | except JS_Break: | |
195 | break | |
196 | except JS_Continue: | |
197 | pass | |
198 | if self.interpret_statement(increment, local_vars, allow_recursion - 1)[1]: | |
a1c5bd82 | 199 | raise self.Exception('Premature return in the initialization of a for loop', constructor) |
404f611f | 200 | return self.interpret_statement(expr, local_vars, allow_recursion - 1)[0] |
201 | ||
230d5c82 | 202 | elif m and m.group('switch'): |
e75bb0d6 | 203 | switch_val, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') |
404f611f | 204 | switch_val = self.interpret_expression(switch_val, local_vars, allow_recursion) |
e75bb0d6 | 205 | body, expr = self._separate_at_paren(remaining, '}') |
a1fc7ca0 | 206 | items = body.replace('default:', 'case default:').split('case ')[1:] |
207 | for default in (False, True): | |
208 | matched = False | |
209 | for item in items: | |
86e5f3ed | 210 | case, stmt = (i.strip() for i in self._separate(item, ':', 1)) |
a1fc7ca0 | 211 | if default: |
212 | matched = matched or case == 'default' | |
213 | elif not matched: | |
214 | matched = case != 'default' and switch_val == self.interpret_expression(case, local_vars, allow_recursion) | |
215 | if not matched: | |
216 | continue | |
404f611f | 217 | try: |
218 | ret, should_abort = self.interpret_statement(stmt, local_vars, allow_recursion - 1) | |
219 | if should_abort: | |
220 | return ret | |
221 | except JS_Break: | |
9e3f1991 | 222 | break |
a1fc7ca0 | 223 | if matched: |
224 | break | |
404f611f | 225 | return self.interpret_statement(expr, local_vars, allow_recursion - 1)[0] |
226 | ||
e75bb0d6 U |
227 | # Comma separated statements |
228 | sub_expressions = list(self._separate(expr)) | |
404f611f | 229 | expr = sub_expressions.pop().strip() if sub_expressions else '' |
230 | for sub_expr in sub_expressions: | |
231 | self.interpret_expression(sub_expr, local_vars, allow_recursion) | |
232 | ||
233 | for m in re.finditer(rf'''(?x) | |
234 | (?P<pre_sign>\+\+|--)(?P<var1>{_NAME_RE})| | |
235 | (?P<var2>{_NAME_RE})(?P<post_sign>\+\+|--)''', expr): | |
236 | var = m.group('var1') or m.group('var2') | |
237 | start, end = m.span() | |
238 | sign = m.group('pre_sign') or m.group('post_sign') | |
239 | ret = local_vars[var] | |
240 | local_vars[var] += 1 if sign[0] == '+' else -1 | |
241 | if m.group('pre_sign'): | |
242 | ret = local_vars[var] | |
243 | expr = expr[:start] + json.dumps(ret) + expr[end:] | |
9e3f1991 | 244 | |
230d5c82 | 245 | if not expr: |
246 | return None | |
9e3f1991 | 247 | |
230d5c82 | 248 | m = re.match(fr'''(?x) |
249 | (?P<assign> | |
250 | (?P<out>{_NAME_RE})(?:\[(?P<index>[^\]]+?)\])?\s* | |
251 | (?P<op>{"|".join(map(re.escape, _OPERATORS))})? | |
252 | =(?P<expr>.*)$ | |
253 | )|(?P<return> | |
254 | (?!if|return|true|false|null)(?P<name>{_NAME_RE})$ | |
255 | )|(?P<indexing> | |
256 | (?P<in>{_NAME_RE})\[(?P<idx>.+)\]$ | |
257 | )|(?P<attribute> | |
258 | (?P<var>{_NAME_RE})(?:\.(?P<member>[^(]+)|\[(?P<member2>[^\]]+)\])\s* | |
259 | )|(?P<function> | |
260 | (?P<fname>{_NAME_RE})\((?P<args>[\w$,]*)\)$ | |
261 | )''', expr) | |
262 | if m and m.group('assign'): | |
263 | if not m.group('op'): | |
264 | opfunc = lambda curr, right: right | |
9e3f1991 | 265 | else: |
230d5c82 | 266 | opfunc = _OPERATORS[m.group('op')] |
267 | right_val = self.interpret_expression(m.group('expr'), local_vars, allow_recursion) | |
268 | left_val = local_vars.get(m.group('out')) | |
269 | ||
270 | if not m.group('index'): | |
271 | local_vars[m.group('out')] = opfunc(left_val, right_val) | |
272 | return local_vars[m.group('out')] | |
273 | elif left_val is None: | |
a1c5bd82 | 274 | raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) |
230d5c82 | 275 | |
276 | idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) | |
277 | if not isinstance(idx, int): | |
a1c5bd82 | 278 | raise self.Exception(f'List index {idx} must be integer', expr) |
230d5c82 | 279 | left_val[idx] = opfunc(left_val[idx], right_val) |
280 | return left_val[idx] | |
9e3f1991 | 281 | |
230d5c82 | 282 | elif expr.isdigit(): |
2b25cb5d PH |
283 | return int(expr) |
284 | ||
230d5c82 | 285 | elif expr == 'break': |
404f611f | 286 | raise JS_Break() |
287 | elif expr == 'continue': | |
288 | raise JS_Continue() | |
289 | ||
230d5c82 | 290 | elif m and m.group('return'): |
291 | return local_vars[m.group('name')] | |
2b25cb5d | 292 | |
19a03940 | 293 | with contextlib.suppress(ValueError): |
825abb81 | 294 | return json.loads(expr) |
825abb81 | 295 | |
230d5c82 | 296 | if m and m.group('indexing'): |
7769f837 | 297 | val = local_vars[m.group('in')] |
404f611f | 298 | idx = self.interpret_expression(m.group('idx'), local_vars, allow_recursion) |
7769f837 S |
299 | return val[idx] |
300 | ||
230d5c82 | 301 | for op, opfunc in _OPERATORS.items(): |
e75bb0d6 U |
302 | separated = list(self._separate(expr, op)) |
303 | if len(separated) < 2: | |
404f611f | 304 | continue |
e75bb0d6 U |
305 | right_val = separated.pop() |
306 | left_val = op.join(separated) | |
404f611f | 307 | left_val, should_abort = self.interpret_statement( |
308 | left_val, local_vars, allow_recursion - 1) | |
309 | if should_abort: | |
a1c5bd82 | 310 | raise self.Exception(f'Premature left-side return of {op}', expr) |
404f611f | 311 | right_val, should_abort = self.interpret_statement( |
312 | right_val, local_vars, allow_recursion - 1) | |
313 | if should_abort: | |
a1c5bd82 | 314 | raise self.Exception(f'Premature right-side return of {op}', expr) |
404f611f | 315 | return opfunc(left_val or 0, right_val) |
316 | ||
230d5c82 | 317 | if m and m.group('attribute'): |
825abb81 | 318 | variable = m.group('var') |
7769f837 | 319 | member = remove_quotes(m.group('member') or m.group('member2')) |
404f611f | 320 | arg_str = expr[m.end():] |
321 | if arg_str.startswith('('): | |
e75bb0d6 | 322 | arg_str, remaining = self._separate_at_paren(arg_str, ')') |
825abb81 | 323 | else: |
404f611f | 324 | arg_str, remaining = None, arg_str |
325 | ||
326 | def assertion(cndn, msg): | |
327 | """ assert, but without risk of getting optimized out """ | |
328 | if not cndn: | |
a1c5bd82 | 329 | raise self.Exception(f'{member} {msg}', expr) |
404f611f | 330 | |
331 | def eval_method(): | |
404f611f | 332 | if variable == 'String': |
333 | obj = str | |
334 | elif variable in local_vars: | |
335 | obj = local_vars[variable] | |
336 | else: | |
337 | if variable not in self._objects: | |
338 | self._objects[variable] = self.extract_object(variable) | |
339 | obj = self._objects[variable] | |
340 | ||
230d5c82 | 341 | # Member access |
404f611f | 342 | if arg_str is None: |
404f611f | 343 | if member == 'length': |
344 | return len(obj) | |
345 | return obj[member] | |
346 | ||
347 | # Function call | |
348 | argvals = [ | |
825abb81 | 349 | self.interpret_expression(v, local_vars, allow_recursion) |
e75bb0d6 | 350 | for v in self._separate(arg_str)] |
404f611f | 351 | |
352 | if obj == str: | |
353 | if member == 'fromCharCode': | |
354 | assertion(argvals, 'takes one or more arguments') | |
355 | return ''.join(map(chr, argvals)) | |
a1c5bd82 | 356 | raise self.Exception(f'Unsupported string method {member}', expr) |
404f611f | 357 | |
358 | if member == 'split': | |
359 | assertion(argvals, 'takes one or more arguments') | |
360 | assertion(argvals == [''], 'with arguments is not implemented') | |
361 | return list(obj) | |
362 | elif member == 'join': | |
363 | assertion(isinstance(obj, list), 'must be applied on a list') | |
364 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
365 | return argvals[0].join(obj) | |
366 | elif member == 'reverse': | |
367 | assertion(not argvals, 'does not take any arguments') | |
368 | obj.reverse() | |
369 | return obj | |
370 | elif member == 'slice': | |
371 | assertion(isinstance(obj, list), 'must be applied on a list') | |
372 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
373 | return obj[argvals[0]:] | |
374 | elif member == 'splice': | |
375 | assertion(isinstance(obj, list), 'must be applied on a list') | |
376 | assertion(argvals, 'takes one or more arguments') | |
57dbe807 | 377 | index, howMany = map(int, (argvals + [len(obj)])[:2]) |
404f611f | 378 | if index < 0: |
379 | index += len(obj) | |
380 | add_items = argvals[2:] | |
381 | res = [] | |
382 | for i in range(index, min(index + howMany, len(obj))): | |
383 | res.append(obj.pop(index)) | |
384 | for i, item in enumerate(add_items): | |
385 | obj.insert(index + i, item) | |
386 | return res | |
387 | elif member == 'unshift': | |
388 | assertion(isinstance(obj, list), 'must be applied on a list') | |
389 | assertion(argvals, 'takes one or more arguments') | |
390 | for item in reversed(argvals): | |
391 | obj.insert(0, item) | |
392 | return obj | |
393 | elif member == 'pop': | |
394 | assertion(isinstance(obj, list), 'must be applied on a list') | |
395 | assertion(not argvals, 'does not take any arguments') | |
396 | if not obj: | |
397 | return | |
398 | return obj.pop() | |
399 | elif member == 'push': | |
400 | assertion(argvals, 'takes one or more arguments') | |
401 | obj.extend(argvals) | |
402 | return obj | |
403 | elif member == 'forEach': | |
404 | assertion(argvals, 'takes one or more arguments') | |
405 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
406 | f, this = (argvals + [''])[:2] | |
407 | return [f((item, idx, obj), this=this) for idx, item in enumerate(obj)] | |
408 | elif member == 'indexOf': | |
409 | assertion(argvals, 'takes one or more arguments') | |
410 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
411 | idx, start = (argvals + [0])[:2] | |
412 | try: | |
413 | return obj.index(idx, start) | |
414 | except ValueError: | |
415 | return -1 | |
416 | ||
230d5c82 | 417 | return obj[int(member) if isinstance(obj, list) else member](argvals) |
404f611f | 418 | |
419 | if remaining: | |
420 | return self.interpret_expression( | |
421 | self._named_object(local_vars, eval_method()) + remaining, | |
422 | local_vars, allow_recursion) | |
423 | else: | |
424 | return eval_method() | |
2b25cb5d | 425 | |
230d5c82 | 426 | elif m and m.group('function'): |
427 | fname = m.group('fname') | |
86e5f3ed | 428 | argvals = tuple( |
825abb81 | 429 | int(v) if v.isdigit() else local_vars[v] |
86e5f3ed | 430 | for v in self._separate(m.group('args'))) |
404f611f | 431 | if fname in local_vars: |
432 | return local_vars[fname](argvals) | |
433 | elif fname not in self._functions: | |
1f749b66 | 434 | self._functions[fname] = self.extract_function(fname) |
2b25cb5d | 435 | return self._functions[fname](argvals) |
9e3f1991 | 436 | |
a1c5bd82 | 437 | raise self.Exception('Unsupported JS expression', expr) |
2b25cb5d | 438 | |
ad25aee2 | 439 | def extract_object(self, objname): |
7769f837 | 440 | _FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')''' |
ad25aee2 JMF |
441 | obj = {} |
442 | obj_m = re.search( | |
0e2d626d S |
443 | r'''(?x) |
444 | (?<!this\.)%s\s*=\s*{\s* | |
445 | (?P<fields>(%s\s*:\s*function\s*\(.*?\)\s*{.*?}(?:,\s*)?)*) | |
446 | }\s*; | |
447 | ''' % (re.escape(objname), _FUNC_NAME_RE), | |
ad25aee2 JMF |
448 | self.code) |
449 | fields = obj_m.group('fields') | |
450 | # Currently, it only supports function definitions | |
451 | fields_m = re.finditer( | |
0e2d626d S |
452 | r'''(?x) |
453 | (?P<key>%s)\s*:\s*function\s*\((?P<args>[a-z,]+)\){(?P<code>[^}]+)} | |
454 | ''' % _FUNC_NAME_RE, | |
ad25aee2 JMF |
455 | fields) |
456 | for f in fields_m: | |
457 | argnames = f.group('args').split(',') | |
7769f837 | 458 | obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code')) |
ad25aee2 JMF |
459 | |
460 | return obj | |
461 | ||
404f611f | 462 | def extract_function_code(self, funcname): |
463 | """ @returns argnames, code """ | |
2b25cb5d | 464 | func_m = re.search( |
9e3f1991 | 465 | r'''(?x) |
230d5c82 | 466 | (?: |
467 | function\s+%(name)s| | |
468 | [{;,]\s*%(name)s\s*=\s*function| | |
469 | var\s+%(name)s\s*=\s*function | |
470 | )\s* | |
9e3f1991 | 471 | \((?P<args>[^)]*)\)\s* |
230d5c82 | 472 | (?P<code>{(?:(?!};)[^"]|"([^"]|\\")*")+})''' % {'name': re.escape(funcname)}, |
2b25cb5d | 473 | self.code) |
e75bb0d6 | 474 | code, _ = self._separate_at_paren(func_m.group('code'), '}') # refine the match |
77ffa957 | 475 | if func_m is None: |
a1c5bd82 | 476 | raise self.Exception(f'Could not find JS function "{funcname}"') |
404f611f | 477 | return func_m.group('args').split(','), code |
2b25cb5d | 478 | |
404f611f | 479 | def extract_function(self, funcname): |
480 | return self.extract_function_from_code(*self.extract_function_code(funcname)) | |
481 | ||
482 | def extract_function_from_code(self, argnames, code, *global_stack): | |
483 | local_vars = {} | |
484 | while True: | |
485 | mobj = re.search(r'function\((?P<args>[^)]*)\)\s*{', code) | |
486 | if mobj is None: | |
487 | break | |
488 | start, body_start = mobj.span() | |
e75bb0d6 | 489 | body, remaining = self._separate_at_paren(code[body_start - 1:], '}') |
230d5c82 | 490 | name = self._named_object(local_vars, self.extract_function_from_code( |
491 | [x.strip() for x in mobj.group('args').split(',')], | |
492 | body, local_vars, *global_stack)) | |
404f611f | 493 | code = code[:start] + name + remaining |
494 | return self.build_function(argnames, code, local_vars, *global_stack) | |
ad25aee2 | 495 | |
9e3f1991 | 496 | def call_function(self, funcname, *args): |
404f611f | 497 | return self.extract_function(funcname)(args) |
498 | ||
499 | def build_function(self, argnames, code, *global_stack): | |
500 | global_stack = list(global_stack) or [{}] | |
404f611f | 501 | |
502 | def resf(args, **kwargs): | |
19a03940 | 503 | global_stack[0].update({ |
404f611f | 504 | **dict(zip(argnames, args)), |
505 | **kwargs | |
506 | }) | |
19a03940 | 507 | var_stack = LocalNameSpace(*global_stack) |
e75bb0d6 | 508 | for stmt in self._separate(code.replace('\n', ''), ';'): |
404f611f | 509 | ret, should_abort = self.interpret_statement(stmt, var_stack) |
510 | if should_abort: | |
9e3f1991 | 511 | break |
404f611f | 512 | return ret |
2b25cb5d | 513 | return resf |