diff --git a/setup/resources.py b/setup/resources.py index 63e1020d11..71647b7ac4 100644 --- a/setup/resources.py +++ b/setup/resources.py @@ -278,9 +278,17 @@ class RapydScript(Command): # {{{ description = 'Compile RapydScript to JavaScript' + def add_options(self, parser): + parser.add_option('--only-module', default=None, + help='Only compile the specified module') + def run(self, opts): - from calibre.utils.rapydscript import compile_srv - compile_srv() + from calibre.utils.rapydscript import compile_srv, compile_editor + if opts.only_module: + locals()['compile_' + opts.only]() + else: + compile_editor() + compile_srv() # }}} diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py index 9e25899925..e430ed0598 100644 --- a/src/calibre/gui2/tweak_book/preview.py +++ b/src/calibre/gui2/tweak_book/preview.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera # check if you can remove the restriction that prevents inspector dock from being undocked # check syncing of position back and forth # check all buttons in preview panel -# rewrite JS from coffeescript to rapydscript # pass user stylesheet with css for split import json @@ -30,7 +29,9 @@ from PyQt5.QtWebEngineWidgets import ( ) from calibre import prints -from calibre.constants import FAKE_HOST, FAKE_PROTOCOL, __version__ +from calibre.constants import ( + FAKE_HOST, FAKE_PROTOCOL, __version__, is_running_from_develop +) from calibre.ebooks.oeb.base import OEB_DOCS, XHTML_MIME, serialize from calibre.ebooks.oeb.polish.parsing import parse from calibre.gui2 import NO_URL_FORMATTING, error_dialog, open_url, secure_webengine @@ -276,11 +277,12 @@ def create_profile(): ans = QWebEngineProfile(QApplication.instance()) ua = 'calibre-editor-preview ' + __version__ ans.setHttpUserAgent(ua) - from calibre.utils.resources import compiled_coffeescript - js = compiled_coffeescript('ebooks.oeb.display.utils', dynamic=False) + if is_running_from_develop: + from calibre.utils.rapydscript import compile_editor + compile_editor() + js = P('editor.js', data=True, allow_user_override=False) js += P('csscolorparser.js', data=True, allow_user_override=False) - js += compiled_coffeescript('ebooks.oeb.polish.preview', dynamic=False) - insert_scripts(ans, create_script('editor-preview.js', js)) + insert_scripts(ans, create_script('editor.js', js)) url_handler = UrlSchemeHandler(ans) ans.installUrlSchemeHandler(QByteArray(FAKE_PROTOCOL.encode('ascii')), url_handler) s = ans.settings() diff --git a/src/calibre/utils/rapydscript.py b/src/calibre/utils/rapydscript.py index c5f71e7c3c..82121cba96 100644 --- a/src/calibre/utils/rapydscript.py +++ b/src/calibre/utils/rapydscript.py @@ -98,7 +98,7 @@ def module_cache_dir(): return _cache_dir -def compile_pyj(data, filename='', beautify=True, private_scope=True, libdir=None, omit_baselib=False): +def compile_pyj(data, filename='', beautify=True, private_scope=True, libdir=None, omit_baselib=False, js_version=5): if isinstance(data, bytes): data = data.decode('utf-8') c = compiler() @@ -109,6 +109,7 @@ def compile_pyj(data, filename='', beautify=True, private_scope=True, lib 'libdir': libdir or default_lib_dir(), 'basedir': getcwd() if not filename or filename == '' else os.path.dirname(filename), 'filename': filename, + 'js_version': js_version, } c.g.rs_source_code = data ok, result = c.eval( @@ -156,12 +157,12 @@ def detect_external_compiler(): return False -def compile_fast(data, filename=None, beautify=True, private_scope=True, libdir=None, omit_baselib=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 '', beautify, private_scope, libdir, omit_baselib) + return compile_pyj(data, filename or '', beautify, private_scope, libdir, omit_baselib, js_version or 6) args = ['--cache-dir', module_cache_dir(), '--import-path', libdir or default_lib_dir()] if not beautify: args.append('--uglify') @@ -169,6 +170,8 @@ def compile_fast(data, filename=None, beautify=True, private_scope=True, libdir= args.append('--bare') if omit_baselib: args.append('--omit-baselib') + if js_version: + args.append('--js-version=' + str(js_version)) if not isinstance(data, bytes): data = data.encode('utf-8') if filename: @@ -199,6 +202,24 @@ def base_dir(): 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(content) + atomic_rename(tname, name) + + +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 = compile_fast(f.read(), fname, js_version=6) + base = os.path.join(base, 'resources') + atomic_write(base, 'editor.js', js) + + def compile_srv(): base = base_dir() iconf = os.path.join(base, 'imgsrc', 'srv', 'generate.py') @@ -223,16 +244,8 @@ def compile_srv(): html = f.read().replace(b'RESET_STYLES', reset, 1).replace(b'ICONS', icons, 1).replace(b'MAIN_JS', js, 1) manifest = create_manifest(html) - - def atomic_write(name, content): - name = os.path.join(base, name) - tname = name + '.tmp' - with lopen(tname, 'wb') as f: - f.write(content) - atomic_rename(tname, name) - - atomic_write('index-generated.html', html) - atomic_write('calibre.appcache', manifest) + atomic_write(base, 'index-generated.html', html) + atomic_write(base, 'calibre.appcache', manifest) # }}} diff --git a/src/calibre/ebooks/oeb/polish/preview.coffee b/src/pyj/editor.pyj similarity index 58% rename from src/calibre/ebooks/oeb/polish/preview.coffee rename to src/pyj/editor.pyj index 9abca8beb4..b8605627ea 100644 --- a/src/calibre/ebooks/oeb/polish/preview.coffee +++ b/src/pyj/editor.pyj @@ -1,38 +1,35 @@ -#!/usr/bin/env coffee -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal +# globals: CSSRule +from __python__ import bound_methods, hash_literals -### - Copyright 2013, Kovid Goyal - Released under the GPLv3 License -### - - -if window?.calibre_utils - log = window.calibre_utils.log - -is_hidden = (elem) -> - while elem - if (elem.style && (elem.style.visibility == 'hidden' || elem.style.display == 'none')) - return true +def is_hidden(elem): + while elem: + if (elem.style and (elem.style.visibility is 'hidden' or elem.style.display is 'none')): + return True elem = elem.parentNode - return false + return False -is_block = (elem) -> + +def is_block(elem): style = window.getComputedStyle(elem) - return style.display in ['block', 'flex-box', 'box'] + return style.display is 'block' or style.display is 'flex-box' or style.display is 'box' -in_table = (elem) -> - while elem - if elem.tagName?.toLowerCase() == 'table' - return true + +def in_table(elem): + while elem: + if elem.tagName?.toLowerCase() is 'table': + return True elem = elem.parentNode - return false + return False -find_containing_block = (elem) -> - while elem and elem.getAttribute('data-is-block') != '1' + +def find_containing_block(elem): + while elem and elem.getAttribute('data-is-block') != '1': elem = elem.parentNode return elem + INHERITED_PROPS = { # {{{ 'azimuth': '2', 'border-collapse': '2', @@ -140,158 +137,166 @@ INHERITED_PROPS = { # {{{ '-ms-word-wrap': 'e' } # }}} -get_sourceline_address = (node) -> - sourceline = parseInt(node.getAttribute('data-lnum')) - tags = [] - for elem in document.querySelectorAll('[data-lnum="' + sourceline + '"]') - tags.push(elem.tagName.toLowerCase()) - if elem is node - break - return [sourceline, tags] -get_color = (property, val) -> - color = null - if property.indexOf('color') > -1 - try - color = parseCSSColor(val) # Use the csscolor library to get an rgba 4-tuple - catch error - color = null +def get_sourceline_address(node): + sourceline = parseInt(node.getAttribute('data-lnum')) + tags = v'[]' + for elem in document.querySelectorAll(f'[data-lnum="{sourceline}"]'): + tags.push(elem.tagName.toLowerCase()) + if elem is node: + break + return v'[sourceline, tags]' + + +def get_color(property, val): + color = None + if property.indexOf('color') > -1: + try: + color = window.parseCSSColor(val) # Use the csscolor library to get an rgba 4-tuple + except: + color = None return color -get_style_properties = (style, all_properties, node_style, is_ancestor) -> + +def get_style_properties(style, all_properties, node_style, is_ancestor): i = 0 - properties = [] - while i < style.length + properties = v'[]' + while i < style.length: property = style.item(i)?.toLowerCase() val = style.getPropertyValue(property) - if property and val and (not is_ancestor or INHERITED_PROPS.hasOwnProperty(property)) - properties.push([property, val, style.getPropertyPriority(property), get_color(property, val)]) - if not all_properties.hasOwnProperty(property) - cval = node_style.getPropertyValue(property) - all_properties[property] = [cval, get_color(property, cval)] + if property and val and (not is_ancestor or INHERITED_PROPS[property]): + properties.push(v'[property, val, style.getPropertyPriority(property), get_color(property, val)]') + if not all_properties.hasOwnProperty(property): + all_properties[property] = v'[node_style.getPropertyValue(property), get_color(property, cval)]' i += 1 return properties -process_rules = (node, cssRules, address, sheet, sheet_index, matching_selectors, all_properties, node_style, is_ancestor, ans) -> - for rule, rule_index in cssRules + +def process_rules(node, cssRules, address, sheet, sheet_index, matching_selectors, all_properties, node_style, is_ancestor, ans): + for rule_index in range(cssRules.length): + rule = cssRules[rule_index] rule_address = address.concat([rule_index]) - if rule.type == CSSRule.MEDIA_RULE + if rule.type is CSSRule.MEDIA_RULE: process_rules(node, rule.cssRules, rule_address, sheet, sheet_index, matching_selectors, all_properties, node_style, is_ancestor, ans) continue - if rule.type != CSSRule.STYLE_RULE + if rule.type is not CSSRule.STYLE_RULE: continue # As a performance improvement, instead of running the match on every # rule, we simply check if its selector is one of the matching # selectors returned by getMatchedCSSRules. However, # getMatchedCSSRules ignores rules in media queries that dont apply, so we check them manually st = rule.selectorText - if st and (matching_selectors.hasOwnProperty(st) or (rule_address.length > 1 and node.webkitMatchesSelector(st))) + if st and (matching_selectors.hasOwnProperty(st) or (rule_address.length > 1 and node.matches(st))): type = 'sheet' href = sheet.href - if href == null + if href is None: href = get_sourceline_address(sheet.ownerNode) type = 'elem' parts = st.split(',') # We only want the first matching selector - if parts.length > 1 - for q in parts - if node.webkitMatchesSelector(q) + if parts.length > 1: + for q in parts: + if node.matches(q): st = q break properties = get_style_properties(rule.style, all_properties, node_style, is_ancestor) - if properties.length > 0 + if properties.length > 0: data = {'selector':st, 'type':type, 'href':href, 'properties':properties, 'rule_address':rule_address, 'sheet_index':sheet_index} ans.push(data) -get_matched_css = (node, is_ancestor, all_properties) -> +def get_matched_css(node, is_ancestor, all_properties): # WebKit sets parentStyleSheet == null for rules returned by getMatchedCSSRules so we cannot use them directly rules = node.ownerDocument.defaultView.getMatchedCSSRules(node, '') - if not rules - rules = [] + if not rules: + rules = v'[]' matching_selectors = {} - for rule in rules - matching_selectors[rule.selectorText] = true - ans = [] + for rule in rules: + matching_selectors[rule.selectorText] = True + ans = v'[]' node_style = window.getComputedStyle(node) - for sheet, sheet_index in document.styleSheets - if sheet.disabled or not sheet.cssRules + sheets = document.styleSheets + for sheet_index in range(sheets.length): + sheet = sheets[sheet_index] + if sheet.disabled or not sheet.cssRules: continue process_rules(node, sheet.cssRules, [], sheet, sheet_index, matching_selectors, all_properties, node_style, is_ancestor, ans) - if node.getAttribute('style') + if node.getAttribute('style'): properties = get_style_properties(node.style, all_properties, node_style, is_ancestor) - if properties.length > 0 - data = {'selector':null, 'type':'inline', 'href':get_sourceline_address(node), 'properties':properties, 'rule_address':null, 'sheet_index':null} + if properties.length > 0: + data = {'selector':None, 'type':'inline', 'href':get_sourceline_address(node), 'properties':properties, 'rule_address':None, 'sheet_index':None} ans.push(data) return ans.reverse() -scroll_to_node = (node) -> - if node is document.body +def scroll_to_node(node): + if node is document.body: window.scrollTo(0, 0) - else + else: node.scrollIntoView() -class PreviewIntegration + +class PreviewIntegration: ### # Namespace to expose all the functions used for integration with the Tweak # Book Preview Panel. ### - constructor: () -> - if not this instanceof arguments.callee - throw new Error('PreviewIntegration constructor called as function') - this.blocks_found = false - this.in_split_mode = false + def __init__(self): + self.blocks_found = False + self.in_split_mode = False - go_to_line: (lnum) => - for node in document.querySelectorAll('[data-lnum="' + lnum + '"]') - if is_hidden(node) + def go_to_line(self, lnum): + for node in document.querySelectorAll(f'[data-lnum="{lnum}"]'): + if is_hidden(node): continue scroll_to_node(node) + break - go_to_sourceline_address: (sourceline, tags) => - for node, index in document.querySelectorAll('[data-lnum="' + sourceline + '"]') - if index >= tags.length or node.tagName.toLowerCase() != tags[index] + def go_to_sourceline_address(self, sourceline, tags): + nodes = document.querySelectorAll(f'[data-lnum="{sourceline}"]') + for index in range(nodes.length): + node = nodes[index] + if index >= tags.length or node.tagName.toLowerCase() is not tags[index]: break - if index == tags.length - 1 and not is_hidden(node) + if index == tags.length - 1 and not is_hidden(node): return scroll_to_node(node) - this.go_to_line(sourceline) + self.go_to_line(sourceline) - line_numbers: () => - found_body = false - ans = [] - for node in document.getElementsByTagName('*') - if not found_body and node.tagName.toLowerCase() == "body" - found_body = true - if found_body - ans.push(node.getAttribute("data-lnum")) + def line_numbers(self): + found_body = False + ans = v'[]' + for node in document.getElementsByTagName('*'): + if not found_body and node.tagName.toLowerCase() is "body": + found_body = True + if found_body: + ans.push(node.dataset.lnum) return ans - find_blocks: () => - if this.blocks_found + def find_blocks(self): + if self.blocks_found: return - for elem in document.body.getElementsByTagName('*') - if is_block(elem) and not in_table(elem) + for elem in document.body.getElementsByTagName('*'): + if is_block(elem) and not in_table(elem): elem.setAttribute('data-is-block', '1') - this.blocks_found = true + self.blocks_found = True - split_mode: (enabled) => - this.in_split_mode = enabled - document.body.setAttribute('data-in-split-mode', if enabled then '1' else '0') - if enabled - this.find_blocks() + def split_mode(self, enabled): + self.in_split_mode = enabled + document.body.setAttribute('data-in-split-mode', '1' if enabled else '0') + if enabled: + self.find_blocks() - report_split: (node) => - loc = [] - totals = [] + def report_split(self, node): + loc = v'[]' + totals = v'[]' parent = find_containing_block(node) - while parent and parent.tagName.toLowerCase() != 'body' + while parent and parent.tagName.toLowerCase() is not 'body': totals.push(parent.parentNode.children.length) num = 0 sibling = parent.previousElementSibling - while sibling + while sibling: num += 1 sibling = sibling.previousElementSibling loc.push(num) @@ -300,57 +305,60 @@ class PreviewIntegration totals.reverse() window.py_bridge.request_split(JSON.stringify(loc), JSON.stringify(totals)) - onload: () => - window.document.body.addEventListener('click', this.onclick, true) + def onload(self): + window.document.body.addEventListener('click', this.onclick, True) - onclick: (event) => + def onclick(self, event): event.preventDefault() - if this.in_split_mode - this.report_split(event.target) - else + if self.in_split_mode: + self.report_split(event.target) + else: e = event.target address = get_sourceline_address(e) # Find the closest containing link, if any href = tn = '' - while e and e != document.body and e != document and (tn != 'a' or not href) + while e and e != document.body and e != document and (tn is not 'a' or not href): tn = e.tagName?.toLowerCase() href = e.getAttribute('href') e = e.parentNode window.py_bridge.request_sync(tn, href, JSON.stringify(address)) - return false + return False - go_to_anchor: (anchor, lnum) => + def go_to_anchor(self, anchor, lnum): elem = document.getElementById(anchor) - if not elem - elem = document.querySelector('[name="' + anchor + '"]') - if elem + if not elem: + elem = document.querySelector(f'[name="{anchor}"]') + if elem: elem.scrollIntoView() address = get_sourceline_address(elem) window.py_bridge.request_sync('', '', address) - live_css: (sourceline, tags) => - target = null + def live_css(self, sourceline, tags): + target = None i = 0 - for node in document.querySelectorAll('[data-lnum="' + sourceline + '"]') - if node.tagName?.toLowerCase() != tags[i] - return JSON.stringify(null) + for node in document.querySelectorAll(f'[data-lnum="{sourceline}"]'): + if node.tagName?.toLowerCase() is not tags[i]: + return JSON.stringify(None) i += 1 target = node - if i >= tags.length + if i >= tags.length: break all_properties = {} - original_target = target - ans = {'nodes':[], 'computed_css':all_properties} - is_ancestor = false - while target and target.ownerDocument + ans = {'nodes':v'[]', 'computed_css':all_properties} + is_ancestor = False + while target and target.ownerDocument: css = get_matched_css(target, is_ancestor, all_properties) # We want to show the Matched CSS rules header even if no rules matched - if css.length > 0 or not is_ancestor - ans['nodes'].push({'name':target.tagName?.toLowerCase(), 'css':css, 'is_ancestor':is_ancestor, 'sourceline':target.getAttribute('data-lnum')}) + if css.length > 0 or not is_ancestor: + ans.nodes.push({ + 'name':target.tagName?.toLowerCase(), + 'css':css, 'is_ancestor':is_ancestor, + 'sourceline':target.getAttribute('data-lnum') + }) target = target.parentNode - is_ancestor = true + is_ancestor = True return JSON.stringify(ans) -window.calibre_preview_integration = new PreviewIntegration() +window.calibre_preview_integration = PreviewIntegration() window.onload = window.calibre_preview_integration.onload