Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Added ES7 support
  • Loading branch information
OpenSourceJesus committed Jun 24, 2026
commit 121a8081145766f0f71717c05ca03235340789bd
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ ROOT := $(CURDIR)

export PYTHONPATH := $(ROOT)

.PHONY: help test test-simple test-es6 test-language test-all
.PHONY: help test test-simple test-es6 test-es7 test-language test-all

help:
@echo "Js2Py test targets:"
@echo " make test Run quick integration tests (default)"
@echo " make test-simple Run simple_test.py (ES5 + ES6 smoke tests)"
@echo " make test-es6 Run tests/test_es6.py"
@echo " make test-es7 Run tests/test_es7.py"
@echo " make test-language Run ES5.1 language suite (tests/run.py, slow)"
@echo " make test-all Run quick tests and the language suite"

test: test-simple test-es6
test: test-simple test-es6 test-es7
@:

test-simple:
Expand All @@ -23,6 +24,9 @@ test-simple:
test-es6:
PYTHONPATH="$(ROOT)" $(PYTHON) "$(ROOT)/tests/test_es6.py"

test-es7:
PYTHONPATH="$(ROOT)" $(PYTHON) "$(ROOT)/tests/test_es7.py"

test-language:
@test -f "$(ROOT)/tests/node_failed.txt" || touch "$(ROOT)/tests/node_failed.txt"
cd "$(ROOT)/tests" && PYTHONPATH="$(ROOT)" $(PYTHON) run.py
Expand Down
2 changes: 1 addition & 1 deletion js2py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
__all__ = [
'EvalJs', 'translate_js', 'import_js', 'eval_js', 'parse_js',
'translate_file', 'run_file', 'disable_pyimport', 'eval_js6',
'translate_js6', 'PyJsException', 'get_file_contents',
'translate_js6', 'eval_js7', 'translate_js7', 'PyJsException', 'get_file_contents',
'write_file_contents', 'require'
]

Expand Down
113 changes: 113 additions & 0 deletions js2py/es7/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""ES7 (ES2016) support: exponentiation operator and related syntax detection."""

import re

_ES7_SYNTAX_RE = re.compile(
r'(?:'
r'(?<![*/])\*\*(?!\*)|' # ** but not */ or ***
r'\.includes\s*\(' # Array.prototype.includes
r')',
re.MULTILINE)

_PATCHED = False


def looks_like_es7(code):
"""Return True if source likely contains ES7 syntax."""
return bool(_ES7_SYNTAX_RE.search(code))


def ensure_pyjsparser_es7():
"""Patch pyjsparser once to parse ** with correct right-associativity."""
global _PATCHED
if _PATCHED:
return
import pyjsparser.parser as parser
import pyjsparser.pyjsparserdata as data
from pyjsparser.pyjsparserdata import Token

data.PRECEDENCE['**'] = 12

_orig_scan = parser.PyJsParser.scanPunctuator

def scan_punctuator(self):
if (self.index < self.length - 1
and self.source[self.index:self.index + 2] == '**'):
start = self.index
self.index += 2
return {
'type': Token.Punctuator,
'value': '**',
'lineNumber': self.lineNumber,
'lineStart': self.lineStart,
'start': start,
'end': self.index,
}
return _orig_scan(self)

def _should_reduce(new_prec, stack_prec, stack_op, new_op):
if new_prec < stack_prec:
return True
if new_prec > stack_prec:
return False
if stack_op == '**' or new_op == '**':
return False
return True

_orig_parse_binary = parser.PyJsParser.parseBinaryExpression

def parse_binary_expression(self):
marker = self.lookahead
left = self.inheritCoverGrammar(self.parseUnaryExpression)

token = self.lookahead
prec = self.binaryPrecedence(token, self.state['allowIn'])
if prec == 0:
return left
self.isAssignmentTarget = self.isBindingElement = parser.false
token['prec'] = prec
self.lex()

markers = [marker, self.lookahead]
right = self.isolateCoverGrammar(self.parseUnaryExpression)

stack = [left, token, right]

while True:
prec = self.binaryPrecedence(self.lookahead, self.state['allowIn'])
if not prec > 0:
break
new_op = self.lookahead['value']
while len(stack) > 2:
stack_prec = stack[len(stack) - 2]['prec']
stack_op = stack[len(stack) - 2]['value']
if not _should_reduce(prec, stack_prec, stack_op, new_op):
break
right = stack.pop()
operator = stack.pop()['value']
left = stack.pop()
markers.pop()
expr = parser.WrappingNode(
markers[len(markers) - 1]).finishBinaryExpression(
operator, left, right)
stack.append(expr)

token = self.lex()
token['prec'] = prec
stack.append(token)
markers.append(self.lookahead)
expr = self.isolateCoverGrammar(self.parseUnaryExpression)
stack.append(expr)

i = len(stack) - 1
expr = stack[i]
markers.pop()
while i > 1:
expr = parser.WrappingNode(markers.pop()).finishBinaryExpression(
stack[i - 1]['value'], stack[i - 2], expr)
i -= 2
return expr

parser.PyJsParser.scanPunctuator = scan_punctuator
parser.PyJsParser.parseBinaryExpression = parse_binary_expression
_PATCHED = True
40 changes: 27 additions & 13 deletions js2py/evaljs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

__all__ = [
'EvalJs', 'translate_js', 'import_js', 'eval_js', 'translate_file',
'eval_js6', 'translate_js6', 'run_file', 'disable_pyimport',
'get_file_contents', 'write_file_contents'
'eval_js6', 'translate_js6', 'eval_js7', 'translate_js7', 'run_file',
'disable_pyimport', 'get_file_contents', 'write_file_contents'
]
DEBUG = False

Expand Down Expand Up @@ -57,12 +57,13 @@ def write_file_contents(path_or_file, contents):
f.write(contents)


def translate_file(input_path, output_path, es6=False):
def translate_file(input_path, output_path, es6=False, es7=False):
'''
Translates input JS file to python and saves the it to the output path.
It appends some convenience code at the end so that it is easy to import JS objects.

es6: False, True, or 'auto' — transpile ES6 via Babel before translation.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

For example we have a file 'example.js' with: var a = function(x) {return x}
translate_file('example.js', 'example.py')
Expand All @@ -74,7 +75,7 @@ def translate_file(input_path, output_path, es6=False):
'''
js = get_file_contents(input_path)

py_code = translate_js(js, es6=es6)
py_code = translate_js(js, es6=es6, es7=es7)
lib_name = os.path.basename(output_path).split('.')[0]
head = '__all__ = [%s]\n\n# Don\'t look below, you will not understand this Python code :) I don\'t.\n\n' % repr(
lib_name)
Expand All @@ -94,12 +95,13 @@ def run_file(path_or_file, context=None):
return eval_value, context


def eval_js(js, es6=False):
def eval_js(js, es6=False, es7=False):
"""Just like javascript eval. Translates javascript to python,
executes and returns python object.
js is javascript source code

