cataract 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/.yardopts +9 -0
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +17 -0
- data/ext/cataract/cataract.c +193 -111
- data/ext/cataract/cataract.h +13 -3
- data/ext/cataract/css_parser.c +298 -54
- data/ext/cataract/flatten.c +6 -2
- data/ext/cataract/specificity.c +13 -2
- data/lib/cataract/at_rule.rb +4 -2
- data/lib/cataract/conditional_group.rb +72 -0
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +31 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1203 -1182
- data/lib/cataract/pure/parser.rb +1953 -1715
- data/lib/cataract/pure/serializer.rb +653 -706
- data/lib/cataract/pure/specificity.rb +173 -144
- data/lib/cataract/pure.rb +74 -101
- data/lib/cataract/rule.rb +14 -7
- data/lib/cataract/stylesheet.rb +84 -25
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +5 -2
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -10,1341 +10,1362 @@
|
|
|
10
10
|
# which is part of the public API. The main public API is Cataract.flatten.
|
|
11
11
|
|
|
12
12
|
module Cataract
|
|
13
|
-
module
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# Separate AtRules (pass-through, unaffected by cascade) from regular
|
|
196
|
-
# Rules (to merge).
|
|
197
|
-
#
|
|
198
|
-
# @param rules [Array<Rule, AtRule>]
|
|
199
|
-
# @return [Array(Array<AtRule>, Array<Rule>)]
|
|
200
|
-
def self.partition_at_rules(rules)
|
|
201
|
-
at_rules = []
|
|
202
|
-
regular_rules = []
|
|
203
|
-
|
|
204
|
-
rules.each do |rule|
|
|
205
|
-
if rule.at_rule?
|
|
206
|
-
at_rules << rule
|
|
207
|
-
else
|
|
208
|
-
regular_rules << rule
|
|
13
|
+
module Backends
|
|
14
|
+
class PureImpl
|
|
15
|
+
class FlattenImpl
|
|
16
|
+
# Property name constants (US-ASCII for merge output)
|
|
17
|
+
PROP_MARGIN = 'margin'.encode(Encoding::US_ASCII).freeze
|
|
18
|
+
PROP_MARGIN_TOP = 'margin-top'.encode(Encoding::US_ASCII).freeze
|
|
19
|
+
PROP_MARGIN_RIGHT = 'margin-right'.encode(Encoding::US_ASCII).freeze
|
|
20
|
+
PROP_MARGIN_BOTTOM = 'margin-bottom'.encode(Encoding::US_ASCII).freeze
|
|
21
|
+
PROP_MARGIN_LEFT = 'margin-left'.encode(Encoding::US_ASCII).freeze
|
|
22
|
+
|
|
23
|
+
PROP_PADDING = 'padding'.encode(Encoding::US_ASCII).freeze
|
|
24
|
+
PROP_PADDING_TOP = 'padding-top'.encode(Encoding::US_ASCII).freeze
|
|
25
|
+
PROP_PADDING_RIGHT = 'padding-right'.encode(Encoding::US_ASCII).freeze
|
|
26
|
+
PROP_PADDING_BOTTOM = 'padding-bottom'.encode(Encoding::US_ASCII).freeze
|
|
27
|
+
PROP_PADDING_LEFT = 'padding-left'.encode(Encoding::US_ASCII).freeze
|
|
28
|
+
|
|
29
|
+
PROP_BORDER = 'border'.encode(Encoding::US_ASCII).freeze
|
|
30
|
+
PROP_BORDER_WIDTH = 'border-width'.encode(Encoding::US_ASCII).freeze
|
|
31
|
+
PROP_BORDER_STYLE = 'border-style'.encode(Encoding::US_ASCII).freeze
|
|
32
|
+
PROP_BORDER_COLOR = 'border-color'.encode(Encoding::US_ASCII).freeze
|
|
33
|
+
|
|
34
|
+
PROP_BORDER_TOP = 'border-top'.encode(Encoding::US_ASCII).freeze
|
|
35
|
+
PROP_BORDER_RIGHT = 'border-right'.encode(Encoding::US_ASCII).freeze
|
|
36
|
+
PROP_BORDER_BOTTOM = 'border-bottom'.encode(Encoding::US_ASCII).freeze
|
|
37
|
+
PROP_BORDER_LEFT = 'border-left'.encode(Encoding::US_ASCII).freeze
|
|
38
|
+
|
|
39
|
+
PROP_BORDER_TOP_WIDTH = 'border-top-width'.encode(Encoding::US_ASCII).freeze
|
|
40
|
+
PROP_BORDER_RIGHT_WIDTH = 'border-right-width'.encode(Encoding::US_ASCII).freeze
|
|
41
|
+
PROP_BORDER_BOTTOM_WIDTH = 'border-bottom-width'.encode(Encoding::US_ASCII).freeze
|
|
42
|
+
PROP_BORDER_LEFT_WIDTH = 'border-left-width'.encode(Encoding::US_ASCII).freeze
|
|
43
|
+
|
|
44
|
+
PROP_BORDER_TOP_STYLE = 'border-top-style'.encode(Encoding::US_ASCII).freeze
|
|
45
|
+
PROP_BORDER_RIGHT_STYLE = 'border-right-style'.encode(Encoding::US_ASCII).freeze
|
|
46
|
+
PROP_BORDER_BOTTOM_STYLE = 'border-bottom-style'.encode(Encoding::US_ASCII).freeze
|
|
47
|
+
PROP_BORDER_LEFT_STYLE = 'border-left-style'.encode(Encoding::US_ASCII).freeze
|
|
48
|
+
|
|
49
|
+
PROP_BORDER_TOP_COLOR = 'border-top-color'.encode(Encoding::US_ASCII).freeze
|
|
50
|
+
PROP_BORDER_RIGHT_COLOR = 'border-right-color'.encode(Encoding::US_ASCII).freeze
|
|
51
|
+
PROP_BORDER_BOTTOM_COLOR = 'border-bottom-color'.encode(Encoding::US_ASCII).freeze
|
|
52
|
+
PROP_BORDER_LEFT_COLOR = 'border-left-color'.encode(Encoding::US_ASCII).freeze
|
|
53
|
+
|
|
54
|
+
PROP_FONT = 'font'.encode(Encoding::US_ASCII).freeze
|
|
55
|
+
PROP_FONT_STYLE = 'font-style'.encode(Encoding::US_ASCII).freeze
|
|
56
|
+
PROP_FONT_VARIANT = 'font-variant'.encode(Encoding::US_ASCII).freeze
|
|
57
|
+
PROP_FONT_WEIGHT = 'font-weight'.encode(Encoding::US_ASCII).freeze
|
|
58
|
+
PROP_FONT_SIZE = 'font-size'.encode(Encoding::US_ASCII).freeze
|
|
59
|
+
PROP_LINE_HEIGHT = 'line-height'.encode(Encoding::US_ASCII).freeze
|
|
60
|
+
PROP_FONT_FAMILY = 'font-family'.encode(Encoding::US_ASCII).freeze
|
|
61
|
+
|
|
62
|
+
PROP_BACKGROUND = 'background'.encode(Encoding::US_ASCII).freeze
|
|
63
|
+
PROP_BACKGROUND_COLOR = 'background-color'.encode(Encoding::US_ASCII).freeze
|
|
64
|
+
PROP_BACKGROUND_IMAGE = 'background-image'.encode(Encoding::US_ASCII).freeze
|
|
65
|
+
PROP_BACKGROUND_REPEAT = 'background-repeat'.encode(Encoding::US_ASCII).freeze
|
|
66
|
+
PROP_BACKGROUND_ATTACHMENT = 'background-attachment'.encode(Encoding::US_ASCII).freeze
|
|
67
|
+
PROP_BACKGROUND_POSITION = 'background-position'.encode(Encoding::US_ASCII).freeze
|
|
68
|
+
|
|
69
|
+
PROP_LIST_STYLE = 'list-style'.encode(Encoding::US_ASCII).freeze
|
|
70
|
+
PROP_LIST_STYLE_TYPE = 'list-style-type'.encode(Encoding::US_ASCII).freeze
|
|
71
|
+
PROP_LIST_STYLE_POSITION = 'list-style-position'.encode(Encoding::US_ASCII).freeze
|
|
72
|
+
PROP_LIST_STYLE_IMAGE = 'list-style-image'.encode(Encoding::US_ASCII).freeze
|
|
73
|
+
|
|
74
|
+
# Shorthand property families
|
|
75
|
+
MARGIN_SIDES = [PROP_MARGIN_TOP, PROP_MARGIN_RIGHT, PROP_MARGIN_BOTTOM, PROP_MARGIN_LEFT].freeze
|
|
76
|
+
PADDING_SIDES = [PROP_PADDING_TOP, PROP_PADDING_RIGHT, PROP_PADDING_BOTTOM, PROP_PADDING_LEFT].freeze
|
|
77
|
+
|
|
78
|
+
BORDER_WIDTHS = [
|
|
79
|
+
PROP_BORDER_TOP_WIDTH,
|
|
80
|
+
PROP_BORDER_RIGHT_WIDTH,
|
|
81
|
+
PROP_BORDER_BOTTOM_WIDTH,
|
|
82
|
+
PROP_BORDER_LEFT_WIDTH
|
|
83
|
+
].freeze
|
|
84
|
+
|
|
85
|
+
BORDER_STYLES = [
|
|
86
|
+
PROP_BORDER_TOP_STYLE,
|
|
87
|
+
PROP_BORDER_RIGHT_STYLE,
|
|
88
|
+
PROP_BORDER_BOTTOM_STYLE,
|
|
89
|
+
PROP_BORDER_LEFT_STYLE
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
92
|
+
BORDER_COLORS = [
|
|
93
|
+
PROP_BORDER_TOP_COLOR,
|
|
94
|
+
PROP_BORDER_RIGHT_COLOR,
|
|
95
|
+
PROP_BORDER_BOTTOM_COLOR,
|
|
96
|
+
PROP_BORDER_LEFT_COLOR
|
|
97
|
+
].freeze
|
|
98
|
+
|
|
99
|
+
# Side name constants (for string operations, not CSS properties)
|
|
100
|
+
SIDE_TOP = 'top'
|
|
101
|
+
SIDE_RIGHT = 'right'
|
|
102
|
+
SIDE_BOTTOM = 'bottom'
|
|
103
|
+
SIDE_LEFT = 'left'
|
|
104
|
+
|
|
105
|
+
# Maps a border side name to its [width, style, color] property constants
|
|
106
|
+
BORDER_SIDE_PROPS = {
|
|
107
|
+
SIDE_TOP => [PROP_BORDER_TOP_WIDTH, PROP_BORDER_TOP_STYLE, PROP_BORDER_TOP_COLOR],
|
|
108
|
+
SIDE_RIGHT => [PROP_BORDER_RIGHT_WIDTH, PROP_BORDER_RIGHT_STYLE, PROP_BORDER_RIGHT_COLOR],
|
|
109
|
+
SIDE_BOTTOM => [PROP_BORDER_BOTTOM_WIDTH, PROP_BORDER_BOTTOM_STYLE, PROP_BORDER_BOTTOM_COLOR],
|
|
110
|
+
SIDE_LEFT => [PROP_BORDER_LEFT_WIDTH, PROP_BORDER_LEFT_STYLE, PROP_BORDER_LEFT_COLOR]
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
FONT_PROPERTIES = [
|
|
114
|
+
PROP_FONT_STYLE,
|
|
115
|
+
PROP_FONT_VARIANT,
|
|
116
|
+
PROP_FONT_WEIGHT,
|
|
117
|
+
PROP_FONT_SIZE,
|
|
118
|
+
PROP_LINE_HEIGHT,
|
|
119
|
+
PROP_FONT_FAMILY
|
|
120
|
+
].freeze
|
|
121
|
+
|
|
122
|
+
BACKGROUND_PROPERTIES = [
|
|
123
|
+
PROP_BACKGROUND_COLOR,
|
|
124
|
+
PROP_BACKGROUND_IMAGE,
|
|
125
|
+
PROP_BACKGROUND_REPEAT,
|
|
126
|
+
PROP_BACKGROUND_POSITION,
|
|
127
|
+
PROP_BACKGROUND_ATTACHMENT
|
|
128
|
+
].freeze
|
|
129
|
+
|
|
130
|
+
LIST_STYLE_PROPERTIES = [PROP_LIST_STYLE_TYPE, PROP_LIST_STYLE_POSITION, PROP_LIST_STYLE_IMAGE].freeze
|
|
131
|
+
BORDER_ALL = (BORDER_WIDTHS + BORDER_STYLES + BORDER_COLORS).freeze
|
|
132
|
+
|
|
133
|
+
# Shorthand property lookup (Hash is faster than Array#include? or Set)
|
|
134
|
+
# Used for fast-path check to avoid calling expand_shorthand for non-shorthands
|
|
135
|
+
SHORTHAND_PROPERTIES = {
|
|
136
|
+
'margin' => true,
|
|
137
|
+
'padding' => true,
|
|
138
|
+
'border' => true,
|
|
139
|
+
'border-top' => true,
|
|
140
|
+
'border-right' => true,
|
|
141
|
+
'border-bottom' => true,
|
|
142
|
+
'border-left' => true,
|
|
143
|
+
'border-width' => true,
|
|
144
|
+
'border-style' => true,
|
|
145
|
+
'border-color' => true,
|
|
146
|
+
'font' => true,
|
|
147
|
+
'background' => true,
|
|
148
|
+
'list-style' => true
|
|
149
|
+
}.freeze
|
|
150
|
+
|
|
151
|
+
# List style keywords
|
|
152
|
+
LIST_STYLE_POSITION_KEYWORDS = %w[inside outside].freeze
|
|
153
|
+
|
|
154
|
+
# Border property keywords
|
|
155
|
+
BORDER_WIDTH_KEYWORDS = %w[thin medium thick].freeze
|
|
156
|
+
BORDER_STYLE_KEYWORDS = %w[none hidden dotted dashed solid double groove ridge inset outset].freeze
|
|
157
|
+
|
|
158
|
+
# Font property keywords
|
|
159
|
+
FONT_SIZE_KEYWORDS = %w[xx-small x-small small medium large x-large xx-large smaller larger].freeze
|
|
160
|
+
FONT_STYLE_KEYWORDS = %w[normal italic oblique].freeze
|
|
161
|
+
FONT_VARIANT_KEYWORDS = %w[normal small-caps].freeze
|
|
162
|
+
FONT_WEIGHT_KEYWORDS = %w[normal bold bolder lighter 100 200 300 400 500 600 700 800 900].freeze
|
|
163
|
+
|
|
164
|
+
# Background property keywords
|
|
165
|
+
BACKGROUND_REPEAT_KEYWORDS = %w[repeat repeat-x repeat-y no-repeat space round].freeze
|
|
166
|
+
BACKGROUND_ATTACHMENT_KEYWORDS = %w[scroll fixed local].freeze
|
|
167
|
+
BACKGROUND_POSITION_KEYWORDS = %w[left right center top bottom].freeze
|
|
168
|
+
|
|
169
|
+
# Merge stylesheet according to CSS cascade rules
|
|
170
|
+
#
|
|
171
|
+
# @param stylesheet [Stylesheet] Stylesheet to merge
|
|
172
|
+
# @param mutate [Boolean] If true, mutate the stylesheet; otherwise create new one
|
|
173
|
+
# @return [Stylesheet] Merged stylesheet
|
|
174
|
+
def flatten(stylesheet, mutate: false)
|
|
175
|
+
at_rules, regular_rules = partition_at_rules(stylesheet.rules)
|
|
176
|
+
|
|
177
|
+
expand_all_shorthands!(regular_rules)
|
|
178
|
+
merged_rules = merge_rules_by_selector_and_media(regular_rules)
|
|
179
|
+
merged_rules.each { |rule| recreate_shorthands!(rule) }
|
|
180
|
+
|
|
181
|
+
# Assign new IDs before checking divergence (so we can build correct selector_lists hash)
|
|
182
|
+
merged_rules.each_with_index { |rule, i| rule.id = i }
|
|
183
|
+
|
|
184
|
+
selector_lists = build_selector_lists(stylesheet, merged_rules)
|
|
185
|
+
|
|
186
|
+
# Add passthrough AtRules to output
|
|
187
|
+
merged_rules.concat(at_rules)
|
|
188
|
+
|
|
189
|
+
media_queries = stylesheet.media_queries
|
|
190
|
+
media_query_lists = stylesheet.instance_variable_get(:@_media_query_lists)
|
|
191
|
+
new_media_index = rebuild_media_index(merged_rules, media_queries, media_query_lists)
|
|
192
|
+
|
|
193
|
+
build_result(stylesheet, merged_rules, new_media_index, media_queries, media_query_lists, selector_lists,
|
|
194
|
+
mutate)
|
|
209
195
|
end
|
|
210
|
-
end
|
|
211
196
|
|
|
212
|
-
|
|
213
|
-
|
|
197
|
+
# Separate AtRules (pass-through, unaffected by cascade) from regular
|
|
198
|
+
# Rules (to merge). Regular rules are duped (including their
|
|
199
|
+
# declarations array) since expand_all_shorthands! mutates
|
|
200
|
+
# declarations in place - without this, the caller's stylesheet
|
|
201
|
+
# would get its shorthand properties silently expanded even when
|
|
202
|
+
# flatten (not flatten!) was called.
|
|
203
|
+
#
|
|
204
|
+
# @param rules [Array<Rule, AtRule>]
|
|
205
|
+
# @return [Array(Array<AtRule>, Array<Rule>)]
|
|
206
|
+
def partition_at_rules(rules)
|
|
207
|
+
at_rules = []
|
|
208
|
+
regular_rules = []
|
|
209
|
+
|
|
210
|
+
rules.each do |rule|
|
|
211
|
+
if rule.at_rule?
|
|
212
|
+
at_rules << rule
|
|
213
|
+
else
|
|
214
|
+
copy = rule.dup
|
|
215
|
+
copy.declarations = rule.declarations.dup
|
|
216
|
+
regular_rules << copy
|
|
217
|
+
end
|
|
218
|
+
end
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
expanded
|
|
231
|
-
|
|
232
|
-
|
|
220
|
+
[at_rules, regular_rules]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Expand shorthands in regular rules only (AtRules don't have declarations).
|
|
224
|
+
# Mutates each rule's declarations in place.
|
|
225
|
+
#
|
|
226
|
+
# NOTE: Using manual each + concat instead of .flat_map for performance.
|
|
227
|
+
# The concise form (.flat_map) is ~5-10% slower depending on number of shorthands to expand.
|
|
228
|
+
# NOTE: Fast-path check for shorthands (Hash lookup) avoids calling expand_shorthand
|
|
229
|
+
# for declarations that are not shorthands (~20% faster than calling method unconditionally).
|
|
230
|
+
#
|
|
231
|
+
# @param regular_rules [Array<Rule>]
|
|
232
|
+
# @return [void]
|
|
233
|
+
def expand_all_shorthands!(regular_rules)
|
|
234
|
+
regular_rules.each do |rule|
|
|
235
|
+
expanded = []
|
|
236
|
+
rule.declarations.each do |decl|
|
|
237
|
+
if SHORTHAND_PROPERTIES[decl.property]
|
|
238
|
+
expanded.concat(expand_shorthand(decl))
|
|
239
|
+
else
|
|
240
|
+
expanded << decl
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
rule.declarations.replace(expanded)
|
|
233
244
|
end
|
|
234
245
|
end
|
|
235
|
-
rule.declarations.replace(expanded)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
246
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
# Group rules by (selector, media_query_id) and merge each group
|
|
248
|
+
# according to CSS cascade rules. Rules with the same selector but
|
|
249
|
+
# different media contexts are NOT merged together.
|
|
250
|
+
#
|
|
251
|
+
# @param regular_rules [Array<Rule>]
|
|
252
|
+
# @return [Array<Rule>] Merged rules, one per (selector, media_query_id) group
|
|
253
|
+
def merge_rules_by_selector_and_media(regular_rules)
|
|
254
|
+
# NOTE: Using manual each instead of .group_by to avoid intermediate hash allocation.
|
|
255
|
+
by_selector_and_media = {}
|
|
256
|
+
regular_rules.each do |rule|
|
|
257
|
+
key = [rule.selector, rule.media_query_id]
|
|
258
|
+
(by_selector_and_media[key] ||= []) << rule
|
|
259
|
+
end
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
merged_rules = []
|
|
262
|
+
by_selector_and_media.each_value do |rules|
|
|
263
|
+
merged_rule = flatten_rules_for_selector(rules.first.selector, rules)
|
|
264
|
+
merged_rules << merged_rule if merged_rule
|
|
265
|
+
end
|
|
266
|
+
merged_rules
|
|
267
|
+
end
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
269
|
+
# Handle selector list divergence: remove rules from selector lists if
|
|
270
|
+
# declarations no longer match. This makes selector_list_id
|
|
271
|
+
# authoritative - if set, declarations MUST be identical. Only
|
|
272
|
+
# processed if selector_lists is enabled in the stylesheet's parser
|
|
273
|
+
# options.
|
|
274
|
+
#
|
|
275
|
+
# @param stylesheet [Stylesheet] Source stylesheet (for parser options)
|
|
276
|
+
# @param merged_rules [Array<Rule>] Merged rules, already assigned final IDs
|
|
277
|
+
# @return [Hash] list_id => Array of rule IDs, empty if not tracking selector lists
|
|
278
|
+
def build_selector_lists(stylesheet, merged_rules)
|
|
279
|
+
selector_lists = {}
|
|
280
|
+
parser_options = stylesheet.instance_variable_get(:@parser_options) || {}
|
|
281
|
+
update_selector_lists_for_divergence!(merged_rules, selector_lists) if parser_options[:selector_lists]
|
|
282
|
+
selector_lists
|
|
283
|
+
end
|
|
276
284
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
285
|
+
# Rebuild media_index from rules' media_query_id. This ensures
|
|
286
|
+
# media_index is consistent with the MediaQuery objects, rather than
|
|
287
|
+
# trying to remap the stylesheet's existing (now-stale) media_index.
|
|
288
|
+
#
|
|
289
|
+
# @param merged_rules [Array<Rule, AtRule>] Final rules, including passthrough AtRules
|
|
290
|
+
# @param media_queries [Array<MediaQuery>]
|
|
291
|
+
# @param media_query_lists [Hash{Integer => Array<Integer>}] list_id => media_query_ids
|
|
292
|
+
# @return [Hash{Symbol => Array<Integer>}] media type => rule IDs
|
|
293
|
+
def rebuild_media_index(merged_rules, media_queries, media_query_lists)
|
|
294
|
+
new_media_index = {}
|
|
295
|
+
|
|
296
|
+
# Build reverse map: media_query_id => list_id (one-time cost)
|
|
297
|
+
mq_id_to_list_id = {}
|
|
298
|
+
media_query_lists.each do |list_id, mq_ids|
|
|
299
|
+
mq_ids.each { |mq_id| mq_id_to_list_id[mq_id] = list_id }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
merged_rules.each do |rule|
|
|
303
|
+
next unless rule.is_a?(Rule) && rule.media_query_id
|
|
293
304
|
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
# Check if this rule's media_query_id is part of a list
|
|
306
|
+
list_id = mq_id_to_list_id[rule.media_query_id]
|
|
296
307
|
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
if list_id
|
|
309
|
+
# This rule is in a compound media query (e.g., "@media screen, print")
|
|
310
|
+
# Index it under ALL media types in the list
|
|
311
|
+
mq_ids = media_query_lists[list_id]
|
|
312
|
+
mq_ids.each do |mq_id|
|
|
313
|
+
mq = media_queries[mq_id]
|
|
314
|
+
next unless mq
|
|
299
315
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
316
|
+
new_media_index[mq.type] ||= []
|
|
317
|
+
new_media_index[mq.type] << rule.id
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
# Single media query - just index under its type
|
|
321
|
+
mq = media_queries[rule.media_query_id]
|
|
322
|
+
next unless mq
|
|
307
323
|
|
|
308
|
-
|
|
309
|
-
|
|
324
|
+
new_media_index[mq.type] ||= []
|
|
325
|
+
new_media_index[mq.type] << rule.id
|
|
326
|
+
end
|
|
310
327
|
end
|
|
311
|
-
else
|
|
312
|
-
# Single media query - just index under its type
|
|
313
|
-
mq = media_queries[rule.media_query_id]
|
|
314
|
-
next unless mq
|
|
315
328
|
|
|
316
|
-
|
|
317
|
-
new_media_index
|
|
329
|
+
# Deduplicate arrays once at the end
|
|
330
|
+
new_media_index.each_value(&:uniq!)
|
|
331
|
+
new_media_index
|
|
318
332
|
end
|
|
319
|
-
end
|
|
320
333
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
# Build the final result: either mutate the input stylesheet in place,
|
|
335
|
+
# or construct a new one, with the merged rules/media_index/selector_lists.
|
|
336
|
+
#
|
|
337
|
+
# @return [Stylesheet]
|
|
338
|
+
def build_result(stylesheet, merged_rules, new_media_index, media_queries, media_query_lists,
|
|
339
|
+
selector_lists, mutate)
|
|
340
|
+
if mutate
|
|
341
|
+
stylesheet.instance_variable_set(:@rules, merged_rules)
|
|
342
|
+
stylesheet.instance_variable_set(:@media_index, new_media_index)
|
|
343
|
+
# @media_queries and @_media_query_lists stay the same - preserved from input
|
|
344
|
+
stylesheet.instance_variable_set(:@_selector_lists, selector_lists)
|
|
345
|
+
stylesheet
|
|
346
|
+
else
|
|
347
|
+
result = Stylesheet.new
|
|
348
|
+
result.instance_variable_set(:@rules, merged_rules)
|
|
349
|
+
result.instance_variable_set(:@media_index, new_media_index)
|
|
350
|
+
result.instance_variable_set(:@media_queries, media_queries)
|
|
351
|
+
result.instance_variable_set(:@_media_query_lists, media_query_lists)
|
|
352
|
+
result.instance_variable_set(:@charset, stylesheet.charset)
|
|
353
|
+
result.instance_variable_set(:@_selector_lists, selector_lists)
|
|
354
|
+
result
|
|
355
|
+
end
|
|
356
|
+
end
|
|
325
357
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
358
|
+
# Merge multiple rules with same selector
|
|
359
|
+
#
|
|
360
|
+
# @param selector [String] The selector
|
|
361
|
+
# @param rules [Array<Rule>] Rules with this selector
|
|
362
|
+
# @return [Rule] Merged rule with cascaded declarations
|
|
363
|
+
def flatten_rules_for_selector(selector, rules)
|
|
364
|
+
# Build declaration map: property => [source_order, specificity, important, value]
|
|
365
|
+
decl_map = {}
|
|
366
|
+
|
|
367
|
+
rules.each do |rule|
|
|
368
|
+
spec = rule.specificity || calculate_specificity(rule.selector)
|
|
369
|
+
|
|
370
|
+
rule.declarations.each_with_index do |decl, idx|
|
|
371
|
+
# Property is already US-ASCII and lowercase from parser
|
|
372
|
+
prop = decl.property
|
|
373
|
+
|
|
374
|
+
# Calculate source order (higher = later)
|
|
375
|
+
source_order = rule.id * 1000 + idx
|
|
376
|
+
|
|
377
|
+
existing = decl_map[prop]
|
|
378
|
+
|
|
379
|
+
# Apply cascade rules:
|
|
380
|
+
# 1. !important always wins over non-important
|
|
381
|
+
# 2. Higher specificity wins
|
|
382
|
+
# 3. Later source order wins (if specificity and importance are equal)
|
|
383
|
+
|
|
384
|
+
if existing.nil?
|
|
385
|
+
decl_map[prop] = [source_order, spec, decl.important, decl.value]
|
|
386
|
+
else
|
|
387
|
+
existing_order, existing_spec, existing_important, _existing_val = existing
|
|
388
|
+
|
|
389
|
+
# Determine winner
|
|
390
|
+
should_replace = false
|
|
391
|
+
|
|
392
|
+
if decl.important && !existing_important
|
|
393
|
+
# New is important, existing is not -> new wins
|
|
394
|
+
should_replace = true
|
|
395
|
+
elsif !decl.important && existing_important
|
|
396
|
+
# Existing is important, new is not -> existing wins
|
|
397
|
+
should_replace = false
|
|
398
|
+
elsif spec > existing_spec
|
|
399
|
+
# Higher specificity wins
|
|
400
|
+
should_replace = true
|
|
401
|
+
elsif spec < existing_spec
|
|
402
|
+
# Lower specificity loses
|
|
403
|
+
should_replace = false
|
|
404
|
+
else
|
|
405
|
+
# Same specificity and importance -> later source order wins
|
|
406
|
+
should_replace = source_order > existing_order
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
if should_replace
|
|
410
|
+
decl_map[prop] = [source_order, spec, decl.important, decl.value]
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
349
415
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
416
|
+
# Build final declarations array
|
|
417
|
+
# NOTE: Using each with << instead of map for performance (1.05-1.11x faster)
|
|
418
|
+
# The << pattern is faster than map's implicit array return (even without YJIT)
|
|
419
|
+
#
|
|
420
|
+
# NOTE: We don't sort by source_order here because:
|
|
421
|
+
# 1. Hash iteration order in Ruby is insertion order (since Ruby 1.9)
|
|
422
|
+
# 2. Declaration order doesn't affect CSS behavior (cascade is already resolved)
|
|
423
|
+
# 3. Sorting would add overhead for purely aesthetic output
|
|
424
|
+
# The output order is roughly source order but may vary when properties are
|
|
425
|
+
# overridden by later rules with higher specificity or importance.
|
|
426
|
+
declarations = []
|
|
427
|
+
decl_map.each do |prop, (_order, _spec, important, value)|
|
|
428
|
+
declarations << Declaration.new(prop, value, important)
|
|
429
|
+
end
|
|
358
430
|
|
|
359
|
-
|
|
360
|
-
|
|
431
|
+
return nil if declarations.empty?
|
|
432
|
+
|
|
433
|
+
# Preserve selector_list_id if all rules in group share the same one
|
|
434
|
+
selector_list_ids = rules.filter_map(&:selector_list_id)
|
|
435
|
+
selector_list_ids.uniq!
|
|
436
|
+
selector_list_id = selector_list_ids.size == 1 ? selector_list_ids.first : nil
|
|
437
|
+
|
|
438
|
+
# All rules being merged have the same media_query_id (they were grouped by it)
|
|
439
|
+
media_query_id = rules.first.media_query_id
|
|
440
|
+
|
|
441
|
+
# Extract conditional_group_id from first rule in group (mirrors
|
|
442
|
+
# media_query_id above - not part of the grouping key, but should
|
|
443
|
+
# be uniform in practice for any group actually worth merging)
|
|
444
|
+
conditional_group_id = rules.first.conditional_group_id
|
|
445
|
+
|
|
446
|
+
# Create merged rule
|
|
447
|
+
Rule.new(
|
|
448
|
+
0, # ID will be updated later
|
|
449
|
+
selector,
|
|
450
|
+
declarations,
|
|
451
|
+
rules.first.specificity, # Use first rule's specificity
|
|
452
|
+
nil, # No parent after flattening
|
|
453
|
+
nil, # No nesting style after flattening
|
|
454
|
+
selector_list_id, # Preserve if all rules share same ID
|
|
455
|
+
media_query_id, # Preserve media context
|
|
456
|
+
conditional_group_id # Preserve conditional-group context
|
|
457
|
+
)
|
|
458
|
+
end
|
|
361
459
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
460
|
+
# Calculate specificity for a selector
|
|
461
|
+
#
|
|
462
|
+
# @param selector [String] CSS selector
|
|
463
|
+
# @return [Integer] Specificity value
|
|
464
|
+
def calculate_specificity(selector)
|
|
465
|
+
Pure.calculate_specificity(selector)
|
|
466
|
+
end
|
|
365
467
|
|
|
366
|
-
|
|
367
|
-
|
|
468
|
+
# Expand a single shorthand declaration into longhand declarations.
|
|
469
|
+
# Returns an array of longhand declarations. If the declaration is not a shorthand,
|
|
470
|
+
# returns an array with just that declaration.
|
|
471
|
+
#
|
|
472
|
+
# @param decl [Declaration] Declaration to expand
|
|
473
|
+
# @return [Array<Declaration>] Array of expanded longhand declarations
|
|
474
|
+
# @api private
|
|
475
|
+
def expand_shorthand(decl)
|
|
476
|
+
case decl.property
|
|
477
|
+
when 'margin'
|
|
478
|
+
expand_margin(decl)
|
|
479
|
+
when 'padding'
|
|
480
|
+
expand_padding(decl)
|
|
481
|
+
when 'border'
|
|
482
|
+
expand_border(decl)
|
|
483
|
+
when 'border-top', 'border-right', 'border-bottom', 'border-left'
|
|
484
|
+
expand_border_side(decl)
|
|
485
|
+
when 'border-width'
|
|
486
|
+
expand_border_width(decl)
|
|
487
|
+
when 'border-style'
|
|
488
|
+
expand_border_style(decl)
|
|
489
|
+
when 'border-color'
|
|
490
|
+
expand_border_color(decl)
|
|
491
|
+
when 'font'
|
|
492
|
+
expand_font(decl)
|
|
493
|
+
when 'background'
|
|
494
|
+
expand_background(decl)
|
|
495
|
+
when 'list-style'
|
|
496
|
+
expand_list_style(decl)
|
|
497
|
+
else
|
|
498
|
+
# Not a shorthand, return as-is in an array
|
|
499
|
+
[decl]
|
|
500
|
+
end
|
|
501
|
+
end
|
|
368
502
|
|
|
369
|
-
|
|
503
|
+
# Expand margin shorthand
|
|
504
|
+
def expand_margin(decl)
|
|
505
|
+
sides = parse_four_sides(decl.value)
|
|
506
|
+
[
|
|
507
|
+
Declaration.new(PROP_MARGIN_TOP, sides[0], decl.important),
|
|
508
|
+
Declaration.new(PROP_MARGIN_RIGHT, sides[1], decl.important),
|
|
509
|
+
Declaration.new(PROP_MARGIN_BOTTOM, sides[2], decl.important),
|
|
510
|
+
Declaration.new(PROP_MARGIN_LEFT, sides[3], decl.important)
|
|
511
|
+
]
|
|
512
|
+
end
|
|
370
513
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
514
|
+
# Expand padding shorthand
|
|
515
|
+
def expand_padding(decl)
|
|
516
|
+
sides = parse_four_sides(decl.value)
|
|
517
|
+
[
|
|
518
|
+
Declaration.new(PROP_PADDING_TOP, sides[0], decl.important),
|
|
519
|
+
Declaration.new(PROP_PADDING_RIGHT, sides[1], decl.important),
|
|
520
|
+
Declaration.new(PROP_PADDING_BOTTOM, sides[2], decl.important),
|
|
521
|
+
Declaration.new(PROP_PADDING_LEFT, sides[3], decl.important)
|
|
522
|
+
]
|
|
523
|
+
end
|
|
375
524
|
|
|
376
|
-
|
|
377
|
-
|
|
525
|
+
# Parse four-sided value (margin/padding)
|
|
526
|
+
# "10px" -> ["10px", "10px", "10px", "10px"]
|
|
527
|
+
# "10px 20px" -> ["10px", "20px", "10px", "20px"]
|
|
528
|
+
# "10px 20px 30px" -> ["10px", "20px", "30px", "20px"]
|
|
529
|
+
# "10px 20px 30px 40px" -> ["10px", "20px", "30px", "40px"]
|
|
530
|
+
def parse_four_sides(value)
|
|
531
|
+
parts = split_on_whitespace(value)
|
|
532
|
+
|
|
533
|
+
case parts.length
|
|
534
|
+
when 1
|
|
535
|
+
[parts[0], parts[0], parts[0], parts[0]]
|
|
536
|
+
when 2
|
|
537
|
+
[parts[0], parts[1], parts[0], parts[1]]
|
|
538
|
+
when 3
|
|
539
|
+
[parts[0], parts[1], parts[2], parts[1]]
|
|
378
540
|
else
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
541
|
+
[parts[0], parts[1], parts[2], parts[3]]
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Split value on whitespace (handling calc() and other functions)
|
|
546
|
+
def split_on_whitespace(value)
|
|
547
|
+
parts = []
|
|
548
|
+
current = String.new
|
|
549
|
+
paren_depth = 0
|
|
550
|
+
|
|
551
|
+
i = 0
|
|
552
|
+
len = value.bytesize
|
|
553
|
+
while i < len
|
|
554
|
+
byte = value.getbyte(i)
|
|
555
|
+
|
|
556
|
+
if byte == BYTE_LPAREN
|
|
557
|
+
paren_depth += 1
|
|
558
|
+
current << byte
|
|
559
|
+
elsif byte == BYTE_RPAREN
|
|
560
|
+
paren_depth -= 1
|
|
561
|
+
current << byte
|
|
562
|
+
elsif byte == BYTE_SPACE && paren_depth == 0
|
|
563
|
+
parts << current unless current.empty?
|
|
564
|
+
current = String.new
|
|
396
565
|
else
|
|
397
|
-
|
|
398
|
-
should_replace = source_order > existing_order
|
|
566
|
+
current << byte
|
|
399
567
|
end
|
|
400
568
|
|
|
401
|
-
|
|
402
|
-
decl_map[prop] = [source_order, spec, decl.important, decl.value]
|
|
403
|
-
end
|
|
569
|
+
i += 1
|
|
404
570
|
end
|
|
571
|
+
|
|
572
|
+
parts << current unless current.empty?
|
|
573
|
+
parts
|
|
405
574
|
end
|
|
406
|
-
end
|
|
407
575
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# NOTE: We don't sort by source_order here because:
|
|
413
|
-
# 1. Hash iteration order in Ruby is insertion order (since Ruby 1.9)
|
|
414
|
-
# 2. Declaration order doesn't affect CSS behavior (cascade is already resolved)
|
|
415
|
-
# 3. Sorting would add overhead for purely aesthetic output
|
|
416
|
-
# The output order is roughly source order but may vary when properties are
|
|
417
|
-
# overridden by later rules with higher specificity or importance.
|
|
418
|
-
declarations = []
|
|
419
|
-
decl_map.each do |prop, (_order, _spec, important, value)|
|
|
420
|
-
declarations << Declaration.new(prop, value, important)
|
|
421
|
-
end
|
|
576
|
+
# Expand border shorthand (e.g., "1px solid red")
|
|
577
|
+
def expand_border(decl)
|
|
578
|
+
# Parse border value
|
|
579
|
+
width, style, color = parse_border_value(decl.value)
|
|
422
580
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# Preserve selector_list_id if all rules in group share the same one
|
|
426
|
-
selector_list_ids = rules.filter_map(&:selector_list_id)
|
|
427
|
-
selector_list_ids.uniq!
|
|
428
|
-
selector_list_id = selector_list_ids.size == 1 ? selector_list_ids.first : nil
|
|
429
|
-
|
|
430
|
-
# All rules being merged have the same media_query_id (they were grouped by it)
|
|
431
|
-
media_query_id = rules.first.media_query_id
|
|
432
|
-
|
|
433
|
-
# Create merged rule
|
|
434
|
-
Rule.new(
|
|
435
|
-
0, # ID will be updated later
|
|
436
|
-
selector,
|
|
437
|
-
declarations,
|
|
438
|
-
rules.first.specificity, # Use first rule's specificity
|
|
439
|
-
nil, # No parent after flattening
|
|
440
|
-
nil, # No nesting style after flattening
|
|
441
|
-
selector_list_id, # Preserve if all rules share same ID
|
|
442
|
-
media_query_id # Preserve media context
|
|
443
|
-
)
|
|
444
|
-
end
|
|
581
|
+
result = []
|
|
445
582
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
583
|
+
# Expand to all sides using property constants
|
|
584
|
+
if width
|
|
585
|
+
result << Declaration.new(PROP_BORDER_TOP_WIDTH, width, decl.important)
|
|
586
|
+
result << Declaration.new(PROP_BORDER_RIGHT_WIDTH, width, decl.important)
|
|
587
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_WIDTH, width, decl.important)
|
|
588
|
+
result << Declaration.new(PROP_BORDER_LEFT_WIDTH, width, decl.important)
|
|
589
|
+
end
|
|
453
590
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
# @api private
|
|
461
|
-
def self.expand_shorthand(decl)
|
|
462
|
-
case decl.property
|
|
463
|
-
when 'margin'
|
|
464
|
-
expand_margin(decl)
|
|
465
|
-
when 'padding'
|
|
466
|
-
expand_padding(decl)
|
|
467
|
-
when 'border'
|
|
468
|
-
expand_border(decl)
|
|
469
|
-
when 'border-top', 'border-right', 'border-bottom', 'border-left'
|
|
470
|
-
expand_border_side(decl)
|
|
471
|
-
when 'border-width'
|
|
472
|
-
expand_border_width(decl)
|
|
473
|
-
when 'border-style'
|
|
474
|
-
expand_border_style(decl)
|
|
475
|
-
when 'border-color'
|
|
476
|
-
expand_border_color(decl)
|
|
477
|
-
when 'font'
|
|
478
|
-
expand_font(decl)
|
|
479
|
-
when 'background'
|
|
480
|
-
expand_background(decl)
|
|
481
|
-
when 'list-style'
|
|
482
|
-
expand_list_style(decl)
|
|
483
|
-
else
|
|
484
|
-
# Not a shorthand, return as-is in an array
|
|
485
|
-
[decl]
|
|
486
|
-
end
|
|
487
|
-
end
|
|
591
|
+
if style
|
|
592
|
+
result << Declaration.new(PROP_BORDER_TOP_STYLE, style, decl.important)
|
|
593
|
+
result << Declaration.new(PROP_BORDER_RIGHT_STYLE, style, decl.important)
|
|
594
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_STYLE, style, decl.important)
|
|
595
|
+
result << Declaration.new(PROP_BORDER_LEFT_STYLE, style, decl.important)
|
|
596
|
+
end
|
|
488
597
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
Declaration.new(PROP_MARGIN_BOTTOM, sides[2], decl.important),
|
|
496
|
-
Declaration.new(PROP_MARGIN_LEFT, sides[3], decl.important)
|
|
497
|
-
]
|
|
498
|
-
end
|
|
598
|
+
if color
|
|
599
|
+
result << Declaration.new(PROP_BORDER_TOP_COLOR, color, decl.important)
|
|
600
|
+
result << Declaration.new(PROP_BORDER_RIGHT_COLOR, color, decl.important)
|
|
601
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_COLOR, color, decl.important)
|
|
602
|
+
result << Declaration.new(PROP_BORDER_LEFT_COLOR, color, decl.important)
|
|
603
|
+
end
|
|
499
604
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
sides = parse_four_sides(decl.value)
|
|
503
|
-
[
|
|
504
|
-
Declaration.new(PROP_PADDING_TOP, sides[0], decl.important),
|
|
505
|
-
Declaration.new(PROP_PADDING_RIGHT, sides[1], decl.important),
|
|
506
|
-
Declaration.new(PROP_PADDING_BOTTOM, sides[2], decl.important),
|
|
507
|
-
Declaration.new(PROP_PADDING_LEFT, sides[3], decl.important)
|
|
508
|
-
]
|
|
509
|
-
end
|
|
605
|
+
result
|
|
606
|
+
end
|
|
510
607
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
parts = split_on_whitespace(value)
|
|
518
|
-
|
|
519
|
-
case parts.length
|
|
520
|
-
when 1
|
|
521
|
-
[parts[0], parts[0], parts[0], parts[0]]
|
|
522
|
-
when 2
|
|
523
|
-
[parts[0], parts[1], parts[0], parts[1]]
|
|
524
|
-
when 3
|
|
525
|
-
[parts[0], parts[1], parts[2], parts[1]]
|
|
526
|
-
else
|
|
527
|
-
[parts[0], parts[1], parts[2], parts[3]]
|
|
528
|
-
end
|
|
529
|
-
end
|
|
608
|
+
# Expand border-side shorthand (e.g., "border-top: 1px solid red")
|
|
609
|
+
def expand_border_side(decl)
|
|
610
|
+
# Extract side from property name (e.g., "border-top" -> "top")
|
|
611
|
+
side = decl.property.byteslice(7..-1) # Skip "border-" prefix
|
|
612
|
+
width, style, color = parse_border_value(decl.value)
|
|
613
|
+
width_prop, style_prop, color_prop = BORDER_SIDE_PROPS[side]
|
|
530
614
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
i = 0
|
|
538
|
-
len = value.bytesize
|
|
539
|
-
while i < len
|
|
540
|
-
byte = value.getbyte(i)
|
|
541
|
-
|
|
542
|
-
if byte == BYTE_LPAREN
|
|
543
|
-
paren_depth += 1
|
|
544
|
-
current << byte
|
|
545
|
-
elsif byte == BYTE_RPAREN
|
|
546
|
-
paren_depth -= 1
|
|
547
|
-
current << byte
|
|
548
|
-
elsif byte == BYTE_SPACE && paren_depth == 0
|
|
549
|
-
parts << current unless current.empty?
|
|
550
|
-
current = String.new
|
|
551
|
-
else
|
|
552
|
-
current << byte
|
|
615
|
+
result = []
|
|
616
|
+
result << Declaration.new(width_prop, width, decl.important) if width
|
|
617
|
+
result << Declaration.new(style_prop, style, decl.important) if style
|
|
618
|
+
result << Declaration.new(color_prop, color, decl.important) if color
|
|
619
|
+
|
|
620
|
+
result
|
|
553
621
|
end
|
|
554
622
|
|
|
555
|
-
|
|
556
|
-
|
|
623
|
+
# Expand border-width shorthand
|
|
624
|
+
def expand_border_width(decl)
|
|
625
|
+
sides = parse_four_sides(decl.value)
|
|
626
|
+
[
|
|
627
|
+
Declaration.new(PROP_BORDER_TOP_WIDTH, sides[0], decl.important),
|
|
628
|
+
Declaration.new(PROP_BORDER_RIGHT_WIDTH, sides[1], decl.important),
|
|
629
|
+
Declaration.new(PROP_BORDER_BOTTOM_WIDTH, sides[2], decl.important),
|
|
630
|
+
Declaration.new(PROP_BORDER_LEFT_WIDTH, sides[3], decl.important)
|
|
631
|
+
]
|
|
632
|
+
end
|
|
557
633
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
634
|
+
# Expand border-style shorthand
|
|
635
|
+
def expand_border_style(decl)
|
|
636
|
+
sides = parse_four_sides(decl.value)
|
|
637
|
+
[
|
|
638
|
+
Declaration.new(PROP_BORDER_TOP_STYLE, sides[0], decl.important),
|
|
639
|
+
Declaration.new(PROP_BORDER_RIGHT_STYLE, sides[1], decl.important),
|
|
640
|
+
Declaration.new(PROP_BORDER_BOTTOM_STYLE, sides[2], decl.important),
|
|
641
|
+
Declaration.new(PROP_BORDER_LEFT_STYLE, sides[3], decl.important)
|
|
642
|
+
]
|
|
643
|
+
end
|
|
561
644
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
645
|
+
# Expand border-color shorthand
|
|
646
|
+
def expand_border_color(decl)
|
|
647
|
+
sides = parse_four_sides(decl.value)
|
|
648
|
+
[
|
|
649
|
+
Declaration.new(PROP_BORDER_TOP_COLOR, sides[0], decl.important),
|
|
650
|
+
Declaration.new(PROP_BORDER_RIGHT_COLOR, sides[1], decl.important),
|
|
651
|
+
Declaration.new(PROP_BORDER_BOTTOM_COLOR, sides[2], decl.important),
|
|
652
|
+
Declaration.new(PROP_BORDER_LEFT_COLOR, sides[3], decl.important)
|
|
653
|
+
]
|
|
654
|
+
end
|
|
566
655
|
|
|
567
|
-
|
|
656
|
+
# Parse border value (e.g., "1px solid red" -> ["1px", "solid", "red"])
|
|
657
|
+
def parse_border_value(value)
|
|
658
|
+
parts = split_on_whitespace(value)
|
|
659
|
+
width = nil
|
|
660
|
+
style = nil
|
|
661
|
+
color = nil
|
|
662
|
+
|
|
663
|
+
# Identify each part by type
|
|
664
|
+
parts.each do |part|
|
|
665
|
+
if is_border_width?(part)
|
|
666
|
+
width = part
|
|
667
|
+
elsif is_border_style?(part)
|
|
668
|
+
style = part
|
|
669
|
+
else
|
|
670
|
+
color = part # Assume color if not width or style
|
|
671
|
+
end
|
|
672
|
+
end
|
|
568
673
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
result << Declaration.new(PROP_BORDER_TOP_WIDTH, width, decl.important)
|
|
572
|
-
result << Declaration.new(PROP_BORDER_RIGHT_WIDTH, width, decl.important)
|
|
573
|
-
result << Declaration.new(PROP_BORDER_BOTTOM_WIDTH, width, decl.important)
|
|
574
|
-
result << Declaration.new(PROP_BORDER_LEFT_WIDTH, width, decl.important)
|
|
575
|
-
end
|
|
674
|
+
[width, style, color]
|
|
675
|
+
end
|
|
576
676
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
677
|
+
# Check if value contains a digit (byte-by-byte)
|
|
678
|
+
def contains_digit?(value)
|
|
679
|
+
i = 0
|
|
680
|
+
len = value.bytesize
|
|
681
|
+
while i < len
|
|
682
|
+
byte = value.getbyte(i)
|
|
683
|
+
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
583
684
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
result << Declaration.new(PROP_BORDER_RIGHT_COLOR, color, decl.important)
|
|
587
|
-
result << Declaration.new(PROP_BORDER_BOTTOM_COLOR, color, decl.important)
|
|
588
|
-
result << Declaration.new(PROP_BORDER_LEFT_COLOR, color, decl.important)
|
|
589
|
-
end
|
|
685
|
+
i += 1
|
|
686
|
+
end
|
|
590
687
|
|
|
591
|
-
|
|
592
|
-
|
|
688
|
+
false
|
|
689
|
+
end
|
|
593
690
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
width, style, color = parse_border_value(decl.value)
|
|
599
|
-
width_prop, style_prop, color_prop = BORDER_SIDE_PROPS[side]
|
|
691
|
+
# Check if value looks like a border width
|
|
692
|
+
def is_border_width?(value)
|
|
693
|
+
# Check for numeric values or width keywords
|
|
694
|
+
return true if BORDER_WIDTH_KEYWORDS.include?(value)
|
|
600
695
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
result << Declaration.new(style_prop, style, decl.important) if style
|
|
604
|
-
result << Declaration.new(color_prop, color, decl.important) if color
|
|
696
|
+
contains_digit?(value)
|
|
697
|
+
end
|
|
605
698
|
|
|
606
|
-
|
|
607
|
-
|
|
699
|
+
# Check if value is a border style
|
|
700
|
+
def is_border_style?(value)
|
|
701
|
+
BORDER_STYLE_KEYWORDS.include?(value)
|
|
702
|
+
end
|
|
608
703
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
704
|
+
# Expand font shorthand
|
|
705
|
+
# Format: [style] [variant] [weight] size[/line-height] family
|
|
706
|
+
def expand_font(decl)
|
|
707
|
+
value = decl.value
|
|
708
|
+
parts = split_on_whitespace(value)
|
|
709
|
+
|
|
710
|
+
# Need at least 2 parts (size and family)
|
|
711
|
+
return [decl] if parts.length < 2
|
|
712
|
+
|
|
713
|
+
# Parse from left to right
|
|
714
|
+
# Optional: style, variant, weight (can appear in any order)
|
|
715
|
+
# Required: size (with optional /line-height), family
|
|
716
|
+
|
|
717
|
+
i = 0
|
|
718
|
+
style = nil
|
|
719
|
+
variant = nil
|
|
720
|
+
weight = nil
|
|
721
|
+
size = nil
|
|
722
|
+
line_height = nil
|
|
723
|
+
family_parts = []
|
|
724
|
+
|
|
725
|
+
# Process optional style/variant/weight
|
|
726
|
+
while i < parts.length - 1 # Leave at least 1 for family
|
|
727
|
+
part = parts[i]
|
|
728
|
+
|
|
729
|
+
# Check if this could be size (has digit or is a size keyword)
|
|
730
|
+
if is_font_size?(part)
|
|
731
|
+
# This is the size, rest is family
|
|
732
|
+
size_part = part
|
|
733
|
+
|
|
734
|
+
# Check for line-height (find '/' byte)
|
|
735
|
+
slash_idx = nil
|
|
736
|
+
j = 0
|
|
737
|
+
len = size_part.bytesize
|
|
738
|
+
while j < len
|
|
739
|
+
if size_part.getbyte(j) == BYTE_SLASH
|
|
740
|
+
slash_idx = j
|
|
741
|
+
break
|
|
742
|
+
end
|
|
743
|
+
j += 1
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
if slash_idx
|
|
747
|
+
size = size_part.byteslice(0, slash_idx)
|
|
748
|
+
line_height = size_part.byteslice((slash_idx + 1)..-1)
|
|
749
|
+
else
|
|
750
|
+
size = size_part
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# Rest is family
|
|
754
|
+
family_parts = parts[(i + 1)..-1]
|
|
755
|
+
break
|
|
756
|
+
elsif is_font_style?(part)
|
|
757
|
+
style = part
|
|
758
|
+
elsif is_font_variant?(part)
|
|
759
|
+
variant = part
|
|
760
|
+
elsif is_font_weight?(part)
|
|
761
|
+
weight = part
|
|
762
|
+
else
|
|
763
|
+
# Unknown, might be start of family - treat everything from here as family
|
|
764
|
+
family_parts = parts[i..-1]
|
|
765
|
+
break
|
|
766
|
+
end
|
|
619
767
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
sides = parse_four_sides(decl.value)
|
|
623
|
-
[
|
|
624
|
-
Declaration.new(PROP_BORDER_TOP_STYLE, sides[0], decl.important),
|
|
625
|
-
Declaration.new(PROP_BORDER_RIGHT_STYLE, sides[1], decl.important),
|
|
626
|
-
Declaration.new(PROP_BORDER_BOTTOM_STYLE, sides[2], decl.important),
|
|
627
|
-
Declaration.new(PROP_BORDER_LEFT_STYLE, sides[3], decl.important)
|
|
628
|
-
]
|
|
629
|
-
end
|
|
768
|
+
i += 1
|
|
769
|
+
end
|
|
630
770
|
|
|
631
|
-
|
|
632
|
-
def self.expand_border_color(decl)
|
|
633
|
-
sides = parse_four_sides(decl.value)
|
|
634
|
-
[
|
|
635
|
-
Declaration.new(PROP_BORDER_TOP_COLOR, sides[0], decl.important),
|
|
636
|
-
Declaration.new(PROP_BORDER_RIGHT_COLOR, sides[1], decl.important),
|
|
637
|
-
Declaration.new(PROP_BORDER_BOTTOM_COLOR, sides[2], decl.important),
|
|
638
|
-
Declaration.new(PROP_BORDER_LEFT_COLOR, sides[3], decl.important)
|
|
639
|
-
]
|
|
640
|
-
end
|
|
771
|
+
family = family_parts.join(' ')
|
|
641
772
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
style = nil
|
|
647
|
-
color = nil
|
|
648
|
-
|
|
649
|
-
# Identify each part by type
|
|
650
|
-
parts.each do |part|
|
|
651
|
-
if is_border_width?(part)
|
|
652
|
-
width = part
|
|
653
|
-
elsif is_border_style?(part)
|
|
654
|
-
style = part
|
|
655
|
-
else
|
|
656
|
-
color = part # Assume color if not width or style
|
|
657
|
-
end
|
|
658
|
-
end
|
|
773
|
+
# Font shorthand sets ALL longhand properties
|
|
774
|
+
# Unspecified values get CSS initial values
|
|
775
|
+
# Size and family are required; if missing, return unexpanded
|
|
776
|
+
return [decl] if !size || family.empty?
|
|
659
777
|
|
|
660
|
-
|
|
661
|
-
|
|
778
|
+
result = []
|
|
779
|
+
result << Declaration.new(PROP_FONT_STYLE, style || 'normal', decl.important)
|
|
780
|
+
result << Declaration.new(PROP_FONT_VARIANT, variant || 'normal', decl.important)
|
|
781
|
+
result << Declaration.new(PROP_FONT_WEIGHT, weight || 'normal', decl.important)
|
|
782
|
+
result << Declaration.new(PROP_FONT_SIZE, size, decl.important)
|
|
783
|
+
result << Declaration.new(PROP_LINE_HEIGHT, line_height || 'normal', decl.important)
|
|
784
|
+
result << Declaration.new(PROP_FONT_FAMILY, family, decl.important)
|
|
662
785
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
i = 0
|
|
666
|
-
len = value.bytesize
|
|
667
|
-
while i < len
|
|
668
|
-
byte = value.getbyte(i)
|
|
669
|
-
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
786
|
+
result
|
|
787
|
+
end
|
|
670
788
|
|
|
671
|
-
|
|
672
|
-
|
|
789
|
+
# Check if value is a font size
|
|
790
|
+
def is_font_size?(value)
|
|
791
|
+
# Has digit or is a keyword
|
|
792
|
+
return true if contains_digit?(value)
|
|
673
793
|
|
|
674
|
-
|
|
675
|
-
|
|
794
|
+
FONT_SIZE_KEYWORDS.include?(value)
|
|
795
|
+
end
|
|
676
796
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
797
|
+
# Check if value is a font style
|
|
798
|
+
def is_font_style?(value)
|
|
799
|
+
FONT_STYLE_KEYWORDS.include?(value)
|
|
800
|
+
end
|
|
681
801
|
|
|
682
|
-
|
|
683
|
-
|
|
802
|
+
# Check if value is a font variant
|
|
803
|
+
def is_font_variant?(value)
|
|
804
|
+
FONT_VARIANT_KEYWORDS.include?(value)
|
|
805
|
+
end
|
|
684
806
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
807
|
+
# Check if value is a font weight
|
|
808
|
+
def is_font_weight?(value)
|
|
809
|
+
# Check for numeric weights like 400, 700
|
|
810
|
+
return true if contains_digit?(value)
|
|
689
811
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
while j < len
|
|
725
|
-
if size_part.getbyte(j) == BYTE_SLASH
|
|
726
|
-
slash_idx = j
|
|
727
|
-
break
|
|
812
|
+
FONT_WEIGHT_KEYWORDS.include?(value)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Expand background shorthand
|
|
816
|
+
# Format: [color] [image] [repeat] [attachment] [position]
|
|
817
|
+
def expand_background(decl)
|
|
818
|
+
value = decl.value
|
|
819
|
+
parts = split_on_whitespace(value)
|
|
820
|
+
|
|
821
|
+
return [decl] if parts.empty?
|
|
822
|
+
|
|
823
|
+
# Parse background components (simple heuristic)
|
|
824
|
+
# According to CSS spec, background shorthand sets ALL properties
|
|
825
|
+
# Any unspecified values get their initial values
|
|
826
|
+
color = nil
|
|
827
|
+
image = nil
|
|
828
|
+
repeat = nil
|
|
829
|
+
attachment = nil
|
|
830
|
+
position = nil
|
|
831
|
+
|
|
832
|
+
parts.each do |part|
|
|
833
|
+
if starts_with_url?(part) || part == 'none'
|
|
834
|
+
image = part
|
|
835
|
+
elsif BACKGROUND_REPEAT_KEYWORDS.include?(part)
|
|
836
|
+
repeat = part
|
|
837
|
+
elsif BACKGROUND_ATTACHMENT_KEYWORDS.include?(part)
|
|
838
|
+
attachment = part
|
|
839
|
+
elsif is_position_value?(part)
|
|
840
|
+
position ||= String.new
|
|
841
|
+
position << ' ' unless position.empty?
|
|
842
|
+
position << part
|
|
843
|
+
else
|
|
844
|
+
# Assume it's a color
|
|
845
|
+
color = part
|
|
728
846
|
end
|
|
729
|
-
j += 1
|
|
730
847
|
end
|
|
731
848
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
849
|
+
# Background shorthand sets ALL longhand properties
|
|
850
|
+
# Unspecified values get CSS initial values
|
|
851
|
+
result = []
|
|
852
|
+
result << Declaration.new(PROP_BACKGROUND_COLOR, color || 'transparent', decl.important)
|
|
853
|
+
result << Declaration.new(PROP_BACKGROUND_IMAGE, image || 'none', decl.important)
|
|
854
|
+
result << Declaration.new(PROP_BACKGROUND_REPEAT, repeat || 'repeat', decl.important)
|
|
855
|
+
result << Declaration.new(PROP_BACKGROUND_ATTACHMENT, attachment || 'scroll', decl.important)
|
|
856
|
+
result << Declaration.new(PROP_BACKGROUND_POSITION, position || '0% 0%', decl.important)
|
|
738
857
|
|
|
739
|
-
|
|
740
|
-
family_parts = parts[(i + 1)..-1]
|
|
741
|
-
break
|
|
742
|
-
elsif is_font_style?(part)
|
|
743
|
-
style = part
|
|
744
|
-
elsif is_font_variant?(part)
|
|
745
|
-
variant = part
|
|
746
|
-
elsif is_font_weight?(part)
|
|
747
|
-
weight = part
|
|
748
|
-
else
|
|
749
|
-
# Unknown, might be start of family - treat everything from here as family
|
|
750
|
-
family_parts = parts[i..-1]
|
|
751
|
-
break
|
|
858
|
+
result
|
|
752
859
|
end
|
|
753
860
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
family = family_parts.join(' ')
|
|
861
|
+
# Check if value starts with 'url('
|
|
862
|
+
def starts_with_url?(value)
|
|
863
|
+
return false if value.bytesize < 4
|
|
758
864
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
865
|
+
value.getbyte(0) == BYTE_LOWER_U &&
|
|
866
|
+
value.getbyte(1) == BYTE_LOWER_R &&
|
|
867
|
+
value.getbyte(2) == BYTE_LOWER_L &&
|
|
868
|
+
value.getbyte(3) == BYTE_LPAREN
|
|
869
|
+
end
|
|
763
870
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
result << Declaration.new(PROP_FONT_SIZE, size, decl.important)
|
|
769
|
-
result << Declaration.new(PROP_LINE_HEIGHT, line_height || 'normal', decl.important)
|
|
770
|
-
result << Declaration.new(PROP_FONT_FAMILY, family, decl.important)
|
|
871
|
+
# Check if value is a position value (for background-position)
|
|
872
|
+
def is_position_value?(value)
|
|
873
|
+
return true if BACKGROUND_POSITION_KEYWORDS.include?(value)
|
|
874
|
+
return true if contains_digit?(value)
|
|
771
875
|
|
|
772
|
-
|
|
773
|
-
|
|
876
|
+
# Check for '%'
|
|
877
|
+
i = 0
|
|
878
|
+
len = value.bytesize
|
|
879
|
+
while i < len
|
|
880
|
+
return true if value.getbyte(i) == BYTE_PERCENT
|
|
774
881
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
882
|
+
i += 1
|
|
883
|
+
end
|
|
884
|
+
false
|
|
885
|
+
end
|
|
779
886
|
|
|
780
|
-
|
|
781
|
-
|
|
887
|
+
# Expand list-style shorthand
|
|
888
|
+
# Format: [type] [position] [image]
|
|
889
|
+
def expand_list_style(decl)
|
|
890
|
+
value = decl.value
|
|
891
|
+
parts = split_on_whitespace(value)
|
|
782
892
|
|
|
783
|
-
|
|
784
|
-
def self.is_font_style?(value)
|
|
785
|
-
FONT_STYLE_KEYWORDS.include?(value)
|
|
786
|
-
end
|
|
893
|
+
return [decl] if parts.empty?
|
|
787
894
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
895
|
+
result = []
|
|
896
|
+
type = nil
|
|
897
|
+
position = nil
|
|
898
|
+
image = nil
|
|
792
899
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
900
|
+
parts.each do |part|
|
|
901
|
+
if starts_with_url?(part) || part == 'none'
|
|
902
|
+
image = part
|
|
903
|
+
elsif LIST_STYLE_POSITION_KEYWORDS.include?(part)
|
|
904
|
+
position = part
|
|
905
|
+
else
|
|
906
|
+
# Assume it's a type (disc, circle, square, etc.)
|
|
907
|
+
type = part
|
|
908
|
+
end
|
|
909
|
+
end
|
|
797
910
|
|
|
798
|
-
|
|
799
|
-
|
|
911
|
+
result << Declaration.new(PROP_LIST_STYLE_TYPE, type, decl.important) if type
|
|
912
|
+
result << Declaration.new(PROP_LIST_STYLE_POSITION, position, decl.important) if position
|
|
913
|
+
result << Declaration.new(PROP_LIST_STYLE_IMAGE, image, decl.important) if image
|
|
800
914
|
|
|
801
|
-
|
|
802
|
-
# Format: [color] [image] [repeat] [attachment] [position]
|
|
803
|
-
def self.expand_background(decl)
|
|
804
|
-
value = decl.value
|
|
805
|
-
parts = split_on_whitespace(value)
|
|
806
|
-
|
|
807
|
-
return [decl] if parts.empty?
|
|
808
|
-
|
|
809
|
-
# Parse background components (simple heuristic)
|
|
810
|
-
# According to CSS spec, background shorthand sets ALL properties
|
|
811
|
-
# Any unspecified values get their initial values
|
|
812
|
-
color = nil
|
|
813
|
-
image = nil
|
|
814
|
-
repeat = nil
|
|
815
|
-
attachment = nil
|
|
816
|
-
position = nil
|
|
817
|
-
|
|
818
|
-
parts.each do |part|
|
|
819
|
-
if starts_with_url?(part) || part == 'none'
|
|
820
|
-
image = part
|
|
821
|
-
elsif BACKGROUND_REPEAT_KEYWORDS.include?(part)
|
|
822
|
-
repeat = part
|
|
823
|
-
elsif BACKGROUND_ATTACHMENT_KEYWORDS.include?(part)
|
|
824
|
-
attachment = part
|
|
825
|
-
elsif is_position_value?(part)
|
|
826
|
-
position ||= String.new
|
|
827
|
-
position << ' ' unless position.empty?
|
|
828
|
-
position << part
|
|
829
|
-
else
|
|
830
|
-
# Assume it's a color
|
|
831
|
-
color = part
|
|
915
|
+
result.empty? ? [decl] : result
|
|
832
916
|
end
|
|
833
|
-
end
|
|
834
917
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
result << Declaration.new(PROP_BACKGROUND_POSITION, position || '0% 0%', decl.important)
|
|
918
|
+
# Remove the individual longhand declarations a shorthand replaces and
|
|
919
|
+
# append the shorthand declaration in their place.
|
|
920
|
+
# Note: We append rather than insert at original position to match C implementation behavior
|
|
921
|
+
def replace_with_shorthand!(rule, longhand_props, shorthand_prop, value, important)
|
|
922
|
+
rule.declarations.reject! { |d| longhand_props.include?(d.property) }
|
|
923
|
+
rule.declarations << Declaration.new(shorthand_prop, value, important)
|
|
924
|
+
end
|
|
843
925
|
|
|
844
|
-
|
|
845
|
-
|
|
926
|
+
# Recreate shorthand properties where possible (mutates declarations)
|
|
927
|
+
#
|
|
928
|
+
# @param rule [Rule] Rule to recreate shorthands in
|
|
929
|
+
def recreate_shorthands!(rule)
|
|
930
|
+
# Build property map
|
|
931
|
+
prop_map = {}
|
|
932
|
+
rule.declarations.each { |d| prop_map[d.property] = d }
|
|
846
933
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return false if value.bytesize < 4
|
|
934
|
+
# Try to recreate margin
|
|
935
|
+
recreate_margin!(rule, prop_map)
|
|
850
936
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
value.getbyte(2) == BYTE_LOWER_L &&
|
|
854
|
-
value.getbyte(3) == BYTE_LPAREN
|
|
855
|
-
end
|
|
937
|
+
# Try to recreate padding
|
|
938
|
+
recreate_padding!(rule, prop_map)
|
|
856
939
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return true if BACKGROUND_POSITION_KEYWORDS.include?(value)
|
|
860
|
-
return true if contains_digit?(value)
|
|
940
|
+
# Try to recreate border
|
|
941
|
+
recreate_border!(rule, prop_map)
|
|
861
942
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
len = value.bytesize
|
|
865
|
-
while i < len
|
|
866
|
-
return true if value.getbyte(i) == BYTE_PERCENT
|
|
943
|
+
# Try to recreate list-style
|
|
944
|
+
recreate_list_style!(rule, prop_map)
|
|
867
945
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
false
|
|
871
|
-
end
|
|
946
|
+
# Try to recreate font
|
|
947
|
+
recreate_font!(rule, prop_map)
|
|
872
948
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
def self.expand_list_style(decl)
|
|
876
|
-
value = decl.value
|
|
877
|
-
parts = split_on_whitespace(value)
|
|
878
|
-
|
|
879
|
-
return [decl] if parts.empty?
|
|
880
|
-
|
|
881
|
-
result = []
|
|
882
|
-
type = nil
|
|
883
|
-
position = nil
|
|
884
|
-
image = nil
|
|
885
|
-
|
|
886
|
-
parts.each do |part|
|
|
887
|
-
if starts_with_url?(part) || part == 'none'
|
|
888
|
-
image = part
|
|
889
|
-
elsif LIST_STYLE_POSITION_KEYWORDS.include?(part)
|
|
890
|
-
position = part
|
|
891
|
-
else
|
|
892
|
-
# Assume it's a type (disc, circle, square, etc.)
|
|
893
|
-
type = part
|
|
949
|
+
# Try to recreate background
|
|
950
|
+
recreate_background!(rule, prop_map)
|
|
894
951
|
end
|
|
895
|
-
end
|
|
896
|
-
|
|
897
|
-
result << Declaration.new(PROP_LIST_STYLE_TYPE, type, decl.important) if type
|
|
898
|
-
result << Declaration.new(PROP_LIST_STYLE_POSITION, position, decl.important) if position
|
|
899
|
-
result << Declaration.new(PROP_LIST_STYLE_IMAGE, image, decl.important) if image
|
|
900
952
|
|
|
901
|
-
|
|
902
|
-
|
|
953
|
+
# Try to recreate margin shorthand
|
|
954
|
+
def recreate_margin!(rule, prop_map)
|
|
955
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
956
|
+
sides = []
|
|
957
|
+
MARGIN_SIDES.each { |s| sides << prop_map[s] }
|
|
958
|
+
return unless sides.all? # Need all four sides
|
|
959
|
+
|
|
960
|
+
# Check if all have same importance
|
|
961
|
+
# Use each + << instead of map
|
|
962
|
+
importances = []
|
|
963
|
+
sides.each { |s| importances << s.important }
|
|
964
|
+
importances.uniq!
|
|
965
|
+
return if importances.length > 1 # Mixed importance, can't create shorthand
|
|
966
|
+
|
|
967
|
+
# Use each + << instead of map
|
|
968
|
+
values = []
|
|
969
|
+
sides.each { |s| values << s.value }
|
|
970
|
+
important = sides.first.important
|
|
971
|
+
|
|
972
|
+
# Create optimized shorthand
|
|
973
|
+
shorthand_value = optimize_four_sides(values)
|
|
974
|
+
|
|
975
|
+
replace_with_shorthand!(rule, MARGIN_SIDES, PROP_MARGIN, shorthand_value, important)
|
|
976
|
+
end
|
|
903
977
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
end
|
|
978
|
+
# Try to recreate padding shorthand
|
|
979
|
+
def recreate_padding!(rule, prop_map)
|
|
980
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
981
|
+
sides = []
|
|
982
|
+
PADDING_SIDES.each { |s| sides << prop_map[s] }
|
|
983
|
+
return unless sides.all?
|
|
911
984
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
prop_map = {}
|
|
918
|
-
rule.declarations.each { |d| prop_map[d.property] = d }
|
|
985
|
+
# Use each + << instead of map
|
|
986
|
+
importances = []
|
|
987
|
+
sides.each { |s| importances << s.important }
|
|
988
|
+
importances.uniq!
|
|
989
|
+
return if importances.length > 1
|
|
919
990
|
|
|
920
|
-
|
|
921
|
-
|
|
991
|
+
# Use each + << instead of map
|
|
992
|
+
values = []
|
|
993
|
+
sides.each { |s| values << s.value }
|
|
994
|
+
important = sides.first.important
|
|
922
995
|
|
|
923
|
-
|
|
924
|
-
recreate_padding!(rule, prop_map)
|
|
996
|
+
shorthand_value = optimize_four_sides(values)
|
|
925
997
|
|
|
926
|
-
|
|
927
|
-
|
|
998
|
+
replace_with_shorthand!(rule, PADDING_SIDES, PROP_PADDING, shorthand_value, important)
|
|
999
|
+
end
|
|
928
1000
|
|
|
929
|
-
|
|
930
|
-
|
|
1001
|
+
# Helper: Check if all declarations have same value and importance
|
|
1002
|
+
# Does single pass instead of multiple .map calls
|
|
1003
|
+
def check_all_same?(decls)
|
|
1004
|
+
return false if decls.empty?
|
|
931
1005
|
|
|
932
|
-
|
|
933
|
-
|
|
1006
|
+
first_val = decls[0].value
|
|
1007
|
+
first_imp = decls[0].important
|
|
934
1008
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1009
|
+
i = 1
|
|
1010
|
+
while i < decls.length
|
|
1011
|
+
return false if decls[i].value != first_val
|
|
1012
|
+
return false if decls[i].important != first_imp
|
|
938
1013
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
942
|
-
sides = []
|
|
943
|
-
MARGIN_SIDES.each { |s| sides << prop_map[s] }
|
|
944
|
-
return unless sides.all? # Need all four sides
|
|
945
|
-
|
|
946
|
-
# Check if all have same importance
|
|
947
|
-
# Use each + << instead of map
|
|
948
|
-
importances = []
|
|
949
|
-
sides.each { |s| importances << s.important }
|
|
950
|
-
importances.uniq!
|
|
951
|
-
return if importances.length > 1 # Mixed importance, can't create shorthand
|
|
952
|
-
|
|
953
|
-
# Use each + << instead of map
|
|
954
|
-
values = []
|
|
955
|
-
sides.each { |s| values << s.value }
|
|
956
|
-
important = sides.first.important
|
|
957
|
-
|
|
958
|
-
# Create optimized shorthand
|
|
959
|
-
shorthand_value = optimize_four_sides(values)
|
|
960
|
-
|
|
961
|
-
replace_with_shorthand!(rule, MARGIN_SIDES, PROP_MARGIN, shorthand_value, important)
|
|
962
|
-
end
|
|
1014
|
+
i += 1
|
|
1015
|
+
end
|
|
963
1016
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
967
|
-
sides = []
|
|
968
|
-
PADDING_SIDES.each { |s| sides << prop_map[s] }
|
|
969
|
-
return unless sides.all?
|
|
1017
|
+
true
|
|
1018
|
+
end
|
|
970
1019
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1020
|
+
# Try to recreate border shorthand
|
|
1021
|
+
def recreate_border!(rule, prop_map)
|
|
1022
|
+
# Check if we have all width/style/color properties with same values for all sides
|
|
1023
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
1024
|
+
widths = []
|
|
1025
|
+
BORDER_WIDTHS.each { |p| widths << prop_map[p] }
|
|
1026
|
+
styles = []
|
|
1027
|
+
BORDER_STYLES.each { |p| styles << prop_map[p] }
|
|
1028
|
+
colors = []
|
|
1029
|
+
BORDER_COLORS.each { |p| colors << prop_map[p] }
|
|
1030
|
+
|
|
1031
|
+
# Check if all sides have same values and can create full border shorthand
|
|
1032
|
+
# Check cheapest condition first (.all?), then do single pass for values/importance
|
|
1033
|
+
widths_all_same = widths.all? && check_all_same?(widths)
|
|
1034
|
+
styles_all_same = styles.all? && check_all_same?(styles)
|
|
1035
|
+
colors_all_same = colors.all? && check_all_same?(colors)
|
|
1036
|
+
|
|
1037
|
+
# Can create FULL border shorthand ONLY if style is present (style is required for border shorthand)
|
|
1038
|
+
# AND all properties that are present have same values and importance
|
|
1039
|
+
if styles_all_same
|
|
1040
|
+
# Check if we have width and/or color with same importance as style
|
|
1041
|
+
can_create_border = true
|
|
1042
|
+
important = styles.first.important
|
|
1043
|
+
|
|
1044
|
+
# If width is present, must be all-same and same importance
|
|
1045
|
+
if widths.all?
|
|
1046
|
+
can_create_border = false unless widths_all_same && widths.first.important == important
|
|
1047
|
+
end
|
|
976
1048
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1049
|
+
# If color is present, must be all-same and same importance
|
|
1050
|
+
if colors.all?
|
|
1051
|
+
can_create_border = false unless colors_all_same && colors.first.important == important
|
|
1052
|
+
end
|
|
981
1053
|
|
|
982
|
-
|
|
1054
|
+
if can_create_border
|
|
1055
|
+
# Create full border shorthand
|
|
1056
|
+
parts = []
|
|
1057
|
+
parts << widths.first.value if widths_all_same
|
|
1058
|
+
parts << styles.first.value
|
|
1059
|
+
parts << colors.first.value if colors_all_same
|
|
983
1060
|
|
|
984
|
-
|
|
985
|
-
end
|
|
1061
|
+
border_value = parts.join(' ')
|
|
986
1062
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1063
|
+
replace_with_shorthand!(rule, BORDER_ALL, PROP_BORDER, border_value, important)
|
|
1064
|
+
return
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
991
1067
|
|
|
992
|
-
|
|
993
|
-
|
|
1068
|
+
# Try to create border-width/style/color shorthands
|
|
1069
|
+
recreate_border_width!(rule, widths) if widths.all?
|
|
1070
|
+
recreate_border_style!(rule, styles) if styles.all?
|
|
1071
|
+
recreate_border_color!(rule, colors) if colors.all?
|
|
1072
|
+
end
|
|
994
1073
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1074
|
+
# Recreate border-width shorthand
|
|
1075
|
+
def recreate_border_width!(rule, widths)
|
|
1076
|
+
importances = widths.map(&:important).uniq
|
|
1077
|
+
return if importances.length > 1
|
|
999
1078
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1079
|
+
values = widths.map(&:value)
|
|
1080
|
+
important = widths.first.important
|
|
1002
1081
|
|
|
1003
|
-
|
|
1004
|
-
end
|
|
1082
|
+
shorthand_value = optimize_four_sides(values)
|
|
1005
1083
|
|
|
1006
|
-
|
|
1007
|
-
def self.recreate_border!(rule, prop_map)
|
|
1008
|
-
# Check if we have all width/style/color properties with same values for all sides
|
|
1009
|
-
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
1010
|
-
widths = []
|
|
1011
|
-
BORDER_WIDTHS.each { |p| widths << prop_map[p] }
|
|
1012
|
-
styles = []
|
|
1013
|
-
BORDER_STYLES.each { |p| styles << prop_map[p] }
|
|
1014
|
-
colors = []
|
|
1015
|
-
BORDER_COLORS.each { |p| colors << prop_map[p] }
|
|
1016
|
-
|
|
1017
|
-
# Check if all sides have same values and can create full border shorthand
|
|
1018
|
-
# Check cheapest condition first (.all?), then do single pass for values/importance
|
|
1019
|
-
widths_all_same = widths.all? && check_all_same?(widths)
|
|
1020
|
-
styles_all_same = styles.all? && check_all_same?(styles)
|
|
1021
|
-
colors_all_same = colors.all? && check_all_same?(colors)
|
|
1022
|
-
|
|
1023
|
-
# Can create FULL border shorthand ONLY if style is present (style is required for border shorthand)
|
|
1024
|
-
# AND all properties that are present have same values and importance
|
|
1025
|
-
if styles_all_same
|
|
1026
|
-
# Check if we have width and/or color with same importance as style
|
|
1027
|
-
can_create_border = true
|
|
1028
|
-
important = styles.first.important
|
|
1029
|
-
|
|
1030
|
-
# If width is present, must be all-same and same importance
|
|
1031
|
-
if widths.all?
|
|
1032
|
-
can_create_border = false unless widths_all_same && widths.first.important == important
|
|
1084
|
+
replace_with_shorthand!(rule, BORDER_WIDTHS, PROP_BORDER_WIDTH, shorthand_value, important)
|
|
1033
1085
|
end
|
|
1034
1086
|
|
|
1035
|
-
#
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1087
|
+
# Recreate border-style shorthand
|
|
1088
|
+
def recreate_border_style!(rule, styles)
|
|
1089
|
+
importances = styles.map(&:important).uniq
|
|
1090
|
+
return if importances.length > 1
|
|
1039
1091
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
parts = []
|
|
1043
|
-
parts << widths.first.value if widths_all_same
|
|
1044
|
-
parts << styles.first.value
|
|
1045
|
-
parts << colors.first.value if colors_all_same
|
|
1092
|
+
values = styles.map(&:value)
|
|
1093
|
+
important = styles.first.important
|
|
1046
1094
|
|
|
1047
|
-
|
|
1095
|
+
shorthand_value = optimize_four_sides(values)
|
|
1048
1096
|
|
|
1049
|
-
replace_with_shorthand!(rule,
|
|
1050
|
-
return
|
|
1097
|
+
replace_with_shorthand!(rule, BORDER_STYLES, PROP_BORDER_STYLE, shorthand_value, important)
|
|
1051
1098
|
end
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
# Try to create border-width/style/color shorthands
|
|
1055
|
-
recreate_border_width!(rule, widths) if widths.all?
|
|
1056
|
-
recreate_border_style!(rule, styles) if styles.all?
|
|
1057
|
-
recreate_border_color!(rule, colors) if colors.all?
|
|
1058
|
-
end
|
|
1059
1099
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1100
|
+
# Recreate border-color shorthand
|
|
1101
|
+
def recreate_border_color!(rule, colors)
|
|
1102
|
+
importances = colors.map(&:important).uniq
|
|
1103
|
+
return if importances.length > 1
|
|
1064
1104
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1105
|
+
values = colors.map(&:value)
|
|
1106
|
+
important = colors.first.important
|
|
1067
1107
|
|
|
1068
|
-
|
|
1108
|
+
shorthand_value = optimize_four_sides(values)
|
|
1069
1109
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
# Recreate border-style shorthand
|
|
1074
|
-
def self.recreate_border_style!(rule, styles)
|
|
1075
|
-
importances = styles.map(&:important).uniq
|
|
1076
|
-
return if importances.length > 1
|
|
1110
|
+
replace_with_shorthand!(rule, BORDER_COLORS, PROP_BORDER_COLOR, shorthand_value, important)
|
|
1111
|
+
end
|
|
1077
1112
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1113
|
+
# Optimize four-sided value representation
|
|
1114
|
+
# ["10px", "10px", "10px", "10px"] -> "10px"
|
|
1115
|
+
# ["10px", "20px", "10px", "20px"] -> "10px 20px"
|
|
1116
|
+
# ["10px", "20px", "30px", "20px"] -> "10px 20px 30px"
|
|
1117
|
+
# ["10px", "20px", "30px", "40px"] -> "10px 20px 30px 40px"
|
|
1118
|
+
def optimize_four_sides(values)
|
|
1119
|
+
top, right, bottom, left = values
|
|
1120
|
+
|
|
1121
|
+
if top == right && right == bottom && bottom == left
|
|
1122
|
+
top
|
|
1123
|
+
elsif top == bottom && right == left
|
|
1124
|
+
"#{top} #{right}"
|
|
1125
|
+
elsif right == left
|
|
1126
|
+
"#{top} #{right} #{bottom}"
|
|
1127
|
+
else
|
|
1128
|
+
"#{top} #{right} #{bottom} #{left}"
|
|
1129
|
+
end
|
|
1130
|
+
end
|
|
1080
1131
|
|
|
1081
|
-
|
|
1132
|
+
# Try to recreate font shorthand
|
|
1133
|
+
# Requires: font-size and font-family (minimum)
|
|
1134
|
+
# Optional: font-style, font-variant, font-weight, line-height
|
|
1135
|
+
def recreate_font!(rule, prop_map)
|
|
1136
|
+
size = prop_map[PROP_FONT_SIZE]
|
|
1137
|
+
family = prop_map[PROP_FONT_FAMILY]
|
|
1082
1138
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1139
|
+
# Need at least size and family
|
|
1140
|
+
return unless size && family
|
|
1085
1141
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
return if importances.length > 1
|
|
1142
|
+
# Check if all font properties have same importance
|
|
1143
|
+
font_decls = FONT_PROPERTIES.filter_map { |p| prop_map[p] }
|
|
1144
|
+
return if font_decls.empty?
|
|
1090
1145
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1146
|
+
importances = font_decls.map(&:important).uniq
|
|
1147
|
+
return if importances.length > 1
|
|
1093
1148
|
|
|
1094
|
-
|
|
1149
|
+
important = font_decls.first.important
|
|
1095
1150
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1151
|
+
# Build font shorthand value
|
|
1152
|
+
# Strategy: Only omit defaults if we have ALL 6 properties (from shorthand expansion)
|
|
1153
|
+
# If we have a partial set, include all non-nil values
|
|
1154
|
+
style = prop_map[PROP_FONT_STYLE]&.value
|
|
1155
|
+
variant = prop_map[PROP_FONT_VARIANT]&.value
|
|
1156
|
+
weight = prop_map[PROP_FONT_WEIGHT]&.value
|
|
1157
|
+
line_height = prop_map[PROP_LINE_HEIGHT]&.value
|
|
1098
1158
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
# ["10px", "20px", "10px", "20px"] -> "10px 20px"
|
|
1102
|
-
# ["10px", "20px", "30px", "20px"] -> "10px 20px 30px"
|
|
1103
|
-
# ["10px", "20px", "30px", "40px"] -> "10px 20px 30px 40px"
|
|
1104
|
-
def self.optimize_four_sides(values)
|
|
1105
|
-
top, right, bottom, left = values
|
|
1106
|
-
|
|
1107
|
-
if top == right && right == bottom && bottom == left
|
|
1108
|
-
top
|
|
1109
|
-
elsif top == bottom && right == left
|
|
1110
|
-
"#{top} #{right}"
|
|
1111
|
-
elsif right == left
|
|
1112
|
-
"#{top} #{right} #{bottom}"
|
|
1113
|
-
else
|
|
1114
|
-
"#{top} #{right} #{bottom} #{left}"
|
|
1115
|
-
end
|
|
1116
|
-
end
|
|
1117
|
-
|
|
1118
|
-
# Try to recreate font shorthand
|
|
1119
|
-
# Requires: font-size and font-family (minimum)
|
|
1120
|
-
# Optional: font-style, font-variant, font-weight, line-height
|
|
1121
|
-
def self.recreate_font!(rule, prop_map)
|
|
1122
|
-
size = prop_map[PROP_FONT_SIZE]
|
|
1123
|
-
family = prop_map[PROP_FONT_FAMILY]
|
|
1124
|
-
|
|
1125
|
-
# Need at least size and family
|
|
1126
|
-
return unless size && family
|
|
1127
|
-
|
|
1128
|
-
# Check if all font properties have same importance
|
|
1129
|
-
font_decls = FONT_PROPERTIES.filter_map { |p| prop_map[p] }
|
|
1130
|
-
return if font_decls.empty?
|
|
1131
|
-
|
|
1132
|
-
importances = font_decls.map(&:important).uniq
|
|
1133
|
-
return if importances.length > 1
|
|
1134
|
-
|
|
1135
|
-
important = font_decls.first.important
|
|
1136
|
-
|
|
1137
|
-
# Build font shorthand value
|
|
1138
|
-
# Strategy: Only omit defaults if we have ALL 6 properties (from shorthand expansion)
|
|
1139
|
-
# If we have a partial set, include all non-nil values
|
|
1140
|
-
style = prop_map[PROP_FONT_STYLE]&.value
|
|
1141
|
-
variant = prop_map[PROP_FONT_VARIANT]&.value
|
|
1142
|
-
weight = prop_map[PROP_FONT_WEIGHT]&.value
|
|
1143
|
-
line_height = prop_map[PROP_LINE_HEIGHT]&.value
|
|
1144
|
-
|
|
1145
|
-
all_present = style && variant && weight && line_height
|
|
1146
|
-
parts = []
|
|
1147
|
-
|
|
1148
|
-
if all_present
|
|
1149
|
-
# All properties present (likely from shorthand expansion) - omit defaults
|
|
1150
|
-
parts << style if style != 'normal'
|
|
1151
|
-
parts << variant if variant != 'normal'
|
|
1152
|
-
parts << weight if weight != 'normal'
|
|
1153
|
-
else
|
|
1154
|
-
# Partial set - include all non-nil values
|
|
1155
|
-
parts << style if style
|
|
1156
|
-
parts << variant if variant
|
|
1157
|
-
parts << weight if weight
|
|
1158
|
-
end
|
|
1159
|
+
all_present = style && variant && weight && line_height
|
|
1160
|
+
parts = []
|
|
1159
1161
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
parts << "#{size.value}/#{line_height}"
|
|
1172
|
-
else
|
|
1173
|
-
parts << size.value
|
|
1174
|
-
end
|
|
1175
|
-
end
|
|
1162
|
+
if all_present
|
|
1163
|
+
# All properties present (likely from shorthand expansion) - omit defaults
|
|
1164
|
+
parts << style if style != 'normal'
|
|
1165
|
+
parts << variant if variant != 'normal'
|
|
1166
|
+
parts << weight if weight != 'normal'
|
|
1167
|
+
else
|
|
1168
|
+
# Partial set - include all non-nil values
|
|
1169
|
+
parts << style if style
|
|
1170
|
+
parts << variant if variant
|
|
1171
|
+
parts << weight if weight
|
|
1172
|
+
end
|
|
1176
1173
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1174
|
+
# Required: size[/line-height]
|
|
1175
|
+
if all_present
|
|
1176
|
+
# Omit line-height if default
|
|
1177
|
+
if line_height != 'normal'
|
|
1178
|
+
parts << "#{size.value}/#{line_height}"
|
|
1179
|
+
else
|
|
1180
|
+
parts << size.value
|
|
1181
|
+
end
|
|
1182
|
+
else
|
|
1183
|
+
# Include line-height if present
|
|
1184
|
+
if line_height
|
|
1185
|
+
parts << "#{size.value}/#{line_height}"
|
|
1186
|
+
else
|
|
1187
|
+
parts << size.value
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1179
1190
|
|
|
1180
|
-
|
|
1191
|
+
# Required: family
|
|
1192
|
+
parts << family.value
|
|
1181
1193
|
|
|
1182
|
-
|
|
1183
|
-
end
|
|
1194
|
+
shorthand_value = parts.join(' ')
|
|
1184
1195
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
def self.recreate_background!(rule, prop_map)
|
|
1188
|
-
bg_props = BACKGROUND_PROPERTIES
|
|
1189
|
-
bg_decls = bg_props.filter_map { |p| prop_map[p] }
|
|
1190
|
-
|
|
1191
|
-
# Need at least 2 properties to create shorthand
|
|
1192
|
-
# Single properties should stay as longhands (e.g., background-color: blue)
|
|
1193
|
-
# because shorthand resets all other properties to initial values
|
|
1194
|
-
return if bg_decls.length < 2
|
|
1195
|
-
|
|
1196
|
-
# Check if all have same importance
|
|
1197
|
-
importances = bg_decls.map(&:important).uniq
|
|
1198
|
-
return if importances.length > 1
|
|
1199
|
-
|
|
1200
|
-
important = bg_decls.first.important
|
|
1201
|
-
|
|
1202
|
-
# Build background shorthand value
|
|
1203
|
-
# Strategy: Only omit defaults if we have ALL 5 properties (from shorthand expansion)
|
|
1204
|
-
# If we have a partial set (explicit longhands), include all values
|
|
1205
|
-
color = prop_map[PROP_BACKGROUND_COLOR]&.value
|
|
1206
|
-
image = prop_map[PROP_BACKGROUND_IMAGE]&.value
|
|
1207
|
-
repeat = prop_map[PROP_BACKGROUND_REPEAT]&.value
|
|
1208
|
-
position = prop_map[PROP_BACKGROUND_POSITION]&.value
|
|
1209
|
-
attachment = prop_map[PROP_BACKGROUND_ATTACHMENT]&.value
|
|
1210
|
-
|
|
1211
|
-
all_present = color && image && repeat && position && attachment
|
|
1212
|
-
parts = []
|
|
1213
|
-
|
|
1214
|
-
if all_present
|
|
1215
|
-
# All 5 properties present (likely from shorthand expansion) - omit defaults
|
|
1216
|
-
parts << color if color != 'transparent'
|
|
1217
|
-
parts << image if image != 'none'
|
|
1218
|
-
parts << repeat if repeat != 'repeat'
|
|
1219
|
-
parts << position if position != '0% 0%'
|
|
1220
|
-
parts << attachment if attachment != 'scroll'
|
|
1221
|
-
else
|
|
1222
|
-
# Partial set (explicit longhands) - include all non-nil values
|
|
1223
|
-
parts << color if color
|
|
1224
|
-
parts << image if image
|
|
1225
|
-
parts << repeat if repeat
|
|
1226
|
-
parts << position if position
|
|
1227
|
-
parts << attachment if attachment
|
|
1228
|
-
end
|
|
1196
|
+
replace_with_shorthand!(rule, FONT_PROPERTIES, PROP_FONT, shorthand_value, important)
|
|
1197
|
+
end
|
|
1229
1198
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1199
|
+
# Try to recreate background shorthand
|
|
1200
|
+
# Can combine: background-color, background-image, background-repeat, etc.
|
|
1201
|
+
def recreate_background!(rule, prop_map)
|
|
1202
|
+
bg_props = BACKGROUND_PROPERTIES
|
|
1203
|
+
bg_decls = bg_props.filter_map { |p| prop_map[p] }
|
|
1204
|
+
|
|
1205
|
+
# Need at least 2 properties to create shorthand
|
|
1206
|
+
# Single properties should stay as longhands (e.g., background-color: blue)
|
|
1207
|
+
# because shorthand resets all other properties to initial values
|
|
1208
|
+
return if bg_decls.length < 2
|
|
1209
|
+
|
|
1210
|
+
# Check if all have same importance
|
|
1211
|
+
importances = bg_decls.map(&:important).uniq
|
|
1212
|
+
return if importances.length > 1
|
|
1213
|
+
|
|
1214
|
+
important = bg_decls.first.important
|
|
1215
|
+
|
|
1216
|
+
# Build background shorthand value
|
|
1217
|
+
# Strategy: Only omit defaults if we have ALL 5 properties (from shorthand expansion)
|
|
1218
|
+
# If we have a partial set (explicit longhands), include all values
|
|
1219
|
+
color = prop_map[PROP_BACKGROUND_COLOR]&.value
|
|
1220
|
+
image = prop_map[PROP_BACKGROUND_IMAGE]&.value
|
|
1221
|
+
repeat = prop_map[PROP_BACKGROUND_REPEAT]&.value
|
|
1222
|
+
position = prop_map[PROP_BACKGROUND_POSITION]&.value
|
|
1223
|
+
attachment = prop_map[PROP_BACKGROUND_ATTACHMENT]&.value
|
|
1224
|
+
|
|
1225
|
+
all_present = color && image && repeat && position && attachment
|
|
1226
|
+
parts = []
|
|
1237
1227
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1228
|
+
if all_present
|
|
1229
|
+
# All 5 properties present (likely from shorthand expansion) - omit defaults
|
|
1230
|
+
parts << color if color != 'transparent'
|
|
1231
|
+
parts << image if image != 'none'
|
|
1232
|
+
parts << repeat if repeat != 'repeat'
|
|
1233
|
+
parts << position if position != '0% 0%'
|
|
1234
|
+
parts << attachment if attachment != 'scroll'
|
|
1235
|
+
else
|
|
1236
|
+
# Partial set (explicit longhands) - include all non-nil values
|
|
1237
|
+
parts << color if color
|
|
1238
|
+
parts << image if image
|
|
1239
|
+
parts << repeat if repeat
|
|
1240
|
+
parts << position if position
|
|
1241
|
+
parts << attachment if attachment
|
|
1242
|
+
end
|
|
1240
1243
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1244
|
+
# If all properties are defaults, the shorthand value would be empty
|
|
1245
|
+
# In this case, use "none" which is equivalent to all-default background
|
|
1246
|
+
shorthand_value = if parts.empty?
|
|
1247
|
+
'none'
|
|
1248
|
+
else
|
|
1249
|
+
parts.join(' ')
|
|
1250
|
+
end
|
|
1246
1251
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1252
|
+
replace_with_shorthand!(rule, BACKGROUND_PROPERTIES, PROP_BACKGROUND, shorthand_value, important)
|
|
1253
|
+
end
|
|
1249
1254
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1255
|
+
# Try to recreate list-style shorthand
|
|
1256
|
+
# Can combine: list-style-type, list-style-position, list-style-image
|
|
1257
|
+
def recreate_list_style!(rule, prop_map)
|
|
1258
|
+
ls_props = LIST_STYLE_PROPERTIES
|
|
1259
|
+
ls_decls = ls_props.filter_map { |p| prop_map[p] }
|
|
1253
1260
|
|
|
1254
|
-
|
|
1261
|
+
# Need at least 2 properties to create shorthand
|
|
1262
|
+
return if ls_decls.length < 2
|
|
1255
1263
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
parts << prop_map[PROP_LIST_STYLE_POSITION].value if prop_map[PROP_LIST_STYLE_POSITION]
|
|
1260
|
-
parts << prop_map[PROP_LIST_STYLE_IMAGE].value if prop_map[PROP_LIST_STYLE_IMAGE]
|
|
1264
|
+
# Check if all have same importance
|
|
1265
|
+
importances = ls_decls.map(&:important).uniq
|
|
1266
|
+
return if importances.length > 1
|
|
1261
1267
|
|
|
1262
|
-
|
|
1268
|
+
important = ls_decls.first.important
|
|
1263
1269
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1270
|
+
# Build list-style shorthand value
|
|
1271
|
+
parts = []
|
|
1272
|
+
parts << prop_map[PROP_LIST_STYLE_TYPE].value if prop_map[PROP_LIST_STYLE_TYPE]
|
|
1273
|
+
parts << prop_map[PROP_LIST_STYLE_POSITION].value if prop_map[PROP_LIST_STYLE_POSITION]
|
|
1274
|
+
parts << prop_map[PROP_LIST_STYLE_IMAGE].value if prop_map[PROP_LIST_STYLE_IMAGE]
|
|
1266
1275
|
|
|
1267
|
-
|
|
1268
|
-
#
|
|
1269
|
-
# After flattening/cascade, rules that were in the same selector list may have
|
|
1270
|
-
# different declarations. This method builds the selector_lists hash with only
|
|
1271
|
-
# rules that still match, and clears selector_list_id for diverged rules.
|
|
1272
|
-
#
|
|
1273
|
-
# @param merged_rules [Array<Rule>] Flattened rules (with new IDs assigned)
|
|
1274
|
-
# @param selector_lists [Hash] Empty hash to populate with list_id => Array of rule IDs
|
|
1275
|
-
def self.update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
1276
|
-
# Group merged rules by selector_list_id (skip rules with no list)
|
|
1277
|
-
# Note: Using manual each loop instead of .select{}.group_by for performance.
|
|
1278
|
-
# The more concise form (.select + .group_by) is ~50-60% slower due to intermediate array allocation.
|
|
1279
|
-
rules_by_list = {}
|
|
1280
|
-
merged_rules.each do |r|
|
|
1281
|
-
next unless r.selector_list_id
|
|
1282
|
-
|
|
1283
|
-
(rules_by_list[r.selector_list_id] ||= []) << r
|
|
1284
|
-
end
|
|
1276
|
+
shorthand_value = parts.join(' ')
|
|
1285
1277
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
# Skip if only one rule in list (nothing to compare)
|
|
1289
|
-
next if rules_in_list.size <= 1
|
|
1278
|
+
replace_with_shorthand!(rule, LIST_STYLE_PROPERTIES, PROP_LIST_STYLE, shorthand_value, important)
|
|
1279
|
+
end
|
|
1290
1280
|
|
|
1291
|
-
#
|
|
1292
|
-
|
|
1293
|
-
|
|
1281
|
+
# Update selector lists to remove diverged rules
|
|
1282
|
+
#
|
|
1283
|
+
# After flattening/cascade, rules that were in the same selector list may have
|
|
1284
|
+
# different declarations. This method builds the selector_lists hash with only
|
|
1285
|
+
# rules that still match, and clears selector_list_id for diverged rules.
|
|
1286
|
+
#
|
|
1287
|
+
# @param merged_rules [Array<Rule>] Flattened rules (with new IDs assigned)
|
|
1288
|
+
# @param selector_lists [Hash] Empty hash to populate with list_id => Array of rule IDs
|
|
1289
|
+
def update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
1290
|
+
# Group merged rules by selector_list_id (skip rules with no list)
|
|
1291
|
+
# Note: Using manual each loop instead of .select{}.group_by for performance.
|
|
1292
|
+
# The more concise form (.select + .group_by) is ~50-60% slower due to intermediate array allocation.
|
|
1293
|
+
rules_by_list = {}
|
|
1294
|
+
merged_rules.each do |r|
|
|
1295
|
+
next unless r.selector_list_id
|
|
1296
|
+
|
|
1297
|
+
(rules_by_list[r.selector_list_id] ||= []) << r
|
|
1298
|
+
end
|
|
1294
1299
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1300
|
+
# For each selector list, check if declarations still match
|
|
1301
|
+
rules_by_list.each do |list_id, rules_in_list|
|
|
1302
|
+
# Skip if only one rule in list (nothing to compare)
|
|
1303
|
+
next if rules_in_list.size <= 1
|
|
1304
|
+
|
|
1305
|
+
# Get first rule as reference
|
|
1306
|
+
reference_rule = rules_in_list.first
|
|
1307
|
+
reference_decls = reference_rule.declarations
|
|
1308
|
+
|
|
1309
|
+
# Find rules that still match (have identical declarations)
|
|
1310
|
+
matching_rules = [reference_rule]
|
|
1311
|
+
|
|
1312
|
+
rules_in_list[1..].each do |rule|
|
|
1313
|
+
if declarations_equal?(reference_decls, rule.declarations)
|
|
1314
|
+
matching_rules << rule
|
|
1315
|
+
else
|
|
1316
|
+
# Clear selector_list_id for diverged rule
|
|
1317
|
+
rule.selector_list_id = nil
|
|
1318
|
+
end
|
|
1319
|
+
end
|
|
1297
1320
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1321
|
+
# Only keep the selector list if at least 2 rules still match
|
|
1322
|
+
if matching_rules.size >= 2
|
|
1323
|
+
# Build selector_lists hash with NEW rule IDs
|
|
1324
|
+
selector_lists[list_id] = matching_rules.map(&:id)
|
|
1325
|
+
else
|
|
1326
|
+
# Clear selector_list_id for the last remaining rule too
|
|
1327
|
+
matching_rules.each { |r| r.selector_list_id = nil }
|
|
1328
|
+
end
|
|
1304
1329
|
end
|
|
1305
1330
|
end
|
|
1306
1331
|
|
|
1307
|
-
#
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1332
|
+
# Check if two declaration arrays are identical
|
|
1333
|
+
#
|
|
1334
|
+
# @param decls1 [Array<Declaration>]
|
|
1335
|
+
# @param decls2 [Array<Declaration>]
|
|
1336
|
+
# @return [Boolean]
|
|
1337
|
+
def declarations_equal?(decls1, decls2)
|
|
1338
|
+
return false if decls1.size != decls2.size
|
|
1339
|
+
|
|
1340
|
+
# Compare each declaration (property, value, important must all match)
|
|
1341
|
+
decls1.zip(decls2).all? do |d1, d2|
|
|
1342
|
+
d1.property == d2.property &&
|
|
1343
|
+
d1.value == d2.value &&
|
|
1344
|
+
d1.important == d2.important
|
|
1345
|
+
end
|
|
1314
1346
|
end
|
|
1315
|
-
end
|
|
1316
|
-
end
|
|
1317
1347
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1348
|
+
# Mark all methods except flatten and expand_shorthand as private
|
|
1349
|
+
private :flatten_rules_for_selector, :calculate_specificity,
|
|
1350
|
+
:expand_margin, :expand_padding, :parse_four_sides, :split_on_whitespace,
|
|
1351
|
+
:expand_border, :expand_border_side, :expand_border_width, :expand_border_style,
|
|
1352
|
+
:expand_border_color, :parse_border_value, :contains_digit?, :is_border_width?,
|
|
1353
|
+
:is_border_style?,
|
|
1354
|
+
:expand_font, :is_font_size?, :is_font_style?, :is_font_variant?, :is_font_weight?,
|
|
1355
|
+
:expand_background, :starts_with_url?, :is_position_value?, :expand_list_style,
|
|
1356
|
+
:replace_with_shorthand!, :recreate_shorthands!, :recreate_margin!, :recreate_padding!,
|
|
1357
|
+
:check_all_same?,
|
|
1358
|
+
:recreate_border!, :recreate_border_width!, :recreate_border_style!, :recreate_border_color!,
|
|
1359
|
+
:optimize_four_sides, :recreate_font!, :recreate_background!, :recreate_list_style!,
|
|
1360
|
+
:update_selector_lists_for_divergence!, :declarations_equal?,
|
|
1361
|
+
:partition_at_rules, :expand_all_shorthands!, :merge_rules_by_selector_and_media,
|
|
1362
|
+
:build_selector_lists, :rebuild_media_index, :build_result
|
|
1331
1363
|
end
|
|
1332
|
-
end
|
|
1333
1364
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
:is_border_style?,
|
|
1340
|
-
:expand_font, :is_font_size?, :is_font_style?, :is_font_variant?, :is_font_weight?,
|
|
1341
|
-
:expand_background, :starts_with_url?, :is_position_value?, :expand_list_style,
|
|
1342
|
-
:replace_with_shorthand!, :recreate_shorthands!, :recreate_margin!, :recreate_padding!,
|
|
1343
|
-
:check_all_same?,
|
|
1344
|
-
:recreate_border!, :recreate_border_width!, :recreate_border_style!, :recreate_border_color!,
|
|
1345
|
-
:optimize_four_sides, :recreate_font!, :recreate_background!, :recreate_list_style!,
|
|
1346
|
-
:update_selector_lists_for_divergence!, :declarations_equal?,
|
|
1347
|
-
:partition_at_rules, :expand_all_shorthands!, :merge_rules_by_selector_and_media,
|
|
1348
|
-
:build_selector_lists, :rebuild_media_index, :build_result
|
|
1365
|
+
# A frozen singleton, same rationale as Pure itself (see pure.rb) - none of
|
|
1366
|
+
# FlattenImpl's methods touch instance state, so one shared instance is as
|
|
1367
|
+
# safe as the bare module this replaces.
|
|
1368
|
+
Flatten = FlattenImpl.new.freeze
|
|
1369
|
+
end
|
|
1349
1370
|
end
|
|
1350
1371
|
end
|