mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Move the live css related client side code into its own module
This commit is contained in:
parent
4eb75032db
commit
8c1a3e0467
@ -647,7 +647,8 @@ class Preview(QWidget):
|
|||||||
self.stop_split()
|
self.stop_split()
|
||||||
|
|
||||||
def request_live_css_data(self, editor_name, sourceline, tags):
|
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):
|
def apply_settings(self):
|
||||||
s = self.view.settings()
|
s = self.view.settings()
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
# globals: CSSRule
|
|
||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
from elementmaker import E
|
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):
|
def is_hidden(elem):
|
||||||
@ -35,210 +35,6 @@ def find_containing_block(elem):
|
|||||||
elem = elem.parentNode
|
elem = elem.parentNode
|
||||||
return elem
|
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):
|
def scroll_to_node(node):
|
||||||
if node is document.body:
|
if node is document.body:
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
|
209
src/pyj/live_css.pyj
Normal file
209
src/pyj/live_css.pyj
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
# 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()
|
Loading…
x
Reference in New Issue
Block a user