WebUI: Delete obsolete css_checker.py tool.
The tool has outlived its usefulness. 1) It is implemented using regular expressions. 2) Because of 1, it is hard to support newer CSS features that introduce new syntax. 3) Warnings coming from css_checker.py have been ignored for a long time and are often incorrect (for example asking for '0' instead of '0px' inside calc() statements). 4) The tool is effectively unowned, because it is hard to maintain and modify. In the future, we should evaluate and possibly leverage existing open source tools for the same task. Fixed: 1316373,1352274 Change-Id: I5c5edea154d9ce749a88d24a6093f31abee321da Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4582012 Reviewed-by: John Lee <johntlee@chromium.org> Commit-Queue: Demetrios Papadopoulos <dpapad@chromium.org> Reviewed-by: Lei Zhang <thestig@chromium.org> Cr-Commit-Position: refs/heads/main@{#1155186}
This commit is contained in:
chrome/browser
tools/web_dev_style
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2015 The Chromium Authors
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Unit test runner for Web Development Style Guide checks."""
|
||||
|
||||
from web_dev_style import css_checker_test, \
|
||||
html_checker_test, \
|
||||
js_checker_test, \
|
||||
resource_checker_test
|
||||
|
||||
_TEST_MODULES = [
|
||||
css_checker_test,
|
||||
html_checker_test,
|
||||
js_checker_test,
|
||||
resource_checker_test
|
||||
]
|
||||
|
||||
for test_module in _TEST_MODULES:
|
||||
test_module.unittest.main(test_module)
|
@ -1,554 +0,0 @@
|
||||
# Copyright 2012 The Chromium Authors
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Presubmit script for Chromium WebUI resources.
|
||||
|
||||
See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
|
||||
for more details about the presubmit API built into depot_tools, and see
|
||||
https://chromium.googlesource.com/chromium/src/+/main/styleguide/web/web.md
|
||||
for the rules we're checking against here.
|
||||
"""
|
||||
|
||||
# TODO(dbeam): Real CSS parser? https://github.com/danbeam/css-py/tree/css3
|
||||
|
||||
class CSSChecker(object):
|
||||
DISABLE_PREFIX = 'csschecker-disable'
|
||||
DISABLE_FORMAT = DISABLE_PREFIX + '(-[a-z]+)+ [a-z-]+(-[a-z-]+)*'
|
||||
DISABLE_LINE = DISABLE_PREFIX + '-line'
|
||||
|
||||
def __init__(self, input_api, output_api, file_filter=None):
|
||||
self.input_api = input_api
|
||||
self.output_api = output_api
|
||||
self.file_filter = file_filter
|
||||
|
||||
def RemoveAtBlocks(self, s):
|
||||
re = self.input_api.re
|
||||
|
||||
def _remove_comments(s):
|
||||
return re.sub(r'/\*.*\*/', '', s)
|
||||
|
||||
lines = s.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = _remove_comments(lines[i]).strip()
|
||||
if (len(line) > 0 and line[0] == '@' and
|
||||
not line[1:].startswith(("apply", "page")) and
|
||||
line[-1] == '{' and not re.match("\d+x\b", line[1:])):
|
||||
j = i
|
||||
open_brackets = 1
|
||||
while open_brackets > 0:
|
||||
j += 1
|
||||
inner_line = _remove_comments(lines[j]).strip()
|
||||
if not inner_line:
|
||||
continue
|
||||
if inner_line[-1] == '{':
|
||||
open_brackets += 1
|
||||
elif inner_line[-1] == '}':
|
||||
# Ignore single line keyframes (from { height: 0; }).
|
||||
if not re.match(r'\s*(from|to|\d+%)\s*{', inner_line):
|
||||
open_brackets -= 1
|
||||
elif len(inner_line) > 1 and inner_line[-2:] == '};':
|
||||
# End of mixin. TODO(dbeam): worth detecting ": {" start?
|
||||
open_brackets -= 1
|
||||
del lines[j] # Later index first, as indices shift with deletion.
|
||||
del lines[i]
|
||||
else:
|
||||
i += 1
|
||||
return '\n'.join(lines)
|
||||
|
||||
def RunChecks(self):
|
||||
# We use this a lot, so make a nick name variable.
|
||||
re = self.input_api.re
|
||||
|
||||
def _collapseable_hex(s):
|
||||
return (len(s) == 6 and s[0] == s[1] and s[2] == s[3] and s[4] == s[5])
|
||||
|
||||
def _is_gray(s):
|
||||
return s[0] == s[1] == s[2] if len(s) == 3 else s[0:2] == s[2:4] == s[4:6]
|
||||
|
||||
def _extract_inline_style(s):
|
||||
return '\n'.join(re.findall(r'<style\b[^>]*>([^<]*)<\/style>', s))
|
||||
|
||||
def _remove_comments_except_for_disables(s):
|
||||
return re.sub(r'/\*(?! %s \*/$).*?\*/' % self.DISABLE_FORMAT, '', s,
|
||||
flags=re.DOTALL | re.MULTILINE)
|
||||
|
||||
def _remove_grit(s):
|
||||
return re.sub(r"""
|
||||
<if[^>]+>.*?<\s*/\s*if[^>]*>| # <if> contents </if>
|
||||
<include[^>]+> # <include>
|
||||
""", '', s, flags=re.DOTALL | re.VERBOSE)
|
||||
|
||||
mixin_shim_reg = r'[\w-]+_-_[\w-]+'
|
||||
|
||||
def _remove_valid_vars(s):
|
||||
valid_vars = r'--(?!' + mixin_shim_reg + r')[\w-]+:\s*([^;{}}]+);\s*'
|
||||
return re.sub(valid_vars, '', s, flags=re.DOTALL)
|
||||
|
||||
def _remove_disable(content, lstrip=False):
|
||||
prefix_reg = ('\s*' if lstrip else '')
|
||||
disable_reg = '/\* %s \*/' % self.DISABLE_FORMAT
|
||||
return re.sub(prefix_reg + disable_reg, '', content, re.MULTILINE)
|
||||
|
||||
def _remove_template_expressions(s):
|
||||
return re.sub(r'\$i18n(Raw)?{[^}]*}', '', s, flags=re.DOTALL)
|
||||
|
||||
def _rgb_from_hex(s):
|
||||
if len(s) == 3:
|
||||
r, g, b = s[0] + s[0], s[1] + s[1], s[2] + s[2]
|
||||
else:
|
||||
r, g, b = s[0:2], s[2:4], s[4:6]
|
||||
return int(r, base=16), int(g, base=16), int(b, base=16)
|
||||
|
||||
def _strip_prefix(s):
|
||||
return re.sub(r'^-(?:o|ms|moz|khtml|webkit)-', '', s)
|
||||
|
||||
def alphabetize_props(contents):
|
||||
errors = []
|
||||
# TODO(dbeam): make this smart enough to detect issues in mixins.
|
||||
strip_rule = lambda t: _remove_disable(t).strip()
|
||||
for rule in re.finditer(r'{(.*?)}', contents, re.DOTALL):
|
||||
semis = [strip_rule(r) for r in rule.group(1).split(';')][:-1]
|
||||
# Ignore all nested rules.
|
||||
rules = [r for r in semis if re.match(r'^[^{]+: ', r)]
|
||||
props = [r[0:r.find(':')] for r in rules]
|
||||
if props != sorted(props):
|
||||
errors.append(' %s;\n' % (';\n '.join(rules)))
|
||||
return errors
|
||||
|
||||
def braces_have_space_before_and_nothing_after(line):
|
||||
brace_space_reg = re.compile(r"""
|
||||
(?:^|\S){| # selector{ or selector\n{ or
|
||||
{\s*\S+\s* # selector { with stuff after it
|
||||
$ # must be at the end of a line
|
||||
""",
|
||||
re.VERBOSE)
|
||||
return brace_space_reg.search(line)
|
||||
|
||||
def classes_use_dashes(line):
|
||||
# Intentionally dumbed down version of CSS 2.1 grammar for class without
|
||||
# non-ASCII, escape chars, or whitespace.
|
||||
class_reg = re.compile(r"""
|
||||
(?<!')\.(-?[\w-]+).* # ., then maybe -, then alpha numeric and -
|
||||
[,{]\s*$ # selectors should end with a , or {
|
||||
""",
|
||||
re.VERBOSE)
|
||||
m = class_reg.search(line)
|
||||
if not m:
|
||||
return False
|
||||
class_name = m.group(1)
|
||||
return class_name.lower() != class_name or '_' in class_name
|
||||
|
||||
end_mixin_reg = re.compile(r'\s*};\s*$')
|
||||
|
||||
def close_brace_on_new_line(line):
|
||||
# Ignore single frames in a @keyframe, i.e. 0% { margin: 50px; }
|
||||
frame_reg = re.compile(r"""
|
||||
\s*(from|to|\d+%)\s*{ # 50% {
|
||||
\s*[\w-]+: # rule:
|
||||
(\s*[\w\(\), -\.]+)+\s*; # value;
|
||||
\s*}\s* # }
|
||||
""",
|
||||
re.VERBOSE)
|
||||
return ('}' in line and re.search(r'[^ }]', line) and
|
||||
not frame_reg.match(line) and not end_mixin_reg.match(line))
|
||||
|
||||
def colons_have_space_after(line):
|
||||
colon_space_reg = re.compile(r"""
|
||||
(?<!data) # ignore data URIs
|
||||
:(?!//) # ignore url(http://), etc.
|
||||
\S[^;]+;\s* # only catch one-line rules for now
|
||||
""",
|
||||
re.VERBOSE)
|
||||
return colon_space_reg.search(line)
|
||||
|
||||
def favor_single_quotes(line):
|
||||
return '"' in line
|
||||
|
||||
# Shared between hex_could_be_shorter and rgb_if_not_gray.
|
||||
hex_reg = re.compile(r"""
|
||||
\#([a-fA-F0-9]{3}|[a-fA-F0-9]{6}) # pound followed by 3 or 6 hex digits
|
||||
(?=[^\w-]|$) # no more alphanum chars or at EOL
|
||||
(?!.*(?:{.*|,\s*)$) # not in a selector
|
||||
""",
|
||||
re.VERBOSE)
|
||||
|
||||
def hex_could_be_shorter(line):
|
||||
m = hex_reg.search(line)
|
||||
return (m and _is_gray(m.group(1)) and _collapseable_hex(m.group(1)))
|
||||
|
||||
def rgb_if_not_gray(line):
|
||||
m = hex_reg.search(line)
|
||||
return (m and not _is_gray(m.group(1)))
|
||||
|
||||
small_seconds_reg = re.compile(r"""
|
||||
(?:^|[^\w-]) # start of a line or a non-alphanumeric char
|
||||
(0?\.[0-9]+)s # 1.0s
|
||||
(?!-?[\w-]) # no following - or alphanumeric chars
|
||||
""",
|
||||
re.VERBOSE)
|
||||
|
||||
def milliseconds_for_small_times(line):
|
||||
return small_seconds_reg.search(line)
|
||||
|
||||
def suggest_ms_from_s(line):
|
||||
ms = int(float(small_seconds_reg.search(line).group(1)) * 1000)
|
||||
return ' (replace with %dms)' % ms
|
||||
|
||||
def no_data_uris_in_source_files(line):
|
||||
return re.search(r'\(\s*\s*data:', line)
|
||||
|
||||
def no_mixin_shims(line):
|
||||
return re.search(r'--' + mixin_shim_reg + r'\s*:', line)
|
||||
|
||||
def no_quotes_in_url(line):
|
||||
return re.search('url\s*\(\s*["\']', line, re.IGNORECASE)
|
||||
|
||||
def one_rule_per_line(line):
|
||||
line = _remove_disable(line)
|
||||
one_rule_reg = re.compile(r"""
|
||||
[\w-](?<!data): # a rule: but no data URIs
|
||||
(?!//)[^;]+; # value; ignoring colons in protocols:// and };
|
||||
\s*[^ }]\s* # any non-space after the end colon
|
||||
""",
|
||||
re.VERBOSE)
|
||||
return one_rule_reg.search(line) and not end_mixin_reg.match(line)
|
||||
|
||||
def pseudo_elements_double_colon(contents):
|
||||
pseudo_elements = ['after',
|
||||
'before',
|
||||
'calendar-picker-indicator',
|
||||
'color-swatch',
|
||||
'color-swatch-wrapper',
|
||||
'date-and-time-container',
|
||||
'date-and-time-value',
|
||||
'datetime-edit',
|
||||
'datetime-edit-ampm-field',
|
||||
'datetime-edit-day-field',
|
||||
'datetime-edit-hour-field',
|
||||
'datetime-edit-millisecond-field',
|
||||
'datetime-edit-minute-field',
|
||||
'datetime-edit-month-field',
|
||||
'datetime-edit-second-field',
|
||||
'datetime-edit-text',
|
||||
'datetime-edit-week-field',
|
||||
'datetime-edit-year-field',
|
||||
'details-marker',
|
||||
'file-upload-button',
|
||||
'first-letter',
|
||||
'first-line',
|
||||
'inner-spin-button',
|
||||
'input-placeholder',
|
||||
'input-speech-button',
|
||||
'media-slider-container',
|
||||
'media-slider-thumb',
|
||||
'meter-bar',
|
||||
'meter-even-less-good-value',
|
||||
'meter-inner-element',
|
||||
'meter-optimum-value',
|
||||
'meter-suboptimum-value',
|
||||
'progress-bar',
|
||||
'progress-inner-element',
|
||||
'progress-value',
|
||||
'resizer',
|
||||
'scrollbar',
|
||||
'scrollbar-button',
|
||||
'scrollbar-corner',
|
||||
'scrollbar-thumb',
|
||||
'scrollbar-track',
|
||||
'scrollbar-track-piece',
|
||||
'search-cancel-button',
|
||||
'search-decoration',
|
||||
'search-results-button',
|
||||
'search-results-decoration',
|
||||
'selection',
|
||||
'slider-container',
|
||||
'slider-runnable-track',
|
||||
'slider-thumb',
|
||||
'textfield-decoration-container',
|
||||
'validation-bubble',
|
||||
'validation-bubble-arrow',
|
||||
'validation-bubble-arrow-clipper',
|
||||
'validation-bubble-heading',
|
||||
'validation-bubble-message',
|
||||
'validation-bubble-text-block']
|
||||
pseudo_reg = re.compile(r"""
|
||||
(?<!:): # a single colon, i.e. :after but not ::after
|
||||
([a-zA-Z-]+) # a pseudo element, class, or function
|
||||
(?=[^{}]+?{) # make sure a selector, not inside { rules }
|
||||
""",
|
||||
re.MULTILINE | re.VERBOSE)
|
||||
errors = []
|
||||
for p in re.finditer(pseudo_reg, contents):
|
||||
pseudo = p.group(1).strip().splitlines()[0]
|
||||
if _strip_prefix(pseudo.lower()) in pseudo_elements:
|
||||
errors.append(' :%s (should be ::%s)' % (pseudo, pseudo))
|
||||
return errors
|
||||
|
||||
def one_selector_per_line(contents):
|
||||
# Ignore all patterns nested in :any(), :is().
|
||||
any_reg = re.compile(
|
||||
r"""
|
||||
:(?:
|
||||
(?:-webkit-)?any # :-webkit-any(a, b, i) selector
|
||||
|is # :is(...) selector
|
||||
)\(
|
||||
""", re.DOTALL | re.VERBOSE)
|
||||
# Iteratively remove nested :is(), :any() patterns from |contents|.
|
||||
while True:
|
||||
m = re.search(any_reg, contents)
|
||||
if m is None:
|
||||
break
|
||||
start, end = m.span()
|
||||
# Find corresponding right parenthesis.
|
||||
pcount = 1
|
||||
while end < len(contents) and pcount > 0:
|
||||
if contents[end] == '(':
|
||||
pcount += 1
|
||||
elif contents[end] == ')':
|
||||
pcount -= 1
|
||||
end += 1
|
||||
contents = contents[:start] + contents[end:]
|
||||
|
||||
multi_sels_reg = re.compile(
|
||||
r"""
|
||||
(?:}\s*)? # ignore 0% { blah: blah; }, from @keyframes
|
||||
([^,]+,(?=[^{};]+?{) # selector junk {, not in a { rule }
|
||||
.*[,{])\s*$ # has to end with , or {
|
||||
""", re.MULTILINE | re.VERBOSE)
|
||||
errors = []
|
||||
for b in re.finditer(multi_sels_reg, contents):
|
||||
errors.append(' ' + b.group(1).strip().splitlines()[-1:][0])
|
||||
return errors
|
||||
|
||||
def suggest_rgb_from_hex(line):
|
||||
suggestions = ['rgb(%d, %d, %d)' % _rgb_from_hex(h.group(1))
|
||||
for h in re.finditer(hex_reg, line)]
|
||||
return ' (replace with %s)' % ', '.join(suggestions)
|
||||
|
||||
def suggest_short_hex(line):
|
||||
h = hex_reg.search(line).group(1)
|
||||
return ' (replace with #%s)' % (h[0] + h[2] + h[4])
|
||||
|
||||
prefixed_logical_axis_reg = re.compile(r"""
|
||||
-webkit-(min-|max-|)logical-(height|width):
|
||||
""", re.VERBOSE)
|
||||
|
||||
def suggest_unprefixed_logical_axis(line):
|
||||
prefix, prop = prefixed_logical_axis_reg.search(line).groups()
|
||||
block_or_inline = 'block' if prop == 'height' else 'inline'
|
||||
return ' (replace with %s)' % (prefix + block_or_inline + '-size')
|
||||
|
||||
def prefixed_logical_axis(line):
|
||||
return prefixed_logical_axis_reg.search(line)
|
||||
|
||||
prefixed_logical_side_reg = re.compile(r"""
|
||||
-webkit-(margin|padding|border)-(before|after|start|end)
|
||||
(?!-collapse)(-\w+|):
|
||||
""", re.VERBOSE)
|
||||
|
||||
def suggest_unprefixed_logical_side(line):
|
||||
prop, pos, suffix = prefixed_logical_side_reg.search(line).groups()
|
||||
if pos == 'before' or pos == 'after':
|
||||
block_or_inline = 'block'
|
||||
else:
|
||||
block_or_inline = 'inline'
|
||||
if pos == 'start' or pos == 'before':
|
||||
start_or_end = 'start'
|
||||
else:
|
||||
start_or_end = 'end'
|
||||
return ' (replace with %s)' % (
|
||||
prop + '-' + block_or_inline + '-' + start_or_end + suffix)
|
||||
|
||||
def prefixed_logical_side(line):
|
||||
return prefixed_logical_side_reg.search(line)
|
||||
|
||||
_LEFT_RIGHT_REG = '(?:(border|margin|padding)-|(text-align): )' \
|
||||
'(left|right)' \
|
||||
'(?:(-[a-z-^:]+):)?(?!.*/\* %s left-right \*/)' % \
|
||||
self.DISABLE_LINE
|
||||
|
||||
def start_end_instead_of_left_right(line):
|
||||
return re.search(_LEFT_RIGHT_REG, line, re.IGNORECASE)
|
||||
|
||||
def suggest_start_end_from_left_right(line):
|
||||
groups = re.search(_LEFT_RIGHT_REG, line, re.IGNORECASE).groups()
|
||||
prop_start, text_align, left_right, prop_end = groups
|
||||
start_end = {'left': 'start', 'right': 'end'}[left_right]
|
||||
if text_align:
|
||||
return ' (replace with text-align: %s)' % start_end
|
||||
prop = '%s-inline-%s%s' % (prop_start, start_end, prop_end or '')
|
||||
return ' (replace with %s)' % prop
|
||||
|
||||
def zero_width_lengths(contents):
|
||||
hsl_reg = re.compile(r"""
|
||||
hsl\([^\)]* # hsl(maybestuff
|
||||
(?:[, ]|(?<=\()) # a comma or space not followed by a (
|
||||
(?:0?\.?)?0% # some equivalent to 0%
|
||||
""",
|
||||
re.VERBOSE)
|
||||
zeros_reg = re.compile(r"""
|
||||
^.*(?:^|[^0-9.]) # start/non-number
|
||||
(?:\.0|0(?:\.0? # .0, 0, or 0.0
|
||||
|px|em|%|in|cm|mm|pc|pt|ex)) # a length unit
|
||||
(?!svg|png|jpg)(?:\D|$) # non-number/end
|
||||
(?=[^{}]+?}).*$ # only { rules }
|
||||
""",
|
||||
re.MULTILINE | re.VERBOSE)
|
||||
errors = []
|
||||
for z in re.finditer(zeros_reg, contents):
|
||||
first_line = z.group(0).strip().splitlines()[0]
|
||||
if not hsl_reg.search(first_line):
|
||||
errors.append(' ' + first_line)
|
||||
return errors
|
||||
|
||||
def mixins(line):
|
||||
return re.search(r'--[\w-]+:\s*({.*?)', line) or re.search(
|
||||
r'@apply', line)
|
||||
|
||||
# NOTE: Currently multi-line checks don't support 'after'. Instead, add
|
||||
# suggestions while parsing the file so another pass isn't necessary.
|
||||
added_or_modified_files_checks = [
|
||||
{ 'desc': 'Alphabetize properties and list vendor specific (i.e. '
|
||||
'-webkit) above standard.',
|
||||
'test': alphabetize_props,
|
||||
'multiline': True,
|
||||
},
|
||||
{ 'desc': 'Start braces ({) end a selector, have a space before them '
|
||||
'and no rules after.',
|
||||
'test': braces_have_space_before_and_nothing_after,
|
||||
},
|
||||
{ 'desc': 'Classes use .dash-form.',
|
||||
'test': classes_use_dashes,
|
||||
},
|
||||
{ 'desc': 'Always put a rule closing brace (}) on a new line.',
|
||||
'test': close_brace_on_new_line,
|
||||
},
|
||||
{ 'desc': 'Colons (:) should have a space after them.',
|
||||
'test': colons_have_space_after,
|
||||
},
|
||||
{ 'desc': 'Use single quotes (\') instead of double quotes (") in '
|
||||
'strings.',
|
||||
'test': favor_single_quotes,
|
||||
},
|
||||
{ 'desc': 'Use abbreviated hex (#rgb) when in form #rrggbb.',
|
||||
'test': hex_could_be_shorter,
|
||||
'after': suggest_short_hex,
|
||||
},
|
||||
{ 'desc': 'Use milliseconds for time measurements under 1 second.',
|
||||
'test': milliseconds_for_small_times,
|
||||
'after': suggest_ms_from_s,
|
||||
},
|
||||
{ 'desc': "Don't use data URIs in source files. Use grit instead.",
|
||||
'test': no_data_uris_in_source_files,
|
||||
},
|
||||
{ 'desc': "Don't override custom properties created by Polymer's mixin "
|
||||
"shim. Set mixins or documented custom properties directly.",
|
||||
'test': no_mixin_shims,
|
||||
},
|
||||
{ 'desc': "Don't use quotes in url().",
|
||||
'test': no_quotes_in_url,
|
||||
},
|
||||
{ 'desc': 'One rule per line (what not to do: color: red; margin: 0;).',
|
||||
'test': one_rule_per_line,
|
||||
},
|
||||
{ 'desc': 'One selector per line (what not to do: a, b {}).',
|
||||
'test': one_selector_per_line,
|
||||
'multiline': True,
|
||||
},
|
||||
{ 'desc': 'Pseudo-elements should use double colon (i.e. ::after).',
|
||||
'test': pseudo_elements_double_colon,
|
||||
'multiline': True,
|
||||
},
|
||||
{ 'desc': 'Use rgb() over #hex when not a shade of gray (like #333).',
|
||||
'test': rgb_if_not_gray,
|
||||
'after': suggest_rgb_from_hex,
|
||||
},
|
||||
{ 'desc': 'Unprefix logical axis property.',
|
||||
'test': prefixed_logical_axis,
|
||||
'after': suggest_unprefixed_logical_axis,
|
||||
},
|
||||
{ 'desc': 'Unprefix logical side property.',
|
||||
'test': prefixed_logical_side,
|
||||
'after': suggest_unprefixed_logical_side,
|
||||
},
|
||||
{
|
||||
'desc': 'Use -start/end instead of -left/right ' \
|
||||
'(https://goo.gl/gQYY7z, add /* %s left-right */ to ' \
|
||||
'suppress)' % self.DISABLE_LINE,
|
||||
'test': start_end_instead_of_left_right,
|
||||
'after': suggest_start_end_from_left_right,
|
||||
},
|
||||
{ 'desc': 'Use "0" for zero-width lengths (i.e. 0px -> 0)',
|
||||
'test': zero_width_lengths,
|
||||
'multiline': True,
|
||||
},
|
||||
{ 'desc': 'Avoid using CSS mixins. Use CSS shadow parts, CSS ' \
|
||||
'variables, or common CSS classes instead.',
|
||||
'test': mixins,
|
||||
},
|
||||
]
|
||||
|
||||
results = []
|
||||
affected_files = self.input_api.AffectedFiles(include_deletes=False,
|
||||
file_filter=self.file_filter)
|
||||
files = []
|
||||
for f in affected_files:
|
||||
path = f.LocalPath()
|
||||
|
||||
is_html = path.endswith('.html')
|
||||
if not is_html and not path.endswith('.css'):
|
||||
continue
|
||||
|
||||
file_contents = '\n'.join(f.NewContents())
|
||||
|
||||
# Remove all /*comments*/, @at-keywords, and grit <if|include> tags; we're
|
||||
# not using a real parser. TODO(dbeam): Check alpha in <if> blocks.
|
||||
|
||||
file_contents = _remove_grit(file_contents) # Must be done first.
|
||||
|
||||
if is_html:
|
||||
# The <style> extraction regex can't handle <if> nor /* <tag> */.
|
||||
prepped_html = _remove_comments_except_for_disables(file_contents)
|
||||
file_contents = _extract_inline_style(prepped_html)
|
||||
|
||||
file_contents = self.RemoveAtBlocks(file_contents)
|
||||
|
||||
if not is_html:
|
||||
file_contents = _remove_comments_except_for_disables(file_contents)
|
||||
|
||||
file_contents = _remove_valid_vars(file_contents)
|
||||
file_contents = _remove_template_expressions(file_contents)
|
||||
|
||||
files.append((path, file_contents))
|
||||
|
||||
for f in files:
|
||||
file_errors = []
|
||||
for check in added_or_modified_files_checks:
|
||||
# If the check is multiline, it receives the whole file and gives us
|
||||
# back a list of things wrong. If the check isn't multiline, we pass it
|
||||
# each line and the check returns something truthy if there's an issue.
|
||||
if ('multiline' in check and check['multiline']):
|
||||
assert not 'after' in check
|
||||
check_errors = check['test'](f[1])
|
||||
if len(check_errors) > 0:
|
||||
file_errors.append('- %s\n%s' %
|
||||
(check['desc'], '\n'.join(check_errors).rstrip()))
|
||||
else:
|
||||
check_errors = []
|
||||
lines = f[1].splitlines()
|
||||
for lnum, line in enumerate(lines):
|
||||
if check['test'](line):
|
||||
error = ' ' + _remove_disable(line, lstrip=True).strip()
|
||||
if 'after' in check:
|
||||
error += check['after'](line)
|
||||
check_errors.append(error)
|
||||
if len(check_errors) > 0:
|
||||
file_errors.append('- %s\n%s' %
|
||||
(check['desc'], '\n'.join(check_errors)))
|
||||
if file_errors:
|
||||
results.append(self.output_api.PresubmitPromptWarning(
|
||||
'%s:\n%s' % (f[0], '\n\n'.join(file_errors))))
|
||||
|
||||
return results
|
@ -1,683 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2015 The Chromium Authors
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
from . import css_checker
|
||||
from os import path as os_path
|
||||
import re
|
||||
from sys import path as sys_path
|
||||
import unittest
|
||||
|
||||
_HERE = os_path.dirname(os_path.abspath(__file__))
|
||||
sys_path.append(os_path.join(_HERE, '..', '..'))
|
||||
|
||||
from PRESUBMIT_test_mocks import MockInputApi, MockOutputApi, MockFile
|
||||
|
||||
|
||||
class CssCheckerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(CssCheckerTest, self).setUp()
|
||||
|
||||
self.input_api = MockInputApi()
|
||||
self.checker = css_checker.CSSChecker(self.input_api, MockOutputApi())
|
||||
|
||||
def _create_file(self, contents, filename):
|
||||
self.input_api.files.append(MockFile(filename, contents.splitlines()))
|
||||
|
||||
def VerifyContentIsValid(self, contents, filename='fake.css'):
|
||||
self._create_file(contents, filename)
|
||||
results = self.checker.RunChecks()
|
||||
self.assertEqual(len(results), 0)
|
||||
|
||||
def VerifyContentsProducesOutput(self, contents, output, filename='fake.css'):
|
||||
self._create_file(contents, filename)
|
||||
results = self.checker.RunChecks()
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].message, filename + ':\n' + output.strip())
|
||||
|
||||
def testCssAlphaWithAtBlock(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
<include src="../shared/css/cr/ui/overlay.css">
|
||||
<include src="chrome://resources/totally-cool.css" />
|
||||
|
||||
/* A hopefully safely ignored comment and @media statement. /**/
|
||||
@media print {
|
||||
div {
|
||||
display: block;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.rule {
|
||||
z-index: 5;
|
||||
<if expr="not is macosx">
|
||||
background-image: url(chrome://resources/BLAH); /* TODO(dbeam): Fix this. */
|
||||
background-color: rgb(235, 239, 249);
|
||||
</if>
|
||||
<if expr="is_macosx">
|
||||
background-color: white;
|
||||
background-image: url(chrome://resources/BLAH2);
|
||||
</if>
|
||||
color: black;
|
||||
}
|
||||
|
||||
<if expr="is_macosx">
|
||||
.language-options-right {
|
||||
visibility: hidden;
|
||||
opacity: 1; /* TODO(dbeam): Fix this. */
|
||||
}
|
||||
</if>
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a[href] {
|
||||
z-index: 3;
|
||||
color: blue;
|
||||
}
|
||||
}""", """
|
||||
- Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
|
||||
display: block;
|
||||
color: red;
|
||||
|
||||
z-index: 5;
|
||||
color: black;
|
||||
|
||||
z-index: 3;
|
||||
color: blue;""")
|
||||
|
||||
def testCssStringWithAt(self):
|
||||
self.VerifyContentIsValid("""
|
||||
#logo {
|
||||
background-image: url(images/google_logo.png@2x);
|
||||
}
|
||||
|
||||
body.alternate-logo #logo {
|
||||
-webkit-mask-image: url(images/google_logo.png@2x);
|
||||
background: none;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-inline-start: 5px;
|
||||
}
|
||||
|
||||
.stuff1 {
|
||||
}
|
||||
|
||||
.stuff2 {
|
||||
}
|
||||
""")
|
||||
|
||||
def testCssAlphaWithNonStandard(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
div {
|
||||
/* A hopefully safely ignored comment and @media statement. /**/
|
||||
color: red;
|
||||
-webkit-margin-before-collapse: discard;
|
||||
}""", """
|
||||
- Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
|
||||
color: red;
|
||||
-webkit-margin-before-collapse: discard;""")
|
||||
|
||||
def testCssAlphaWithLongerDashedProps(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
div {
|
||||
border-inline-start: 5px; /* A hopefully removed comment. */
|
||||
border: 5px solid red;
|
||||
}""", """
|
||||
- Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
|
||||
border-inline-start: 5px;
|
||||
border: 5px solid red;""")
|
||||
|
||||
def testCssAlphaWithVariables(self):
|
||||
self.VerifyContentIsValid("""
|
||||
#id {
|
||||
--zzyxx-xylophone: 3px;
|
||||
--aardvark-animal: var(--zzyxz-xylophone);
|
||||
}
|
||||
""")
|
||||
|
||||
def testCssBracesHaveSpaceBeforeAndNothingAfter(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
/* Hello! */div/* Comment here*/{
|
||||
display: block;
|
||||
}
|
||||
|
||||
blah /* hey! */
|
||||
{
|
||||
rule: value;
|
||||
}
|
||||
|
||||
.mixed-in {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.this.is { /* allowed */
|
||||
rule: value;
|
||||
}""", """
|
||||
- Start braces ({) end a selector, have a space before them and no rules after.
|
||||
div{
|
||||
{""")
|
||||
|
||||
def testMixins(self):
|
||||
self.VerifyContentsProducesOutput(
|
||||
"""
|
||||
.mixed-in {
|
||||
--css-mixin: {
|
||||
color: red;
|
||||
}
|
||||
}""", """
|
||||
- Avoid using CSS mixins. Use CSS shadow parts, CSS variables, or common CSS \
|
||||
classes instead.
|
||||
--css-mixin: {""")
|
||||
|
||||
def testCssClassesUseDashes(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
.className,
|
||||
.ClassName,
|
||||
.class-name /* We should not catch this. */,
|
||||
.class_name,
|
||||
[i18n-values*='.innerHTML:'] {
|
||||
display: block;
|
||||
}""", """
|
||||
- Classes use .dash-form.
|
||||
.className,
|
||||
.ClassName,
|
||||
.class_name,""")
|
||||
|
||||
def testCssCloseBraceOnNewLine(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
@-webkit-keyframe blah {
|
||||
from { height: rotate(-10turn); }
|
||||
100% { height: 500px; }
|
||||
}
|
||||
|
||||
#id { /* $i18n{*} and $i18nRaw{*} should be ignored. */
|
||||
rule: $i18n{someValue};
|
||||
rule2: $i18nRaw{someValue};
|
||||
}
|
||||
|
||||
#rule {
|
||||
rule: value; }""", """
|
||||
- Always put a rule closing brace (}) on a new line.
|
||||
rule: value; }""")
|
||||
|
||||
def testCssColonsHaveSpaceAfter(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
div:not(.class):not([attr=5]), /* We should not catch this. */
|
||||
div:not(.class):not([attr]) /* Nor this. */ {
|
||||
background: url(data:image/jpeg,asdfasdfsadf); /* Ignore this. */
|
||||
background: -webkit-linear-gradient(left, red,
|
||||
80% blah blee blar);
|
||||
color: red;
|
||||
display:block;
|
||||
}""", """
|
||||
- Colons (:) should have a space after them.
|
||||
display:block;
|
||||
|
||||
- Don't use data URIs in source files. Use grit instead.
|
||||
background: url(data:image/jpeg,asdfasdfsadf);""")
|
||||
|
||||
def testCssFavorSingleQuotes(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
html[dir="rtl"] body,
|
||||
html[dir=ltr] body /* TODO(dbeam): Require '' around rtl in future? */ {
|
||||
font-family: "Open Sans";
|
||||
<if expr="is_macosx">
|
||||
blah: blee;
|
||||
</if>
|
||||
}""", """
|
||||
- Use single quotes (') instead of double quotes (") in strings.
|
||||
html[dir="rtl"] body,
|
||||
font-family: "Open Sans";""")
|
||||
|
||||
def testCssHexCouldBeShorter(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
#abc,
|
||||
#abc-,
|
||||
#abc-ghij,
|
||||
#abcdef-,
|
||||
#abcdef-ghij,
|
||||
#aaaaaa,
|
||||
#bbaacc {
|
||||
background-color: #336699; /* Ignore short hex rule if not gray. */
|
||||
color: #999999;
|
||||
color: #666;
|
||||
}""", """
|
||||
- Use abbreviated hex (#rgb) when in form #rrggbb.
|
||||
color: #999999; (replace with #999)
|
||||
|
||||
- Use rgb() over #hex when not a shade of gray (like #333).
|
||||
background-color: #336699; (replace with rgb(51, 102, 153))""")
|
||||
|
||||
def testCssUseMillisecondsForSmallTimes(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
.transition-0s /* This is gross but may happen. */ {
|
||||
transform: one 0.2s;
|
||||
transform: two .1s;
|
||||
transform: tree 1s;
|
||||
transform: four 300ms;
|
||||
}""", """
|
||||
- Use milliseconds for time measurements under 1 second.
|
||||
transform: one 0.2s; (replace with 200ms)
|
||||
transform: two .1s; (replace with 100ms)""")
|
||||
|
||||
def testCssNoDataUrisInSourceFiles(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
img {
|
||||
background: url( data:image/jpeg,4\/\/350|\/|3|2 );
|
||||
}""", """
|
||||
- Don't use data URIs in source files. Use grit instead.
|
||||
background: url( data:image/jpeg,4\/\/350|\/|3|2 );""")
|
||||
|
||||
def testCssNoMixinShims(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
:host {
|
||||
--good-property: red;
|
||||
--not-okay-mixin_-_not-okay-property: green;
|
||||
}""", """
|
||||
- Don't override custom properties created by Polymer's mixin shim. Set \
|
||||
mixins or documented custom properties directly.
|
||||
--not-okay-mixin_-_not-okay-property: green;""")
|
||||
|
||||
def testCssNoQuotesInUrl(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
img {
|
||||
background: url('chrome://resources/images/blah.jpg');
|
||||
background: url("../../folder/hello.png");
|
||||
}""", """
|
||||
- Use single quotes (') instead of double quotes (") in strings.
|
||||
background: url("../../folder/hello.png");
|
||||
|
||||
- Don't use quotes in url().
|
||||
background: url('chrome://resources/images/blah.jpg');
|
||||
background: url("../../folder/hello.png");""")
|
||||
|
||||
def testCssOneRulePerLine(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type,
|
||||
a:not([hidden]):not(.custom-appearance):not([version=1]):first-of-type ~
|
||||
input[type='checkbox']:not([hidden]),
|
||||
div {
|
||||
background: url(chrome://resources/BLAH);
|
||||
rule: value; /* rule: value; */
|
||||
rule: value; rule: value;
|
||||
}
|
||||
""", """
|
||||
- One rule per line (what not to do: color: red; margin: 0;).
|
||||
rule: value; rule: value;""")
|
||||
|
||||
def testCssOneSelectorPerLine(self):
|
||||
self.VerifyContentsProducesOutput(
|
||||
"""
|
||||
a,
|
||||
div,a,
|
||||
div,/* Hello! */ span,
|
||||
#id.class([dir=rtl]):not(.class):any(a, b, d),
|
||||
div :is(:not(a), #b, .c) {
|
||||
rule: value;
|
||||
}
|
||||
|
||||
a,
|
||||
div,a {
|
||||
some-other: rule here;
|
||||
}""", """
|
||||
- One selector per line (what not to do: a, b {}).
|
||||
div,a,
|
||||
div, span,
|
||||
div,a {""")
|
||||
|
||||
def testCssPseudoElementDoubleColon(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
a:href,
|
||||
br::after,
|
||||
::-webkit-scrollbar-thumb,
|
||||
a:not([empty]):hover:focus:active, /* shouldn't catch here and above */
|
||||
abbr:after,
|
||||
.tree-label:empty:after,
|
||||
b:before,
|
||||
:-WebKit-ScrollBar {
|
||||
rule: value;
|
||||
}""", """
|
||||
- Pseudo-elements should use double colon (i.e. ::after).
|
||||
:after (should be ::after)
|
||||
:after (should be ::after)
|
||||
:before (should be ::before)
|
||||
:-WebKit-ScrollBar (should be ::-WebKit-ScrollBar)
|
||||
""")
|
||||
|
||||
def testCssRgbIfNotGray(self):
|
||||
self.VerifyContentsProducesOutput(
|
||||
"""
|
||||
#abc,
|
||||
#aaa,
|
||||
#aabbcc {
|
||||
background: -webkit-linear-gradient(left, from(#abc), to(#def));
|
||||
color: #bad;
|
||||
color: #bada55;
|
||||
}""", """
|
||||
- Use rgb() over #hex when not a shade of gray (like #333).
|
||||
background: -webkit-linear-gradient(left, from(#abc), to(#def)); """
|
||||
"""(replace with rgb(170, 187, 204), rgb(221, 238, 255))
|
||||
color: #bad; (replace with rgb(187, 170, 221))
|
||||
color: #bada55; (replace with rgb(186, 218, 85))""")
|
||||
|
||||
def testPrefixedLogicalAxis(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
.test {
|
||||
-webkit-logical-height: 50%;
|
||||
-webkit-logical-width: 50%;
|
||||
-webkit-max-logical-height: 200px;
|
||||
-webkit-max-logical-width: 200px;
|
||||
-webkit-min-logical-height: 100px;
|
||||
-webkit-min-logical-width: 100px;
|
||||
}
|
||||
""", """
|
||||
- Unprefix logical axis property.
|
||||
-webkit-logical-height: 50%; (replace with block-size)
|
||||
-webkit-logical-width: 50%; (replace with inline-size)
|
||||
-webkit-max-logical-height: 200px; (replace with max-block-size)
|
||||
-webkit-max-logical-width: 200px; (replace with max-inline-size)
|
||||
-webkit-min-logical-height: 100px; (replace with min-block-size)
|
||||
-webkit-min-logical-width: 100px; (replace with min-inline-size)""")
|
||||
|
||||
def testPrefixedLogicalSide(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
.test {
|
||||
-webkit-border-after: 1px solid blue;
|
||||
-webkit-border-after-color: green;
|
||||
-webkit-border-after-style: dotted;
|
||||
-webkit-border-after-width: 10px;
|
||||
-webkit-border-before: 2px solid blue;
|
||||
-webkit-border-before-color: green;
|
||||
-webkit-border-before-style: dotted;
|
||||
-webkit-border-before-width: 20px;
|
||||
-webkit-border-end: 3px solid blue;
|
||||
-webkit-border-end-color: green;
|
||||
-webkit-border-end-style: dotted;
|
||||
-webkit-border-end-width: 30px;
|
||||
-webkit-border-start: 4px solid blue;
|
||||
-webkit-border-start-color: green;
|
||||
-webkit-border-start-style: dotted;
|
||||
-webkit-border-start-width: 40px;
|
||||
-webkit-margin-after: 1px;
|
||||
-webkit-margin-after-collapse: discard;
|
||||
-webkit-margin-before: 2px;
|
||||
-webkit-margin-before-collapse: discard;
|
||||
-webkit-margin-end: 3px;
|
||||
-webkit-margin-end-collapse: discard;
|
||||
-webkit-margin-start: 4px;
|
||||
-webkit-margin-start-collapse: discard;
|
||||
-webkit-padding-after: 1px;
|
||||
-webkit-padding-before: 2px;
|
||||
-webkit-padding-end: 3px;
|
||||
-webkit-padding-start: 4px;
|
||||
}
|
||||
""", """
|
||||
- Unprefix logical side property.
|
||||
-webkit-border-after: 1px solid blue; (replace with border-block-end)
|
||||
-webkit-border-after-color: green; (replace with border-block-end-color)
|
||||
-webkit-border-after-style: dotted; (replace with border-block-end-style)
|
||||
-webkit-border-after-width: 10px; (replace with border-block-end-width)
|
||||
-webkit-border-before: 2px solid blue; (replace with border-block-start)
|
||||
-webkit-border-before-color: green; (replace with border-block-start-color)
|
||||
-webkit-border-before-style: dotted; (replace with border-block-start-style)
|
||||
-webkit-border-before-width: 20px; (replace with border-block-start-width)
|
||||
-webkit-border-end: 3px solid blue; (replace with border-inline-end)
|
||||
-webkit-border-end-color: green; (replace with border-inline-end-color)
|
||||
-webkit-border-end-style: dotted; (replace with border-inline-end-style)
|
||||
-webkit-border-end-width: 30px; (replace with border-inline-end-width)
|
||||
-webkit-border-start: 4px solid blue; (replace with border-inline-start)
|
||||
-webkit-border-start-color: green; (replace with border-inline-start-color)
|
||||
-webkit-border-start-style: dotted; (replace with border-inline-start-style)
|
||||
-webkit-border-start-width: 40px; (replace with border-inline-start-width)
|
||||
-webkit-margin-after: 1px; (replace with margin-block-end)
|
||||
-webkit-margin-before: 2px; (replace with margin-block-start)
|
||||
-webkit-margin-end: 3px; (replace with margin-inline-end)
|
||||
-webkit-margin-start: 4px; (replace with margin-inline-start)
|
||||
-webkit-padding-after: 1px; (replace with padding-block-end)
|
||||
-webkit-padding-before: 2px; (replace with padding-block-start)
|
||||
-webkit-padding-end: 3px; (replace with padding-inline-end)
|
||||
-webkit-padding-start: 4px; (replace with padding-inline-start)""")
|
||||
|
||||
def testStartEndInsteadOfLeftRight(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
.inline-node {
|
||||
--var-is-ignored-left: 10px;
|
||||
--var-is-ignored-right: 10px;
|
||||
border-left-color: black;
|
||||
border-right: 1px solid blue; /* csschecker-disable-line left-right */
|
||||
margin-right: 5px;
|
||||
padding-left: 10px; /* csschecker-disable-line some-other-thing */
|
||||
text-align: right;
|
||||
}""", """
|
||||
- Use -start/end instead of -left/right (https://goo.gl/gQYY7z, add /* csschecker-disable-line left-right */ to suppress)
|
||||
border-left-color: black; (replace with border-inline-start-color)
|
||||
margin-right: 5px; (replace with margin-inline-end)
|
||||
padding-left: 10px; (replace with padding-inline-start)
|
||||
text-align: right; (replace with text-align: end)
|
||||
""")
|
||||
|
||||
def testCssZeroWidthLengths(self):
|
||||
self.VerifyContentsProducesOutput("""
|
||||
@-webkit-keyframe anim {
|
||||
0% { /* Ignore key frames */
|
||||
width: 0px;
|
||||
}
|
||||
10% {
|
||||
width: 10px;
|
||||
}
|
||||
50% { background-image: url(blah.svg); }
|
||||
100% {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
#logo {
|
||||
background-image: url(images/google_logo.png@2x);
|
||||
}
|
||||
|
||||
body.alternate-logo #logo {
|
||||
-webkit-mask-image: url(images/google_logo.png@2x);
|
||||
}
|
||||
|
||||
/* http://crbug.com/359682 */
|
||||
#spinner-container #spinner {
|
||||
-webkit-animation-duration: 1.0s;
|
||||
background-image: url(images/google_logo0.svg);
|
||||
}
|
||||
|
||||
.media-button.play > .state0.active,
|
||||
.media-button[state='0'] > .state0.normal /* blah */, /* blee */
|
||||
.media-button[state='0']:not(.disabled):hover > .state0.hover {
|
||||
-webkit-animation: anim 0s;
|
||||
-webkit-animation-duration: anim 0ms;
|
||||
-webkit-transform: scale(0%);
|
||||
background-position-x: 0em;
|
||||
background-position-y: 0ex;
|
||||
border-width: 0em;
|
||||
color: hsl(0, 0%, 85%); /* Shouldn't trigger error. */
|
||||
opacity: .0;
|
||||
opacity: 0.0;
|
||||
opacity: 0.;
|
||||
}
|
||||
|
||||
@page {
|
||||
border-width: 0mm;
|
||||
height: 0cm;
|
||||
width: 0in;
|
||||
}""", """
|
||||
- Use "0" for zero-width lengths (i.e. 0px -> 0)
|
||||
width: 0px;
|
||||
-webkit-transform: scale(0%);
|
||||
background-position-x: 0em;
|
||||
background-position-y: 0ex;
|
||||
border-width: 0em;
|
||||
opacity: .0;
|
||||
opacity: 0.0;
|
||||
opacity: 0.;
|
||||
border-width: 0mm;
|
||||
height: 0cm;
|
||||
width: 0in;
|
||||
""")
|
||||
|
||||
def testInlineStyleInHtml(self):
|
||||
self.VerifyContentsProducesOutput("""<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Don't warn about problems outside of style tags
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
flex-direction:column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>""", """
|
||||
- Colons (:) should have a space after them.
|
||||
flex-direction:column;
|
||||
""", filename='test.html')
|
||||
|
||||
def testInlineStyleInHtmlWithIncludes(self):
|
||||
self.VerifyContentsProducesOutput("""<!doctype html>
|
||||
<html>
|
||||
<style include="fake-shared-css other-shared-css">
|
||||
body {
|
||||
flex-direction:column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>""", """
|
||||
- Colons (:) should have a space after them.
|
||||
flex-direction:column;
|
||||
""", filename='test.html')
|
||||
|
||||
def testInlineStyleInHtmlWithTagsInComments(self):
|
||||
self.VerifyContentsProducesOutput("""<!doctype html>
|
||||
<html>
|
||||
<style>
|
||||
body {
|
||||
/* You better ignore the <tag> in this comment! */
|
||||
flex-direction:column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>""", """
|
||||
- Colons (:) should have a space after them.
|
||||
flex-direction:column;
|
||||
""", filename='test.html')
|
||||
|
||||
def testRemoveAtBlocks(self):
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.magic {
|
||||
color: #000;
|
||||
}
|
||||
}"""), """
|
||||
.magic {
|
||||
color: #000;
|
||||
}""")
|
||||
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.magic {
|
||||
--mixin-definition: {
|
||||
color: red;
|
||||
};
|
||||
}
|
||||
}"""), """
|
||||
.magic {
|
||||
--mixin-definition: {
|
||||
color: red;
|
||||
};
|
||||
}""")
|
||||
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@keyframes jiggle {
|
||||
from { left: 0; }
|
||||
50% { left: 100%; }
|
||||
to { left: 10%; }
|
||||
}"""), """
|
||||
from { left: 0; }
|
||||
50% { left: 100%; }
|
||||
to { left: 10%; }""")
|
||||
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@media print {
|
||||
.rule1 {
|
||||
color: black;
|
||||
}
|
||||
.rule2 {
|
||||
margin: 1in;
|
||||
}
|
||||
}"""), """
|
||||
.rule1 {
|
||||
color: black;
|
||||
}
|
||||
.rule2 {
|
||||
margin: 1in;
|
||||
}""")
|
||||
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.rule1 {
|
||||
color: gray;
|
||||
}
|
||||
.rule2 {
|
||||
margin: .5in;
|
||||
}
|
||||
@keyframe dark-fade {
|
||||
0% { background: black; }
|
||||
100% { background: darkgray; }
|
||||
}
|
||||
}"""), """
|
||||
.rule1 {
|
||||
color: gray;
|
||||
}
|
||||
.rule2 {
|
||||
margin: .5in;
|
||||
}
|
||||
0% { background: black; }
|
||||
100% { background: darkgray; }""")
|
||||
|
||||
self.assertEqual(self.checker.RemoveAtBlocks("""
|
||||
@-webkit-keyframe anim {
|
||||
0% { /* Ignore key frames */
|
||||
width: 0px;
|
||||
}
|
||||
10% {
|
||||
width: 10px;
|
||||
}
|
||||
50% { background-image: url(blah.svg); }
|
||||
100% {
|
||||
width: 100px;
|
||||
}
|
||||
}"""), """
|
||||
0% { /* Ignore key frames */
|
||||
width: 0px;
|
||||
}
|
||||
10% {
|
||||
width: 10px;
|
||||
}
|
||||
50% { background-image: url(blah.svg); }
|
||||
100% {
|
||||
width: 100px;
|
||||
}""")
|
||||
|
||||
def testCssNested(self):
|
||||
self.VerifyContentIsValid("""
|
||||
#logo {
|
||||
background: radial-gradiant(blue 18px, transparent 19px);
|
||||
.child {
|
||||
background: radial-gradiant(red 10px, transparent 20px);
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -3,7 +3,6 @@
|
||||
# found in the LICENSE file.
|
||||
|
||||
|
||||
from . import css_checker
|
||||
from . import html_checker
|
||||
from . import js_checker
|
||||
from . import resource_checker
|
||||
@ -18,7 +17,6 @@ def CheckStyle(input_api, output_api, file_filter=lambda f: True):
|
||||
apis = input_api, output_api
|
||||
wrapped_filter = lambda f: file_filter(f) and IsResource(f)
|
||||
checkers = [
|
||||
css_checker.CSSChecker(*apis, file_filter=wrapped_filter),
|
||||
html_checker.HtmlChecker(*apis, file_filter=wrapped_filter),
|
||||
js_checker.JSChecker(*apis, file_filter=wrapped_filter),
|
||||
resource_checker.ResourceChecker(*apis, file_filter=wrapped_filter),
|
||||
|
Reference in New Issue
Block a user