cataract 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/.yardopts +9 -0
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +17 -0
- data/ext/cataract/cataract.c +193 -111
- data/ext/cataract/cataract.h +13 -3
- data/ext/cataract/css_parser.c +298 -54
- data/ext/cataract/flatten.c +6 -2
- data/ext/cataract/specificity.c +13 -2
- data/lib/cataract/at_rule.rb +4 -2
- data/lib/cataract/conditional_group.rb +72 -0
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +31 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1203 -1182
- data/lib/cataract/pure/parser.rb +1953 -1715
- data/lib/cataract/pure/serializer.rb +653 -706
- data/lib/cataract/pure/specificity.rb +173 -144
- data/lib/cataract/pure.rb +74 -101
- data/lib/cataract/rule.rb +14 -7
- data/lib/cataract/stylesheet.rb +84 -25
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +5 -2
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -4,159 +4,188 @@
|
|
|
4
4
|
# NO REGEXP ALLOWED - char-by-char parsing only
|
|
5
5
|
|
|
6
6
|
module Cataract
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
7
|
+
module Backends
|
|
8
|
+
class PureImpl
|
|
9
|
+
# Legacy CSS2 pseudo-elements written with a single colon (e.g. :before)
|
|
10
|
+
# that must still be counted as pseudo-elements, not pseudo-classes.
|
|
11
|
+
PSEUDO_ELEMENT_KEYWORDS = %w[before after first-line first-letter selection].freeze
|
|
12
|
+
|
|
13
|
+
# Check if byte is a letter (a-z, A-Z)
|
|
14
|
+
# @param byte [Integer] Byte value from String#getbyte
|
|
15
|
+
# @return [Boolean] true if letter
|
|
16
|
+
def letter?(byte)
|
|
17
|
+
(byte >= BYTE_LOWER_A && byte <= BYTE_LOWER_Z) ||
|
|
18
|
+
(byte >= BYTE_UPPER_A && byte <= BYTE_UPPER_Z)
|
|
19
|
+
end
|
|
20
|
+
private :letter?
|
|
21
|
+
|
|
22
|
+
# Check if byte is alphanumeric, hyphen, or underscore (CSS identifier char)
|
|
23
|
+
# @param byte [Integer] Byte value from String#getbyte
|
|
24
|
+
# @return [Boolean] true if valid identifier character
|
|
25
|
+
def ident_char?(byte)
|
|
26
|
+
letter?(byte) || (byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9) || byte == BYTE_HYPHEN || byte == BYTE_UNDERSCORE
|
|
27
|
+
end
|
|
28
|
+
private :ident_char?
|
|
29
|
+
|
|
30
|
+
# Calculate CSS specificity for a selector
|
|
31
|
+
#
|
|
32
|
+
# @param selector [String] CSS selector
|
|
33
|
+
# @return [Integer] Specificity value
|
|
34
|
+
#
|
|
35
|
+
# Specificity calculation (per CSS spec):
|
|
36
|
+
# - Count IDs (#id) - each worth 100
|
|
37
|
+
# - Count classes/attributes/pseudo-classes (.class, [attr], :pseudo) - each worth 10
|
|
38
|
+
# - Count elements/pseudo-elements (div, ::before) - each worth 1
|
|
39
|
+
def calculate_specificity(selector)
|
|
40
|
+
return 0 if selector.nil? || selector.empty?
|
|
41
|
+
|
|
42
|
+
# Counters for specificity components
|
|
43
|
+
id_count = 0
|
|
44
|
+
class_count = 0
|
|
45
|
+
attr_count = 0
|
|
46
|
+
pseudo_class_count = 0
|
|
47
|
+
pseudo_element_count = 0
|
|
48
|
+
element_count = 0
|
|
49
|
+
|
|
50
|
+
i = 0
|
|
51
|
+
len = selector.length
|
|
52
|
+
|
|
53
|
+
while i < len
|
|
54
|
+
byte = selector.getbyte(i)
|
|
55
|
+
|
|
56
|
+
if byte == BYTE_HASH
|
|
57
|
+
id_count += 1
|
|
58
|
+
i = skip_identifier(selector, i + 1, len)
|
|
59
|
+
elsif byte == BYTE_DOT
|
|
60
|
+
class_count += 1
|
|
61
|
+
i = skip_identifier(selector, i + 1, len)
|
|
62
|
+
elsif byte == BYTE_LBRACKET
|
|
63
|
+
attr_count += 1
|
|
64
|
+
i = skip_attribute_selector(selector, i, len)
|
|
65
|
+
elsif byte == BYTE_COLON
|
|
66
|
+
i, is_not, not_content, counts_as_element = parse_pseudo(selector, i, len)
|
|
67
|
+
if not_content
|
|
68
|
+
# :not() doesn't count itself, but its content does
|
|
69
|
+
not_specificity = calculate_specificity(not_content)
|
|
70
|
+
id_count += not_specificity / 100
|
|
71
|
+
class_count += (not_specificity % 100) / 10
|
|
72
|
+
element_count += not_specificity % 10
|
|
73
|
+
elsif !is_not
|
|
74
|
+
if counts_as_element
|
|
75
|
+
pseudo_element_count += 1
|
|
76
|
+
else
|
|
77
|
+
pseudo_class_count += 1
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
elsif letter?(byte)
|
|
81
|
+
ident_end = skip_identifier(selector, i + 1, len)
|
|
82
|
+
if ident_end < len && selector.getbyte(ident_end) == BYTE_PIPE
|
|
83
|
+
# Namespace prefix (ns|E, css-namespaces-3 qname grammar) -
|
|
84
|
+
# contributes nothing itself; the local name after '|' is the
|
|
85
|
+
# real type selector and gets counted on the next iteration.
|
|
86
|
+
i = ident_end + 1
|
|
87
|
+
else
|
|
88
|
+
element_count += 1
|
|
89
|
+
i = ident_end
|
|
90
|
+
end
|
|
57
91
|
else
|
|
58
|
-
|
|
92
|
+
# Whitespace, combinators, and the universal selector (*) all have
|
|
93
|
+
# zero specificity - just skip a single byte.
|
|
94
|
+
i += 1
|
|
59
95
|
end
|
|
60
96
|
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# zero specificity - just skip a single byte.
|
|
67
|
-
i += 1
|
|
97
|
+
|
|
98
|
+
# Calculate specificity using W3C formula
|
|
99
|
+
(id_count * 100) +
|
|
100
|
+
((class_count + attr_count + pseudo_class_count) * 10) +
|
|
101
|
+
((element_count + pseudo_element_count) * 1)
|
|
68
102
|
end
|
|
69
|
-
end
|
|
70
103
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
# Advances past an identifier (used after #id, .class, element names, and
|
|
105
|
+
# pseudo names), returning the index of the first non-identifier byte.
|
|
106
|
+
def skip_identifier(selector, pos, len)
|
|
107
|
+
pos += 1 while pos < len && ident_char?(selector.getbyte(pos))
|
|
108
|
+
pos
|
|
109
|
+
end
|
|
110
|
+
private :skip_identifier
|
|
76
111
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
# Advances past a bracketed [attr] selector, honoring nested brackets,
|
|
113
|
+
# returning the index just after the matching closing bracket.
|
|
114
|
+
def skip_attribute_selector(selector, pos, len)
|
|
115
|
+
skip_balanced(selector, pos + 1, len, BYTE_LBRACKET, BYTE_RBRACKET)
|
|
116
|
+
end
|
|
117
|
+
private :skip_attribute_selector
|
|
118
|
+
|
|
119
|
+
# Advances past a balanced open/close byte pair (already past the opening
|
|
120
|
+
# byte, with depth 1), returning the index just after the matching close.
|
|
121
|
+
def skip_balanced(selector, pos, len, open_byte, close_byte)
|
|
122
|
+
depth = 1
|
|
123
|
+
while pos < len && depth > 0
|
|
124
|
+
b = selector.getbyte(pos)
|
|
125
|
+
depth += 1 if b == open_byte
|
|
126
|
+
depth -= 1 if b == close_byte
|
|
127
|
+
pos += 1
|
|
128
|
+
end
|
|
129
|
+
pos
|
|
130
|
+
end
|
|
131
|
+
private :skip_balanced
|
|
132
|
+
|
|
133
|
+
# Advances past a balanced open/close byte pair (already past the opening
|
|
134
|
+
# byte, with depth 1), returning the index OF the matching close byte
|
|
135
|
+
# (rather than past it), so the caller can capture the content in between.
|
|
136
|
+
def find_balanced_close(selector, pos, len, open_byte, close_byte)
|
|
137
|
+
depth = 1
|
|
138
|
+
while pos < len && depth > 0
|
|
139
|
+
b = selector.getbyte(pos)
|
|
140
|
+
depth += 1 if b == open_byte
|
|
141
|
+
depth -= 1 if b == close_byte
|
|
142
|
+
pos += 1 if depth > 0
|
|
143
|
+
end
|
|
144
|
+
pos
|
|
145
|
+
end
|
|
146
|
+
private :find_balanced_close
|
|
147
|
+
|
|
148
|
+
# Parses a :pseudo-class, ::pseudo-element, or :not(...) token starting at
|
|
149
|
+
# the colon byte. Returns [new_pos, is_not, not_content, counts_as_element]:
|
|
150
|
+
# - new_pos: index just after the fully-consumed token (incl. any (...) args)
|
|
151
|
+
# - is_not: whether this token is :not (which never counts itself)
|
|
152
|
+
# - not_content: the non-empty content of :not(...), or nil otherwise
|
|
153
|
+
# - counts_as_element: whether this token counts toward pseudo-elements
|
|
154
|
+
# (::foo, or a legacy single-colon pseudo-element like :before) rather
|
|
155
|
+
# than pseudo-classes
|
|
156
|
+
def parse_pseudo(selector, pos, len)
|
|
157
|
+
pos += 1
|
|
158
|
+
is_pseudo_element = false
|
|
159
|
+
if pos < len && selector.getbyte(pos) == BYTE_COLON
|
|
160
|
+
is_pseudo_element = true
|
|
161
|
+
pos += 1
|
|
162
|
+
end
|
|
84
163
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# Advances past a balanced open/close byte pair (already past the opening
|
|
107
|
-
# byte, with depth 1), returning the index OF the matching close byte
|
|
108
|
-
# (rather than past it), so the caller can capture the content in between.
|
|
109
|
-
def self.find_balanced_close(selector, pos, len, open_byte, close_byte)
|
|
110
|
-
depth = 1
|
|
111
|
-
while pos < len && depth > 0
|
|
112
|
-
b = selector.getbyte(pos)
|
|
113
|
-
depth += 1 if b == open_byte
|
|
114
|
-
depth -= 1 if b == close_byte
|
|
115
|
-
pos += 1 if depth > 0
|
|
116
|
-
end
|
|
117
|
-
pos
|
|
118
|
-
end
|
|
119
|
-
private_class_method :find_balanced_close
|
|
120
|
-
|
|
121
|
-
# Parses a :pseudo-class, ::pseudo-element, or :not(...) token starting at
|
|
122
|
-
# the colon byte. Returns [new_pos, is_not, not_content, counts_as_element]:
|
|
123
|
-
# - new_pos: index just after the fully-consumed token (incl. any (...) args)
|
|
124
|
-
# - is_not: whether this token is :not (which never counts itself)
|
|
125
|
-
# - not_content: the non-empty content of :not(...), or nil otherwise
|
|
126
|
-
# - counts_as_element: whether this token counts toward pseudo-elements
|
|
127
|
-
# (::foo, or a legacy single-colon pseudo-element like :before) rather
|
|
128
|
-
# than pseudo-classes
|
|
129
|
-
def self.parse_pseudo(selector, pos, len)
|
|
130
|
-
pos += 1
|
|
131
|
-
is_pseudo_element = false
|
|
132
|
-
if pos < len && selector.getbyte(pos) == BYTE_COLON
|
|
133
|
-
is_pseudo_element = true
|
|
134
|
-
pos += 1
|
|
135
|
-
end
|
|
164
|
+
pseudo_start = pos
|
|
165
|
+
pos = skip_identifier(selector, pos, len)
|
|
166
|
+
pseudo_name = selector[pseudo_start...pos]
|
|
167
|
+
|
|
168
|
+
is_legacy_pseudo_element = !is_pseudo_element && !pseudo_name.empty? &&
|
|
169
|
+
PSEUDO_ELEMENT_KEYWORDS.include?(pseudo_name)
|
|
170
|
+
is_not = (pseudo_name == 'not')
|
|
171
|
+
not_content = nil
|
|
172
|
+
|
|
173
|
+
if pos < len && selector.getbyte(pos) == BYTE_LPAREN
|
|
174
|
+
pos += 1
|
|
175
|
+
if is_not
|
|
176
|
+
content_start = pos
|
|
177
|
+
pos = find_balanced_close(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
178
|
+
content = selector[content_start...pos]
|
|
179
|
+
not_content = content unless content.empty?
|
|
180
|
+
pos += 1 # Skip closing paren
|
|
181
|
+
else
|
|
182
|
+
pos = skip_balanced(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
136
185
|
|
|
137
|
-
|
|
138
|
-
pos = skip_identifier(selector, pos, len)
|
|
139
|
-
pseudo_name = selector[pseudo_start...pos]
|
|
140
|
-
|
|
141
|
-
is_legacy_pseudo_element = !is_pseudo_element && !pseudo_name.empty? &&
|
|
142
|
-
PSEUDO_ELEMENT_KEYWORDS.include?(pseudo_name)
|
|
143
|
-
is_not = (pseudo_name == 'not')
|
|
144
|
-
not_content = nil
|
|
145
|
-
|
|
146
|
-
if pos < len && selector.getbyte(pos) == BYTE_LPAREN
|
|
147
|
-
pos += 1
|
|
148
|
-
if is_not
|
|
149
|
-
content_start = pos
|
|
150
|
-
pos = find_balanced_close(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
151
|
-
content = selector[content_start...pos]
|
|
152
|
-
not_content = content unless content.empty?
|
|
153
|
-
pos += 1 # Skip closing paren
|
|
154
|
-
else
|
|
155
|
-
pos = skip_balanced(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
186
|
+
[pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
|
|
156
187
|
end
|
|
188
|
+
private :parse_pseudo
|
|
157
189
|
end
|
|
158
|
-
|
|
159
|
-
[pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
|
|
160
190
|
end
|
|
161
|
-
private_class_method :parse_pseudo
|
|
162
191
|
end
|
data/lib/cataract/pure.rb
CHANGED
|
@@ -7,16 +7,24 @@
|
|
|
7
7
|
# NO REGEXP ALLOWED - consume chars one at a time like the C version.
|
|
8
8
|
# ==================================================================
|
|
9
9
|
#
|
|
10
|
-
# Load this instead of the C extension
|
|
11
|
-
#
|
|
10
|
+
# Load this instead of the C extension by setting CATARACT_PURE before
|
|
11
|
+
# requiring 'cataract' (not by requiring this file directly - lib/cataract.rb
|
|
12
|
+
# is what sets up Cataract::Backends.active and the top-level identity
|
|
13
|
+
# constants like IMPLEMENTATION; this file alone does not):
|
|
14
|
+
# ENV['CATARACT_PURE'] = '1'
|
|
15
|
+
# require 'cataract'
|
|
12
16
|
#
|
|
13
17
|
# Or run tests with:
|
|
14
18
|
# CATARACT_PURE=1 rake test
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
#
|
|
20
|
+
# Everything backend-specific here lives under Cataract::Backends::Pure -
|
|
21
|
+
# nothing is defined directly on Cataract itself, so this file can be loaded
|
|
22
|
+
# in the same process as the native backend without either clobbering the
|
|
23
|
+
# other. The one exception is Stylesheet#convert_colors! below: color
|
|
24
|
+
# conversion has no pure-Ruby implementation at all, so the stub simply
|
|
25
|
+
# raises - native's real implementation (ext/cataract_color, loaded
|
|
26
|
+
# separately and only opt-in) defines its own convert_colors! and is
|
|
27
|
+
# unaffected by this file being loaded or not.
|
|
20
28
|
|
|
21
29
|
require_relative 'error'
|
|
22
30
|
|
|
@@ -24,126 +32,91 @@ require_relative 'version'
|
|
|
24
32
|
require_relative 'constants'
|
|
25
33
|
|
|
26
34
|
# Load struct definitions and supporting files
|
|
27
|
-
# (These are also loaded by lib/cataract.rb, but we need them here for direct require)
|
|
35
|
+
# (These are also loaded by lib/cataract/native.rb, but we need them here for direct require)
|
|
28
36
|
require_relative 'declaration'
|
|
29
37
|
require_relative 'rule'
|
|
30
38
|
require_relative 'at_rule'
|
|
31
39
|
require_relative 'media_query'
|
|
40
|
+
require_relative 'conditional_group'
|
|
32
41
|
require_relative 'import_statement'
|
|
33
42
|
require_relative 'stylesheet_scope'
|
|
34
43
|
require_relative 'stylesheet'
|
|
35
44
|
require_relative 'declarations'
|
|
36
45
|
require_relative 'import_resolver'
|
|
37
46
|
|
|
38
|
-
# Add to_s method to Declarations class for pure Ruby mode
|
|
39
|
-
module Cataract
|
|
40
|
-
class Declarations
|
|
41
|
-
# Serialize declarations to CSS string
|
|
42
|
-
def to_s
|
|
43
|
-
result = String.new
|
|
44
|
-
@values.each_with_index do |decl, i|
|
|
45
|
-
result << decl.property
|
|
46
|
-
result << ': '
|
|
47
|
-
result << decl.value
|
|
48
|
-
result << ' !important' if decl.important
|
|
49
|
-
result << ';'
|
|
50
|
-
result << ' ' if i < @values.length - 1 # Add space after semicolon except for last
|
|
51
|
-
end
|
|
52
|
-
result
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
47
|
# Load pure Ruby implementation modules
|
|
58
48
|
require_relative 'pure/byte_constants'
|
|
59
|
-
require_relative 'pure/helpers'
|
|
60
49
|
require_relative 'pure/specificity'
|
|
61
50
|
require_relative 'pure/serializer'
|
|
62
51
|
require_relative 'pure/parser'
|
|
63
52
|
require_relative 'pure/flatten'
|
|
53
|
+
require_relative 'pure/declarations'
|
|
64
54
|
|
|
65
55
|
module Cataract
|
|
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
|
-
# Need to untangle this eventually
|
|
98
|
-
def self.parse_css(css, **options)
|
|
99
|
-
Stylesheet.parse(css, **options)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Flatten stylesheet rules according to CSS cascade rules
|
|
103
|
-
#
|
|
104
|
-
# @param stylesheet [Stylesheet] Stylesheet to flatten
|
|
105
|
-
# @return [Stylesheet] New stylesheet with flattened rules
|
|
106
|
-
def self.flatten(stylesheet)
|
|
107
|
-
Flatten.flatten(stylesheet, mutate: false)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Flatten stylesheet rules in-place (mutates receiver)
|
|
111
|
-
#
|
|
112
|
-
# @param stylesheet [Stylesheet] Stylesheet to flatten
|
|
113
|
-
# @return [Stylesheet] Same stylesheet (mutated)
|
|
114
|
-
def self.flatten!(stylesheet)
|
|
115
|
-
Flatten.flatten(stylesheet, mutate: true)
|
|
116
|
-
end
|
|
56
|
+
module Backends
|
|
57
|
+
class PureImpl
|
|
58
|
+
# Flag to indicate the pure Ruby backend is loaded
|
|
59
|
+
PURE_RUBY_LOADED = true
|
|
60
|
+
|
|
61
|
+
# Implementation type constant
|
|
62
|
+
IMPLEMENTATION = :ruby
|
|
63
|
+
|
|
64
|
+
# Compile flags (mimic C version)
|
|
65
|
+
COMPILE_FLAGS = {
|
|
66
|
+
debug: false,
|
|
67
|
+
str_buf_optimization: false,
|
|
68
|
+
pure_ruby: true
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
# Parse CSS string and return hash with rules, media_index, charset, etc.
|
|
72
|
+
# Called by Stylesheet#add_block - not meant for direct use.
|
|
73
|
+
#
|
|
74
|
+
# @param css_string [String] CSS to parse
|
|
75
|
+
# @param parser_options [Hash] Parser configuration options
|
|
76
|
+
# @option parser_options [Boolean] :selector_lists (true) Track selector lists
|
|
77
|
+
# @return [Hash] {
|
|
78
|
+
# rules: Array<Rule>, # Flat array of Rule/AtRule structs
|
|
79
|
+
# _media_index: Hash, # Symbol => Array of rule IDs
|
|
80
|
+
# charset: String|nil, # @charset value if present
|
|
81
|
+
# _has_nesting: Boolean # Whether any nested rules exist
|
|
82
|
+
# }
|
|
83
|
+
def parse(css_string, parser_options = {})
|
|
84
|
+
parser = Parser.new(css_string, parser_options: parser_options)
|
|
85
|
+
parser.parse
|
|
86
|
+
end
|
|
117
87
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
88
|
+
# Flatten stylesheet rules according to CSS cascade rules
|
|
89
|
+
#
|
|
90
|
+
# @param stylesheet [Stylesheet] Stylesheet to flatten
|
|
91
|
+
# @return [Stylesheet] New stylesheet with flattened rules
|
|
92
|
+
def flatten(stylesheet)
|
|
93
|
+
Flatten.flatten(stylesheet, mutate: false)
|
|
94
|
+
end
|
|
123
95
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
96
|
+
# Expand a single shorthand declaration into longhand declarations.
|
|
97
|
+
# Called by Rule#expanded_declarations - not meant for direct use.
|
|
98
|
+
#
|
|
99
|
+
# @param decl [Declaration] Declaration to expand
|
|
100
|
+
# @return [Array<Declaration>] Array of expanded longhand declarations
|
|
101
|
+
def expand_shorthand(decl)
|
|
102
|
+
Flatten.expand_shorthand(decl)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
129
105
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def self.expand_shorthand(decl)
|
|
137
|
-
Flatten.expand_shorthand(decl)
|
|
106
|
+
# The active-facing constant is a single frozen instance, not the class
|
|
107
|
+
# itself - none of PureImpl's methods touch instance state, so one shared
|
|
108
|
+
# instance is exactly as safe as the bare module this replaces, while
|
|
109
|
+
# giving its methods genuine instance-level `private` instead of
|
|
110
|
+
# `private_class_method`.
|
|
111
|
+
Pure = PureImpl.new.freeze
|
|
138
112
|
end
|
|
139
113
|
|
|
140
|
-
# Add stub method to Stylesheet for pure Ruby implementation
|
|
141
114
|
class Stylesheet
|
|
142
|
-
# Color conversion
|
|
115
|
+
# Color conversion has no pure-Ruby implementation.
|
|
143
116
|
#
|
|
144
|
-
# @raise [NotImplementedError] Always raises -
|
|
117
|
+
# @raise [NotImplementedError] Always raises - not implemented for pure Ruby
|
|
145
118
|
def convert_colors!(*_args)
|
|
146
|
-
raise NotImplementedError, 'convert_colors! is
|
|
119
|
+
raise NotImplementedError, 'convert_colors! is not yet implemented in Cataract'
|
|
147
120
|
end
|
|
148
121
|
end
|
|
149
122
|
end
|
data/lib/cataract/rule.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Cataract
|
|
|
26
26
|
# @attr [Integer, nil] nesting_style 0=implicit, 1=explicit, nil=not nested
|
|
27
27
|
# @attr [Integer, nil] selector_list_id ID linking rules from same selector list (e.g., "h1, h2")
|
|
28
28
|
# @attr [Integer, nil] media_query_id ID of the MediaQuery this rule belongs to (nil if not in media query)
|
|
29
|
+
# @attr [Integer, nil] conditional_group_id ID of the ConditionalGroup (@supports/@layer/@container/@scope) this rule belongs to (nil if not in one)
|
|
29
30
|
Rule = Struct.new(
|
|
30
31
|
:id,
|
|
31
32
|
:selector,
|
|
@@ -34,7 +35,8 @@ module Cataract
|
|
|
34
35
|
:parent_rule_id,
|
|
35
36
|
:nesting_style,
|
|
36
37
|
:selector_list_id,
|
|
37
|
-
:media_query_id
|
|
38
|
+
:media_query_id,
|
|
39
|
+
:conditional_group_id
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
class Rule
|
|
@@ -49,6 +51,7 @@ module Cataract
|
|
|
49
51
|
# @param nesting_style [Integer, nil] Nesting style (0=implicit, 1=explicit, nil=not nested)
|
|
50
52
|
# @param selector_list_id [Integer, nil] Selector list ID for grouping
|
|
51
53
|
# @param media_query_id [Integer, nil] MediaQuery ID for rules in media queries
|
|
54
|
+
# @param conditional_group_id [Integer, nil] ConditionalGroup ID for rules in @supports/@layer/@container/@scope
|
|
52
55
|
# @return [Rule] New rule instance
|
|
53
56
|
#
|
|
54
57
|
# @example Create a rule with keyword arguments
|
|
@@ -60,10 +63,11 @@ module Cataract
|
|
|
60
63
|
# parent_rule_id: nil,
|
|
61
64
|
# nesting_style: nil,
|
|
62
65
|
# selector_list_id: nil,
|
|
63
|
-
# media_query_id: nil
|
|
66
|
+
# media_query_id: nil,
|
|
67
|
+
# conditional_group_id: nil
|
|
64
68
|
# )
|
|
65
|
-
def self.make(id:, selector:, declarations:, specificity: nil, parent_rule_id: nil, nesting_style: nil, selector_list_id: nil, media_query_id: nil)
|
|
66
|
-
new(id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id)
|
|
69
|
+
def self.make(id:, selector:, declarations:, specificity: nil, parent_rule_id: nil, nesting_style: nil, selector_list_id: nil, media_query_id: nil, conditional_group_id: nil)
|
|
70
|
+
new(id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id, conditional_group_id)
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
# Silence warning about method redefinition. We redefine below to lazily calculate
|
|
@@ -87,8 +91,11 @@ module Cataract
|
|
|
87
91
|
def specificity
|
|
88
92
|
return self[:specificity] unless self[:specificity].nil?
|
|
89
93
|
|
|
90
|
-
# Calculate and cache
|
|
91
|
-
|
|
94
|
+
# Calculate and cache. Specificity is a pure function of the selector
|
|
95
|
+
# string, identical across backends by design, so this always goes
|
|
96
|
+
# through the process's active backend regardless of which backend
|
|
97
|
+
# actually produced this Rule.
|
|
98
|
+
calculated = Cataract::Backends.active.calculate_specificity(selector)
|
|
92
99
|
self[:specificity] = calculated
|
|
93
100
|
calculated
|
|
94
101
|
end
|
|
@@ -215,7 +222,7 @@ module Cataract
|
|
|
215
222
|
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
216
223
|
def expanded_declarations
|
|
217
224
|
@_expanded_declarations ||= begin
|
|
218
|
-
expanded = declarations.flat_map { |decl| Cataract.expand_shorthand(decl) }
|
|
225
|
+
expanded = declarations.flat_map { |decl| Cataract::Backends.active.expand_shorthand(decl) }
|
|
219
226
|
expanded.sort_by! { |d| [d.property, d.value, d.important ? 1 : 0] }
|
|
220
227
|
expanded
|
|
221
228
|
end
|