cataract 0.2.2 → 0.2.4
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/.rubocop.yml +7 -3
- data/BENCHMARKS.md +32 -32
- data/CHANGELOG.md +13 -0
- data/Gemfile +3 -0
- data/ext/cataract/cataract.c +219 -112
- data/ext/cataract/cataract.h +5 -1
- data/ext/cataract/css_parser.c +523 -33
- data/ext/cataract/flatten.c +233 -91
- data/ext/cataract/shorthand_expander.c +7 -0
- data/lib/cataract/at_rule.rb +2 -1
- data/lib/cataract/constants.rb +10 -0
- data/lib/cataract/import_resolver.rb +18 -87
- data/lib/cataract/import_statement.rb +29 -5
- data/lib/cataract/media_query.rb +98 -0
- data/lib/cataract/pure/byte_constants.rb +11 -0
- data/lib/cataract/pure/flatten.rb +127 -15
- data/lib/cataract/pure/parser.rb +654 -270
- data/lib/cataract/pure/serializer.rb +216 -115
- data/lib/cataract/pure.rb +6 -7
- data/lib/cataract/rule.rb +9 -5
- data/lib/cataract/stylesheet.rb +321 -99
- data/lib/cataract/stylesheet_scope.rb +10 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +4 -8
- data/lib/tasks/profile.rake +210 -0
- metadata +4 -2
- data/lib/cataract/pure/imports.rb +0 -268
|
@@ -131,10 +131,8 @@ module Cataract
|
|
|
131
131
|
|
|
132
132
|
# Get base rules set
|
|
133
133
|
rules = if @filters[:base_only]
|
|
134
|
-
# Get rules not in any media query
|
|
135
|
-
|
|
136
|
-
media_rule_ids = media_index.values.flatten.uniq
|
|
137
|
-
@stylesheet.rules.select.with_index { |_rule, idx| !media_rule_ids.include?(idx) }
|
|
134
|
+
# Get rules not in any media query (media_query_id is nil)
|
|
135
|
+
@stylesheet.rules.select { |r| r.is_a?(Rule) && r.media_query_id.nil? }
|
|
138
136
|
elsif @filters[:media]
|
|
139
137
|
media_array = Array(@filters[:media])
|
|
140
138
|
|
|
@@ -142,9 +140,14 @@ module Cataract
|
|
|
142
140
|
if media_array.include?(:all)
|
|
143
141
|
@stylesheet.rules
|
|
144
142
|
else
|
|
145
|
-
media_index
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
# Use media_index for efficient lookup (it handles compound media queries)
|
|
144
|
+
matching_rule_ids = Set.new
|
|
145
|
+
media_array.each do |media_sym|
|
|
146
|
+
rule_ids = @stylesheet.media_index[media_sym]
|
|
147
|
+
matching_rule_ids.merge(rule_ids) if rule_ids
|
|
148
|
+
end
|
|
149
|
+
# Filter rules by ID
|
|
150
|
+
@stylesheet.rules.select { |r| matching_rule_ids.include?(r.id) }
|
|
148
151
|
end
|
|
149
152
|
else
|
|
150
153
|
@stylesheet.rules
|
data/lib/cataract/version.rb
CHANGED
data/lib/cataract.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'cataract/version'
|
|
4
|
+
require_relative 'cataract/constants'
|
|
4
5
|
|
|
5
6
|
# Load struct definitions first (before C extension or pure Ruby)
|
|
6
7
|
require_relative 'cataract/declaration'
|
|
7
8
|
require_relative 'cataract/rule'
|
|
8
9
|
require_relative 'cataract/at_rule'
|
|
10
|
+
require_relative 'cataract/media_query'
|
|
9
11
|
require_relative 'cataract/import_statement'
|
|
10
12
|
|
|
11
13
|
# Load pure Ruby or C extension based on ENV var
|
|
@@ -73,14 +75,8 @@ module Cataract
|
|
|
73
75
|
# @see Stylesheet#parse
|
|
74
76
|
# @see Stylesheet.parse
|
|
75
77
|
unless method_defined?(:parse_css)
|
|
76
|
-
def parse_css(css,
|
|
77
|
-
|
|
78
|
-
# The new flow: parse first (extract @import), then resolve them
|
|
79
|
-
if imports
|
|
80
|
-
Stylesheet.parse(css, import: imports)
|
|
81
|
-
else
|
|
82
|
-
Stylesheet.parse(css)
|
|
83
|
-
end
|
|
78
|
+
def parse_css(css, **options)
|
|
79
|
+
Stylesheet.parse(css, **options)
|
|
84
80
|
end
|
|
85
81
|
end
|
|
86
82
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :profile do
|
|
4
|
+
desc 'Profile pure Ruby parser with bootstrap.css (outputs JSON for speedscope)'
|
|
5
|
+
task :parsing do
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
# Check for stackprof
|
|
9
|
+
begin
|
|
10
|
+
require 'stackprof'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
abort('stackprof gem not found. Install with: gem install stackprof')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Ensure we're using pure Ruby implementation
|
|
16
|
+
ENV['CATARACT_PURE'] = '1'
|
|
17
|
+
|
|
18
|
+
fixture_path = File.expand_path('../../test/fixtures/bootstrap.css', __dir__)
|
|
19
|
+
unless File.exist?(fixture_path)
|
|
20
|
+
abort("Fixture not found: #{fixture_path}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
output_dir = 'tmp/profile'
|
|
24
|
+
FileUtils.mkdir_p(output_dir)
|
|
25
|
+
json_output = File.join(output_dir, 'stackprof-parsing.json')
|
|
26
|
+
|
|
27
|
+
puts 'Profiling pure Ruby parser with bootstrap.css'
|
|
28
|
+
puts '=' * 80
|
|
29
|
+
puts "Fixture: #{fixture_path}"
|
|
30
|
+
puts "Output: #{json_output}"
|
|
31
|
+
puts '=' * 80
|
|
32
|
+
|
|
33
|
+
# Load the CSS content
|
|
34
|
+
css_content = File.read(fixture_path)
|
|
35
|
+
puts "CSS size: #{css_content.bytesize} bytes"
|
|
36
|
+
puts
|
|
37
|
+
|
|
38
|
+
require_relative '../../lib/cataract/pure'
|
|
39
|
+
require 'json'
|
|
40
|
+
|
|
41
|
+
# Use higher sampling rate (interval in microseconds, default is 1000)
|
|
42
|
+
# Lower interval = higher sampling rate = more detailed profile
|
|
43
|
+
profile = StackProf.run(mode: :wall, raw: true, interval: 100) do
|
|
44
|
+
# Parse multiple times to get better signal
|
|
45
|
+
100.times do
|
|
46
|
+
Cataract.parse_css(css_content)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Write JSON output
|
|
51
|
+
File.write(json_output, JSON.generate(profile))
|
|
52
|
+
|
|
53
|
+
puts
|
|
54
|
+
puts 'Profile complete!'
|
|
55
|
+
puts "JSON output: #{json_output}"
|
|
56
|
+
puts
|
|
57
|
+
puts 'View in speedscope:'
|
|
58
|
+
puts ' 1. Visit https://www.speedscope.app/'
|
|
59
|
+
puts " 2. Drag and drop: #{json_output}"
|
|
60
|
+
puts
|
|
61
|
+
puts 'Or use stackprof CLI:'
|
|
62
|
+
puts " stackprof #{json_output} --text"
|
|
63
|
+
puts " stackprof #{json_output} --method 'Cataract'"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc 'Profile pure Ruby flatten with bootstrap.css (outputs JSON for speedscope)'
|
|
67
|
+
task :flatten do
|
|
68
|
+
require 'fileutils'
|
|
69
|
+
|
|
70
|
+
# Check for stackprof
|
|
71
|
+
begin
|
|
72
|
+
require 'stackprof'
|
|
73
|
+
rescue LoadError
|
|
74
|
+
abort('stackprof gem not found. Install with: gem install stackprof')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
fixture_path = File.expand_path('../../test/fixtures/bootstrap.css', __dir__)
|
|
78
|
+
unless File.exist?(fixture_path)
|
|
79
|
+
abort("Fixture not found: #{fixture_path}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
output_dir = 'tmp/profile'
|
|
83
|
+
FileUtils.mkdir_p(output_dir)
|
|
84
|
+
json_output = File.join(output_dir, 'stackprof-flatten.json')
|
|
85
|
+
|
|
86
|
+
puts 'Profiling pure Ruby flatten with bootstrap.css'
|
|
87
|
+
puts '=' * 80
|
|
88
|
+
puts "Fixture: #{fixture_path}"
|
|
89
|
+
puts "Output: #{json_output}"
|
|
90
|
+
puts '=' * 80
|
|
91
|
+
|
|
92
|
+
# Load the CSS content
|
|
93
|
+
css_content = File.read(fixture_path)
|
|
94
|
+
puts "CSS size: #{css_content.bytesize} bytes"
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
require_relative '../../lib/cataract/pure'
|
|
98
|
+
require 'json'
|
|
99
|
+
|
|
100
|
+
# Parse once outside profiling to get stylesheet
|
|
101
|
+
stylesheet = Cataract.parse_css(css_content)
|
|
102
|
+
puts "Parsed: #{stylesheet.rules.size} rules"
|
|
103
|
+
puts
|
|
104
|
+
|
|
105
|
+
# Profile flatten only
|
|
106
|
+
# Use higher sampling rate (interval in microseconds, default is 1000)
|
|
107
|
+
# Lower interval = higher sampling rate = more detailed profile
|
|
108
|
+
profile = StackProf.run(mode: :wall, raw: true, interval: 100) do
|
|
109
|
+
# Flatten multiple times to get better signal
|
|
110
|
+
100.times do
|
|
111
|
+
Cataract.flatten(stylesheet.dup)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Write JSON output
|
|
116
|
+
File.write(json_output, JSON.generate(profile))
|
|
117
|
+
|
|
118
|
+
puts
|
|
119
|
+
puts 'Profile complete!'
|
|
120
|
+
puts "JSON output: #{json_output}"
|
|
121
|
+
puts
|
|
122
|
+
puts 'View in speedscope:'
|
|
123
|
+
puts ' 1. Visit https://www.speedscope.app/'
|
|
124
|
+
puts " 2. Drag and drop: #{json_output}"
|
|
125
|
+
puts
|
|
126
|
+
puts 'Or use stackprof CLI:'
|
|
127
|
+
puts " stackprof #{json_output} --text"
|
|
128
|
+
puts " stackprof #{json_output} --method 'Cataract'"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
desc 'Profile pure Ruby serialization with bootstrap.css (outputs JSON for speedscope)'
|
|
132
|
+
task :serialization do
|
|
133
|
+
require 'fileutils'
|
|
134
|
+
|
|
135
|
+
# Check for stackprof
|
|
136
|
+
begin
|
|
137
|
+
require 'stackprof'
|
|
138
|
+
rescue LoadError
|
|
139
|
+
abort('stackprof gem not found. Install with: gem install stackprof')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Ensure we're using pure Ruby implementation
|
|
143
|
+
ENV['CATARACT_PURE'] = '1'
|
|
144
|
+
|
|
145
|
+
fixture_path = File.expand_path('../../test/fixtures/bootstrap.css', __dir__)
|
|
146
|
+
unless File.exist?(fixture_path)
|
|
147
|
+
abort("Fixture not found: #{fixture_path}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
output_dir = 'tmp/profile'
|
|
151
|
+
FileUtils.mkdir_p(output_dir)
|
|
152
|
+
json_output = File.join(output_dir, 'stackprof-serialization.json')
|
|
153
|
+
|
|
154
|
+
puts 'Profiling pure Ruby serialization with bootstrap.css'
|
|
155
|
+
puts '=' * 80
|
|
156
|
+
puts "Fixture: #{fixture_path}"
|
|
157
|
+
puts "Output: #{json_output}"
|
|
158
|
+
puts '=' * 80
|
|
159
|
+
|
|
160
|
+
# Load the CSS content
|
|
161
|
+
css_content = File.read(fixture_path)
|
|
162
|
+
puts "CSS size: #{css_content.bytesize} bytes"
|
|
163
|
+
puts
|
|
164
|
+
|
|
165
|
+
require_relative '../../lib/cataract/pure'
|
|
166
|
+
require 'json'
|
|
167
|
+
|
|
168
|
+
# Parse once outside profiling to get stylesheet
|
|
169
|
+
stylesheet = Cataract.parse_css(css_content)
|
|
170
|
+
puts "Parsed: #{stylesheet.rules.size} rules"
|
|
171
|
+
puts
|
|
172
|
+
|
|
173
|
+
# Profile serialization only (to_s)
|
|
174
|
+
# Use higher sampling rate (interval in microseconds, default is 1000)
|
|
175
|
+
# Lower interval = higher sampling rate = more detailed profile
|
|
176
|
+
profile = StackProf.run(mode: :wall, raw: true, interval: 100) do
|
|
177
|
+
# Serialize multiple times to get better signal
|
|
178
|
+
100.times do
|
|
179
|
+
stylesheet.dup.to_s
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Write JSON output
|
|
184
|
+
File.write(json_output, JSON.generate(profile))
|
|
185
|
+
|
|
186
|
+
puts
|
|
187
|
+
puts 'Profile complete!'
|
|
188
|
+
puts "JSON output: #{json_output}"
|
|
189
|
+
puts
|
|
190
|
+
puts 'View in speedscope:'
|
|
191
|
+
puts ' 1. Visit https://www.speedscope.app/'
|
|
192
|
+
puts " 2. Drag and drop: #{json_output}"
|
|
193
|
+
puts
|
|
194
|
+
puts 'Or use stackprof CLI:'
|
|
195
|
+
puts " stackprof #{json_output} --text"
|
|
196
|
+
puts " stackprof #{json_output} --method 'Cataract'"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
desc 'Profile pure Ruby parser, flatten, and serialization (outputs JSON for speedscope)'
|
|
201
|
+
task profile: ['profile:parsing', 'profile:flatten', 'profile:serialization'] do
|
|
202
|
+
puts
|
|
203
|
+
puts '=' * 80
|
|
204
|
+
puts 'All profiles complete!'
|
|
205
|
+
puts 'Artifacts in: tmp/profile/'
|
|
206
|
+
puts ' - stackprof-parsing.json'
|
|
207
|
+
puts ' - stackprof-flatten.json'
|
|
208
|
+
puts ' - stackprof-serialization.json'
|
|
209
|
+
puts '=' * 80
|
|
210
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cataract
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
@@ -80,15 +80,16 @@ files:
|
|
|
80
80
|
- lib/cataract.rb
|
|
81
81
|
- lib/cataract/at_rule.rb
|
|
82
82
|
- lib/cataract/color_conversion.rb
|
|
83
|
+
- lib/cataract/constants.rb
|
|
83
84
|
- lib/cataract/declaration.rb
|
|
84
85
|
- lib/cataract/declarations.rb
|
|
85
86
|
- lib/cataract/import_resolver.rb
|
|
86
87
|
- lib/cataract/import_statement.rb
|
|
88
|
+
- lib/cataract/media_query.rb
|
|
87
89
|
- lib/cataract/pure.rb
|
|
88
90
|
- lib/cataract/pure/byte_constants.rb
|
|
89
91
|
- lib/cataract/pure/flatten.rb
|
|
90
92
|
- lib/cataract/pure/helpers.rb
|
|
91
|
-
- lib/cataract/pure/imports.rb
|
|
92
93
|
- lib/cataract/pure/parser.rb
|
|
93
94
|
- lib/cataract/pure/serializer.rb
|
|
94
95
|
- lib/cataract/pure/specificity.rb
|
|
@@ -97,6 +98,7 @@ files:
|
|
|
97
98
|
- lib/cataract/stylesheet_scope.rb
|
|
98
99
|
- lib/cataract/version.rb
|
|
99
100
|
- lib/tasks/gem.rake
|
|
101
|
+
- lib/tasks/profile.rake
|
|
100
102
|
homepage: https://github.com/jamescook/cataract
|
|
101
103
|
licenses:
|
|
102
104
|
- MIT
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Pure Ruby CSS parser - Import extraction
|
|
4
|
-
# NO REGEXP ALLOWED - char-by-char parsing only
|
|
5
|
-
|
|
6
|
-
module Cataract
|
|
7
|
-
# Helper: Case-insensitive ASCII byte comparison
|
|
8
|
-
# Compares bytes at given position with ASCII pattern (case-insensitive)
|
|
9
|
-
# Safe to use even if position is in middle of multi-byte UTF-8 characters
|
|
10
|
-
# Returns true if match, false otherwise
|
|
11
|
-
def self.match_ascii_ci?(str, pos, pattern)
|
|
12
|
-
pattern_len = pattern.bytesize
|
|
13
|
-
return false if pos + pattern_len > str.bytesize
|
|
14
|
-
|
|
15
|
-
i = 0
|
|
16
|
-
while i < pattern_len
|
|
17
|
-
str_byte = str.getbyte(pos + i)
|
|
18
|
-
pat_byte = pattern.getbyte(i)
|
|
19
|
-
|
|
20
|
-
# Convert both to lowercase for comparison (ASCII only: A-Z -> a-z)
|
|
21
|
-
str_byte += BYTE_CASE_DIFF if str_byte >= BYTE_UPPER_A && str_byte <= BYTE_UPPER_Z
|
|
22
|
-
pat_byte += BYTE_CASE_DIFF if pat_byte >= BYTE_UPPER_A && pat_byte <= BYTE_UPPER_Z
|
|
23
|
-
|
|
24
|
-
return false if str_byte != pat_byte
|
|
25
|
-
|
|
26
|
-
i += 1
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
true
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Extract @import statements from CSS
|
|
33
|
-
#
|
|
34
|
-
# @param css_string [String] CSS to scan for @imports
|
|
35
|
-
# @return [Array<Hash>] Array of import hashes with :url, :media, :full_match
|
|
36
|
-
def self.extract_imports(css_string)
|
|
37
|
-
imports = []
|
|
38
|
-
|
|
39
|
-
i = 0
|
|
40
|
-
len = css_string.length
|
|
41
|
-
|
|
42
|
-
while i < len
|
|
43
|
-
# Skip whitespace and comments
|
|
44
|
-
while i < len
|
|
45
|
-
byte = css_string.getbyte(i)
|
|
46
|
-
if is_whitespace?(byte)
|
|
47
|
-
i += 1
|
|
48
|
-
elsif i + 1 < len && css_string.getbyte(i) == BYTE_SLASH && css_string.getbyte(i + 1) == BYTE_STAR
|
|
49
|
-
# Skip /* */ comment
|
|
50
|
-
i += 2
|
|
51
|
-
while i + 1 < len && !(css_string.getbyte(i) == BYTE_STAR && css_string.getbyte(i + 1) == BYTE_SLASH)
|
|
52
|
-
i += 1
|
|
53
|
-
end
|
|
54
|
-
i += 2 if i + 1 < len # Skip */
|
|
55
|
-
else
|
|
56
|
-
break
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
break if i >= len
|
|
61
|
-
|
|
62
|
-
# Check for @import (case-insensitive byte comparison)
|
|
63
|
-
if match_ascii_ci?(css_string, i, '@import')
|
|
64
|
-
import_start = i
|
|
65
|
-
i += 7
|
|
66
|
-
|
|
67
|
-
# Skip whitespace after @import
|
|
68
|
-
while i < len && is_whitespace?(css_string.getbyte(i))
|
|
69
|
-
i += 1
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Check for optional url( (case-insensitive byte comparison)
|
|
73
|
-
has_url_function = false
|
|
74
|
-
if match_ascii_ci?(css_string, i, 'url(')
|
|
75
|
-
has_url_function = true
|
|
76
|
-
i += 4
|
|
77
|
-
while i < len && is_whitespace?(css_string.getbyte(i))
|
|
78
|
-
i += 1
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Find opening quote
|
|
83
|
-
byte = css_string.getbyte(i) if i < len
|
|
84
|
-
if i >= len || (byte != BYTE_DQUOTE && byte != BYTE_SQUOTE)
|
|
85
|
-
# Invalid @import, skip to next semicolon
|
|
86
|
-
while i < len && css_string.getbyte(i) != BYTE_SEMICOLON
|
|
87
|
-
i += 1
|
|
88
|
-
end
|
|
89
|
-
i += 1 if i < len # Skip semicolon
|
|
90
|
-
next
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
quote_char = byte
|
|
94
|
-
i += 1 # Skip opening quote
|
|
95
|
-
|
|
96
|
-
url_start = i
|
|
97
|
-
|
|
98
|
-
# Find closing quote (handle escaped quotes)
|
|
99
|
-
while i < len && css_string.getbyte(i) != quote_char
|
|
100
|
-
if css_string.getbyte(i) == BYTE_BACKSLASH && i + 1 < len
|
|
101
|
-
i += 2 # Skip escaped character
|
|
102
|
-
else
|
|
103
|
-
i += 1
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
break if i >= len # Unterminated string
|
|
108
|
-
|
|
109
|
-
url_end = i
|
|
110
|
-
i += 1 # Skip closing quote
|
|
111
|
-
|
|
112
|
-
# Skip closing paren if we had url(
|
|
113
|
-
if has_url_function
|
|
114
|
-
while i < len && is_whitespace?(css_string.getbyte(i))
|
|
115
|
-
i += 1
|
|
116
|
-
end
|
|
117
|
-
if i < len && css_string.getbyte(i) == BYTE_RPAREN
|
|
118
|
-
i += 1
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Skip whitespace before optional media query or semicolon
|
|
123
|
-
while i < len && is_whitespace?(css_string.getbyte(i))
|
|
124
|
-
i += 1
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Check for optional media query (everything until semicolon)
|
|
128
|
-
media_start = nil
|
|
129
|
-
media_end = nil
|
|
130
|
-
|
|
131
|
-
if i < len && css_string.getbyte(i) != BYTE_SEMICOLON
|
|
132
|
-
media_start = i
|
|
133
|
-
|
|
134
|
-
# Find semicolon
|
|
135
|
-
while i < len && css_string.getbyte(i) != BYTE_SEMICOLON
|
|
136
|
-
i += 1
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
media_end = i
|
|
140
|
-
|
|
141
|
-
# Trim trailing whitespace from media query
|
|
142
|
-
while media_end > media_start && is_whitespace?(css_string.getbyte(media_end - 1))
|
|
143
|
-
media_end -= 1
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Skip semicolon
|
|
148
|
-
i += 1 if i < len && css_string.getbyte(i) == BYTE_SEMICOLON
|
|
149
|
-
|
|
150
|
-
import_end = i
|
|
151
|
-
|
|
152
|
-
# Build result hash
|
|
153
|
-
url = css_string[url_start...url_end]
|
|
154
|
-
media = media_start && media_end > media_start ? css_string[media_start...media_end] : nil
|
|
155
|
-
full_match = css_string[import_start...import_end]
|
|
156
|
-
|
|
157
|
-
imports << { url: url, media: media, full_match: full_match }
|
|
158
|
-
elsif match_ascii_ci?(css_string, i, '@charset')
|
|
159
|
-
# Skip @charset if present - it can come before @import
|
|
160
|
-
while i < len && css_string.getbyte(i) != BYTE_SEMICOLON
|
|
161
|
-
i += 1
|
|
162
|
-
end
|
|
163
|
-
i += 1 if i < len # Skip semicolon
|
|
164
|
-
else
|
|
165
|
-
# If we hit any other content (rules, other at-rules), stop scanning
|
|
166
|
-
# Per CSS spec, @import must be at the top (only @charset can come before)
|
|
167
|
-
byte = css_string.getbyte(i) if i < len
|
|
168
|
-
if i < len && !is_whitespace?(byte)
|
|
169
|
-
break
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
i += 1
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
imports
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Parse media query symbol into array of media types
|
|
180
|
-
#
|
|
181
|
-
# @param media_query_sym [Symbol] Media query as symbol (e.g., :screen, :"print, screen")
|
|
182
|
-
# @return [Array<Symbol>] Array of individual media types
|
|
183
|
-
#
|
|
184
|
-
# @example
|
|
185
|
-
# parse_media_types(:screen) #=> [:screen]
|
|
186
|
-
# parse_media_types(:"print, screen") #=> [:print, :screen]
|
|
187
|
-
def self.parse_media_types(media_query_sym)
|
|
188
|
-
query = media_query_sym.to_s
|
|
189
|
-
types = []
|
|
190
|
-
|
|
191
|
-
i = 0
|
|
192
|
-
len = query.length
|
|
193
|
-
|
|
194
|
-
kwords = %w[and or not only]
|
|
195
|
-
|
|
196
|
-
while i < len
|
|
197
|
-
# Skip whitespace
|
|
198
|
-
while i < len && is_whitespace?(query.getbyte(i))
|
|
199
|
-
i += 1
|
|
200
|
-
end
|
|
201
|
-
break if i >= len
|
|
202
|
-
|
|
203
|
-
# Check for opening paren - skip conditions like "(min-width: 768px)"
|
|
204
|
-
if query.getbyte(i) == BYTE_LPAREN
|
|
205
|
-
# Skip to matching closing paren
|
|
206
|
-
paren_depth = 1
|
|
207
|
-
i += 1
|
|
208
|
-
while i < len && paren_depth > 0
|
|
209
|
-
byte = query.getbyte(i)
|
|
210
|
-
if byte == BYTE_LPAREN
|
|
211
|
-
paren_depth += 1
|
|
212
|
-
elsif byte == BYTE_RPAREN
|
|
213
|
-
paren_depth -= 1
|
|
214
|
-
end
|
|
215
|
-
i += 1
|
|
216
|
-
end
|
|
217
|
-
next
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Find end of word (media type or keyword)
|
|
221
|
-
word_start = i
|
|
222
|
-
byte = query.getbyte(i)
|
|
223
|
-
while i < len && !is_whitespace?(byte) && byte != BYTE_COMMA && byte != BYTE_LPAREN && byte != BYTE_COLON
|
|
224
|
-
i += 1
|
|
225
|
-
byte = query.getbyte(i) if i < len
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
if i > word_start
|
|
229
|
-
word = query[word_start...i]
|
|
230
|
-
|
|
231
|
-
# Check if this is a media feature (followed by ':')
|
|
232
|
-
is_media_feature = (i < len && query.getbyte(i) == BYTE_COLON)
|
|
233
|
-
|
|
234
|
-
# Check if it's a keyword (and, or, not, only)
|
|
235
|
-
is_keyword = kwords.include?(word)
|
|
236
|
-
|
|
237
|
-
if !is_keyword && !is_media_feature
|
|
238
|
-
# This is a media type - add it as symbol
|
|
239
|
-
types << word.to_sym
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Skip to comma or end
|
|
244
|
-
while i < len && query.getbyte(i) != BYTE_COMMA
|
|
245
|
-
if query.getbyte(i) == BYTE_LPAREN
|
|
246
|
-
# Skip condition
|
|
247
|
-
paren_depth = 1
|
|
248
|
-
i += 1
|
|
249
|
-
while i < len && paren_depth > 0
|
|
250
|
-
byte = query.getbyte(i)
|
|
251
|
-
if byte == BYTE_LPAREN
|
|
252
|
-
paren_depth += 1
|
|
253
|
-
elsif byte == BYTE_RPAREN
|
|
254
|
-
paren_depth -= 1
|
|
255
|
-
end
|
|
256
|
-
i += 1
|
|
257
|
-
end
|
|
258
|
-
else
|
|
259
|
-
i += 1
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
i += 1 if i < len && query.getbyte(i) == BYTE_COMMA # Skip comma
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
types
|
|
267
|
-
end
|
|
268
|
-
end
|