diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 5ff693817a..86458686e6 100644 Binary files a/resources/compiled_coffeescript.zip and b/resources/compiled_coffeescript.zip differ diff --git a/src/calibre/ebooks/oeb/polish/preview.coffee b/src/calibre/ebooks/oeb/polish/preview.coffee index c4abe96db9..b7433cd36d 100644 --- a/src/calibre/ebooks/oeb/polish/preview.coffee +++ b/src/calibre/ebooks/oeb/polish/preview.coffee @@ -33,6 +33,174 @@ find_containing_block = (elem) -> elem = elem.parentNode return elem +INHERITED_PROPS = { # {{{ + 'azimuth': '2', + 'border-collapse': '2', + 'border-spacing': '2', + 'caption-side': '2', + 'color': '2', + 'cursor': '2', + 'direction': '2', + 'elevation': '2', + 'empty-cells': '2', + 'fit': '3', + 'fit-position': '3', + 'font': '2', + 'font-family': '2', + 'font-size': '2', + 'font-size-adjust': '2', + 'font-stretch': '2', + 'font-style': '2', + 'font-variant': '2', + 'font-weight': '2', + 'hanging-punctuation': '3', + 'hyphenate-after': '3', + 'hyphenate-before': '3', + 'hyphenate-character': '3', + 'hyphenate-lines': '3', + 'hyphenate-resource': '3', + 'hyphens': '3', + 'image-resolution': '3', + 'letter-spacing': '2', + 'line-height': '2', + 'line-stacking': '3', + 'line-stacking-ruby': '3', + 'line-stacking-shift': '3', + 'line-stacking-strategy': '3', + 'list-style': '2', + 'list-style-image': '2', + 'list-style-position': '2', + 'list-style-type': '2', + 'marquee-direction': '3', + 'orphans': '2', + 'overflow-style': '3', + 'page': '2', + 'page-break-inside': '2', + 'pitch': '2', + 'pitch-range': '2', + 'presentation-level': '3', + 'punctuation-trim': '3', + 'quotes': '2', + 'richness': '2', + 'ruby-align': '3', + 'ruby-overhang': '3', + 'ruby-position': '3', + 'speak': '2', + 'speak-header': '2', + 'speak-numeral': '2', + 'speak-punctuation': '2', + 'speech-rate': '2', + 'stress': '2', + 'text-align': '2', + 'text-align-last': '3', + 'text-emphasis': '3', + 'text-height': '3', + 'text-indent': '2', + 'text-justify': '3', + 'text-outline': '3', + 'text-replace': '?', + 'text-shadow': '3', + 'text-transform': '2', + 'text-wrap': '3', + 'visibility': '2', + 'voice-balance': '3', + 'voice-family': '2', + 'voice-rate': '3', + 'voice-pitch': '3', + 'voice-pitch-range': '3', + 'voice-stress': '3', + 'voice-volume': '3', + 'volume': '2', + 'white-space': '2', + 'white-space-collapse': '3', + 'widows': '2', + 'word-break': '3', + 'word-spacing': '2', + 'word-wrap': '3', + + # the mozilla extensions are all proprietary properties + '-moz-force-broken-image-icon': 'm', + '-moz-image-region': 'm', + '-moz-stack-sizing': 'm', + '-moz-user-input': 'm', + '-x-system-font': 'm', + + # the opera extensions are all draft implementations of CSS3 properties + '-xv-voice-balance': 'o', + '-xv-voice-pitch': 'o', + '-xv-voice-pitch-range': 'o', + '-xv-voice-rate': 'o', + '-xv-voice-stress': 'o', + '-xv-voice-volume': 'o', + + # the explorer extensions are all draft implementations of CSS3 properties + '-ms-text-align-last': 'e', + '-ms-text-justify': 'e', + '-ms-word-break': 'e', + '-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_style_properties = (style, all_properties, node_style, is_ancestor) -> + i = 0 + properties = [] + 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]) + if not all_properties.hasOwnProperty(property) + all_properties[property] = node_style.getPropertyValue(property) + i += 1 + return properties + +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 = [] + matching_selectors = {} + for rule in rules + matching_selectors[rule.selectorText] = true + ans = [] + node_style = window.getComputedStyle(node) + + for sheet in document.styleSheets + if sheet.disabled + continue + for rule in sheet.cssRules + if rule.type != CSSRule.STYLE_RULE + continue + # Either use matching_selectors for speed or matches for + # correctness + # if rule.selectorText and node.webkitMatchesSelector(rule.selectorText) + if rule.selectorText and matching_selectors.hasOwnProperty(rule.selectorText) + type = 'sheet' + href = sheet.href + if href == null + href = get_sourceline_address(sheet.ownerNode) + type = 'elem' + properties = get_style_properties(rule.style, all_properties, node_style, is_ancestor) + if properties.length > 0 + data = {'selector':rule.selectorText, 'type':type, 'href':href, 'properties':properties, 'is_ancestor':is_ancestor} + ans.push(data) + + 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, 'is_ancestor':is_ancestor} + ans.push(data) + + return ans.reverse() + class PreviewIntegration ### @@ -125,6 +293,29 @@ class PreviewIntegration lnum = elem.getAttribute('data-lnum') window.py_bridge.request_sync('', '', lnum) + live_css: (sourceline, tags) => + target = null + i = 0 + for node in document.querySelectorAll('[data-lnum="' + sourceline + '"]') + if node.tagName?.toLowerCase() != tags[i] + return JSON.stringify(null) + i += 1 + target = node + if i >= tags.length + break + all_properties = {} + original_target = target + ans = {'nodes':[], 'computed_css':all_properties} + is_ancestor = false + while target and target.ownerDocument + css = get_matched_css(target, is_ancestor, all_properties) + if css.length > 0 + ans['nodes'].push({'name':target.tagName?.toLowerCase(), 'css':css}) + target = target.parentNode + is_ancestor = true + return JSON.stringify(ans) + + window.calibre_preview_integration = new PreviewIntegration() window.onload = window.calibre_preview_integration.onload diff --git a/src/calibre/gui2/tweak_book/editor/smart/html.py b/src/calibre/gui2/tweak_book/editor/smart/html.py index 9ffe7ea697..f1afc51af2 100644 --- a/src/calibre/gui2/tweak_book/editor/smart/html.py +++ b/src/calibre/gui2/tweak_book/editor/smart/html.py @@ -329,7 +329,7 @@ class HTMLSmarts(NullSmarts): if tag is None: return None, None start_block, start_offset = tag.start_block, tag.start_offset - sourceline = start_block.blockNumber() + sourceline = start_block.blockNumber() + 1 # blockNumber() is zero based ud = start_block.userData() if ud is None: return None, None diff --git a/src/calibre/gui2/tweak_book/live_css.py b/src/calibre/gui2/tweak_book/live_css.py index 74e1f526f6..affae3073a 100644 --- a/src/calibre/gui2/tweak_book/live_css.py +++ b/src/calibre/gui2/tweak_book/live_css.py @@ -6,9 +6,12 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import json + from PyQt4.Qt import (QWidget, QTimer) -from calibre.gui2.tweak_book import editors, actions +from calibre.constants import iswindows +from calibre.gui2.tweak_book import editors, actions, current_container class LiveCSS(QWidget): @@ -27,7 +30,29 @@ class LiveCSS(QWidget): if sourceline is None: self.clear() else: - pass # TODO: Do update + data = self.read_data(sourceline, tags) + if data is None: + self.clear() + return + + def read_data(self, sourceline, tags): + mf = self.preview.view.page().mainFrame() + tags = [x.lower() for x in tags] + result = unicode(mf.evaluateJavaScript( + 'window.calibre_preview_integration.live_css(%s, %s)' % ( + json.dumps(sourceline), json.dumps(tags))).toString()) + result = json.loads(result) + if result is not None: + for node in result['nodes']: + for item in node['css']: + href = item['href'] + if hasattr(href, 'startswith') and href.startswith('file://'): + href = href[len('file://'):] + if iswindows and href.startswith('/'): + href = href[1:] + if href: + item['href'] = current_container().abspath_to_name(href, root=self.preview.current_root) + return result @property def current_name(self):