text: #!/usr/bin/env python
  
  '''
      Simple Javascript engines' wrapper
      
      Description:
          This library wraps the system's built-in Javascript interpreter to python.
      Platform:
          macOS:   Use JavascriptCore
          Linux:   Use gjs on Gnome, cjs on Cinnamon or NodeJS if installed
          Windows: Use Win10's built-in Chakra, if not available use NodeJS
      
      Usage:
      
          from jsengine import JSEngine, javascript_is_supported
          
          if not javascript_is_supported:  # always check this first!
              ......
          
          ctx = JSEngine()
          ctx.eval('1 + 1')  # => 2
      
          ctx2 = JSEngine("""
              function add(x, y) {
              return x + y;
          }
          """)
          ctx2.call("add", 1, 2)  # => 3
  '''
  
  from __future__ import print_function
  from distutils.spawn import find_executable
  from subprocess import Popen, PIPE
  import io
  import json
  import os
  import platform
  import re
  import sys
  import tempfile
  
  ### Before using this library, check this variable first!!!
  javascript_is_supported = True
  
  # Exceptions
  class ProgramError(Exception):
      pass
  
  class RuntimeError(Exception):
      pass
  
  
  use_chakra = False
  
  # Choose javascript interpreter
  # macOS: use built-in JavaScriptCore
  if platform.system() == 'Darwin':
      interpreter = ['/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc']
  
  # Windows: Try Chakra, if fails, load Node.js
  elif platform.system() == 'Windows':
      from .jsengine_chakra import ChakraHandle, chakra_available
      if chakra_available:
          use_chakra = True
      elif find_executable('node') is not None:
          interpreter = ['node']
      else:
          print('Please install Node.js!', file=sys.stderr)
          javascript_is_supported = False
  
  # Linux: use gjs on Gnome, cjs on Cinnamon or JavaScriptCore/NodeJS if installed
  elif platform.system() == 'Linux':
      if find_executable('gjs') is not None:
          interpreter = ['gjs']
      elif find_executable('cjs') is not None:
          interpreter = ['cjs']
      elif find_executable('jsc') is not None:
          interpreter = ['jsc']
      elif find_executable('nodejs') is not None:
          interpreter = ['nodejs']
      elif find_executable('node') is not None:
          interpreter = ['node']
      else:
          print('Please install at least one of the following Javascript interpreter: gjs, cjs, nodejs', file=sys.stderr)
          javascript_is_supported = False
  else:
      print('Sorry, the Javascript engine is currently not supported on your system', file=sys.stderr)
      javascript_is_supported = False
  
  
  
  # Inject to the script to let it return jsonlized value to python
  injected_script = r'''
  var exports = undefined;
  (function(program, execJS) { execJS(program) })(
  function() {
      return eval(#{encoded_source});
  },
  function(program) {
      var print = (this.print === undefined) ? console.log : this.print;
      try {
          result = program();
          print("");
          if (typeof result == 'undefined' && result !== null) {
              print('["ok"]');
          }
          else {
              try {
                  print(JSON.stringify(['ok', result]));
              }
              catch (err) {
                  print('["err", "Script returns a value with an unknown type"]');
              }
          }
      }
      catch (err) {
          print(JSON.stringify(['err', '' + err]));
      }
  });
  '''
  
  
  
  class AbstractJSEngine:
      def __init__(self, source=''):
          self._source = source
      
      def call(self, identifier, *args):
          args = json.dumps(args)
          code = '{identifier}.apply(this,{args})'.format(identifier=identifier, args=args)
          return self._eval(code)
          
      def eval(self, code=''):
          return self._eval(code)
  
  
  class ChakraJSEngine(AbstractJSEngine):
      def __init__(self, source=''):
          AbstractJSEngine.__init__(self, source)
          self.chakra = ChakraHandle()
          if source:
              self.chakra.eval_js(source)
              
      def _eval(self, code):
          if not code.strip():
              return None
          data =  json.dumps(code, ensure_ascii=True)
          code = 'JSON.stringify([eval({data})]);'.format(data=data);
          ok, result = self.chakra.eval_js(code)
          if ok:
              return json.loads(result)[0]
          else:
              raise ProgramError(str(result))
  
  
  class ExternalJSEngine(AbstractJSEngine):
      def __init__(self, source=''):
          AbstractJSEngine.__init__(self, source)
          self._last_code = ''
  
      def _eval(self, code):
          # TODO: may need a thread lock, if running multithreading
          if not code.strip():
              return None
          if self._last_code:
              self._source += '\n' + self._last_code
          self._last_code = code
          data = json.dumps(code, ensure_ascii=True)
          code = 'return eval({data});'.format(data=data)
          return self._exec(code)
      
      def _exec(self, code):
          if self._source:
              code = self._source + '\n' + code
          code = self._inject_script(code)
          output = self._run_interpreter_with_tempfile(code)
          output = output.replace('\r\n', '\n').replace('\r', '\n')
          last_line = output.split('\n')[-2]
          ret = json.loads(last_line)
          if len(ret) == 1:
              return None
          status, value = ret
          if status == 'ok':
              return value
          else:
              raise ProgramError(value)
          
      def _run_interpreter_with_tempfile(self, code):
          (fd, filename) = tempfile.mkstemp(prefix='execjs', suffix='.js')
          os.close(fd)
          try:
              # decoding in python2
              if hasattr(code, 'decode'):
                  code = code.decode('utf8')
              with io.open(filename, 'w', encoding='utf8') as fp:
                  fp.write(code)
              
              cmd = interpreter + [filename]
              p = None
              try:
                  p = Popen(cmd, stdout=PIPE, universal_newlines=True)
                  stdoutdata, stderrdata = p.communicate()
                  ret = p.wait()
              finally:
                  del p
              if ret != 0:
                  raise RuntimeError('Javascript interpreter returns non-zero value!')
              return stdoutdata
          finally:
              os.remove(filename)
  
      def _inject_script(self, source):
          encoded_source = \
              '(function(){ ' + \
              self._encode_unicode_codepoints(source) + \
              ' })()'
          return injected_script.replace('#{encoded_source}', json.dumps(encoded_source))
  
      def _encode_unicode_codepoints(self, str):
          codepoint_format = '\\u{0:04x}'.format
          def codepoint(m):
              return codepoint_format(ord(m.group(0)))
          return re.sub('[^\x00-\x7f]', codepoint, str)
  
  
  if use_chakra:
      class JSEngine(ChakraJSEngine):
          pass
  else:
      class JSEngine(ExternalJSEngine):
          pass
  