cataract 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +9 -0
- data/ext/cataract/cataract.c +72 -103
- data/ext/cataract/cataract.h +0 -1
- data/ext/cataract/css_parser.c +0 -18
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1197 -1182
- data/lib/cataract/pure/parser.rb +1725 -1729
- data/lib/cataract/pure/serializer.rb +575 -715
- data/lib/cataract/pure/specificity.rb +165 -144
- data/lib/cataract/pure.rb +73 -101
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +19 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +3 -2
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -4,159 +4,180 @@
|
|
|
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
|
+
element_count += 1
|
|
82
|
+
i = skip_identifier(selector, i + 1, len)
|
|
57
83
|
else
|
|
58
|
-
|
|
84
|
+
# Whitespace, combinators, and the universal selector (*) all have
|
|
85
|
+
# zero specificity - just skip a single byte.
|
|
86
|
+
i += 1
|
|
59
87
|
end
|
|
60
88
|
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# zero specificity - just skip a single byte.
|
|
67
|
-
i += 1
|
|
89
|
+
|
|
90
|
+
# Calculate specificity using W3C formula
|
|
91
|
+
(id_count * 100) +
|
|
92
|
+
((class_count + attr_count + pseudo_class_count) * 10) +
|
|
93
|
+
((element_count + pseudo_element_count) * 1)
|
|
68
94
|
end
|
|
69
|
-
end
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
# Advances past an identifier (used after #id, .class, element names, and
|
|
97
|
+
# pseudo names), returning the index of the first non-identifier byte.
|
|
98
|
+
def skip_identifier(selector, pos, len)
|
|
99
|
+
pos += 1 while pos < len && ident_char?(selector.getbyte(pos))
|
|
100
|
+
pos
|
|
101
|
+
end
|
|
102
|
+
private :skip_identifier
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
# Advances past a bracketed [attr] selector, honoring nested brackets,
|
|
105
|
+
# returning the index just after the matching closing bracket.
|
|
106
|
+
def skip_attribute_selector(selector, pos, len)
|
|
107
|
+
skip_balanced(selector, pos + 1, len, BYTE_LBRACKET, BYTE_RBRACKET)
|
|
108
|
+
end
|
|
109
|
+
private :skip_attribute_selector
|
|
110
|
+
|
|
111
|
+
# Advances past a balanced open/close byte pair (already past the opening
|
|
112
|
+
# byte, with depth 1), returning the index just after the matching close.
|
|
113
|
+
def skip_balanced(selector, pos, len, open_byte, close_byte)
|
|
114
|
+
depth = 1
|
|
115
|
+
while pos < len && depth > 0
|
|
116
|
+
b = selector.getbyte(pos)
|
|
117
|
+
depth += 1 if b == open_byte
|
|
118
|
+
depth -= 1 if b == close_byte
|
|
119
|
+
pos += 1
|
|
120
|
+
end
|
|
121
|
+
pos
|
|
122
|
+
end
|
|
123
|
+
private :skip_balanced
|
|
124
|
+
|
|
125
|
+
# Advances past a balanced open/close byte pair (already past the opening
|
|
126
|
+
# byte, with depth 1), returning the index OF the matching close byte
|
|
127
|
+
# (rather than past it), so the caller can capture the content in between.
|
|
128
|
+
def find_balanced_close(selector, pos, len, open_byte, close_byte)
|
|
129
|
+
depth = 1
|
|
130
|
+
while pos < len && depth > 0
|
|
131
|
+
b = selector.getbyte(pos)
|
|
132
|
+
depth += 1 if b == open_byte
|
|
133
|
+
depth -= 1 if b == close_byte
|
|
134
|
+
pos += 1 if depth > 0
|
|
135
|
+
end
|
|
136
|
+
pos
|
|
137
|
+
end
|
|
138
|
+
private :find_balanced_close
|
|
139
|
+
|
|
140
|
+
# Parses a :pseudo-class, ::pseudo-element, or :not(...) token starting at
|
|
141
|
+
# the colon byte. Returns [new_pos, is_not, not_content, counts_as_element]:
|
|
142
|
+
# - new_pos: index just after the fully-consumed token (incl. any (...) args)
|
|
143
|
+
# - is_not: whether this token is :not (which never counts itself)
|
|
144
|
+
# - not_content: the non-empty content of :not(...), or nil otherwise
|
|
145
|
+
# - counts_as_element: whether this token counts toward pseudo-elements
|
|
146
|
+
# (::foo, or a legacy single-colon pseudo-element like :before) rather
|
|
147
|
+
# than pseudo-classes
|
|
148
|
+
def parse_pseudo(selector, pos, len)
|
|
149
|
+
pos += 1
|
|
150
|
+
is_pseudo_element = false
|
|
151
|
+
if pos < len && selector.getbyte(pos) == BYTE_COLON
|
|
152
|
+
is_pseudo_element = true
|
|
153
|
+
pos += 1
|
|
154
|
+
end
|
|
84
155
|
|
|
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
|
|
156
|
+
pseudo_start = pos
|
|
157
|
+
pos = skip_identifier(selector, pos, len)
|
|
158
|
+
pseudo_name = selector[pseudo_start...pos]
|
|
159
|
+
|
|
160
|
+
is_legacy_pseudo_element = !is_pseudo_element && !pseudo_name.empty? &&
|
|
161
|
+
PSEUDO_ELEMENT_KEYWORDS.include?(pseudo_name)
|
|
162
|
+
is_not = (pseudo_name == 'not')
|
|
163
|
+
not_content = nil
|
|
164
|
+
|
|
165
|
+
if pos < len && selector.getbyte(pos) == BYTE_LPAREN
|
|
166
|
+
pos += 1
|
|
167
|
+
if is_not
|
|
168
|
+
content_start = pos
|
|
169
|
+
pos = find_balanced_close(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
170
|
+
content = selector[content_start...pos]
|
|
171
|
+
not_content = content unless content.empty?
|
|
172
|
+
pos += 1 # Skip closing paren
|
|
173
|
+
else
|
|
174
|
+
pos = skip_balanced(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
136
177
|
|
|
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)
|
|
178
|
+
[pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
|
|
156
179
|
end
|
|
180
|
+
private :parse_pseudo
|
|
157
181
|
end
|
|
158
|
-
|
|
159
|
-
[pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
|
|
160
182
|
end
|
|
161
|
-
private_class_method :parse_pseudo
|
|
162
183
|
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,7 +32,7 @@ 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'
|
|
@@ -35,115 +43,79 @@ require_relative 'stylesheet'
|
|
|
35
43
|
require_relative 'declarations'
|
|
36
44
|
require_relative 'import_resolver'
|
|
37
45
|
|
|
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
46
|
# Load pure Ruby implementation modules
|
|
58
47
|
require_relative 'pure/byte_constants'
|
|
59
|
-
require_relative 'pure/helpers'
|
|
60
48
|
require_relative 'pure/specificity'
|
|
61
49
|
require_relative 'pure/serializer'
|
|
62
50
|
require_relative 'pure/parser'
|
|
63
51
|
require_relative 'pure/flatten'
|
|
52
|
+
require_relative 'pure/declarations'
|
|
64
53
|
|
|
65
54
|
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
|
|
55
|
+
module Backends
|
|
56
|
+
class PureImpl
|
|
57
|
+
# Flag to indicate the pure Ruby backend is loaded
|
|
58
|
+
PURE_RUBY_LOADED = true
|
|
59
|
+
|
|
60
|
+
# Implementation type constant
|
|
61
|
+
IMPLEMENTATION = :ruby
|
|
62
|
+
|
|
63
|
+
# Compile flags (mimic C version)
|
|
64
|
+
COMPILE_FLAGS = {
|
|
65
|
+
debug: false,
|
|
66
|
+
str_buf_optimization: false,
|
|
67
|
+
pure_ruby: true
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
# Parse CSS string and return hash with rules, media_index, charset, etc.
|
|
71
|
+
# Called by Stylesheet#add_block - not meant for direct use.
|
|
72
|
+
#
|
|
73
|
+
# @param css_string [String] CSS to parse
|
|
74
|
+
# @param parser_options [Hash] Parser configuration options
|
|
75
|
+
# @option parser_options [Boolean] :selector_lists (true) Track selector lists
|
|
76
|
+
# @return [Hash] {
|
|
77
|
+
# rules: Array<Rule>, # Flat array of Rule/AtRule structs
|
|
78
|
+
# _media_index: Hash, # Symbol => Array of rule IDs
|
|
79
|
+
# charset: String|nil, # @charset value if present
|
|
80
|
+
# _has_nesting: Boolean # Whether any nested rules exist
|
|
81
|
+
# }
|
|
82
|
+
def parse(css_string, parser_options = {})
|
|
83
|
+
parser = Parser.new(css_string, parser_options: parser_options)
|
|
84
|
+
parser.parse
|
|
85
|
+
end
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
87
|
+
# Flatten stylesheet rules according to CSS cascade rules
|
|
88
|
+
#
|
|
89
|
+
# @param stylesheet [Stylesheet] Stylesheet to flatten
|
|
90
|
+
# @return [Stylesheet] New stylesheet with flattened rules
|
|
91
|
+
def flatten(stylesheet)
|
|
92
|
+
Flatten.flatten(stylesheet, mutate: false)
|
|
93
|
+
end
|
|
123
94
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
95
|
+
# Expand a single shorthand declaration into longhand declarations.
|
|
96
|
+
# Called by Rule#expanded_declarations - not meant for direct use.
|
|
97
|
+
#
|
|
98
|
+
# @param decl [Declaration] Declaration to expand
|
|
99
|
+
# @return [Array<Declaration>] Array of expanded longhand declarations
|
|
100
|
+
def expand_shorthand(decl)
|
|
101
|
+
Flatten.expand_shorthand(decl)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
129
104
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def self.expand_shorthand(decl)
|
|
137
|
-
Flatten.expand_shorthand(decl)
|
|
105
|
+
# The active-facing constant is a single frozen instance, not the class
|
|
106
|
+
# itself - none of PureImpl's methods touch instance state, so one shared
|
|
107
|
+
# instance is exactly as safe as the bare module this replaces, while
|
|
108
|
+
# giving its methods genuine instance-level `private` instead of
|
|
109
|
+
# `private_class_method`.
|
|
110
|
+
Pure = PureImpl.new.freeze
|
|
138
111
|
end
|
|
139
112
|
|
|
140
|
-
# Add stub method to Stylesheet for pure Ruby implementation
|
|
141
113
|
class Stylesheet
|
|
142
|
-
# Color conversion
|
|
114
|
+
# Color conversion has no pure-Ruby implementation.
|
|
143
115
|
#
|
|
144
|
-
# @raise [NotImplementedError] Always raises -
|
|
116
|
+
# @raise [NotImplementedError] Always raises - not implemented for pure Ruby
|
|
145
117
|
def convert_colors!(*_args)
|
|
146
|
-
raise NotImplementedError, 'convert_colors! is
|
|
118
|
+
raise NotImplementedError, 'convert_colors! is not yet implemented in Cataract'
|
|
147
119
|
end
|
|
148
120
|
end
|
|
149
121
|
end
|
data/lib/cataract/rule.rb
CHANGED
|
@@ -87,8 +87,11 @@ module Cataract
|
|
|
87
87
|
def specificity
|
|
88
88
|
return self[:specificity] unless self[:specificity].nil?
|
|
89
89
|
|
|
90
|
-
# Calculate and cache
|
|
91
|
-
|
|
90
|
+
# Calculate and cache. Specificity is a pure function of the selector
|
|
91
|
+
# string, identical across backends by design, so this always goes
|
|
92
|
+
# through the process's active backend regardless of which backend
|
|
93
|
+
# actually produced this Rule.
|
|
94
|
+
calculated = Cataract::Backends.active.calculate_specificity(selector)
|
|
92
95
|
self[:specificity] = calculated
|
|
93
96
|
calculated
|
|
94
97
|
end
|
|
@@ -215,7 +218,7 @@ module Cataract
|
|
|
215
218
|
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
216
219
|
def expanded_declarations
|
|
217
220
|
@_expanded_declarations ||= begin
|
|
218
|
-
expanded = declarations.flat_map { |decl| Cataract.expand_shorthand(decl) }
|
|
221
|
+
expanded = declarations.flat_map { |decl| Cataract::Backends.active.expand_shorthand(decl) }
|
|
219
222
|
expanded.sort_by! { |d| [d.property, d.value, d.important ? 1 : 0] }
|
|
220
223
|
expanded
|
|
221
224
|
end
|
data/lib/cataract/stylesheet.rb
CHANGED
|
@@ -112,6 +112,13 @@ module Cataract
|
|
|
112
112
|
# Type validation
|
|
113
113
|
raise TypeError, "options must be a Hash, got #{options.class}" unless options.is_a?(Hash)
|
|
114
114
|
|
|
115
|
+
# Which backend (Cataract::Backends::Native / ::Pure) produced this
|
|
116
|
+
# stylesheet - defaults to whichever this process picked as active.
|
|
117
|
+
# Not part of the public options contract (used internally by
|
|
118
|
+
# Backends::Native.parse / Backends::Pure.parse), so it's popped off
|
|
119
|
+
# before @options is built.
|
|
120
|
+
@backend = options.delete(:backend) || Cataract::Backends.active
|
|
121
|
+
|
|
115
122
|
# Support :imports as alias for :import (backwards compatibility)
|
|
116
123
|
options[:import] = options.delete(:imports) if options.key?(:imports) && !options.key?(:import)
|
|
117
124
|
|
|
@@ -169,6 +176,7 @@ module Cataract
|
|
|
169
176
|
# @param source [Stylesheet] Source stylesheet being copied
|
|
170
177
|
def initialize_copy(source)
|
|
171
178
|
super
|
|
179
|
+
@backend = source.backend
|
|
172
180
|
@options = source.options.dup
|
|
173
181
|
@rules = source.rules.dup
|
|
174
182
|
@media_queries = source.media_queries.dup
|
|
@@ -466,7 +474,7 @@ module Cataract
|
|
|
466
474
|
# @example Filter to multiple media types
|
|
467
475
|
# sheet.to_s(media: [:screen, :print]) # => "@media screen { ... } @media print { ... }"
|
|
468
476
|
def to_s(media: :all)
|
|
469
|
-
|
|
477
|
+
@backend.stylesheet_to_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists)
|
|
470
478
|
end
|
|
471
479
|
alias to_css to_s
|
|
472
480
|
|
|
@@ -492,7 +500,7 @@ module Cataract
|
|
|
492
500
|
#
|
|
493
501
|
# @see #to_s For compact single-line output
|
|
494
502
|
def to_formatted_s(media: :all)
|
|
495
|
-
|
|
503
|
+
@backend.stylesheet_to_formatted_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists)
|
|
496
504
|
end
|
|
497
505
|
|
|
498
506
|
# Get number of rules
|
|
@@ -718,7 +726,7 @@ module Cataract
|
|
|
718
726
|
parse_options = build_parse_options(effective_base_uri, effective_absolute_paths)
|
|
719
727
|
|
|
720
728
|
# Parse CSS first (this extracts @import statements into result[:imports])
|
|
721
|
-
result =
|
|
729
|
+
result = @backend.parse(css, parse_options)
|
|
722
730
|
|
|
723
731
|
merge_parsed_block!(result, effective_base_uri, effective_base_dir)
|
|
724
732
|
|
|
@@ -794,7 +802,9 @@ module Cataract
|
|
|
794
802
|
#
|
|
795
803
|
# @return [Stylesheet] New stylesheet with cascade applied
|
|
796
804
|
def flatten
|
|
797
|
-
|
|
805
|
+
result = @backend.flatten(self)
|
|
806
|
+
result.backend = @backend
|
|
807
|
+
result
|
|
798
808
|
end
|
|
799
809
|
alias cascade flatten
|
|
800
810
|
|
|
@@ -813,7 +823,7 @@ module Cataract
|
|
|
813
823
|
#
|
|
814
824
|
# @return [self] Returns self for method chaining
|
|
815
825
|
def flatten!
|
|
816
|
-
flattened =
|
|
826
|
+
flattened = @backend.flatten(self)
|
|
817
827
|
@rules = flattened.rules
|
|
818
828
|
@media_index = flattened.media_index_cache
|
|
819
829
|
@_has_nesting = flattened.has_nesting
|
|
@@ -930,6 +940,7 @@ module Cataract
|
|
|
930
940
|
# raw cache these need).
|
|
931
941
|
|
|
932
942
|
attr_reader :options, :parser_options
|
|
943
|
+
attr_accessor :backend
|
|
933
944
|
|
|
934
945
|
def next_media_query_id
|
|
935
946
|
@_next_media_query_id
|
|
@@ -1080,7 +1091,7 @@ module Cataract
|
|
|
1080
1091
|
# rules, media queries, selector/media-query lists, and index, then
|
|
1081
1092
|
# resolve any @import statements it contained.
|
|
1082
1093
|
#
|
|
1083
|
-
# @param result [Hash] Return value of
|
|
1094
|
+
# @param result [Hash] Return value of the backend's parse
|
|
1084
1095
|
# @return [void]
|
|
1085
1096
|
def merge_parsed_block!(result, effective_base_uri, effective_base_dir)
|
|
1086
1097
|
offset = @_last_rule_id || 0
|
|
@@ -1263,7 +1274,8 @@ module Cataract
|
|
|
1263
1274
|
# Build parse options for imported CSS
|
|
1264
1275
|
parse_opts = {
|
|
1265
1276
|
import: opts.merge(imported_urls: imported_urls_copy, depth: depth + 1, base_uri: imported_base_uri),
|
|
1266
|
-
parser: @parser_options.dup # Inherit parent's parser options (including selector_lists)
|
|
1277
|
+
parser: @parser_options.dup, # Inherit parent's parser options (including selector_lists)
|
|
1278
|
+
backend: @backend # Imported stylesheets are produced by the same backend as their parent
|
|
1267
1279
|
}
|
|
1268
1280
|
|
|
1269
1281
|
# If URL conversion is enabled (base_uri present), enable it for imported files too
|