cataract 0.3.0 → 0.4.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/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +9 -0
- data/ext/cataract/cataract.c +72 -103
- data/ext/cataract/cataract.h +0 -1
- data/ext/cataract/css_parser.c +0 -18
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1197 -1182
- data/lib/cataract/pure/parser.rb +1725 -1729
- data/lib/cataract/pure/serializer.rb +575 -715
- data/lib/cataract/pure/specificity.rb +165 -144
- data/lib/cataract/pure.rb +73 -101
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +19 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +3 -2
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -10,1341 +10,1356 @@
|
|
|
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
|
+
# Create merged rule
|
|
442
|
+
Rule.new(
|
|
443
|
+
0, # ID will be updated later
|
|
444
|
+
selector,
|
|
445
|
+
declarations,
|
|
446
|
+
rules.first.specificity, # Use first rule's specificity
|
|
447
|
+
nil, # No parent after flattening
|
|
448
|
+
nil, # No nesting style after flattening
|
|
449
|
+
selector_list_id, # Preserve if all rules share same ID
|
|
450
|
+
media_query_id # Preserve media context
|
|
451
|
+
)
|
|
452
|
+
end
|
|
361
453
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
454
|
+
# Calculate specificity for a selector
|
|
455
|
+
#
|
|
456
|
+
# @param selector [String] CSS selector
|
|
457
|
+
# @return [Integer] Specificity value
|
|
458
|
+
def calculate_specificity(selector)
|
|
459
|
+
Pure.calculate_specificity(selector)
|
|
460
|
+
end
|
|
365
461
|
|
|
366
|
-
|
|
367
|
-
|
|
462
|
+
# Expand a single shorthand declaration into longhand declarations.
|
|
463
|
+
# Returns an array of longhand declarations. If the declaration is not a shorthand,
|
|
464
|
+
# returns an array with just that declaration.
|
|
465
|
+
#
|
|
466
|
+
# @param decl [Declaration] Declaration to expand
|
|
467
|
+
# @return [Array<Declaration>] Array of expanded longhand declarations
|
|
468
|
+
# @api private
|
|
469
|
+
def expand_shorthand(decl)
|
|
470
|
+
case decl.property
|
|
471
|
+
when 'margin'
|
|
472
|
+
expand_margin(decl)
|
|
473
|
+
when 'padding'
|
|
474
|
+
expand_padding(decl)
|
|
475
|
+
when 'border'
|
|
476
|
+
expand_border(decl)
|
|
477
|
+
when 'border-top', 'border-right', 'border-bottom', 'border-left'
|
|
478
|
+
expand_border_side(decl)
|
|
479
|
+
when 'border-width'
|
|
480
|
+
expand_border_width(decl)
|
|
481
|
+
when 'border-style'
|
|
482
|
+
expand_border_style(decl)
|
|
483
|
+
when 'border-color'
|
|
484
|
+
expand_border_color(decl)
|
|
485
|
+
when 'font'
|
|
486
|
+
expand_font(decl)
|
|
487
|
+
when 'background'
|
|
488
|
+
expand_background(decl)
|
|
489
|
+
when 'list-style'
|
|
490
|
+
expand_list_style(decl)
|
|
491
|
+
else
|
|
492
|
+
# Not a shorthand, return as-is in an array
|
|
493
|
+
[decl]
|
|
494
|
+
end
|
|
495
|
+
end
|
|
368
496
|
|
|
369
|
-
|
|
497
|
+
# Expand margin shorthand
|
|
498
|
+
def expand_margin(decl)
|
|
499
|
+
sides = parse_four_sides(decl.value)
|
|
500
|
+
[
|
|
501
|
+
Declaration.new(PROP_MARGIN_TOP, sides[0], decl.important),
|
|
502
|
+
Declaration.new(PROP_MARGIN_RIGHT, sides[1], decl.important),
|
|
503
|
+
Declaration.new(PROP_MARGIN_BOTTOM, sides[2], decl.important),
|
|
504
|
+
Declaration.new(PROP_MARGIN_LEFT, sides[3], decl.important)
|
|
505
|
+
]
|
|
506
|
+
end
|
|
370
507
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
508
|
+
# Expand padding shorthand
|
|
509
|
+
def expand_padding(decl)
|
|
510
|
+
sides = parse_four_sides(decl.value)
|
|
511
|
+
[
|
|
512
|
+
Declaration.new(PROP_PADDING_TOP, sides[0], decl.important),
|
|
513
|
+
Declaration.new(PROP_PADDING_RIGHT, sides[1], decl.important),
|
|
514
|
+
Declaration.new(PROP_PADDING_BOTTOM, sides[2], decl.important),
|
|
515
|
+
Declaration.new(PROP_PADDING_LEFT, sides[3], decl.important)
|
|
516
|
+
]
|
|
517
|
+
end
|
|
375
518
|
|
|
376
|
-
|
|
377
|
-
|
|
519
|
+
# Parse four-sided value (margin/padding)
|
|
520
|
+
# "10px" -> ["10px", "10px", "10px", "10px"]
|
|
521
|
+
# "10px 20px" -> ["10px", "20px", "10px", "20px"]
|
|
522
|
+
# "10px 20px 30px" -> ["10px", "20px", "30px", "20px"]
|
|
523
|
+
# "10px 20px 30px 40px" -> ["10px", "20px", "30px", "40px"]
|
|
524
|
+
def parse_four_sides(value)
|
|
525
|
+
parts = split_on_whitespace(value)
|
|
526
|
+
|
|
527
|
+
case parts.length
|
|
528
|
+
when 1
|
|
529
|
+
[parts[0], parts[0], parts[0], parts[0]]
|
|
530
|
+
when 2
|
|
531
|
+
[parts[0], parts[1], parts[0], parts[1]]
|
|
532
|
+
when 3
|
|
533
|
+
[parts[0], parts[1], parts[2], parts[1]]
|
|
378
534
|
else
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
535
|
+
[parts[0], parts[1], parts[2], parts[3]]
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Split value on whitespace (handling calc() and other functions)
|
|
540
|
+
def split_on_whitespace(value)
|
|
541
|
+
parts = []
|
|
542
|
+
current = String.new
|
|
543
|
+
paren_depth = 0
|
|
544
|
+
|
|
545
|
+
i = 0
|
|
546
|
+
len = value.bytesize
|
|
547
|
+
while i < len
|
|
548
|
+
byte = value.getbyte(i)
|
|
549
|
+
|
|
550
|
+
if byte == BYTE_LPAREN
|
|
551
|
+
paren_depth += 1
|
|
552
|
+
current << byte
|
|
553
|
+
elsif byte == BYTE_RPAREN
|
|
554
|
+
paren_depth -= 1
|
|
555
|
+
current << byte
|
|
556
|
+
elsif byte == BYTE_SPACE && paren_depth == 0
|
|
557
|
+
parts << current unless current.empty?
|
|
558
|
+
current = String.new
|
|
396
559
|
else
|
|
397
|
-
|
|
398
|
-
should_replace = source_order > existing_order
|
|
560
|
+
current << byte
|
|
399
561
|
end
|
|
400
562
|
|
|
401
|
-
|
|
402
|
-
decl_map[prop] = [source_order, spec, decl.important, decl.value]
|
|
403
|
-
end
|
|
563
|
+
i += 1
|
|
404
564
|
end
|
|
565
|
+
|
|
566
|
+
parts << current unless current.empty?
|
|
567
|
+
parts
|
|
405
568
|
end
|
|
406
|
-
end
|
|
407
569
|
|
|
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
|
|
570
|
+
# Expand border shorthand (e.g., "1px solid red")
|
|
571
|
+
def expand_border(decl)
|
|
572
|
+
# Parse border value
|
|
573
|
+
width, style, color = parse_border_value(decl.value)
|
|
422
574
|
|
|
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
|
|
575
|
+
result = []
|
|
445
576
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
577
|
+
# Expand to all sides using property constants
|
|
578
|
+
if width
|
|
579
|
+
result << Declaration.new(PROP_BORDER_TOP_WIDTH, width, decl.important)
|
|
580
|
+
result << Declaration.new(PROP_BORDER_RIGHT_WIDTH, width, decl.important)
|
|
581
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_WIDTH, width, decl.important)
|
|
582
|
+
result << Declaration.new(PROP_BORDER_LEFT_WIDTH, width, decl.important)
|
|
583
|
+
end
|
|
453
584
|
|
|
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
|
|
585
|
+
if style
|
|
586
|
+
result << Declaration.new(PROP_BORDER_TOP_STYLE, style, decl.important)
|
|
587
|
+
result << Declaration.new(PROP_BORDER_RIGHT_STYLE, style, decl.important)
|
|
588
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_STYLE, style, decl.important)
|
|
589
|
+
result << Declaration.new(PROP_BORDER_LEFT_STYLE, style, decl.important)
|
|
590
|
+
end
|
|
488
591
|
|
|
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
|
|
592
|
+
if color
|
|
593
|
+
result << Declaration.new(PROP_BORDER_TOP_COLOR, color, decl.important)
|
|
594
|
+
result << Declaration.new(PROP_BORDER_RIGHT_COLOR, color, decl.important)
|
|
595
|
+
result << Declaration.new(PROP_BORDER_BOTTOM_COLOR, color, decl.important)
|
|
596
|
+
result << Declaration.new(PROP_BORDER_LEFT_COLOR, color, decl.important)
|
|
597
|
+
end
|
|
499
598
|
|
|
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
|
|
599
|
+
result
|
|
600
|
+
end
|
|
510
601
|
|
|
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
|
|
602
|
+
# Expand border-side shorthand (e.g., "border-top: 1px solid red")
|
|
603
|
+
def expand_border_side(decl)
|
|
604
|
+
# Extract side from property name (e.g., "border-top" -> "top")
|
|
605
|
+
side = decl.property.byteslice(7..-1) # Skip "border-" prefix
|
|
606
|
+
width, style, color = parse_border_value(decl.value)
|
|
607
|
+
width_prop, style_prop, color_prop = BORDER_SIDE_PROPS[side]
|
|
530
608
|
|
|
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
|
|
609
|
+
result = []
|
|
610
|
+
result << Declaration.new(width_prop, width, decl.important) if width
|
|
611
|
+
result << Declaration.new(style_prop, style, decl.important) if style
|
|
612
|
+
result << Declaration.new(color_prop, color, decl.important) if color
|
|
613
|
+
|
|
614
|
+
result
|
|
553
615
|
end
|
|
554
616
|
|
|
555
|
-
|
|
556
|
-
|
|
617
|
+
# Expand border-width shorthand
|
|
618
|
+
def expand_border_width(decl)
|
|
619
|
+
sides = parse_four_sides(decl.value)
|
|
620
|
+
[
|
|
621
|
+
Declaration.new(PROP_BORDER_TOP_WIDTH, sides[0], decl.important),
|
|
622
|
+
Declaration.new(PROP_BORDER_RIGHT_WIDTH, sides[1], decl.important),
|
|
623
|
+
Declaration.new(PROP_BORDER_BOTTOM_WIDTH, sides[2], decl.important),
|
|
624
|
+
Declaration.new(PROP_BORDER_LEFT_WIDTH, sides[3], decl.important)
|
|
625
|
+
]
|
|
626
|
+
end
|
|
557
627
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
628
|
+
# Expand border-style shorthand
|
|
629
|
+
def expand_border_style(decl)
|
|
630
|
+
sides = parse_four_sides(decl.value)
|
|
631
|
+
[
|
|
632
|
+
Declaration.new(PROP_BORDER_TOP_STYLE, sides[0], decl.important),
|
|
633
|
+
Declaration.new(PROP_BORDER_RIGHT_STYLE, sides[1], decl.important),
|
|
634
|
+
Declaration.new(PROP_BORDER_BOTTOM_STYLE, sides[2], decl.important),
|
|
635
|
+
Declaration.new(PROP_BORDER_LEFT_STYLE, sides[3], decl.important)
|
|
636
|
+
]
|
|
637
|
+
end
|
|
561
638
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
639
|
+
# Expand border-color shorthand
|
|
640
|
+
def expand_border_color(decl)
|
|
641
|
+
sides = parse_four_sides(decl.value)
|
|
642
|
+
[
|
|
643
|
+
Declaration.new(PROP_BORDER_TOP_COLOR, sides[0], decl.important),
|
|
644
|
+
Declaration.new(PROP_BORDER_RIGHT_COLOR, sides[1], decl.important),
|
|
645
|
+
Declaration.new(PROP_BORDER_BOTTOM_COLOR, sides[2], decl.important),
|
|
646
|
+
Declaration.new(PROP_BORDER_LEFT_COLOR, sides[3], decl.important)
|
|
647
|
+
]
|
|
648
|
+
end
|
|
566
649
|
|
|
567
|
-
|
|
650
|
+
# Parse border value (e.g., "1px solid red" -> ["1px", "solid", "red"])
|
|
651
|
+
def parse_border_value(value)
|
|
652
|
+
parts = split_on_whitespace(value)
|
|
653
|
+
width = nil
|
|
654
|
+
style = nil
|
|
655
|
+
color = nil
|
|
656
|
+
|
|
657
|
+
# Identify each part by type
|
|
658
|
+
parts.each do |part|
|
|
659
|
+
if is_border_width?(part)
|
|
660
|
+
width = part
|
|
661
|
+
elsif is_border_style?(part)
|
|
662
|
+
style = part
|
|
663
|
+
else
|
|
664
|
+
color = part # Assume color if not width or style
|
|
665
|
+
end
|
|
666
|
+
end
|
|
568
667
|
|
|
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
|
|
668
|
+
[width, style, color]
|
|
669
|
+
end
|
|
576
670
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
671
|
+
# Check if value contains a digit (byte-by-byte)
|
|
672
|
+
def contains_digit?(value)
|
|
673
|
+
i = 0
|
|
674
|
+
len = value.bytesize
|
|
675
|
+
while i < len
|
|
676
|
+
byte = value.getbyte(i)
|
|
677
|
+
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
583
678
|
|
|
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
|
|
679
|
+
i += 1
|
|
680
|
+
end
|
|
590
681
|
|
|
591
|
-
|
|
592
|
-
|
|
682
|
+
false
|
|
683
|
+
end
|
|
593
684
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
width, style, color = parse_border_value(decl.value)
|
|
599
|
-
width_prop, style_prop, color_prop = BORDER_SIDE_PROPS[side]
|
|
685
|
+
# Check if value looks like a border width
|
|
686
|
+
def is_border_width?(value)
|
|
687
|
+
# Check for numeric values or width keywords
|
|
688
|
+
return true if BORDER_WIDTH_KEYWORDS.include?(value)
|
|
600
689
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
result << Declaration.new(style_prop, style, decl.important) if style
|
|
604
|
-
result << Declaration.new(color_prop, color, decl.important) if color
|
|
690
|
+
contains_digit?(value)
|
|
691
|
+
end
|
|
605
692
|
|
|
606
|
-
|
|
607
|
-
|
|
693
|
+
# Check if value is a border style
|
|
694
|
+
def is_border_style?(value)
|
|
695
|
+
BORDER_STYLE_KEYWORDS.include?(value)
|
|
696
|
+
end
|
|
608
697
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
698
|
+
# Expand font shorthand
|
|
699
|
+
# Format: [style] [variant] [weight] size[/line-height] family
|
|
700
|
+
def expand_font(decl)
|
|
701
|
+
value = decl.value
|
|
702
|
+
parts = split_on_whitespace(value)
|
|
703
|
+
|
|
704
|
+
# Need at least 2 parts (size and family)
|
|
705
|
+
return [decl] if parts.length < 2
|
|
706
|
+
|
|
707
|
+
# Parse from left to right
|
|
708
|
+
# Optional: style, variant, weight (can appear in any order)
|
|
709
|
+
# Required: size (with optional /line-height), family
|
|
710
|
+
|
|
711
|
+
i = 0
|
|
712
|
+
style = nil
|
|
713
|
+
variant = nil
|
|
714
|
+
weight = nil
|
|
715
|
+
size = nil
|
|
716
|
+
line_height = nil
|
|
717
|
+
family_parts = []
|
|
718
|
+
|
|
719
|
+
# Process optional style/variant/weight
|
|
720
|
+
while i < parts.length - 1 # Leave at least 1 for family
|
|
721
|
+
part = parts[i]
|
|
722
|
+
|
|
723
|
+
# Check if this could be size (has digit or is a size keyword)
|
|
724
|
+
if is_font_size?(part)
|
|
725
|
+
# This is the size, rest is family
|
|
726
|
+
size_part = part
|
|
727
|
+
|
|
728
|
+
# Check for line-height (find '/' byte)
|
|
729
|
+
slash_idx = nil
|
|
730
|
+
j = 0
|
|
731
|
+
len = size_part.bytesize
|
|
732
|
+
while j < len
|
|
733
|
+
if size_part.getbyte(j) == BYTE_SLASH
|
|
734
|
+
slash_idx = j
|
|
735
|
+
break
|
|
736
|
+
end
|
|
737
|
+
j += 1
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
if slash_idx
|
|
741
|
+
size = size_part.byteslice(0, slash_idx)
|
|
742
|
+
line_height = size_part.byteslice((slash_idx + 1)..-1)
|
|
743
|
+
else
|
|
744
|
+
size = size_part
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Rest is family
|
|
748
|
+
family_parts = parts[(i + 1)..-1]
|
|
749
|
+
break
|
|
750
|
+
elsif is_font_style?(part)
|
|
751
|
+
style = part
|
|
752
|
+
elsif is_font_variant?(part)
|
|
753
|
+
variant = part
|
|
754
|
+
elsif is_font_weight?(part)
|
|
755
|
+
weight = part
|
|
756
|
+
else
|
|
757
|
+
# Unknown, might be start of family - treat everything from here as family
|
|
758
|
+
family_parts = parts[i..-1]
|
|
759
|
+
break
|
|
760
|
+
end
|
|
619
761
|
|
|
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
|
|
762
|
+
i += 1
|
|
763
|
+
end
|
|
630
764
|
|
|
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
|
|
765
|
+
family = family_parts.join(' ')
|
|
641
766
|
|
|
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
|
|
767
|
+
# Font shorthand sets ALL longhand properties
|
|
768
|
+
# Unspecified values get CSS initial values
|
|
769
|
+
# Size and family are required; if missing, return unexpanded
|
|
770
|
+
return [decl] if !size || family.empty?
|
|
659
771
|
|
|
660
|
-
|
|
661
|
-
|
|
772
|
+
result = []
|
|
773
|
+
result << Declaration.new(PROP_FONT_STYLE, style || 'normal', decl.important)
|
|
774
|
+
result << Declaration.new(PROP_FONT_VARIANT, variant || 'normal', decl.important)
|
|
775
|
+
result << Declaration.new(PROP_FONT_WEIGHT, weight || 'normal', decl.important)
|
|
776
|
+
result << Declaration.new(PROP_FONT_SIZE, size, decl.important)
|
|
777
|
+
result << Declaration.new(PROP_LINE_HEIGHT, line_height || 'normal', decl.important)
|
|
778
|
+
result << Declaration.new(PROP_FONT_FAMILY, family, decl.important)
|
|
662
779
|
|
|
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
|
|
780
|
+
result
|
|
781
|
+
end
|
|
670
782
|
|
|
671
|
-
|
|
672
|
-
|
|
783
|
+
# Check if value is a font size
|
|
784
|
+
def is_font_size?(value)
|
|
785
|
+
# Has digit or is a keyword
|
|
786
|
+
return true if contains_digit?(value)
|
|
673
787
|
|
|
674
|
-
|
|
675
|
-
|
|
788
|
+
FONT_SIZE_KEYWORDS.include?(value)
|
|
789
|
+
end
|
|
676
790
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
791
|
+
# Check if value is a font style
|
|
792
|
+
def is_font_style?(value)
|
|
793
|
+
FONT_STYLE_KEYWORDS.include?(value)
|
|
794
|
+
end
|
|
681
795
|
|
|
682
|
-
|
|
683
|
-
|
|
796
|
+
# Check if value is a font variant
|
|
797
|
+
def is_font_variant?(value)
|
|
798
|
+
FONT_VARIANT_KEYWORDS.include?(value)
|
|
799
|
+
end
|
|
684
800
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
801
|
+
# Check if value is a font weight
|
|
802
|
+
def is_font_weight?(value)
|
|
803
|
+
# Check for numeric weights like 400, 700
|
|
804
|
+
return true if contains_digit?(value)
|
|
689
805
|
|
|
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
|
|
806
|
+
FONT_WEIGHT_KEYWORDS.include?(value)
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Expand background shorthand
|
|
810
|
+
# Format: [color] [image] [repeat] [attachment] [position]
|
|
811
|
+
def expand_background(decl)
|
|
812
|
+
value = decl.value
|
|
813
|
+
parts = split_on_whitespace(value)
|
|
814
|
+
|
|
815
|
+
return [decl] if parts.empty?
|
|
816
|
+
|
|
817
|
+
# Parse background components (simple heuristic)
|
|
818
|
+
# According to CSS spec, background shorthand sets ALL properties
|
|
819
|
+
# Any unspecified values get their initial values
|
|
820
|
+
color = nil
|
|
821
|
+
image = nil
|
|
822
|
+
repeat = nil
|
|
823
|
+
attachment = nil
|
|
824
|
+
position = nil
|
|
825
|
+
|
|
826
|
+
parts.each do |part|
|
|
827
|
+
if starts_with_url?(part) || part == 'none'
|
|
828
|
+
image = part
|
|
829
|
+
elsif BACKGROUND_REPEAT_KEYWORDS.include?(part)
|
|
830
|
+
repeat = part
|
|
831
|
+
elsif BACKGROUND_ATTACHMENT_KEYWORDS.include?(part)
|
|
832
|
+
attachment = part
|
|
833
|
+
elsif is_position_value?(part)
|
|
834
|
+
position ||= String.new
|
|
835
|
+
position << ' ' unless position.empty?
|
|
836
|
+
position << part
|
|
837
|
+
else
|
|
838
|
+
# Assume it's a color
|
|
839
|
+
color = part
|
|
728
840
|
end
|
|
729
|
-
j += 1
|
|
730
841
|
end
|
|
731
842
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
843
|
+
# Background shorthand sets ALL longhand properties
|
|
844
|
+
# Unspecified values get CSS initial values
|
|
845
|
+
result = []
|
|
846
|
+
result << Declaration.new(PROP_BACKGROUND_COLOR, color || 'transparent', decl.important)
|
|
847
|
+
result << Declaration.new(PROP_BACKGROUND_IMAGE, image || 'none', decl.important)
|
|
848
|
+
result << Declaration.new(PROP_BACKGROUND_REPEAT, repeat || 'repeat', decl.important)
|
|
849
|
+
result << Declaration.new(PROP_BACKGROUND_ATTACHMENT, attachment || 'scroll', decl.important)
|
|
850
|
+
result << Declaration.new(PROP_BACKGROUND_POSITION, position || '0% 0%', decl.important)
|
|
738
851
|
|
|
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
|
|
852
|
+
result
|
|
752
853
|
end
|
|
753
854
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
family = family_parts.join(' ')
|
|
855
|
+
# Check if value starts with 'url('
|
|
856
|
+
def starts_with_url?(value)
|
|
857
|
+
return false if value.bytesize < 4
|
|
758
858
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
859
|
+
value.getbyte(0) == BYTE_LOWER_U &&
|
|
860
|
+
value.getbyte(1) == BYTE_LOWER_R &&
|
|
861
|
+
value.getbyte(2) == BYTE_LOWER_L &&
|
|
862
|
+
value.getbyte(3) == BYTE_LPAREN
|
|
863
|
+
end
|
|
763
864
|
|
|
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)
|
|
865
|
+
# Check if value is a position value (for background-position)
|
|
866
|
+
def is_position_value?(value)
|
|
867
|
+
return true if BACKGROUND_POSITION_KEYWORDS.include?(value)
|
|
868
|
+
return true if contains_digit?(value)
|
|
771
869
|
|
|
772
|
-
|
|
773
|
-
|
|
870
|
+
# Check for '%'
|
|
871
|
+
i = 0
|
|
872
|
+
len = value.bytesize
|
|
873
|
+
while i < len
|
|
874
|
+
return true if value.getbyte(i) == BYTE_PERCENT
|
|
774
875
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
876
|
+
i += 1
|
|
877
|
+
end
|
|
878
|
+
false
|
|
879
|
+
end
|
|
779
880
|
|
|
780
|
-
|
|
781
|
-
|
|
881
|
+
# Expand list-style shorthand
|
|
882
|
+
# Format: [type] [position] [image]
|
|
883
|
+
def expand_list_style(decl)
|
|
884
|
+
value = decl.value
|
|
885
|
+
parts = split_on_whitespace(value)
|
|
782
886
|
|
|
783
|
-
|
|
784
|
-
def self.is_font_style?(value)
|
|
785
|
-
FONT_STYLE_KEYWORDS.include?(value)
|
|
786
|
-
end
|
|
887
|
+
return [decl] if parts.empty?
|
|
787
888
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
889
|
+
result = []
|
|
890
|
+
type = nil
|
|
891
|
+
position = nil
|
|
892
|
+
image = nil
|
|
792
893
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
894
|
+
parts.each do |part|
|
|
895
|
+
if starts_with_url?(part) || part == 'none'
|
|
896
|
+
image = part
|
|
897
|
+
elsif LIST_STYLE_POSITION_KEYWORDS.include?(part)
|
|
898
|
+
position = part
|
|
899
|
+
else
|
|
900
|
+
# Assume it's a type (disc, circle, square, etc.)
|
|
901
|
+
type = part
|
|
902
|
+
end
|
|
903
|
+
end
|
|
797
904
|
|
|
798
|
-
|
|
799
|
-
|
|
905
|
+
result << Declaration.new(PROP_LIST_STYLE_TYPE, type, decl.important) if type
|
|
906
|
+
result << Declaration.new(PROP_LIST_STYLE_POSITION, position, decl.important) if position
|
|
907
|
+
result << Declaration.new(PROP_LIST_STYLE_IMAGE, image, decl.important) if image
|
|
800
908
|
|
|
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
|
|
909
|
+
result.empty? ? [decl] : result
|
|
832
910
|
end
|
|
833
|
-
end
|
|
834
911
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
result << Declaration.new(PROP_BACKGROUND_POSITION, position || '0% 0%', decl.important)
|
|
912
|
+
# Remove the individual longhand declarations a shorthand replaces and
|
|
913
|
+
# append the shorthand declaration in their place.
|
|
914
|
+
# Note: We append rather than insert at original position to match C implementation behavior
|
|
915
|
+
def replace_with_shorthand!(rule, longhand_props, shorthand_prop, value, important)
|
|
916
|
+
rule.declarations.reject! { |d| longhand_props.include?(d.property) }
|
|
917
|
+
rule.declarations << Declaration.new(shorthand_prop, value, important)
|
|
918
|
+
end
|
|
843
919
|
|
|
844
|
-
|
|
845
|
-
|
|
920
|
+
# Recreate shorthand properties where possible (mutates declarations)
|
|
921
|
+
#
|
|
922
|
+
# @param rule [Rule] Rule to recreate shorthands in
|
|
923
|
+
def recreate_shorthands!(rule)
|
|
924
|
+
# Build property map
|
|
925
|
+
prop_map = {}
|
|
926
|
+
rule.declarations.each { |d| prop_map[d.property] = d }
|
|
846
927
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return false if value.bytesize < 4
|
|
928
|
+
# Try to recreate margin
|
|
929
|
+
recreate_margin!(rule, prop_map)
|
|
850
930
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
value.getbyte(2) == BYTE_LOWER_L &&
|
|
854
|
-
value.getbyte(3) == BYTE_LPAREN
|
|
855
|
-
end
|
|
931
|
+
# Try to recreate padding
|
|
932
|
+
recreate_padding!(rule, prop_map)
|
|
856
933
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return true if BACKGROUND_POSITION_KEYWORDS.include?(value)
|
|
860
|
-
return true if contains_digit?(value)
|
|
934
|
+
# Try to recreate border
|
|
935
|
+
recreate_border!(rule, prop_map)
|
|
861
936
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
len = value.bytesize
|
|
865
|
-
while i < len
|
|
866
|
-
return true if value.getbyte(i) == BYTE_PERCENT
|
|
937
|
+
# Try to recreate list-style
|
|
938
|
+
recreate_list_style!(rule, prop_map)
|
|
867
939
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
false
|
|
871
|
-
end
|
|
940
|
+
# Try to recreate font
|
|
941
|
+
recreate_font!(rule, prop_map)
|
|
872
942
|
|
|
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
|
|
943
|
+
# Try to recreate background
|
|
944
|
+
recreate_background!(rule, prop_map)
|
|
894
945
|
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
946
|
|
|
901
|
-
|
|
902
|
-
|
|
947
|
+
# Try to recreate margin shorthand
|
|
948
|
+
def recreate_margin!(rule, prop_map)
|
|
949
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
950
|
+
sides = []
|
|
951
|
+
MARGIN_SIDES.each { |s| sides << prop_map[s] }
|
|
952
|
+
return unless sides.all? # Need all four sides
|
|
953
|
+
|
|
954
|
+
# Check if all have same importance
|
|
955
|
+
# Use each + << instead of map
|
|
956
|
+
importances = []
|
|
957
|
+
sides.each { |s| importances << s.important }
|
|
958
|
+
importances.uniq!
|
|
959
|
+
return if importances.length > 1 # Mixed importance, can't create shorthand
|
|
960
|
+
|
|
961
|
+
# Use each + << instead of map
|
|
962
|
+
values = []
|
|
963
|
+
sides.each { |s| values << s.value }
|
|
964
|
+
important = sides.first.important
|
|
965
|
+
|
|
966
|
+
# Create optimized shorthand
|
|
967
|
+
shorthand_value = optimize_four_sides(values)
|
|
968
|
+
|
|
969
|
+
replace_with_shorthand!(rule, MARGIN_SIDES, PROP_MARGIN, shorthand_value, important)
|
|
970
|
+
end
|
|
903
971
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
end
|
|
972
|
+
# Try to recreate padding shorthand
|
|
973
|
+
def recreate_padding!(rule, prop_map)
|
|
974
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
975
|
+
sides = []
|
|
976
|
+
PADDING_SIDES.each { |s| sides << prop_map[s] }
|
|
977
|
+
return unless sides.all?
|
|
911
978
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
prop_map = {}
|
|
918
|
-
rule.declarations.each { |d| prop_map[d.property] = d }
|
|
979
|
+
# Use each + << instead of map
|
|
980
|
+
importances = []
|
|
981
|
+
sides.each { |s| importances << s.important }
|
|
982
|
+
importances.uniq!
|
|
983
|
+
return if importances.length > 1
|
|
919
984
|
|
|
920
|
-
|
|
921
|
-
|
|
985
|
+
# Use each + << instead of map
|
|
986
|
+
values = []
|
|
987
|
+
sides.each { |s| values << s.value }
|
|
988
|
+
important = sides.first.important
|
|
922
989
|
|
|
923
|
-
|
|
924
|
-
recreate_padding!(rule, prop_map)
|
|
990
|
+
shorthand_value = optimize_four_sides(values)
|
|
925
991
|
|
|
926
|
-
|
|
927
|
-
|
|
992
|
+
replace_with_shorthand!(rule, PADDING_SIDES, PROP_PADDING, shorthand_value, important)
|
|
993
|
+
end
|
|
928
994
|
|
|
929
|
-
|
|
930
|
-
|
|
995
|
+
# Helper: Check if all declarations have same value and importance
|
|
996
|
+
# Does single pass instead of multiple .map calls
|
|
997
|
+
def check_all_same?(decls)
|
|
998
|
+
return false if decls.empty?
|
|
931
999
|
|
|
932
|
-
|
|
933
|
-
|
|
1000
|
+
first_val = decls[0].value
|
|
1001
|
+
first_imp = decls[0].important
|
|
934
1002
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1003
|
+
i = 1
|
|
1004
|
+
while i < decls.length
|
|
1005
|
+
return false if decls[i].value != first_val
|
|
1006
|
+
return false if decls[i].important != first_imp
|
|
938
1007
|
|
|
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
|
|
1008
|
+
i += 1
|
|
1009
|
+
end
|
|
963
1010
|
|
|
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?
|
|
1011
|
+
true
|
|
1012
|
+
end
|
|
970
1013
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1014
|
+
# Try to recreate border shorthand
|
|
1015
|
+
def recreate_border!(rule, prop_map)
|
|
1016
|
+
# Check if we have all width/style/color properties with same values for all sides
|
|
1017
|
+
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
1018
|
+
widths = []
|
|
1019
|
+
BORDER_WIDTHS.each { |p| widths << prop_map[p] }
|
|
1020
|
+
styles = []
|
|
1021
|
+
BORDER_STYLES.each { |p| styles << prop_map[p] }
|
|
1022
|
+
colors = []
|
|
1023
|
+
BORDER_COLORS.each { |p| colors << prop_map[p] }
|
|
1024
|
+
|
|
1025
|
+
# Check if all sides have same values and can create full border shorthand
|
|
1026
|
+
# Check cheapest condition first (.all?), then do single pass for values/importance
|
|
1027
|
+
widths_all_same = widths.all? && check_all_same?(widths)
|
|
1028
|
+
styles_all_same = styles.all? && check_all_same?(styles)
|
|
1029
|
+
colors_all_same = colors.all? && check_all_same?(colors)
|
|
1030
|
+
|
|
1031
|
+
# Can create FULL border shorthand ONLY if style is present (style is required for border shorthand)
|
|
1032
|
+
# AND all properties that are present have same values and importance
|
|
1033
|
+
if styles_all_same
|
|
1034
|
+
# Check if we have width and/or color with same importance as style
|
|
1035
|
+
can_create_border = true
|
|
1036
|
+
important = styles.first.important
|
|
1037
|
+
|
|
1038
|
+
# If width is present, must be all-same and same importance
|
|
1039
|
+
if widths.all?
|
|
1040
|
+
can_create_border = false unless widths_all_same && widths.first.important == important
|
|
1041
|
+
end
|
|
976
1042
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1043
|
+
# If color is present, must be all-same and same importance
|
|
1044
|
+
if colors.all?
|
|
1045
|
+
can_create_border = false unless colors_all_same && colors.first.important == important
|
|
1046
|
+
end
|
|
981
1047
|
|
|
982
|
-
|
|
1048
|
+
if can_create_border
|
|
1049
|
+
# Create full border shorthand
|
|
1050
|
+
parts = []
|
|
1051
|
+
parts << widths.first.value if widths_all_same
|
|
1052
|
+
parts << styles.first.value
|
|
1053
|
+
parts << colors.first.value if colors_all_same
|
|
983
1054
|
|
|
984
|
-
|
|
985
|
-
end
|
|
1055
|
+
border_value = parts.join(' ')
|
|
986
1056
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1057
|
+
replace_with_shorthand!(rule, BORDER_ALL, PROP_BORDER, border_value, important)
|
|
1058
|
+
return
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
991
1061
|
|
|
992
|
-
|
|
993
|
-
|
|
1062
|
+
# Try to create border-width/style/color shorthands
|
|
1063
|
+
recreate_border_width!(rule, widths) if widths.all?
|
|
1064
|
+
recreate_border_style!(rule, styles) if styles.all?
|
|
1065
|
+
recreate_border_color!(rule, colors) if colors.all?
|
|
1066
|
+
end
|
|
994
1067
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1068
|
+
# Recreate border-width shorthand
|
|
1069
|
+
def recreate_border_width!(rule, widths)
|
|
1070
|
+
importances = widths.map(&:important).uniq
|
|
1071
|
+
return if importances.length > 1
|
|
999
1072
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1073
|
+
values = widths.map(&:value)
|
|
1074
|
+
important = widths.first.important
|
|
1002
1075
|
|
|
1003
|
-
|
|
1004
|
-
end
|
|
1076
|
+
shorthand_value = optimize_four_sides(values)
|
|
1005
1077
|
|
|
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
|
|
1078
|
+
replace_with_shorthand!(rule, BORDER_WIDTHS, PROP_BORDER_WIDTH, shorthand_value, important)
|
|
1033
1079
|
end
|
|
1034
1080
|
|
|
1035
|
-
#
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1081
|
+
# Recreate border-style shorthand
|
|
1082
|
+
def recreate_border_style!(rule, styles)
|
|
1083
|
+
importances = styles.map(&:important).uniq
|
|
1084
|
+
return if importances.length > 1
|
|
1039
1085
|
|
|
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
|
|
1086
|
+
values = styles.map(&:value)
|
|
1087
|
+
important = styles.first.important
|
|
1046
1088
|
|
|
1047
|
-
|
|
1089
|
+
shorthand_value = optimize_four_sides(values)
|
|
1048
1090
|
|
|
1049
|
-
replace_with_shorthand!(rule,
|
|
1050
|
-
return
|
|
1091
|
+
replace_with_shorthand!(rule, BORDER_STYLES, PROP_BORDER_STYLE, shorthand_value, important)
|
|
1051
1092
|
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
1093
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1094
|
+
# Recreate border-color shorthand
|
|
1095
|
+
def recreate_border_color!(rule, colors)
|
|
1096
|
+
importances = colors.map(&:important).uniq
|
|
1097
|
+
return if importances.length > 1
|
|
1064
1098
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1099
|
+
values = colors.map(&:value)
|
|
1100
|
+
important = colors.first.important
|
|
1067
1101
|
|
|
1068
|
-
|
|
1102
|
+
shorthand_value = optimize_four_sides(values)
|
|
1069
1103
|
|
|
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
|
|
1104
|
+
replace_with_shorthand!(rule, BORDER_COLORS, PROP_BORDER_COLOR, shorthand_value, important)
|
|
1105
|
+
end
|
|
1077
1106
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1107
|
+
# Optimize four-sided value representation
|
|
1108
|
+
# ["10px", "10px", "10px", "10px"] -> "10px"
|
|
1109
|
+
# ["10px", "20px", "10px", "20px"] -> "10px 20px"
|
|
1110
|
+
# ["10px", "20px", "30px", "20px"] -> "10px 20px 30px"
|
|
1111
|
+
# ["10px", "20px", "30px", "40px"] -> "10px 20px 30px 40px"
|
|
1112
|
+
def optimize_four_sides(values)
|
|
1113
|
+
top, right, bottom, left = values
|
|
1114
|
+
|
|
1115
|
+
if top == right && right == bottom && bottom == left
|
|
1116
|
+
top
|
|
1117
|
+
elsif top == bottom && right == left
|
|
1118
|
+
"#{top} #{right}"
|
|
1119
|
+
elsif right == left
|
|
1120
|
+
"#{top} #{right} #{bottom}"
|
|
1121
|
+
else
|
|
1122
|
+
"#{top} #{right} #{bottom} #{left}"
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1080
1125
|
|
|
1081
|
-
|
|
1126
|
+
# Try to recreate font shorthand
|
|
1127
|
+
# Requires: font-size and font-family (minimum)
|
|
1128
|
+
# Optional: font-style, font-variant, font-weight, line-height
|
|
1129
|
+
def recreate_font!(rule, prop_map)
|
|
1130
|
+
size = prop_map[PROP_FONT_SIZE]
|
|
1131
|
+
family = prop_map[PROP_FONT_FAMILY]
|
|
1082
1132
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1133
|
+
# Need at least size and family
|
|
1134
|
+
return unless size && family
|
|
1085
1135
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
return if importances.length > 1
|
|
1136
|
+
# Check if all font properties have same importance
|
|
1137
|
+
font_decls = FONT_PROPERTIES.filter_map { |p| prop_map[p] }
|
|
1138
|
+
return if font_decls.empty?
|
|
1090
1139
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1140
|
+
importances = font_decls.map(&:important).uniq
|
|
1141
|
+
return if importances.length > 1
|
|
1093
1142
|
|
|
1094
|
-
|
|
1143
|
+
important = font_decls.first.important
|
|
1095
1144
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1145
|
+
# Build font shorthand value
|
|
1146
|
+
# Strategy: Only omit defaults if we have ALL 6 properties (from shorthand expansion)
|
|
1147
|
+
# If we have a partial set, include all non-nil values
|
|
1148
|
+
style = prop_map[PROP_FONT_STYLE]&.value
|
|
1149
|
+
variant = prop_map[PROP_FONT_VARIANT]&.value
|
|
1150
|
+
weight = prop_map[PROP_FONT_WEIGHT]&.value
|
|
1151
|
+
line_height = prop_map[PROP_LINE_HEIGHT]&.value
|
|
1098
1152
|
|
|
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
|
|
1153
|
+
all_present = style && variant && weight && line_height
|
|
1154
|
+
parts = []
|
|
1159
1155
|
|
|
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
|
|
1156
|
+
if all_present
|
|
1157
|
+
# All properties present (likely from shorthand expansion) - omit defaults
|
|
1158
|
+
parts << style if style != 'normal'
|
|
1159
|
+
parts << variant if variant != 'normal'
|
|
1160
|
+
parts << weight if weight != 'normal'
|
|
1161
|
+
else
|
|
1162
|
+
# Partial set - include all non-nil values
|
|
1163
|
+
parts << style if style
|
|
1164
|
+
parts << variant if variant
|
|
1165
|
+
parts << weight if weight
|
|
1166
|
+
end
|
|
1176
1167
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1168
|
+
# Required: size[/line-height]
|
|
1169
|
+
if all_present
|
|
1170
|
+
# Omit line-height if default
|
|
1171
|
+
if line_height != 'normal'
|
|
1172
|
+
parts << "#{size.value}/#{line_height}"
|
|
1173
|
+
else
|
|
1174
|
+
parts << size.value
|
|
1175
|
+
end
|
|
1176
|
+
else
|
|
1177
|
+
# Include line-height if present
|
|
1178
|
+
if line_height
|
|
1179
|
+
parts << "#{size.value}/#{line_height}"
|
|
1180
|
+
else
|
|
1181
|
+
parts << size.value
|
|
1182
|
+
end
|
|
1183
|
+
end
|
|
1179
1184
|
|
|
1180
|
-
|
|
1185
|
+
# Required: family
|
|
1186
|
+
parts << family.value
|
|
1181
1187
|
|
|
1182
|
-
|
|
1183
|
-
end
|
|
1188
|
+
shorthand_value = parts.join(' ')
|
|
1184
1189
|
|
|
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
|
|
1190
|
+
replace_with_shorthand!(rule, FONT_PROPERTIES, PROP_FONT, shorthand_value, important)
|
|
1191
|
+
end
|
|
1229
1192
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1193
|
+
# Try to recreate background shorthand
|
|
1194
|
+
# Can combine: background-color, background-image, background-repeat, etc.
|
|
1195
|
+
def recreate_background!(rule, prop_map)
|
|
1196
|
+
bg_props = BACKGROUND_PROPERTIES
|
|
1197
|
+
bg_decls = bg_props.filter_map { |p| prop_map[p] }
|
|
1198
|
+
|
|
1199
|
+
# Need at least 2 properties to create shorthand
|
|
1200
|
+
# Single properties should stay as longhands (e.g., background-color: blue)
|
|
1201
|
+
# because shorthand resets all other properties to initial values
|
|
1202
|
+
return if bg_decls.length < 2
|
|
1203
|
+
|
|
1204
|
+
# Check if all have same importance
|
|
1205
|
+
importances = bg_decls.map(&:important).uniq
|
|
1206
|
+
return if importances.length > 1
|
|
1207
|
+
|
|
1208
|
+
important = bg_decls.first.important
|
|
1209
|
+
|
|
1210
|
+
# Build background shorthand value
|
|
1211
|
+
# Strategy: Only omit defaults if we have ALL 5 properties (from shorthand expansion)
|
|
1212
|
+
# If we have a partial set (explicit longhands), include all values
|
|
1213
|
+
color = prop_map[PROP_BACKGROUND_COLOR]&.value
|
|
1214
|
+
image = prop_map[PROP_BACKGROUND_IMAGE]&.value
|
|
1215
|
+
repeat = prop_map[PROP_BACKGROUND_REPEAT]&.value
|
|
1216
|
+
position = prop_map[PROP_BACKGROUND_POSITION]&.value
|
|
1217
|
+
attachment = prop_map[PROP_BACKGROUND_ATTACHMENT]&.value
|
|
1218
|
+
|
|
1219
|
+
all_present = color && image && repeat && position && attachment
|
|
1220
|
+
parts = []
|
|
1237
1221
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1222
|
+
if all_present
|
|
1223
|
+
# All 5 properties present (likely from shorthand expansion) - omit defaults
|
|
1224
|
+
parts << color if color != 'transparent'
|
|
1225
|
+
parts << image if image != 'none'
|
|
1226
|
+
parts << repeat if repeat != 'repeat'
|
|
1227
|
+
parts << position if position != '0% 0%'
|
|
1228
|
+
parts << attachment if attachment != 'scroll'
|
|
1229
|
+
else
|
|
1230
|
+
# Partial set (explicit longhands) - include all non-nil values
|
|
1231
|
+
parts << color if color
|
|
1232
|
+
parts << image if image
|
|
1233
|
+
parts << repeat if repeat
|
|
1234
|
+
parts << position if position
|
|
1235
|
+
parts << attachment if attachment
|
|
1236
|
+
end
|
|
1240
1237
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1238
|
+
# If all properties are defaults, the shorthand value would be empty
|
|
1239
|
+
# In this case, use "none" which is equivalent to all-default background
|
|
1240
|
+
shorthand_value = if parts.empty?
|
|
1241
|
+
'none'
|
|
1242
|
+
else
|
|
1243
|
+
parts.join(' ')
|
|
1244
|
+
end
|
|
1246
1245
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1246
|
+
replace_with_shorthand!(rule, BACKGROUND_PROPERTIES, PROP_BACKGROUND, shorthand_value, important)
|
|
1247
|
+
end
|
|
1249
1248
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1249
|
+
# Try to recreate list-style shorthand
|
|
1250
|
+
# Can combine: list-style-type, list-style-position, list-style-image
|
|
1251
|
+
def recreate_list_style!(rule, prop_map)
|
|
1252
|
+
ls_props = LIST_STYLE_PROPERTIES
|
|
1253
|
+
ls_decls = ls_props.filter_map { |p| prop_map[p] }
|
|
1253
1254
|
|
|
1254
|
-
|
|
1255
|
+
# Need at least 2 properties to create shorthand
|
|
1256
|
+
return if ls_decls.length < 2
|
|
1255
1257
|
|
|
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]
|
|
1258
|
+
# Check if all have same importance
|
|
1259
|
+
importances = ls_decls.map(&:important).uniq
|
|
1260
|
+
return if importances.length > 1
|
|
1261
1261
|
|
|
1262
|
-
|
|
1262
|
+
important = ls_decls.first.important
|
|
1263
1263
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1264
|
+
# Build list-style shorthand value
|
|
1265
|
+
parts = []
|
|
1266
|
+
parts << prop_map[PROP_LIST_STYLE_TYPE].value if prop_map[PROP_LIST_STYLE_TYPE]
|
|
1267
|
+
parts << prop_map[PROP_LIST_STYLE_POSITION].value if prop_map[PROP_LIST_STYLE_POSITION]
|
|
1268
|
+
parts << prop_map[PROP_LIST_STYLE_IMAGE].value if prop_map[PROP_LIST_STYLE_IMAGE]
|
|
1266
1269
|
|
|
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
|
|
1270
|
+
shorthand_value = parts.join(' ')
|
|
1285
1271
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
# Skip if only one rule in list (nothing to compare)
|
|
1289
|
-
next if rules_in_list.size <= 1
|
|
1272
|
+
replace_with_shorthand!(rule, LIST_STYLE_PROPERTIES, PROP_LIST_STYLE, shorthand_value, important)
|
|
1273
|
+
end
|
|
1290
1274
|
|
|
1291
|
-
#
|
|
1292
|
-
|
|
1293
|
-
|
|
1275
|
+
# Update selector lists to remove diverged rules
|
|
1276
|
+
#
|
|
1277
|
+
# After flattening/cascade, rules that were in the same selector list may have
|
|
1278
|
+
# different declarations. This method builds the selector_lists hash with only
|
|
1279
|
+
# rules that still match, and clears selector_list_id for diverged rules.
|
|
1280
|
+
#
|
|
1281
|
+
# @param merged_rules [Array<Rule>] Flattened rules (with new IDs assigned)
|
|
1282
|
+
# @param selector_lists [Hash] Empty hash to populate with list_id => Array of rule IDs
|
|
1283
|
+
def update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
1284
|
+
# Group merged rules by selector_list_id (skip rules with no list)
|
|
1285
|
+
# Note: Using manual each loop instead of .select{}.group_by for performance.
|
|
1286
|
+
# The more concise form (.select + .group_by) is ~50-60% slower due to intermediate array allocation.
|
|
1287
|
+
rules_by_list = {}
|
|
1288
|
+
merged_rules.each do |r|
|
|
1289
|
+
next unless r.selector_list_id
|
|
1290
|
+
|
|
1291
|
+
(rules_by_list[r.selector_list_id] ||= []) << r
|
|
1292
|
+
end
|
|
1294
1293
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1294
|
+
# For each selector list, check if declarations still match
|
|
1295
|
+
rules_by_list.each do |list_id, rules_in_list|
|
|
1296
|
+
# Skip if only one rule in list (nothing to compare)
|
|
1297
|
+
next if rules_in_list.size <= 1
|
|
1298
|
+
|
|
1299
|
+
# Get first rule as reference
|
|
1300
|
+
reference_rule = rules_in_list.first
|
|
1301
|
+
reference_decls = reference_rule.declarations
|
|
1302
|
+
|
|
1303
|
+
# Find rules that still match (have identical declarations)
|
|
1304
|
+
matching_rules = [reference_rule]
|
|
1305
|
+
|
|
1306
|
+
rules_in_list[1..].each do |rule|
|
|
1307
|
+
if declarations_equal?(reference_decls, rule.declarations)
|
|
1308
|
+
matching_rules << rule
|
|
1309
|
+
else
|
|
1310
|
+
# Clear selector_list_id for diverged rule
|
|
1311
|
+
rule.selector_list_id = nil
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1297
1314
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1315
|
+
# Only keep the selector list if at least 2 rules still match
|
|
1316
|
+
if matching_rules.size >= 2
|
|
1317
|
+
# Build selector_lists hash with NEW rule IDs
|
|
1318
|
+
selector_lists[list_id] = matching_rules.map(&:id)
|
|
1319
|
+
else
|
|
1320
|
+
# Clear selector_list_id for the last remaining rule too
|
|
1321
|
+
matching_rules.each { |r| r.selector_list_id = nil }
|
|
1322
|
+
end
|
|
1304
1323
|
end
|
|
1305
1324
|
end
|
|
1306
1325
|
|
|
1307
|
-
#
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1326
|
+
# Check if two declaration arrays are identical
|
|
1327
|
+
#
|
|
1328
|
+
# @param decls1 [Array<Declaration>]
|
|
1329
|
+
# @param decls2 [Array<Declaration>]
|
|
1330
|
+
# @return [Boolean]
|
|
1331
|
+
def declarations_equal?(decls1, decls2)
|
|
1332
|
+
return false if decls1.size != decls2.size
|
|
1333
|
+
|
|
1334
|
+
# Compare each declaration (property, value, important must all match)
|
|
1335
|
+
decls1.zip(decls2).all? do |d1, d2|
|
|
1336
|
+
d1.property == d2.property &&
|
|
1337
|
+
d1.value == d2.value &&
|
|
1338
|
+
d1.important == d2.important
|
|
1339
|
+
end
|
|
1314
1340
|
end
|
|
1315
|
-
end
|
|
1316
|
-
end
|
|
1317
1341
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1342
|
+
# Mark all methods except flatten and expand_shorthand as private
|
|
1343
|
+
private :flatten_rules_for_selector, :calculate_specificity,
|
|
1344
|
+
:expand_margin, :expand_padding, :parse_four_sides, :split_on_whitespace,
|
|
1345
|
+
:expand_border, :expand_border_side, :expand_border_width, :expand_border_style,
|
|
1346
|
+
:expand_border_color, :parse_border_value, :contains_digit?, :is_border_width?,
|
|
1347
|
+
:is_border_style?,
|
|
1348
|
+
:expand_font, :is_font_size?, :is_font_style?, :is_font_variant?, :is_font_weight?,
|
|
1349
|
+
:expand_background, :starts_with_url?, :is_position_value?, :expand_list_style,
|
|
1350
|
+
:replace_with_shorthand!, :recreate_shorthands!, :recreate_margin!, :recreate_padding!,
|
|
1351
|
+
:check_all_same?,
|
|
1352
|
+
:recreate_border!, :recreate_border_width!, :recreate_border_style!, :recreate_border_color!,
|
|
1353
|
+
:optimize_four_sides, :recreate_font!, :recreate_background!, :recreate_list_style!,
|
|
1354
|
+
:update_selector_lists_for_divergence!, :declarations_equal?,
|
|
1355
|
+
:partition_at_rules, :expand_all_shorthands!, :merge_rules_by_selector_and_media,
|
|
1356
|
+
:build_selector_lists, :rebuild_media_index, :build_result
|
|
1331
1357
|
end
|
|
1332
|
-
end
|
|
1333
1358
|
|
|
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
|
|
1359
|
+
# A frozen singleton, same rationale as Pure itself (see pure.rb) - none of
|
|
1360
|
+
# FlattenImpl's methods touch instance state, so one shared instance is as
|
|
1361
|
+
# safe as the bare module this replaces.
|
|
1362
|
+
Flatten = FlattenImpl.new.freeze
|
|
1363
|
+
end
|
|
1349
1364
|
end
|
|
1350
1365
|
end
|