-
Notifications
You must be signed in to change notification settings - Fork 465
/
shorthand.rb
161 lines (133 loc) · 4.62 KB
/
shorthand.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
module SCSSLint
# Checks for the use of the shortest form for properties that can be written
# in shorthand.
class Linter::Shorthand < Linter
include LinterRegistry
def visit_root(*)
@shorthands_forbidden = @config['allowed_shorthands'] == []
yield # Continue linting children
end
# @param node [Sass::Tree::Node]
def visit_prop(node)
property_name = node.name.join
return unless SHORTHANDABLE_PROPERTIES.include?(property_name)
if @shorthands_forbidden
add_lint(node, "The `#{property_name}` shorthand property is " \
'forbidden since the `allowed_shorthands` option ' \
'is set to an empty list.')
end
case node.value.first
when Sass::Script::Tree::Literal
check_script_literal(property_name, node.value.first)
when Sass::Script::Tree::ListLiteral
check_script_list(property_name, node.value.first)
end
end
private
SHORTHANDABLE_PROPERTIES = %w[
border-color
border-radius
border-style
border-width
margin
padding
].freeze
# @param prop [String]
# @param list [Sass::Script::Tree::ListLiteral]
def check_script_list(prop, list)
check_shorthand(prop, list, list.children.map(&:to_sass))
end
# @param prop [String]
# @param literal [Sass::Script::Tree::Literal]
def check_script_literal(prop, literal)
value = literal.value
# HACK: node_parent may not be initialized at this point, so we need to
# set it ourselves
value.node_parent = literal
return unless value.is_a?(Sass::Script::Value::String)
check_script_string(prop, value)
end
LIST_LITERAL_REGEX = /
\A
(\S+\s+\S+(\s+\S+){0,2}) # Two to four values separated by spaces
(\s+!\w+)? # Ignore `!important` priority overrides
\z
/x.freeze
# @param prop [String]
# @param script_string [Sass::Script::Value::String]
def check_script_string(prop, script_string)
return unless script_string.type == :identifier
return unless values = script_string.value.strip[LIST_LITERAL_REGEX, 1]
check_shorthand(prop, script_string, values.split)
end
# @param prop [String]
# @param node [Sass::Script::Value::String]
# @param values [Array<String>]
def check_shorthand(prop, node, values)
values = shorthand_values(values)
unless allowed?(values.count)
add_lint(node, "Shorthands of length `#{values.count}` are not allowed. " \
"Value was `#{values.join(' ')}`")
end
return unless (2..4).member?(values.count)
shortest_form = condensed_shorthand(*values)
return if values == shortest_form
add_lint(node, "Shorthand form for property `#{prop}` should be " \
"written more concisely as `#{shortest_form.join(' ')}` " \
"instead of `#{values.join(' ')}`")
end
# @param top [String]
# @param right [String]
# @param bottom [String]
# @param left [String]
# @return [Array]
def condensed_shorthand(top, right, bottom = nil, left = nil)
if condense_to_one_value?(top, right, bottom, left)
[top]
elsif condense_to_two_values?(top, right, bottom, left)
[top, right]
elsif condense_to_three_values?(top, right, bottom, left)
[top, right, bottom]
else
[top, right, bottom, left].compact
end
end
# @param top [String]
# @param right [String]
# @param bottom [String]
# @param left [String]
# @return [Boolean]
def condense_to_one_value?(top, right, bottom, left)
return unless allowed?(1)
return unless top == right
top == bottom && (bottom == left || left.nil?) ||
bottom.nil? && left.nil?
end
# @param top [String]
# @param right [String]
# @param bottom [String]
# @param left [String]
# @return [Boolean]
def condense_to_two_values?(top, right, bottom, left)
return unless allowed?(2)
top == bottom && right == left ||
top == bottom && left.nil? && top != right
end
# @param right [String]
# @param left [String]
# @return [Boolean]
def condense_to_three_values?(_, right, _, left)
return unless allowed?(3)
right == left
end
# @param size [Number]
# @return [Boolean]
def allowed?(size)
return false unless config['allowed_shorthands']
config['allowed_shorthands'].map(&:to_i).include?(size)
end
def shorthand_values(values)
values.take(4).take_while { |value| !value.to_s.start_with?('!') }
end
end
end