%PDF- %PDF-
Direktori : /lib/calibre/calibre/utils/ |
Current File : //lib/calibre/calibre/utils/rapydscript.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> import errno import json import os import re import subprocess import sys from calibre import force_unicode from calibre.constants import ( FAKE_HOST, FAKE_PROTOCOL, __appname__, __version__, builtin_colors_dark, builtin_colors_light, builtin_decorations, dark_link_color ) from calibre.ptempfile import TemporaryDirectory from calibre.utils.filenames import atomic_rename from polyglot.builtins import as_bytes, as_unicode, exec_path COMPILER_PATH = 'rapydscript/compiler.js.xz' special_title = '__webengine_messages_pending__' def abspath(x): return os.path.realpath(os.path.abspath(x)) # Update RapydScript {{{ def update_rapydscript(): import lzma d = os.path.dirname base = d(d(d(d(d(abspath(__file__)))))) base = os.path.join(base, 'rapydscript') with TemporaryDirectory() as tdir: subprocess.check_call(['node', '--harmony', os.path.join(base, 'bin', 'web-repl-export'), tdir]) with open(os.path.join(tdir, 'rapydscript.js'), 'rb') as f: raw = f.read() path = P(COMPILER_PATH, allow_user_override=False) with lzma.open(path, 'wb', format=lzma.FORMAT_XZ) as f: f.write(raw) # }}} # Compiler {{{ def to_dict(obj): return dict(zip(list(obj.keys()), list(obj.values()))) def compiler(): import lzma ans = getattr(compiler, 'ans', None) if ans is not None: return ans from qt.core import QApplication, QEventLoop from qt.webengine import QWebEnginePage, QWebEngineScript from calibre import walk from calibre.gui2 import must_use_qt from calibre.gui2.webengine import secure_webengine must_use_qt() with lzma.open(P(COMPILER_PATH, allow_user_override=False)) as lzf: compiler_script = lzf.read().decode('utf-8') base = base_dir() rapydscript_dir = os.path.join(base, 'src', 'pyj') cache_path = os.path.join(module_cache_dir(), 'embedded-compiler-write-cache.json') def create_vfs(): ans = {} for x in walk(rapydscript_dir): if x.endswith('.pyj'): r = os.path.relpath(x, rapydscript_dir).replace('\\', '/') with open(x, 'rb') as f: ans['__stdlib__/' + r] = f.read().decode('utf-8') return ans def vfs_script(): try: with open(cache_path, 'rb') as f: write_cache = f.read().decode('utf-8') except Exception: write_cache = '{}' return ''' (function() { "use strict"; var vfs = VFS; function read_file_sync(name) { var ans = vfs[name]; if (typeof ans === "string") return ans; ans = write_cache[name]; if (typeof ans === "string") return ans; return null; } function write_file_sync(name, data) { write_cache[name] = data; } RapydScript.virtual_file_system = { 'read_file_sync': read_file_sync, 'write_file_sync': write_file_sync }; window.compiler = RapydScript.create_embedded_compiler(); document.title = 'compiler initialized'; })(); '''.replace('VFS', json.dumps(create_vfs()) + ';\n' + 'window.write_cache = ' + write_cache, 1) def create_script(src, name): s = QWebEngineScript() s.setName(name) s.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) s.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld) s.setRunsOnSubFrames(True) s.setSourceCode(src) return s class Compiler(QWebEnginePage): def __init__(self): QWebEnginePage.__init__(self) self.errors = [] secure_webengine(self) script = compiler_script script += '\n\n;;\n\n' + vfs_script() self.scripts().insert(create_script(script, 'rapydscript.js')) self.setHtml('<p>initialize') while self.title() != 'compiler initialized': self.spin_loop() def spin_loop(self): QApplication.instance().processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) def javaScriptConsoleMessage(self, level, msg, line_num, source_id): if level: self.errors.append(msg) else: print(f'{source_id}:{line_num}:{msg}') def __call__(self, src, options): self.compiler_result = null = object() self.errors = [] self.working = True options['basedir'] = '__stdlib__' options['write_name'] = True options['keep_docstrings'] = False src = 'var js = window.compiler.compile({}, {}); [js, window.write_cache]'.format(*map(json.dumps, (src, options))) self.runJavaScript(src, QWebEngineScript.ScriptWorldId.ApplicationWorld, self.compilation_done) while self.working: self.spin_loop() if self.compiler_result is null or self.compiler_result is None: raise CompileFailure('Failed to compile rapydscript code with error: ' + '\n'.join(self.errors)) write_cache = self.compiler_result[1] with open(cache_path, 'wb') as f: f.write(as_bytes(json.dumps(write_cache))) return self.compiler_result[0] def eval(self, js): self.compiler_result = null = object() self.errors = [] self.working = True self.runJavaScript(js, QWebEngineScript.ScriptWorldId.ApplicationWorld, self.compilation_done) while self.working: self.spin_loop() if self.compiler_result is null: raise CompileFailure('Failed to eval JS with error: ' + '\n'.join(self.errors)) return self.compiler_result def compilation_done(self, js): self.working = False self.compiler_result = js compiler.ans = Compiler() return compiler.ans class CompileFailure(ValueError): pass _cache_dir = None def module_cache_dir(): global _cache_dir if _cache_dir is None: d = os.path.dirname base = d(d(d(d(abspath(__file__))))) _cache_dir = os.path.join(base, '.build-cache', 'pyj') try: os.makedirs(_cache_dir) except OSError as e: if e.errno != errno.EEXIST: raise return _cache_dir def ok_to_import_webengine(): from qt.core import QApplication if QApplication.instance() is None: return True if 'PyQt5.QtWebEngineWidgets' in sys.modules: return True return False OUTPUT_SENTINEL = b'-----RS webengine compiler output starts here------' def forked_compile(): c = compiler() stdin = getattr(sys.stdin, 'buffer', sys.stdin) data = stdin.read().decode('utf-8') options = json.loads(sys.argv[-1]) result = c(data, options) stdout = getattr(sys.stdout, 'buffer', sys.stdout) stdout.write(OUTPUT_SENTINEL) stdout.write(as_bytes(result)) stdout.close() def compile_pyj( data, filename='<stdin>', beautify=True, private_scope=True, libdir=None, omit_baselib=False, js_version=5, ): if isinstance(data, bytes): data = data.decode('utf-8') options = { 'beautify':beautify, 'private_scope':private_scope, 'keep_baselib': not omit_baselib, 'filename': filename, 'js_version': js_version, } if not ok_to_import_webengine(): from calibre.debug import run_calibre_debug p = run_calibre_debug('-c', 'from calibre.utils.rapydscript import *; forked_compile()', json.dumps(options), stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdout = p.communicate(as_bytes(data))[0] if p.wait() != 0: raise SystemExit(p.returncode) idx = stdout.find(OUTPUT_SENTINEL) result = as_unicode(stdout[idx+len(OUTPUT_SENTINEL):]) else: c = compiler() result = c(data, options) return result has_external_compiler = None def detect_external_compiler(): from calibre.utils.filenames import find_executable_in_path rs = find_executable_in_path('rapydscript') try: raw = subprocess.check_output([rs, '--version']) except Exception: raw = b'' if raw.startswith(b'rapydscript-ng '): ver = raw.partition(b' ')[-1] try: ver = tuple(map(int, ver.split(b'.'))) except Exception: ver = (0, 0, 0) if ver >= (0, 7, 5): return rs return False def compile_fast( data, filename=None, beautify=True, private_scope=True, libdir=None, omit_baselib=False, js_version=None, ): global has_external_compiler if has_external_compiler is None: has_external_compiler = detect_external_compiler() if not has_external_compiler: return compile_pyj(data, filename or '<stdin>', beautify, private_scope, libdir, omit_baselib, js_version or 6) args = ['--cache-dir', module_cache_dir()] if libdir: args += ['--import-path', libdir] if not beautify: args.append('--uglify') if not private_scope: args.append('--bare') if omit_baselib: args.append('--omit-baselib') if js_version: args.append(f'--js-version={js_version or 6}') if not isinstance(data, bytes): data = data.encode('utf-8') if filename: args.append('--filename-for-stdin'), args.append(filename) p = subprocess.Popen([has_external_compiler, 'compile'] + args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) js, stderr = p.communicate(data) if p.wait() != 0: raise CompileFailure(force_unicode(stderr, 'utf-8')) return js.decode('utf-8') def base_dir(): d = os.path.dirname return d(d(d(d(os.path.abspath(__file__))))) def atomic_write(base, name, content): name = os.path.join(base, name) tname = name + '.tmp' with lopen(tname, 'wb') as f: f.write(as_bytes(content)) atomic_rename(tname, name) def run_rapydscript_tests(): from urllib.parse import parse_qs from qt.core import QApplication, QByteArray, QEventLoop, QUrl from qt.webengine import ( QWebEnginePage, QWebEngineProfile, QWebEngineScript, QWebEngineUrlRequestJob, QWebEngineUrlScheme, QWebEngineUrlSchemeHandler ) from calibre.constants import FAKE_HOST, FAKE_PROTOCOL from calibre.gui2 import must_use_qt from calibre.gui2.viewer.web_view import send_reply from calibre.gui2.webengine import secure_webengine, insert_scripts, create_script must_use_qt() scheme = QWebEngineUrlScheme(FAKE_PROTOCOL.encode('ascii')) scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host) scheme.setFlags(QWebEngineUrlScheme.Flag.SecureScheme) QWebEngineUrlScheme.registerScheme(scheme) base = base_dir() rapydscript_dir = os.path.join(base, 'src', 'pyj') fname = os.path.join(rapydscript_dir, 'test.pyj') with lopen(fname, 'rb') as f: js = compile_fast(f.read(), fname) class UrlSchemeHandler(QWebEngineUrlSchemeHandler): def __init__(self, parent=None): QWebEngineUrlSchemeHandler.__init__(self, parent) self.allowed_hosts = (FAKE_HOST,) self.registered_data = {} def requestStarted(self, rq): if bytes(rq.requestMethod()) != b'GET': return self.fail_request(rq, QWebEngineUrlRequestJob.Error.RequestDenied) url = rq.requestUrl() host = url.host() if host not in self.allowed_hosts: return self.fail_request(rq) q = parse_qs(url.query()) if not q: return self.fail_request(rq) mt = q.get('mime-type', ('text/plain',))[0] data = q.get('data', ('',))[0].encode('utf-8') send_reply(rq, mt, data) def fail_request(self, rq, fail_code=None): if fail_code is None: fail_code = QWebEngineUrlRequestJob.Error.UrlNotFound rq.fail(fail_code) print(f"Blocking FAKE_PROTOCOL request: {rq.requestUrl().toString()}", file=sys.stderr) class Tester(QWebEnginePage): def __init__(self): profile = QWebEngineProfile(QApplication.instance()) profile.setHttpUserAgent('calibre-tester') insert_scripts(profile, create_script('test-rapydscript.js', js, on_subframes=False)) url_handler = UrlSchemeHandler(profile) profile.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), url_handler) QWebEnginePage.__init__(self, profile, None) self.titleChanged.connect(self.title_changed) secure_webengine(self) self.setHtml('<p>initialize', QUrl(f'{FAKE_PROTOCOL}://{FAKE_HOST}/index.html')) self.working = True def title_changed(self, title): if title == 'initialized': self.titleChanged.disconnect() self.runJavaScript('window.main()', QWebEngineScript.ScriptWorldId.ApplicationWorld, self.callback) def spin_loop(self): while self.working: QApplication.instance().processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) return self.result def callback(self, result): self.result = result self.working = False def javaScriptConsoleMessage(self, level, msg, line_num, source_id): print(msg, file=sys.stderr if level > 0 else sys.stdout) tester = Tester() result = tester.spin_loop() raise SystemExit(int(result)) def set_data(src, **kw): for k, v in { '__SPECIAL_TITLE__': special_title, '__FAKE_PROTOCOL__': FAKE_PROTOCOL, '__FAKE_HOST__': FAKE_HOST, '__CALIBRE_VERSION__': __version__, '__DARK_LINK_COLOR__': dark_link_color, '__BUILTIN_COLORS_LIGHT__': json.dumps(builtin_colors_light), '__BUILTIN_COLORS_DARK__': json.dumps(builtin_colors_dark), '__BUILTIN_DECORATIONS__': json.dumps(builtin_decorations) }.items(): src = src.replace(k, v, 1) for k, v in kw.items(): src = src.replace(k, v, 1) return src def compile_editor(): base = base_dir() rapydscript_dir = os.path.join(base, 'src', 'pyj') fname = os.path.join(rapydscript_dir, 'editor.pyj') with lopen(fname, 'rb') as f: js = set_data(compile_fast(f.read(), fname)) base = os.path.join(base, 'resources') atomic_write(base, 'editor.js', js) def compile_viewer(): base = base_dir() iconf = os.path.join(base, 'imgsrc', 'srv', 'generate.py') g = {'__file__': iconf} exec_path(iconf, g) icons = g['merge']() with lopen(os.path.join(base, 'resources', 'content-server', 'reset.css'), 'rb') as f: reset = f.read().decode('utf-8') html = '<!DOCTYPE html>\n<html><head><style>{reset}</style></head><body>{icons}</body></html>'.format( icons=icons, reset=reset) rapydscript_dir = os.path.join(base, 'src', 'pyj') fname = os.path.join(rapydscript_dir, 'viewer-main.pyj') with lopen(fname, 'rb') as f: js = set_data(compile_fast(f.read(), fname)) base = os.path.join(base, 'resources') atomic_write(base, 'viewer.js', js) atomic_write(base, 'viewer.html', html) def compile_srv(): base = base_dir() iconf = os.path.join(base, 'imgsrc', 'srv', 'generate.py') g = {'__file__': iconf} exec_path(iconf, g) icons = g['merge']().encode('utf-8') with lopen(os.path.join(base, 'resources', 'content-server', 'reset.css'), 'rb') as f: reset = f.read() rapydscript_dir = os.path.join(base, 'src', 'pyj') rb = os.path.join(base, 'src', 'calibre', 'srv', 'render_book.py') with lopen(rb, 'rb') as f: rv = str(int(re.search(br'^RENDER_VERSION\s+=\s+(\d+)', f.read(), re.M).group(1))) mathjax_version = json.loads(P('mathjax/manifest.json', data=True, allow_user_override=False))['etag'] base = os.path.join(base, 'resources', 'content-server') fname = os.path.join(rapydscript_dir, 'srv.pyj') with lopen(fname, 'rb') as f: js = set_data( compile_fast(f.read(), fname), __RENDER_VERSION__=rv, __MATHJAX_VERSION__=mathjax_version ).encode('utf-8') with lopen(os.path.join(base, 'index.html'), 'rb') as f: html = f.read().replace(b'RESET_STYLES', reset, 1).replace(b'ICONS', icons, 1).replace(b'MAIN_JS', js, 1) atomic_write(base, 'index-generated.html', html) # }}} # Translations {{{ def create_pot(source_files): c = compiler() gettext_options = json.dumps({ 'package_name': __appname__, 'package_version': __version__, 'bugs_address': 'https://bugs.launchpad.net/calibre' }) c.eval(f'window.catalog = {{}}; window.gettext_options = {gettext_options}; 1') for fname in source_files: with open(fname, 'rb') as f: code = f.read().decode('utf-8') fname = fname c.eval('RapydScript.gettext_parse(window.catalog, {}, {}); 1'.format(*map(json.dumps, (code, fname)))) buf = c.eval('ans = []; RapydScript.gettext_output(window.catalog, window.gettext_options, ans.push.bind(ans)); ans;') return ''.join(buf) def msgfmt(po_data_as_string): c = compiler() return c.eval('RapydScript.msgfmt({}, {})'.format( json.dumps(po_data_as_string), json.dumps({'use_fuzzy': False}))) # }}}