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