# 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: 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()