1from __future__ import unicode_literals
2import os
3import sys
4
5try:
6    import urlparse
7except ImportError:
8    import urllib.parse as urlparse
9
10import csv
11import cssutils
12
13import requests
14from lxml import etree
15from inlinestyler.cssselect import CSSSelector, ExpressionError
16
17
18class Conversion(object):
19    def __init__(self):
20        self.CSSErrors = []
21        self.CSSUnsupportErrors = dict()
22        self.supportPercentage = 100
23        self.convertedHTML = ""
24
25    def perform(self, document, sourceHTML, sourceURL, encoding='unicode'):
26        aggregate_css = ""
27
28        # Retrieve CSS rel links from html pasted and aggregate into one string
29        CSSRelSelector = CSSSelector("link[rel=stylesheet],link[rel=StyleSheet],link[rel=STYLESHEET]")
30        matching = CSSRelSelector.evaluate(document)
31        for element in matching:
32            try:
33                csspath = element.get("href")
34                if len(sourceURL):
35                    if element.get("href").lower().find("http://", 0) < 0:
36                        parsed_url = urlparse.urlparse(sourceURL)
37                        csspath = urlparse.urljoin(parsed_url.scheme + "://" + parsed_url.hostname, csspath)
38
39                css_content = requests.get(csspath).text
40                aggregate_css += ''.join(css_content)
41
42                element.getparent().remove(element)
43            except:
44                raise IOError('The stylesheet ' + element.get("href") + ' could not be found')
45
46        # Include inline style elements
47        CSSStyleSelector = CSSSelector("style,Style")
48        matching = CSSStyleSelector.evaluate(document)
49        for element in matching:
50            aggregate_css += element.text
51            element.getparent().remove(element)
52
53        # Convert document to a style dictionary compatible with etree
54        styledict = self.get_view(document, aggregate_css)
55
56        # Set inline style attribute if not one of the elements not worth styling
57        ignore_list = ['html', 'head', 'title', 'meta', 'link', 'script']
58        for element, style in styledict.items():
59            if element.tag not in ignore_list:
60                v = style.getCssText(separator='')
61                element.set('style', v)
62
63        self.convertedHTML = etree.tostring(document, method="html", pretty_print=True, encoding=encoding)
64        return self
65
66    def styleattribute(self, element):
67        """
68          returns css.CSSStyleDeclaration of inline styles, for html: @style
69          """
70        css_text = element.get('style')
71        if css_text:
72            return cssutils.css.CSSStyleDeclaration(cssText=css_text)
73        else:
74            return None
75
76    def get_view(self, document, css):
77
78        view = {}
79        specificities = {}
80        supportratios = {}
81        support_failrate = 0
82        support_totalrate = 0
83        compliance = dict()
84
85        with open(os.path.join(os.path.dirname(__file__), "css_compliance.csv")) as csv_file:
86            compat_list = csv_file.readlines()
87
88        mycsv = csv.DictReader(compat_list, delimiter=str(','))
89
90        for row in mycsv:
91            # Count clients so we can calculate an overall support percentage later
92            client_count = len(row)
93            compliance[row['property'].strip()] = dict(row)
94
95        # Decrement client count to account for first col which is property name
96        client_count -= 1
97
98        sheet = cssutils.parseString(css)
99
100        rules = (rule for rule in sheet if rule.type == rule.STYLE_RULE)
101        for rule in rules:
102
103            for selector in rule.selectorList:
104                try:
105                    cssselector = CSSSelector(selector.selectorText)
106                    matching = cssselector.evaluate(document)
107
108                    for element in matching:
109                        # add styles for all matching DOM elements
110                        if element not in view:
111                            # add initial
112                            view[element] = cssutils.css.CSSStyleDeclaration()
113                            specificities[element] = {}
114
115                            # add inline style if present
116                            inlinestyletext = element.get('style')
117                            if inlinestyletext:
118                                inlinestyle = cssutils.css.CSSStyleDeclaration(cssText=inlinestyletext)
119                            else:
120                                inlinestyle = None
121                            if inlinestyle:
122                                for p in inlinestyle:
123                                    # set inline style specificity
124                                    view[element].setProperty(p)
125                                    specificities[element][p.name] = (1, 0, 0, 0)
126
127                        for p in rule.style:
128                            if p.name not in supportratios:
129                                supportratios[p.name] = {'usage': 0, 'failedClients': 0}
130
131                            supportratios[p.name]['usage'] += 1
132
133                            try:
134                                if p.name not in self.CSSUnsupportErrors:
135                                    for client, support in compliance[p.name].items():
136                                        if support == "N" or support == "P":
137                                            # Increment client failure count for this property
138                                            supportratios[p.name]['failedClients'] += 1
139                                            if p.name not in self.CSSUnsupportErrors:
140                                                if support == "P":
141                                                    self.CSSUnsupportErrors[p.name] = [client + ' (partial support)']
142                                                else:
143                                                    self.CSSUnsupportErrors[p.name] = [client]
144                                            else:
145                                                if support == "P":
146                                                    self.CSSUnsupportErrors[p.name].append(client + ' (partial support)')
147                                                else:
148                                                    self.CSSUnsupportErrors[p.name].append(client)
149
150                            except KeyError:
151                                pass
152
153                            # update styles
154                            if p not in view[element]:
155                                view[element].setProperty(p.name, p.value, p.priority)
156                                specificities[element][p.name] = selector.specificity
157                            else:
158                                sameprio = (p.priority == view[element].getPropertyPriority(p.name))
159                                if not sameprio and bool(p.priority) or (sameprio and selector.specificity >= specificities[element][p.name]):
160                                    # later, more specific or higher prio
161                                    view[element].setProperty(p.name, p.value, p.priority)
162
163                except ExpressionError:
164                    if str(sys.exc_info()[1]) not in self.CSSErrors:
165                        self.CSSErrors.append(str(sys.exc_info()[1]))
166                    pass
167
168        for props, propvals in supportratios.items():
169            support_failrate += (propvals['usage']) * int(propvals['failedClients'])
170            support_totalrate += int(propvals['usage']) * client_count
171
172        if support_failrate and support_totalrate:
173            self.supportPercentage = 100 - ((float(support_failrate) / float(support_totalrate)) * 100)
174        return view
175