cataract 0.2.5 → 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/.github/workflows/test.yml +6 -6
- data/.gitignore +17 -6
- data/.rubocop_todo.yml +11 -19
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +28 -1
- data/Gemfile +2 -2
- data/examples/css_analyzer/analyzer.rb +9 -3
- data/examples/css_analyzer/analyzers/base.rb +1 -1
- data/ext/cataract/cataract.c +312 -550
- data/ext/cataract/cataract.h +126 -16
- data/ext/cataract/css_parser.c +782 -806
- data/ext/cataract/extconf.rb +1 -2
- data/ext/cataract/flatten.c +279 -991
- data/ext/cataract/shorthand_expander.c +80 -102
- data/ext/cataract_color/color_conversion.c +1 -1
- data/lib/cataract/declarations.rb +26 -10
- data/lib/cataract/import_resolver.rb +36 -16
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -67
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1195 -1182
- data/lib/cataract/pure/parser.rb +1721 -1652
- data/lib/cataract/pure/serializer.rb +575 -710
- data/lib/cataract/pure/specificity.rb +152 -175
- data/lib/cataract/pure.rb +73 -108
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +475 -415
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +4 -14
- data/ext/cataract/import_scanner.c +0 -174
- data/ext/cataract_old/cataract.c +0 -393
- data/ext/cataract_old/cataract.h +0 -250
- data/ext/cataract_old/css_parser.c +0 -933
- data/ext/cataract_old/extconf.rb +0 -67
- data/ext/cataract_old/import_scanner.c +0 -174
- data/ext/cataract_old/merge.c +0 -776
- data/ext/cataract_old/shorthand_expander.c +0 -902
- data/ext/cataract_old/specificity.c +0 -213
- data/ext/cataract_old/stylesheet.c +0 -290
- data/ext/cataract_old/value_splitter.c +0 -116
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -10,1343 +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
|
-
|
|
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)
|
|
175
195
|
end
|
|
176
|
-
end
|
|
177
196
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
218
|
end
|
|
191
|
-
end
|
|
192
|
-
rule.declarations.replace(expanded)
|
|
193
|
-
end
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
# Group by (selector, media_query_id) instead of just selector
|
|
198
|
-
# Rules with same selector but different media contexts should NOT be merged
|
|
199
|
-
# NOTE: Using manual each instead of .group_by to avoid intermediate hash allocation.
|
|
200
|
-
by_selector_and_media = {}
|
|
201
|
-
regular_rules.each do |rule|
|
|
202
|
-
media_query_id = rule.media_query_id
|
|
203
|
-
key = [rule.selector, media_query_id]
|
|
204
|
-
(by_selector_and_media[key] ||= []) << rule
|
|
205
|
-
end
|
|
220
|
+
[at_rules, regular_rules]
|
|
221
|
+
end
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
212
246
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
216
260
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
219
265
|
end
|
|
266
|
+
merged_rules
|
|
220
267
|
end
|
|
221
|
-
merged_rules << merged_rule
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Recreate shorthands where possible
|
|
225
|
-
merged_rules.each { |rule| recreate_shorthands!(rule) }
|
|
226
268
|
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
229
284
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
238
301
|
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
merged_rules.each do |rule|
|
|
303
|
+
next unless rule.is_a?(Rule) && rule.media_query_id
|
|
241
304
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
media_queries = stylesheet.instance_variable_get(:@media_queries)
|
|
245
|
-
media_query_lists = stylesheet.instance_variable_get(:@_media_query_lists)
|
|
246
|
-
new_media_index = {}
|
|
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]
|
|
247
307
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
253
315
|
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
256
323
|
|
|
257
|
-
|
|
258
|
-
|
|
324
|
+
new_media_index[mq.type] ||= []
|
|
325
|
+
new_media_index[mq.type] << rule.id
|
|
326
|
+
end
|
|
327
|
+
end
|
|
259
328
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
mq_ids.each do |mq_id|
|
|
265
|
-
mq = media_queries[mq_id]
|
|
266
|
-
next unless mq
|
|
329
|
+
# Deduplicate arrays once at the end
|
|
330
|
+
new_media_index.each_value(&:uniq!)
|
|
331
|
+
new_media_index
|
|
332
|
+
end
|
|
267
333
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
355
|
end
|
|
272
|
-
else
|
|
273
|
-
# Single media query - just index under its type
|
|
274
|
-
mq = media_queries[rule.media_query_id]
|
|
275
|
-
next unless mq
|
|
276
|
-
|
|
277
|
-
media_type = mq.type
|
|
278
|
-
new_media_index[media_type] ||= []
|
|
279
|
-
new_media_index[media_type] << rule.id
|
|
280
356
|
end
|
|
281
|
-
end
|
|
282
357
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
306
415
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
315
430
|
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
318
453
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
322
461
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
325
496
|
|
|
326
|
-
|
|
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
|
|
327
507
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
332
518
|
|
|
333
|
-
|
|
334
|
-
|
|
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]]
|
|
335
534
|
else
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
353
559
|
else
|
|
354
|
-
|
|
355
|
-
should_replace = source_order > existing_order
|
|
560
|
+
current << byte
|
|
356
561
|
end
|
|
357
562
|
|
|
358
|
-
|
|
359
|
-
decl_map[prop] = [source_order, spec, decl.important, decl.value]
|
|
360
|
-
end
|
|
563
|
+
i += 1
|
|
361
564
|
end
|
|
565
|
+
|
|
566
|
+
parts << current unless current.empty?
|
|
567
|
+
parts
|
|
362
568
|
end
|
|
363
|
-
end
|
|
364
569
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# NOTE: We don't sort by source_order here because:
|
|
370
|
-
# 1. Hash iteration order in Ruby is insertion order (since Ruby 1.9)
|
|
371
|
-
# 2. Declaration order doesn't affect CSS behavior (cascade is already resolved)
|
|
372
|
-
# 3. Sorting would add overhead for purely aesthetic output
|
|
373
|
-
# The output order is roughly source order but may vary when properties are
|
|
374
|
-
# overridden by later rules with higher specificity or importance.
|
|
375
|
-
declarations = []
|
|
376
|
-
decl_map.each do |prop, (_order, _spec, important, value)|
|
|
377
|
-
declarations << Declaration.new(prop, value, important)
|
|
378
|
-
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)
|
|
379
574
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
# Preserve selector_list_id if all rules in group share the same one
|
|
383
|
-
selector_list_ids = rules.filter_map(&:selector_list_id)
|
|
384
|
-
selector_list_ids.uniq!
|
|
385
|
-
selector_list_id = selector_list_ids.size == 1 ? selector_list_ids.first : nil
|
|
386
|
-
|
|
387
|
-
# All rules being merged have the same media_query_id (they were grouped by it)
|
|
388
|
-
media_query_id = rules.first.media_query_id
|
|
389
|
-
|
|
390
|
-
# Create merged rule
|
|
391
|
-
Rule.new(
|
|
392
|
-
0, # ID will be updated later
|
|
393
|
-
selector,
|
|
394
|
-
declarations,
|
|
395
|
-
rules.first.specificity, # Use first rule's specificity
|
|
396
|
-
nil, # No parent after flattening
|
|
397
|
-
nil, # No nesting style after flattening
|
|
398
|
-
selector_list_id, # Preserve if all rules share same ID
|
|
399
|
-
media_query_id # Preserve media context
|
|
400
|
-
)
|
|
401
|
-
end
|
|
575
|
+
result = []
|
|
402
576
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
410
584
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
# @api private
|
|
418
|
-
def self.expand_shorthand(decl)
|
|
419
|
-
case decl.property
|
|
420
|
-
when 'margin'
|
|
421
|
-
expand_margin(decl)
|
|
422
|
-
when 'padding'
|
|
423
|
-
expand_padding(decl)
|
|
424
|
-
when 'border'
|
|
425
|
-
expand_border(decl)
|
|
426
|
-
when 'border-top', 'border-right', 'border-bottom', 'border-left'
|
|
427
|
-
expand_border_side(decl)
|
|
428
|
-
when 'border-width'
|
|
429
|
-
expand_border_width(decl)
|
|
430
|
-
when 'border-style'
|
|
431
|
-
expand_border_style(decl)
|
|
432
|
-
when 'border-color'
|
|
433
|
-
expand_border_color(decl)
|
|
434
|
-
when 'font'
|
|
435
|
-
expand_font(decl)
|
|
436
|
-
when 'background'
|
|
437
|
-
expand_background(decl)
|
|
438
|
-
when 'list-style'
|
|
439
|
-
expand_list_style(decl)
|
|
440
|
-
else
|
|
441
|
-
# Not a shorthand, return as-is in an array
|
|
442
|
-
[decl]
|
|
443
|
-
end
|
|
444
|
-
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
|
|
445
591
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Declaration.new(PROP_MARGIN_BOTTOM, sides[2], decl.important),
|
|
453
|
-
Declaration.new(PROP_MARGIN_LEFT, sides[3], decl.important)
|
|
454
|
-
]
|
|
455
|
-
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
|
|
456
598
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
sides = parse_four_sides(decl.value)
|
|
460
|
-
[
|
|
461
|
-
Declaration.new(PROP_PADDING_TOP, sides[0], decl.important),
|
|
462
|
-
Declaration.new(PROP_PADDING_RIGHT, sides[1], decl.important),
|
|
463
|
-
Declaration.new(PROP_PADDING_BOTTOM, sides[2], decl.important),
|
|
464
|
-
Declaration.new(PROP_PADDING_LEFT, sides[3], decl.important)
|
|
465
|
-
]
|
|
466
|
-
end
|
|
599
|
+
result
|
|
600
|
+
end
|
|
467
601
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
parts = split_on_whitespace(value)
|
|
475
|
-
|
|
476
|
-
case parts.length
|
|
477
|
-
when 1
|
|
478
|
-
[parts[0], parts[0], parts[0], parts[0]]
|
|
479
|
-
when 2
|
|
480
|
-
[parts[0], parts[1], parts[0], parts[1]]
|
|
481
|
-
when 3
|
|
482
|
-
[parts[0], parts[1], parts[2], parts[1]]
|
|
483
|
-
else
|
|
484
|
-
[parts[0], parts[1], parts[2], parts[3]]
|
|
485
|
-
end
|
|
486
|
-
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]
|
|
487
608
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
i = 0
|
|
495
|
-
len = value.bytesize
|
|
496
|
-
while i < len
|
|
497
|
-
byte = value.getbyte(i)
|
|
498
|
-
|
|
499
|
-
if byte == BYTE_LPAREN
|
|
500
|
-
paren_depth += 1
|
|
501
|
-
current << byte
|
|
502
|
-
elsif byte == BYTE_RPAREN
|
|
503
|
-
paren_depth -= 1
|
|
504
|
-
current << byte
|
|
505
|
-
elsif byte == BYTE_SPACE && paren_depth == 0
|
|
506
|
-
parts << current unless current.empty?
|
|
507
|
-
current = String.new
|
|
508
|
-
else
|
|
509
|
-
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
|
|
510
615
|
end
|
|
511
616
|
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
514
627
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
518
638
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
523
649
|
|
|
524
|
-
|
|
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
|
|
525
667
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
result << Declaration.new(PROP_BORDER_TOP_WIDTH, width, decl.important)
|
|
529
|
-
result << Declaration.new(PROP_BORDER_RIGHT_WIDTH, width, decl.important)
|
|
530
|
-
result << Declaration.new(PROP_BORDER_BOTTOM_WIDTH, width, decl.important)
|
|
531
|
-
result << Declaration.new(PROP_BORDER_LEFT_WIDTH, width, decl.important)
|
|
532
|
-
end
|
|
668
|
+
[width, style, color]
|
|
669
|
+
end
|
|
533
670
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
540
678
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
result << Declaration.new(PROP_BORDER_RIGHT_COLOR, color, decl.important)
|
|
544
|
-
result << Declaration.new(PROP_BORDER_BOTTOM_COLOR, color, decl.important)
|
|
545
|
-
result << Declaration.new(PROP_BORDER_LEFT_COLOR, color, decl.important)
|
|
546
|
-
end
|
|
679
|
+
i += 1
|
|
680
|
+
end
|
|
547
681
|
|
|
548
|
-
|
|
549
|
-
|
|
682
|
+
false
|
|
683
|
+
end
|
|
550
684
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
width, style, color = parse_border_value(decl.value)
|
|
556
|
-
|
|
557
|
-
result = []
|
|
558
|
-
|
|
559
|
-
# Map side to property constants
|
|
560
|
-
if width
|
|
561
|
-
width_prop = case side
|
|
562
|
-
when SIDE_TOP then PROP_BORDER_TOP_WIDTH
|
|
563
|
-
when SIDE_RIGHT then PROP_BORDER_RIGHT_WIDTH
|
|
564
|
-
when SIDE_BOTTOM then PROP_BORDER_BOTTOM_WIDTH
|
|
565
|
-
when SIDE_LEFT then PROP_BORDER_LEFT_WIDTH
|
|
566
|
-
end
|
|
567
|
-
result << Declaration.new(width_prop, width, decl.important)
|
|
568
|
-
end
|
|
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)
|
|
569
689
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
when SIDE_TOP then PROP_BORDER_TOP_STYLE
|
|
573
|
-
when SIDE_RIGHT then PROP_BORDER_RIGHT_STYLE
|
|
574
|
-
when SIDE_BOTTOM then PROP_BORDER_BOTTOM_STYLE
|
|
575
|
-
when SIDE_LEFT then PROP_BORDER_LEFT_STYLE
|
|
576
|
-
end
|
|
577
|
-
result << Declaration.new(style_prop, style, decl.important)
|
|
578
|
-
end
|
|
690
|
+
contains_digit?(value)
|
|
691
|
+
end
|
|
579
692
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
when SIDE_BOTTOM then PROP_BORDER_BOTTOM_COLOR
|
|
585
|
-
when SIDE_LEFT then PROP_BORDER_LEFT_COLOR
|
|
586
|
-
end
|
|
587
|
-
result << Declaration.new(color_prop, color, decl.important)
|
|
588
|
-
end
|
|
693
|
+
# Check if value is a border style
|
|
694
|
+
def is_border_style?(value)
|
|
695
|
+
BORDER_STYLE_KEYWORDS.include?(value)
|
|
696
|
+
end
|
|
589
697
|
|
|
590
|
-
|
|
591
|
-
|
|
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
|
|
592
761
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
sides = parse_four_sides(decl.value)
|
|
596
|
-
[
|
|
597
|
-
Declaration.new(PROP_BORDER_TOP_WIDTH, sides[0], decl.important),
|
|
598
|
-
Declaration.new(PROP_BORDER_RIGHT_WIDTH, sides[1], decl.important),
|
|
599
|
-
Declaration.new(PROP_BORDER_BOTTOM_WIDTH, sides[2], decl.important),
|
|
600
|
-
Declaration.new(PROP_BORDER_LEFT_WIDTH, sides[3], decl.important)
|
|
601
|
-
]
|
|
602
|
-
end
|
|
762
|
+
i += 1
|
|
763
|
+
end
|
|
603
764
|
|
|
604
|
-
|
|
605
|
-
def self.expand_border_style(decl)
|
|
606
|
-
sides = parse_four_sides(decl.value)
|
|
607
|
-
[
|
|
608
|
-
Declaration.new(PROP_BORDER_TOP_STYLE, sides[0], decl.important),
|
|
609
|
-
Declaration.new(PROP_BORDER_RIGHT_STYLE, sides[1], decl.important),
|
|
610
|
-
Declaration.new(PROP_BORDER_BOTTOM_STYLE, sides[2], decl.important),
|
|
611
|
-
Declaration.new(PROP_BORDER_LEFT_STYLE, sides[3], decl.important)
|
|
612
|
-
]
|
|
613
|
-
end
|
|
765
|
+
family = family_parts.join(' ')
|
|
614
766
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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?
|
|
771
|
+
|
|
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)
|
|
625
779
|
|
|
626
|
-
|
|
627
|
-
def self.parse_border_value(value)
|
|
628
|
-
parts = split_on_whitespace(value)
|
|
629
|
-
width = nil
|
|
630
|
-
style = nil
|
|
631
|
-
color = nil
|
|
632
|
-
|
|
633
|
-
# Identify each part by type
|
|
634
|
-
parts.each do |part|
|
|
635
|
-
if is_border_width?(part)
|
|
636
|
-
width = part
|
|
637
|
-
elsif is_border_style?(part)
|
|
638
|
-
style = part
|
|
639
|
-
else
|
|
640
|
-
color = part # Assume color if not width or style
|
|
780
|
+
result
|
|
641
781
|
end
|
|
642
|
-
end
|
|
643
782
|
|
|
644
|
-
|
|
645
|
-
|
|
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)
|
|
646
787
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
# Check for numeric values or width keywords
|
|
650
|
-
return true if BORDER_WIDTH_KEYWORDS.include?(value)
|
|
788
|
+
FONT_SIZE_KEYWORDS.include?(value)
|
|
789
|
+
end
|
|
651
790
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
byte = value.getbyte(i)
|
|
657
|
-
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
791
|
+
# Check if value is a font style
|
|
792
|
+
def is_font_style?(value)
|
|
793
|
+
FONT_STYLE_KEYWORDS.include?(value)
|
|
794
|
+
end
|
|
658
795
|
|
|
659
|
-
|
|
660
|
-
|
|
796
|
+
# Check if value is a font variant
|
|
797
|
+
def is_font_variant?(value)
|
|
798
|
+
FONT_VARIANT_KEYWORDS.include?(value)
|
|
799
|
+
end
|
|
661
800
|
|
|
662
|
-
|
|
663
|
-
|
|
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)
|
|
664
805
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
BORDER_STYLE_KEYWORDS.include?(value)
|
|
668
|
-
end
|
|
806
|
+
FONT_WEIGHT_KEYWORDS.include?(value)
|
|
807
|
+
end
|
|
669
808
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
slash_idx = nil
|
|
702
|
-
j = 0
|
|
703
|
-
len = size_part.bytesize
|
|
704
|
-
while j < len
|
|
705
|
-
if size_part.getbyte(j) == BYTE_SLASH_FWD
|
|
706
|
-
slash_idx = j
|
|
707
|
-
break
|
|
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
|
|
708
840
|
end
|
|
709
|
-
j += 1
|
|
710
841
|
end
|
|
711
842
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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)
|
|
718
851
|
|
|
719
|
-
|
|
720
|
-
family_parts = parts[(i + 1)..-1]
|
|
721
|
-
break
|
|
722
|
-
elsif is_font_style?(part)
|
|
723
|
-
style = part
|
|
724
|
-
elsif is_font_variant?(part)
|
|
725
|
-
variant = part
|
|
726
|
-
elsif is_font_weight?(part)
|
|
727
|
-
weight = part
|
|
728
|
-
else
|
|
729
|
-
# Unknown, might be start of family - treat everything from here as family
|
|
730
|
-
family_parts = parts[i..-1]
|
|
731
|
-
break
|
|
852
|
+
result
|
|
732
853
|
end
|
|
733
854
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
family = family_parts.join(' ')
|
|
855
|
+
# Check if value starts with 'url('
|
|
856
|
+
def starts_with_url?(value)
|
|
857
|
+
return false if value.bytesize < 4
|
|
738
858
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
743
864
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
result << Declaration.new(PROP_FONT_SIZE, size, decl.important)
|
|
749
|
-
result << Declaration.new(PROP_LINE_HEIGHT, line_height || 'normal', decl.important)
|
|
750
|
-
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)
|
|
751
869
|
|
|
752
|
-
|
|
753
|
-
|
|
870
|
+
# Check for '%'
|
|
871
|
+
i = 0
|
|
872
|
+
len = value.bytesize
|
|
873
|
+
while i < len
|
|
874
|
+
return true if value.getbyte(i) == BYTE_PERCENT
|
|
754
875
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
len = value.bytesize
|
|
760
|
-
while i < len
|
|
761
|
-
byte = value.getbyte(i)
|
|
762
|
-
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
876
|
+
i += 1
|
|
877
|
+
end
|
|
878
|
+
false
|
|
879
|
+
end
|
|
763
880
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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)
|
|
768
886
|
|
|
769
|
-
|
|
770
|
-
def self.is_font_style?(value)
|
|
771
|
-
FONT_STYLE_KEYWORDS.include?(value)
|
|
772
|
-
end
|
|
887
|
+
return [decl] if parts.empty?
|
|
773
888
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
889
|
+
result = []
|
|
890
|
+
type = nil
|
|
891
|
+
position = nil
|
|
892
|
+
image = nil
|
|
778
893
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
787
904
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
end
|
|
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
|
|
792
908
|
|
|
793
|
-
|
|
794
|
-
# Format: [color] [image] [repeat] [attachment] [position]
|
|
795
|
-
def self.expand_background(decl)
|
|
796
|
-
value = decl.value
|
|
797
|
-
parts = split_on_whitespace(value)
|
|
798
|
-
|
|
799
|
-
return [decl] if parts.empty?
|
|
800
|
-
|
|
801
|
-
# Parse background components (simple heuristic)
|
|
802
|
-
# According to CSS spec, background shorthand sets ALL properties
|
|
803
|
-
# Any unspecified values get their initial values
|
|
804
|
-
color = nil
|
|
805
|
-
image = nil
|
|
806
|
-
repeat = nil
|
|
807
|
-
attachment = nil
|
|
808
|
-
position = nil
|
|
809
|
-
|
|
810
|
-
parts.each do |part|
|
|
811
|
-
if starts_with_url?(part) || part == 'none'
|
|
812
|
-
image = part
|
|
813
|
-
elsif BACKGROUND_REPEAT_KEYWORDS.include?(part)
|
|
814
|
-
repeat = part
|
|
815
|
-
elsif BACKGROUND_ATTACHMENT_KEYWORDS.include?(part)
|
|
816
|
-
attachment = part
|
|
817
|
-
elsif is_position_value?(part)
|
|
818
|
-
position ||= String.new
|
|
819
|
-
position << ' ' unless position.empty?
|
|
820
|
-
position << part
|
|
821
|
-
else
|
|
822
|
-
# Assume it's a color
|
|
823
|
-
color = part
|
|
909
|
+
result.empty? ? [decl] : result
|
|
824
910
|
end
|
|
825
|
-
end
|
|
826
911
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
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
|
|
835
919
|
|
|
836
|
-
|
|
837
|
-
|
|
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 }
|
|
838
927
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return false if value.bytesize < 4
|
|
928
|
+
# Try to recreate margin
|
|
929
|
+
recreate_margin!(rule, prop_map)
|
|
842
930
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
value.getbyte(2) == BYTE_LOWER_L &&
|
|
846
|
-
value.getbyte(3) == BYTE_LPAREN
|
|
847
|
-
end
|
|
931
|
+
# Try to recreate padding
|
|
932
|
+
recreate_padding!(rule, prop_map)
|
|
848
933
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return true if BACKGROUND_POSITION_KEYWORDS.include?(value)
|
|
934
|
+
# Try to recreate border
|
|
935
|
+
recreate_border!(rule, prop_map)
|
|
852
936
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
len = value.bytesize
|
|
856
|
-
while i < len
|
|
857
|
-
byte = value.getbyte(i)
|
|
858
|
-
return true if byte == BYTE_PERCENT
|
|
859
|
-
return true if byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
|
|
937
|
+
# Try to recreate list-style
|
|
938
|
+
recreate_list_style!(rule, prop_map)
|
|
860
939
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
false
|
|
864
|
-
end
|
|
940
|
+
# Try to recreate font
|
|
941
|
+
recreate_font!(rule, prop_map)
|
|
865
942
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
def self.expand_list_style(decl)
|
|
869
|
-
value = decl.value
|
|
870
|
-
parts = split_on_whitespace(value)
|
|
871
|
-
|
|
872
|
-
return [decl] if parts.empty?
|
|
873
|
-
|
|
874
|
-
result = []
|
|
875
|
-
type = nil
|
|
876
|
-
position = nil
|
|
877
|
-
image = nil
|
|
878
|
-
|
|
879
|
-
parts.each do |part|
|
|
880
|
-
if starts_with_url?(part) || part == 'none'
|
|
881
|
-
image = part
|
|
882
|
-
elsif LIST_STYLE_POSITION_KEYWORDS.include?(part)
|
|
883
|
-
position = part
|
|
884
|
-
else
|
|
885
|
-
# Assume it's a type (disc, circle, square, etc.)
|
|
886
|
-
type = part
|
|
943
|
+
# Try to recreate background
|
|
944
|
+
recreate_background!(rule, prop_map)
|
|
887
945
|
end
|
|
888
|
-
end
|
|
889
946
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
|
893
971
|
|
|
894
|
-
|
|
895
|
-
|
|
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?
|
|
896
978
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
prop_map = {}
|
|
903
|
-
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
|
|
904
984
|
|
|
905
|
-
|
|
906
|
-
|
|
985
|
+
# Use each + << instead of map
|
|
986
|
+
values = []
|
|
987
|
+
sides.each { |s| values << s.value }
|
|
988
|
+
important = sides.first.important
|
|
907
989
|
|
|
908
|
-
|
|
909
|
-
recreate_padding!(rule, prop_map)
|
|
990
|
+
shorthand_value = optimize_four_sides(values)
|
|
910
991
|
|
|
911
|
-
|
|
912
|
-
|
|
992
|
+
replace_with_shorthand!(rule, PADDING_SIDES, PROP_PADDING, shorthand_value, important)
|
|
993
|
+
end
|
|
913
994
|
|
|
914
|
-
|
|
915
|
-
|
|
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?
|
|
916
999
|
|
|
917
|
-
|
|
918
|
-
|
|
1000
|
+
first_val = decls[0].value
|
|
1001
|
+
first_imp = decls[0].important
|
|
919
1002
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
923
1007
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
927
|
-
sides = []
|
|
928
|
-
MARGIN_SIDES.each { |s| sides << prop_map[s] }
|
|
929
|
-
return unless sides.all? # Need all four sides
|
|
930
|
-
|
|
931
|
-
# Check if all have same importance
|
|
932
|
-
# Use each + << instead of map
|
|
933
|
-
importances = []
|
|
934
|
-
sides.each { |s| importances << s.important }
|
|
935
|
-
importances.uniq!
|
|
936
|
-
return if importances.length > 1 # Mixed importance, can't create shorthand
|
|
937
|
-
|
|
938
|
-
# Use each + << instead of map
|
|
939
|
-
values = []
|
|
940
|
-
sides.each { |s| values << s.value }
|
|
941
|
-
important = sides.first.important
|
|
942
|
-
|
|
943
|
-
# Create optimized shorthand
|
|
944
|
-
shorthand_value = optimize_four_sides(values)
|
|
945
|
-
|
|
946
|
-
# Remove individual sides and append shorthand
|
|
947
|
-
# Note: We append rather than insert at original position to match C implementation behavior
|
|
948
|
-
rule.declarations.reject! { |d| MARGIN_SIDES.include?(d.property) }
|
|
949
|
-
rule.declarations << Declaration.new(PROP_MARGIN, shorthand_value, important)
|
|
950
|
-
end
|
|
1008
|
+
i += 1
|
|
1009
|
+
end
|
|
951
1010
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
# Use each + << instead of map (1.05-1.20x faster, called once per rule during merge)
|
|
955
|
-
sides = []
|
|
956
|
-
PADDING_SIDES.each { |s| sides << prop_map[s] }
|
|
957
|
-
return unless sides.all?
|
|
958
|
-
|
|
959
|
-
# Use each + << instead of map
|
|
960
|
-
importances = []
|
|
961
|
-
sides.each { |s| importances << s.important }
|
|
962
|
-
importances.uniq!
|
|
963
|
-
return if importances.length > 1
|
|
964
|
-
|
|
965
|
-
# Use each + << instead of map
|
|
966
|
-
values = []
|
|
967
|
-
sides.each { |s| values << s.value }
|
|
968
|
-
important = sides.first.important
|
|
969
|
-
|
|
970
|
-
shorthand_value = optimize_four_sides(values)
|
|
971
|
-
|
|
972
|
-
# Remove individual sides and append shorthand
|
|
973
|
-
# Note: We append rather than insert at original position to match C implementation behavior
|
|
974
|
-
rule.declarations.reject! { |d| PADDING_SIDES.include?(d.property) }
|
|
975
|
-
rule.declarations << Declaration.new(PROP_PADDING, shorthand_value, important)
|
|
976
|
-
end
|
|
1011
|
+
true
|
|
1012
|
+
end
|
|
977
1013
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
|
982
1042
|
|
|
983
|
-
|
|
984
|
-
|
|
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
|
|
985
1047
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
|
990
1054
|
|
|
991
|
-
|
|
992
|
-
end
|
|
1055
|
+
border_value = parts.join(' ')
|
|
993
1056
|
|
|
994
|
-
|
|
995
|
-
|
|
1057
|
+
replace_with_shorthand!(rule, BORDER_ALL, PROP_BORDER, border_value, important)
|
|
1058
|
+
return
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
996
1061
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
widths = []
|
|
1002
|
-
BORDER_WIDTHS.each { |p| widths << prop_map[p] }
|
|
1003
|
-
styles = []
|
|
1004
|
-
BORDER_STYLES.each { |p| styles << prop_map[p] }
|
|
1005
|
-
colors = []
|
|
1006
|
-
BORDER_COLORS.each { |p| colors << prop_map[p] }
|
|
1007
|
-
|
|
1008
|
-
# Check if all sides have same values and can create full border shorthand
|
|
1009
|
-
# Check cheapest condition first (.all?), then do single pass for values/importance
|
|
1010
|
-
widths_all_same = widths.all? && check_all_same?(widths)
|
|
1011
|
-
styles_all_same = styles.all? && check_all_same?(styles)
|
|
1012
|
-
colors_all_same = colors.all? && check_all_same?(colors)
|
|
1013
|
-
|
|
1014
|
-
# Can create FULL border shorthand ONLY if style is present (style is required for border shorthand)
|
|
1015
|
-
# AND all properties that are present have same values and importance
|
|
1016
|
-
if styles_all_same
|
|
1017
|
-
# Check if we have width and/or color with same importance as style
|
|
1018
|
-
can_create_border = true
|
|
1019
|
-
important = styles.first.important
|
|
1020
|
-
|
|
1021
|
-
# If width is present, must be all-same and same importance
|
|
1022
|
-
if widths.all?
|
|
1023
|
-
can_create_border = false unless widths_all_same && widths.first.important == important
|
|
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?
|
|
1024
1066
|
end
|
|
1025
1067
|
|
|
1026
|
-
#
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1068
|
+
# Recreate border-width shorthand
|
|
1069
|
+
def recreate_border_width!(rule, widths)
|
|
1070
|
+
importances = widths.map(&:important).uniq
|
|
1071
|
+
return if importances.length > 1
|
|
1030
1072
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
parts = []
|
|
1034
|
-
parts << widths.first.value if widths_all_same
|
|
1035
|
-
parts << styles.first.value
|
|
1036
|
-
parts << colors.first.value if colors_all_same
|
|
1073
|
+
values = widths.map(&:value)
|
|
1074
|
+
important = widths.first.important
|
|
1037
1075
|
|
|
1038
|
-
|
|
1076
|
+
shorthand_value = optimize_four_sides(values)
|
|
1039
1077
|
|
|
1040
|
-
|
|
1041
|
-
# Note: We append rather than insert at original position to match C implementation behavior
|
|
1042
|
-
rule.declarations.reject! { |d| BORDER_ALL.include?(d.property) }
|
|
1043
|
-
rule.declarations << Declaration.new(PROP_BORDER, border_value, important)
|
|
1044
|
-
return
|
|
1078
|
+
replace_with_shorthand!(rule, BORDER_WIDTHS, PROP_BORDER_WIDTH, shorthand_value, important)
|
|
1045
1079
|
end
|
|
1046
|
-
end
|
|
1047
1080
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
end
|
|
1081
|
+
# Recreate border-style shorthand
|
|
1082
|
+
def recreate_border_style!(rule, styles)
|
|
1083
|
+
importances = styles.map(&:important).uniq
|
|
1084
|
+
return if importances.length > 1
|
|
1053
1085
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
importances = widths.map(&:important).uniq
|
|
1057
|
-
return if importances.length > 1
|
|
1086
|
+
values = styles.map(&:value)
|
|
1087
|
+
important = styles.first.important
|
|
1058
1088
|
|
|
1059
|
-
|
|
1060
|
-
important = widths.first.important
|
|
1089
|
+
shorthand_value = optimize_four_sides(values)
|
|
1061
1090
|
|
|
1062
|
-
|
|
1091
|
+
replace_with_shorthand!(rule, BORDER_STYLES, PROP_BORDER_STYLE, shorthand_value, important)
|
|
1092
|
+
end
|
|
1063
1093
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1094
|
+
# Recreate border-color shorthand
|
|
1095
|
+
def recreate_border_color!(rule, colors)
|
|
1096
|
+
importances = colors.map(&:important).uniq
|
|
1097
|
+
return if importances.length > 1
|
|
1067
1098
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
importances = styles.map(&:important).uniq
|
|
1071
|
-
return if importances.length > 1
|
|
1099
|
+
values = colors.map(&:value)
|
|
1100
|
+
important = colors.first.important
|
|
1072
1101
|
|
|
1073
|
-
|
|
1074
|
-
important = styles.first.important
|
|
1102
|
+
shorthand_value = optimize_four_sides(values)
|
|
1075
1103
|
|
|
1076
|
-
|
|
1104
|
+
replace_with_shorthand!(rule, BORDER_COLORS, PROP_BORDER_COLOR, shorthand_value, important)
|
|
1105
|
+
end
|
|
1077
1106
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
|
1081
1125
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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]
|
|
1086
1132
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1133
|
+
# Need at least size and family
|
|
1134
|
+
return unless size && family
|
|
1089
1135
|
|
|
1090
|
-
|
|
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?
|
|
1091
1139
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
end
|
|
1140
|
+
importances = font_decls.map(&:important).uniq
|
|
1141
|
+
return if importances.length > 1
|
|
1095
1142
|
|
|
1096
|
-
|
|
1097
|
-
# ["10px", "10px", "10px", "10px"] -> "10px"
|
|
1098
|
-
# ["10px", "20px", "10px", "20px"] -> "10px 20px"
|
|
1099
|
-
# ["10px", "20px", "30px", "20px"] -> "10px 20px 30px"
|
|
1100
|
-
# ["10px", "20px", "30px", "40px"] -> "10px 20px 30px 40px"
|
|
1101
|
-
def self.optimize_four_sides(values)
|
|
1102
|
-
top, right, bottom, left = values
|
|
1103
|
-
|
|
1104
|
-
if top == right && right == bottom && bottom == left
|
|
1105
|
-
top
|
|
1106
|
-
elsif top == bottom && right == left
|
|
1107
|
-
"#{top} #{right}"
|
|
1108
|
-
elsif right == left
|
|
1109
|
-
"#{top} #{right} #{bottom}"
|
|
1110
|
-
else
|
|
1111
|
-
"#{top} #{right} #{bottom} #{left}"
|
|
1112
|
-
end
|
|
1113
|
-
end
|
|
1143
|
+
important = font_decls.first.important
|
|
1114
1144
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
# Need at least size and family
|
|
1123
|
-
return unless size && family
|
|
1124
|
-
|
|
1125
|
-
# Check if all font properties have same importance
|
|
1126
|
-
font_decls = FONT_PROPERTIES.filter_map { |p| prop_map[p] }
|
|
1127
|
-
return if font_decls.empty?
|
|
1128
|
-
|
|
1129
|
-
importances = font_decls.map(&:important).uniq
|
|
1130
|
-
return if importances.length > 1
|
|
1131
|
-
|
|
1132
|
-
important = font_decls.first.important
|
|
1133
|
-
|
|
1134
|
-
# Build font shorthand value
|
|
1135
|
-
# Strategy: Only omit defaults if we have ALL 6 properties (from shorthand expansion)
|
|
1136
|
-
# If we have a partial set, include all non-nil values
|
|
1137
|
-
style = prop_map[PROP_FONT_STYLE]&.value
|
|
1138
|
-
variant = prop_map[PROP_FONT_VARIANT]&.value
|
|
1139
|
-
weight = prop_map[PROP_FONT_WEIGHT]&.value
|
|
1140
|
-
line_height = prop_map[PROP_LINE_HEIGHT]&.value
|
|
1141
|
-
|
|
1142
|
-
all_present = style && variant && weight && line_height
|
|
1143
|
-
parts = []
|
|
1144
|
-
|
|
1145
|
-
if all_present
|
|
1146
|
-
# All properties present (likely from shorthand expansion) - omit defaults
|
|
1147
|
-
parts << style if style != 'normal'
|
|
1148
|
-
parts << variant if variant != 'normal'
|
|
1149
|
-
parts << weight if weight != 'normal'
|
|
1150
|
-
else
|
|
1151
|
-
# Partial set - include all non-nil values
|
|
1152
|
-
parts << style if style
|
|
1153
|
-
parts << variant if variant
|
|
1154
|
-
parts << weight if weight
|
|
1155
|
-
end
|
|
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
|
|
1156
1152
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1153
|
+
all_present = style && variant && weight && line_height
|
|
1154
|
+
parts = []
|
|
1155
|
+
|
|
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
|
|
1167
|
+
|
|
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
|
|
1173
1184
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1185
|
+
# Required: family
|
|
1186
|
+
parts << family.value
|
|
1176
1187
|
|
|
1177
|
-
|
|
1188
|
+
shorthand_value = parts.join(' ')
|
|
1178
1189
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
rule.declarations.reject! { |d| FONT_PROPERTIES.include?(d.property) }
|
|
1182
|
-
rule.declarations << Declaration.new(PROP_FONT, shorthand_value, important)
|
|
1183
|
-
end
|
|
1190
|
+
replace_with_shorthand!(rule, FONT_PROPERTIES, PROP_FONT, shorthand_value, important)
|
|
1191
|
+
end
|
|
1184
1192
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
|
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 = []
|
|
1229
1221
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
|
1243
1237
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
|
1249
1245
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1246
|
+
replace_with_shorthand!(rule, BACKGROUND_PROPERTIES, PROP_BACKGROUND, shorthand_value, important)
|
|
1247
|
+
end
|
|
1252
1248
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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] }
|
|
1256
1254
|
|
|
1257
|
-
|
|
1255
|
+
# Need at least 2 properties to create shorthand
|
|
1256
|
+
return if ls_decls.length < 2
|
|
1258
1257
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
parts << prop_map[PROP_LIST_STYLE_POSITION].value if prop_map[PROP_LIST_STYLE_POSITION]
|
|
1263
|
-
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
|
|
1264
1261
|
|
|
1265
|
-
|
|
1262
|
+
important = ls_decls.first.important
|
|
1266
1263
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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]
|
|
1272
1269
|
|
|
1273
|
-
|
|
1274
|
-
#
|
|
1275
|
-
# After flattening/cascade, rules that were in the same selector list may have
|
|
1276
|
-
# different declarations. This method builds the selector_lists hash with only
|
|
1277
|
-
# rules that still match, and clears selector_list_id for diverged rules.
|
|
1278
|
-
#
|
|
1279
|
-
# @param merged_rules [Array<Rule>] Flattened rules (with new IDs assigned)
|
|
1280
|
-
# @param selector_lists [Hash] Empty hash to populate with list_id => Array of rule IDs
|
|
1281
|
-
def self.update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
1282
|
-
# Group merged rules by selector_list_id (skip rules with no list)
|
|
1283
|
-
# Note: Using manual each loop instead of .select{}.group_by for performance.
|
|
1284
|
-
# The more concise form (.select + .group_by) is ~50-60% slower due to intermediate array allocation.
|
|
1285
|
-
rules_by_list = {}
|
|
1286
|
-
merged_rules.each do |r|
|
|
1287
|
-
next unless r.selector_list_id
|
|
1288
|
-
|
|
1289
|
-
(rules_by_list[r.selector_list_id] ||= []) << r
|
|
1290
|
-
end
|
|
1270
|
+
shorthand_value = parts.join(' ')
|
|
1291
1271
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
# Skip if only one rule in list (nothing to compare)
|
|
1295
|
-
next if rules_in_list.size <= 1
|
|
1272
|
+
replace_with_shorthand!(rule, LIST_STYLE_PROPERTIES, PROP_LIST_STYLE, shorthand_value, important)
|
|
1273
|
+
end
|
|
1296
1274
|
|
|
1297
|
-
#
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
|
1300
1293
|
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1303
1314
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
1310
1323
|
end
|
|
1311
1324
|
end
|
|
1312
1325
|
|
|
1313
|
-
#
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
|
1320
1340
|
end
|
|
1321
|
-
end
|
|
1322
|
-
end
|
|
1323
1341
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
|
1337
1357
|
end
|
|
1338
|
-
end
|
|
1339
1358
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
:expand_font, :is_font_size?, :is_font_style?, :is_font_variant?, :is_font_weight?,
|
|
1346
|
-
:expand_background, :starts_with_url?, :is_position_value?, :expand_list_style,
|
|
1347
|
-
:recreate_shorthands!, :recreate_margin!, :recreate_padding!, :check_all_same?,
|
|
1348
|
-
:recreate_border!, :recreate_border_width!, :recreate_border_style!, :recreate_border_color!,
|
|
1349
|
-
:optimize_four_sides, :recreate_font!, :recreate_background!, :recreate_list_style!,
|
|
1350
|
-
:update_selector_lists_for_divergence!, :declarations_equal?
|
|
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
|
|
1351
1364
|
end
|
|
1352
1365
|
end
|