1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import absolute_import
6from __future__ import unicode_literals
7
8import re
9import six
10
11
12class EntityPos(int):
13    pass
14
15
16mochibake = re.compile('\ufffd')
17
18
19class Checker(object):
20    '''Abstract class to implement checks per file type.
21    '''
22    pattern = None
23    # if a check uses all reference entities, set this to True
24    needs_reference = False
25
26    @classmethod
27    def use(cls, file):
28        return cls.pattern.match(file.file)
29
30    def __init__(self, extra_tests, locale=None):
31        self.extra_tests = extra_tests
32        self.locale = locale
33        self.reference = None
34
35    def check(self, refEnt, l10nEnt):
36        '''Given the reference and localized Entities, performs checks.
37
38        This is a generator yielding tuples of
39        - "warning" or "error", depending on what should be reported,
40        - tuple of line, column info for the error within the string
41        - description string to be shown in the report
42
43        By default, check for possible encoding errors.
44        '''
45        for m in mochibake.finditer(l10nEnt.all):
46            yield (
47                "warning",
48                EntityPos(m.start()),
49                "\ufffd in: {}".format(l10nEnt.key),
50                "encodings"
51            )
52
53    def set_reference(self, reference):
54        '''Set the reference entities.
55        Only do this if self.needs_reference is True.
56        '''
57        self.reference = reference
58
59
60class CSSCheckMixin(object):
61    def maybe_style(self, ref_value, l10n_value):
62        ref_map, _ = self.parse_css_spec(ref_value)
63        if not ref_map:
64            return
65        l10n_map, errors = self.parse_css_spec(l10n_value)
66        for t in self.check_style(ref_map, l10n_map, errors):
67            yield t
68
69    def check_style(self, ref_map, l10n_map, errors):
70        if not l10n_map:
71            yield ('error', 0, 'reference is a CSS spec', 'css')
72            return
73        if errors:
74            yield ('error', 0, 'reference is a CSS spec', 'css')
75            return
76        msgs = []
77        for prop, unit in l10n_map.items():
78            if prop not in ref_map:
79                msgs.insert(0, '%s only in l10n' % prop)
80                continue
81            else:
82                ref_unit = ref_map.pop(prop)
83                if unit != ref_unit:
84                    msgs.append("units for %s don't match "
85                                "(%s != %s)" % (prop, unit, ref_unit))
86        for prop in six.iterkeys(ref_map):
87            msgs.insert(0, '%s only in reference' % prop)
88        if msgs:
89            yield ('warning', 0, ', '.join(msgs), 'css')
90
91    def parse_css_spec(self, val):
92        if not hasattr(self, '_css_spec'):
93            self._css_spec = re.compile(
94                r'(?:'
95                r'(?P<prop>(?:min\-|max\-)?(?:width|height))'
96                r'[ \t\r\n]*:[ \t\r\n]*'
97                r'(?P<length>[0-9]+|[0-9]*\.[0-9]+)'
98                r'(?P<unit>ch|em|ex|rem|px|cm|mm|in|pc|pt)'
99                r')'
100                r'|\Z'
101            )
102            self._css_sep = re.compile(r'[ \t\r\n]*(?P<semi>;)?[ \t\r\n]*$')
103        refMap = errors = None
104        end = 0
105        for m in self._css_spec.finditer(val):
106            if end == 0 and m.start() == m.end():
107                # no CSS spec found, just immediately end of string
108                return None, None
109            if m.start() > end:
110                split = self._css_sep.match(val, end, m.start())
111                if split is None:
112                    errors = errors or []
113                    errors.append({
114                        'pos': end,
115                        'code': 'css-bad-content',
116                    })
117                elif end > 0 and split.group('semi') is None:
118                    errors = errors or []
119                    errors.append({
120                        'pos': end,
121                        'code': 'css-missing-semicolon',
122                    })
123            if m.group('prop'):
124                refMap = refMap or {}
125                refMap[m.group('prop')] = m.group('unit')
126            end = m.end()
127        return refMap, errors
128