es6: False, True, or 'auto' — see translate_js.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

EXAMPLE:
>>> import js2py
Expand All @@ -116,17 +118,27 @@ def eval_js(js, es6=False):
If you really want to convert object to python dict you can use to_dict method.
"""
e = EvalJs()
return e.eval(js, es6=es6)
return e.eval(js, es6=es6, es7=es7)


def eval_js6(js):
"""Just like eval_js but with experimental support for js6 via babel."""
return eval_js(_prepare_js_source(js, True))
return eval_js(_prepare_js_source(js, es6=True))


def translate_js6(js):
"""Just like translate_js but with experimental support for js6 via babel."""
return translate_js(_prepare_js_source(js, True))
return translate_js(_prepare_js_source(js, es6=True))


def eval_js7(js):
"""Like eval_js with ES7 (ES2016) support enabled."""
return eval_js(js, es7=True)


def translate_js7(js):
"""Like translate_js with ES7 (ES2016) support enabled."""
return translate_js(js, es7=True)


class EvalJs(object):
Expand Down Expand Up @@ -175,10 +187,11 @@ def _js_require_impl(npm_module_name):
for k, v in six.iteritems(context):
setattr(self._var, k, v)

def execute(self, js=None, use_compilation_plan=False, es6=False):
def execute(self, js=None, use_compilation_plan=False, es6=False, es7=False):
"""executes javascript js in current context

es6: False, True, or 'auto' — transpile ES6 via Babel before translation.
es7: False, True, or 'auto' — enable ES2016 features (e.g. **).

During initial execute() the converted js is cached for re-use. That means next time you
run the same javascript snippet you save many instructions needed to parse and convert the
Expand All @@ -194,19 +207,20 @@ def execute(self, js=None, use_compilation_plan=False, es6=False):
cache = self.__dict__['cache']
except KeyError:
cache = self.__dict__['cache'] = {}
cache_key = (hashlib.md5(js.encode('utf-8')).digest(), es6)
cache_key = (hashlib.md5(js.encode('utf-8')).digest(), es6, es7)
try:
compiled = cache[cache_key]
except KeyError:
code = translate_js(
js, '', use_compilation_plan=use_compilation_plan, es6=es6)
js, '', use_compilation_plan=use_compilation_plan, es6=es6,
es7=es7)
compiled = cache[cache_key] = compile(code, '<EvalJS snippet>',
'exec')
exec (compiled, self._context)

