diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py index 4d07824338..9ce2d28269 100644 --- a/src/calibre/gui2/tweak_book/preview.py +++ b/src/calibre/gui2/tweak_book/preview.py @@ -647,7 +647,8 @@ class Preview(QWidget): self.stop_split() def request_live_css_data(self, editor_name, sourceline, tags): - self.view._page.bridge.live_css(editor_name, sourceline, tags) + if self.view._page.bridge.ready: + self.view._page.bridge.live_css(editor_name, sourceline, tags) def apply_settings(self): s = self.view.settings() diff --git a/src/pyj/editor.pyj b/src/pyj/editor.pyj index 58b0d48562..b1d2c385b8 100644 --- a/src/pyj/editor.pyj +++ b/src/pyj/editor.pyj @@ -1,11 +1,11 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2018, Kovid Goyal -# globals: CSSRule from __python__ import bound_methods, hash_literals from elementmaker import E -from qt import to_python, from_python +from live_css import get_matched_css, get_sourceline_address +from qt import from_python, to_python def is_hidden(elem): @@ -35,210 +35,6 @@ def 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' -} # }}} - - -def get_sourceline_address(node): - sourceline = parseInt(node.dataset.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 - - -def get_style_properties(style, all_properties, node_style, is_ancestor): - i = 0 - properties = v'[]' - while i < style.length: - property = style.item(i) - if property: - property = property.toLowerCase() - val = style.getPropertyValue(property) - else: - val = None - 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 - - -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 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 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.matches(st))): - type = 'sheet' - href = sheet.href - 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.matches(q): - st = q - break - properties = get_style_properties(rule.style, all_properties, node_style, is_ancestor) - if properties.length > 0: - data = {'selector':st, 'type':type, 'href':href, 'properties':properties, 'rule_address':rule_address, 'sheet_index':sheet_index} - ans.push(data) - -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 = v'[]' - matching_selectors = {} - for rule in rules: - matching_selectors[rule.selectorText] = True - ans = v'[]' - node_style = window.getComputedStyle(node) - - 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'): - properties = get_style_properties(node.style, all_properties, node_style, is_ancestor) - 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() - def scroll_to_node(node): if node is document.body: window.scrollTo(0, 0) diff --git a/src/pyj/live_css.pyj b/src/pyj/live_css.pyj new file mode 100644 index 0000000000..c93de37b16 --- /dev/null +++ b/src/pyj/live_css.pyj @@ -0,0 +1,209 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal +# globals: CSSRule + +from __python__ import bound_methods, hash_literals + + +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' +} # }}} + + +def get_sourceline_address(node): + sourceline = parseInt(node.dataset.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 + + +def get_style_properties(style, all_properties, node_style, is_ancestor): + i = 0 + properties = v'[]' + while i < style.length: + property = style.item(i) + if property: + property = property.toLowerCase() + val = style.getPropertyValue(property) + else: + val = None + 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 + + +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 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 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.matches(st))): + type = 'sheet' + href = sheet.href + 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.matches(q): + st = q + break + properties = get_style_properties(rule.style, all_properties, node_style, is_ancestor) + if properties.length > 0: + data = {'selector':st, 'type':type, 'href':href, 'properties':properties, 'rule_address':rule_address, 'sheet_index':sheet_index} + ans.push(data) + +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 = v'[]' + matching_selectors = {} + for rule in rules: + matching_selectors[rule.selectorText] = True + ans = v'[]' + node_style = window.getComputedStyle(node) + + 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'): + properties = get_style_properties(node.style, all_properties, node_style, is_ancestor) + 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()