"""
Helpers around images and :epkg:`javascript`.
See also:
* `pyduktape <https://github.com/stefano/pyduktape>`_
* `Python Mini Racer <https://github.com/sqreen/PyMiniRacer>`_
* `python-requirejs <https://github.com/wq/python-requirejs>`_
.. versionadded:: 1.7
:githublink:`%|py|12`
"""
import os
from ..loghelper import run_cmd, noLOG
[docs]class NodeJsException(Exception):
"""
Raised if :epkg:`node.js` fails.
:githublink:`%|py|19`
"""
pass
[docs]def run_js_fct(script, required=None):
"""
Assuming *script* contains some :epkg:`javascript`
which produces :epkg:`SVG`. This functions runs
the code.
:param script: :epkg:`javascript`
:param required: required libraries (does not guaranteed to work)
:return: :epkg:`python` function
The module relies on :epkg:`js2py` and :epkg:`node.js`.
Dependencies must be installed with :epkg:`npm`:.
::
npm install babel-core babel-cli babel-preset-es2015 babel-polyfill babelify browserify babel-preset-env
Function :func:`install_node_js_modules <pyquickhelper.imghelper.js_helper.install_node_js_modules>` can be run with admin right for that.
:epkg:`js2py` tries to convert a dependency into :epkg:`Python`
:githublink:`%|py|42`
"""
from js2py import eval_js, require, node_import # pylint: disable=W0621
# To skip npm installation.
node_import.DID_INIT = True
if required:
if not isinstance(required, list):
required = [required]
for r in required:
require(r)
fct = eval_js(script)
return fct
[docs]def install_node_js_modules(dest, module_list=None, fLOG=noLOG):
"""
Installs missing dependencies to compile a convert a :epkg:`javascript`
libraries.
:param dest: installation folder
:param module_list: list of modules to install
:param fLOG: logging function
If *module_list is None*, it is replaced by:
::
['babel-core', 'babel-cli', 'babel-preset-env',
'babel-polyfill', 'babelify', 'browserify',
'babel-preset-es2015']
:githublink:`%|py|71`
"""
if module_list is None:
module_list = ['babel-core', 'babel-cli', 'babel-preset-env',
'babel-polyfill', 'babelify', 'browserify',
'babel-preset-es2015']
dir_name = dest
node_modules = os.path.join(dir_name, "node_modules")
should = [os.path.join(node_modules, n) for n in module_list]
if any(map(lambda x: not os.path.exists(x), should)):
cmds = ['npm install ' + ' '.join(module_list)]
errs = []
for cmd in cmds:
fLOG("[install_node_js_modules] run ", cmd)
err = run_cmd(cmd, wait=True, change_path=dir_name, fLOG=fLOG)[1]
errs.append(err)
if not os.path.exists(node_modules):
raise RuntimeError( # pragma: no cover
"Unable to run from '{0}' commands line:\n{1}\n--due to--\n{2}".format(
dir_name, "\n".join(cmds), "\n".join(errs)))
[docs]def nodejs_version():
"""
Returns :epkg:`node.js` version.
:githublink:`%|py|95`
"""
out, err = run_cmd('node -v', wait=True)
if len(err) > 0:
raise NodeJsException( # pragma: no cover
"Unable to find node\n{0}".format(err))
return out
[docs]def run_js_with_nodejs(script, path_dependencies=None, fLOG=noLOG):
"""
Runs a :epkg:`javascript` script with :epkg:`node.js`.
:param script: script to run
:param path_dependencies: where dependencies can be found if needed
:param fLOG: logging function
:return: output of the script
:githublink:`%|py|111`
"""
script_clean = script.replace("\"", "\\\"").replace("\n", " ")
cmd = 'node -e "{0}"'.format(script_clean)
out, err = run_cmd(cmd, change_path=path_dependencies,
fLOG=fLOG, wait=True)
if len(err) > 0:
filtered = "\n".join(_ for _ in err.split('\n')
if not _.startswith("[BABEL] Note:"))
else:
filtered = err
if len(filtered) > 0:
raise NodeJsException( # pragma: no cover
"Execution of node.js failed.\n--CMD--\n{0}\n--ERR--\n{1}\n--OUT--\n{2}\n"
"--SCRIPT--\n{3}".format(cmd, err, out, script))
return out
_require_cache = {}
[docs]def require(module_name, cache_folder='.', suffix='_pyq', update=False, fLOG=noLOG):
"""
Modified version of function *require* in
`node_import.py <https://github.com/PiotrDabkowski/Js2Py/blob/master/js2py/node_import.py>`_.
:param module_name: required library name
:param cache_folder: location of the files the function creates
:param suffix: change the suffix if you use the same folder for multiple files
:param update: update the converted script
:param fLOG: logging function
:return: outcome of the javascript script
The function is not fully tested.
:githublink:`%|py|144`
"""
if module_name.endswith('.js'):
raise ValueError( # pragma: no cover
"module_name must the name without extension .js")
global _require_cache
if module_name in _require_cache and not update:
py_code = _require_cache[module_name]
else:
from js2py.node_import import ADD_TO_GLOBALS_FUNC, GET_FROM_GLOBALS_FUNC
from js2py import translate_js
py_name = module_name.replace('-', '_')
module_filename = '%s.py' % py_name
full_name = os.path.join(cache_folder, module_filename)
var_name = py_name.rpartition('/')[-1]
in_file_name = os.path.join(
cache_folder, "require_{0}_in{1}.js".format(module_name, suffix))
out_file_name = os.path.join(
cache_folder, "require_{0}_out{1}.js".format(module_name, suffix))
code = ADD_TO_GLOBALS_FUNC
code += """
var module_temp_love_python = require('{0}');
addToGlobals('{0}', module_temp_love_python);
""".format(module_name)
with open(in_file_name, 'w', encoding='utf-8') as f:
f.write(code)
pkg_name = module_name.partition('/')[0]
install_node_js_modules(cache_folder, [pkg_name], fLOG=fLOG)
inline_script = "(require('browserify')('%s').bundle(function (err,data)" + \
"{fs.writeFile('%s',require('babel-core').transform(data," + \
"{'presets':require('babel-preset-es2015')}).code,()=>{});}))"
inline_script = inline_script % (in_file_name.replace("\\", "\\\\"),
out_file_name.replace("\\", "\\\\"))
run_js_with_nodejs(inline_script, fLOG=fLOG,
path_dependencies=cache_folder)
with open(out_file_name, "r", encoding="utf-8") as f:
js_code = f.read()
js_code += GET_FROM_GLOBALS_FUNC
js_code += ";var {0} = getFromGlobals('{1}');{0}".format(
var_name, module_name)
fLOG('[require] translating', out_file_name)
py_code = translate_js(js_code)
with open(full_name, 'w', encoding="utf-8") as f:
f.write(py_code)
_require_cache[module_name] = py_code
context = {}
exec(py_code, context)
return context['var'][var_name].to_py()