def eval(self, expression, use_compilation_plan=False, es6=False):
def eval(self, expression, use_compilation_plan=False, es6=False, es7=False):
"""evaluates expression in current context and returns its value"""
expression = _prepare_js_source(expression, es6)
expression = _prepare_js_source(expression, es6, es7)
code = 'PyJsEvalResult = eval(%s)' % json.dumps(expression)
self.execute(code, use_compilation_plan=use_compilation_plan)
return self['PyJsEvalResult']
Expand Down
26 changes: 26 additions & 0 deletions js2py/prototypes/jsarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,32 @@ def indexOf(searchElement):
k += 1
return -1

def includes(searchElement):
array = this.to_object()
arr_len = array.get('length').to_uint32()
if arr_len == 0:
return False
if len(arguments) > 1:
n = arguments[1].to_int()
else:
n = 0
if n >= arr_len:
return False
if n >= 0:
k = n
else:
k = arr_len - abs(n)
if k < 0:
k = 0
while k < arr_len:
if array.has_property(str(k)):
elementK = array.get(str(k))
if (searchElement.is_nan() and elementK.is_nan()
or searchElement.strict_equality_comparison(elementK)):
return True
k += 1
return False

def lastIndexOf(searchElement):
array = this.to_object()
arr_len = array.get('length').to_uint32()
Expand Down
6 changes: 5 additions & 1 deletion js2py/translators/friendly_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ def js_mod(a, b):
return '(' + a + '%' + b + ')'


def js_pow(a, b):
return 'Js((%s).to_number().value ** (%s).to_number().value)' % (a, b)


def js_typeof(a):
cand = list(bracket_split(a, ('()', )))
if len(cand) == 2 and cand[0] == 'var.get':
Expand Down Expand Up @@ -347,7 +351,7 @@ def js_post_dec(a):

ADDS = {'+': js_add, '-': js_sub}

MULTS = {'*': js_mul, '/': js_div, '%': js_mod}
MULTS = {'*': js_mul, '/': js_div, '%': js_mod, '**': js_pow}
BINARY = {}
BINARY.update(ADDS)
BINARY.update(MULTS)
Expand Down
25 changes: 20 additions & 5 deletions js2py/translators/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
js6_to_js5 = None
looks_like_es6 = None

try:
from ..es7 import looks_like_es7, ensure_pyjsparser_es7
except ImportError:
looks_like_es7 = None
ensure_pyjsparser_es7 = None

# Enable Js2Py exceptions and pyimport in parser
pyjsparser.parser.ENABLE_PYIMPORT = True

Expand Down Expand Up @@ -67,13 +73,21 @@ def pyjsparser_parse_fn(code):
parser = pyjsparser.PyJsParser()
return parser.parse(code)

def _prepare_js_source(js, es6=False):
"""Optionally downlevel ES6 source to ES5 before translation."""
def _prepare_js_source(js, es6=False, es7=False):
"""Optionally enable ES7 parsing and downlevel ES6 source before translation."""
if es7 == 'auto':
if looks_like_es7 and looks_like_es7(js):
es7 = True
else:
es7 = False
if es6 == 'auto':
if looks_like_es6 and looks_like_es6(js):
es6 = True
else:
es6 = False
if es7 or es6:
if ensure_pyjsparser_es7:
ensure_pyjsparser_es7()
if es6:
if js6_to_js5 is None:
raise RuntimeError('ES6 support is not available')
Expand All @@ -82,13 +96,14 @@ def _prepare_js_source(js, es6=False):


def translate_js(js, HEADER=DEFAULT_HEADER, use_compilation_plan=False,
parse_fn=pyjsparser_parse_fn, es6=False):
parse_fn=pyjsparser_parse_fn, es6=False, es7=False):
"""js has to be a javascript source code.
returns equivalent python code.

es6: False (ES5 only), True (always transpile via Babel), or 'auto'
(transpile when ES6 syntax is detected)."""
js = _prepare_js_source(js, es6)
(transpile when ES6 syntax is detected).
es7: False, True, or 'auto' — enable ES2016 features (e.g. **)."""
js = _prepare_js_source(js, es6, es7)
if use_compilation_plan and not '//' in js and not '/*' in js:
return translate_js_with_compilation_plan(js, HEADER=HEADER)

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
version='0.74',

packages=['js2py', 'js2py.utils', 'js2py.prototypes', 'js2py.translators',
'js2py.constructors', 'js2py.host', 'js2py.es6', 'js2py.internals',
'js2py.constructors', 'js2py.host', 'js2py.es6', 'js2py.es7',
'js2py.internals',
'js2py.internals.prototypes', 'js2py.internals.constructors', 'js2py.py_node_modules'],
url='https://github.com/PiotrDabkowski/Js2Py',
install_requires = ['tzlocal>=1.2', 'six>=1.10', 'pyjsparser>=2.5.1'],
Expand Down
Loading