calibre/src/pyj/live_css.pyj

231 lines
7.2 KiB
Python

# 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:
pass
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[property]:
cval = node_style.getPropertyValue(property)
cval
all_properties[property] = v'[cval, get_color(property, cval)]'
i += 1
return properties
def get_sheet_rules(sheet):
if sheet.disabled or not sheet.cssRules:
return v'[]'
sheet_media = sheet.media and sheet.media.mediaText
if sheet_media and sheet_media.length and not window.matchMedia(sheet_media).matches:
return v'[]'
return sheet.cssRules
def selector_matches(node, selector):
try:
return node.matches(selector)
except:
# happens if the selector uses epub|type
if 'epub|' in selector:
sanitized_sel = str.replace(selector, 'epub|', '*|')
try:
# TODO: Actually parse the selector and extract the attribute
# and check it manually
return node.matches(sanitized_sel)
except:
return False
return False
def process_rules(node, rules, address, sheet, sheet_index, all_properties, node_style, is_ancestor, ans):
offset = 0
for rule_index in range(rules.length):
rule = rules[rule_index]
rule_address = address.concat(v'[rule_index - offset]')
if rule.type is CSSRule.MEDIA_RULE:
process_rules(node, rule.cssRules, rule_address, sheet, sheet_index, all_properties, node_style, is_ancestor, ans)
continue
if rule.type is not CSSRule.STYLE_RULE:
if rule.type is CSSRule.NAMESPACE_RULE:
offset += 1
continue
st = rule.selectorText
if not selector_matches(node, st):
continue
type = 'sheet'
href = sheet.href
if not href:
href = get_sourceline_address(sheet.ownerNode)
type = 'elem'
# Technically, we should check for the highest specificity
# matching selector, but we cheat and use the first one
parts = st.split(',')
if parts.length > 1:
for q in parts:
if selector_matches(node, 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):
ans = v'[]'
node_style = window.getComputedStyle(node)
sheets = document.styleSheets
for sheet_index in range(sheets.length):
sheet = sheets[sheet_index]
process_rules(node, get_sheet_rules(sheet), v'[]', sheet, sheet_index, 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()