From 833faa475ca1e616539f3b5a545e2c6870245336 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 18 May 2014 17:32:01 +0530 Subject: [PATCH] Implement code to retrieve style information for the Live CSS panel --- resources/compiled_coffeescript.zip | Bin 82522 -> 89571 bytes src/calibre/ebooks/oeb/polish/preview.coffee | 191 ++++++++++++++++++ .../gui2/tweak_book/editor/smart/html.py | 2 +- src/calibre/gui2/tweak_book/live_css.py | 29 ++- 4 files changed, 219 insertions(+), 3 deletions(-) diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 5ff693817a9fc5f1935ca1fae37be1d8f16dc397..86458686e666fddfa76237422e18286866a2aaa4 100644 GIT binary patch delta 5832 zcmZ`-U2Ggz70!f2iA`hNBz7F7ajs*WS(BZu?X>x+ane=^qOHhpB`R5Fdp&#Cd+XVm z?flqroM=Eo6;DXj6--}B%_9%cf{>!9QhDGBDTwfj)RqeI(yD?NctHZrxp)5V?8Xn? z-Fwcx=bZ0+=iD>%mmlu^+lPC<|65~nX5<&_HpNNP1lb^EqNJn02+b>iI3^Tejyov=^{JvYWkLK-plP>xh`-Nd zk-%@O=LI~@PADXvD^>HL+pu7@X_q@&`jNOE@H=`)QX5w-+Ik}j1{@WHK7*X8v*Hw4 zwGC6$Z)1b{oDOnolkv5VZlOcnHGtphMXnu2IX-!GovmJnxikd#+9C7JxC}}mTxD&~ zm#z%hX?LhkZQMP0GjdO&KVUELQSvj}8cVJz!}=cHrEAPWeJAQmlEXr@e%tXj3qN!H9;8BZ4Hv9c zpV4)TyM1Nd<2k=mddn5d2Xa=D(x-Yu9sbev<>TdR=V7Z62#C))2(?fvr^v`T)1O-W>~)gYf#^ zI&)Pkv$(Bp>~5A~shG9(HFclIrY@EZ8gJFKN$b)mfTqvgUL<)~$ZlJ~RuHl-xS=Ro z5^Y$0srPb^ZpWE8PiIM}$%>Q?(8xG4KvZOWfN&$uvtCHMfhDU!wyeB>S?+WuFIAo< zlxdR98PnP?$}-5zVe%BYw>S*L-N4CFJl#G%4s%wwPKv3rPC~jCB>#Nc7#}WH#n1mU znY^-SJaKS#QO=5}1jFR=%HnWxykVR@T+@s)N+2lcvww3I`7M=G50> z_D+Gp!=P{-MrxdVlSqpVhq-HEC$mK|Gh48yhR4FEQiMj(t?}99Up2$r4Ol*GO~j zn*^0@2oy?hLj1}$ljbpFpZLUn^5uTjWi-*qoP8B$4MaWkBUUy6ahA+tJmR%qPVK`l zK!D`qNn=L%kH+>Od6OH{#;wu1;jN2ke1$d1j^a)l^`(TIH?I!xF(uC9Lw378lmM>Qv++nL`#P{}?j% zi>DqQ#gQuG3|v5cXsSnoA{H}!c)^c88R7+p$oi4AF6}E^0gd8^)ptlK9?&fPf)mC2 z508vOmvPe`h6Otdc#eym4@Y8EM3^|FGj^F5LU;~YjY3v4Wg9k2CK{d z!+{w%DzBZ4L+D0kBKc&d9##@PJ}rww;BdE%ULJkA85pEvO8tWdO1*|95gfb8J%#Ts1qrBGC^ z>csOoylkOPGo(J?hU16&Y=VCB(-C7_ynAX`oH{qQSB?S6uSbjtar><&Pnye^I?I8l z!=;lZ3`B)v>2xGMc}QUcPnh zY6C`1xLx33tcIBvqmRa)D_wk8Im*HGs0?pa*qmHcN2YZx{&#*xy#BrYvCdQ2i`R!L z!wLW(QwB1*x*E1~dYTvkK7=f<6f`kyuL~2sdWQ)z-QKW#LnQBJWwVqAu-B$eklsf5 zER_Zcy!*0Sy`P_UIw;{=bI~R+s@Px&O|h(fdmEJ@fb_7wl)|+1$c8`x%=qoNb@8&$Q{8vuuIdXXc+>U1+nj$YseV$2iRZV=9x$c(OQeiWkvQ;f~_hf$23Od7_PX$FSLNft?Irlw|==E;^O g21aRSmPQ7KNk&G-sV1fdM&@Zrrbg2z#WDs00L33D)Bpeg 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):