cataract 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 547e3a6efbb79ec3d4594ebde36ac09ce59ea300f944faa266689a203d08fa61
4
- data.tar.gz: 904df3cee8bc56e50dd2b856f5606a9219ac9daa3ad9eb9a8629ab2b3c9502b3
3
+ metadata.gz: 89396bf500fc32cfa8d632d90f8b1ff5e80b93a24793cc8c6fd183ca47619f7e
4
+ data.tar.gz: bef49a87c354f9947616bb4d07047bb876e7a2861a7de2e2846f2dd0cd9cd8e8
5
5
  SHA512:
6
- metadata.gz: 49d86a56a1fc93390172eb9ccb8d523a9ee3ee6c4162fc8959158bc27a12f3afb146cae5e0e5073426c4fda7e5a4bd362f8d0800dcb1b0f29660bb496a47aa0c
7
- data.tar.gz: 8da8740393b687304acaa81856b0e7235f6fc3ea33bc34bebf7fa5fc567ca5d7cff90e7a8975a79236374fa2610afaa5ad9795084589c6bc18368c6436b9a2b7
6
+ metadata.gz: 0d395bcb60f2a6646daae924eabe1c83c2aa79e174de9186b6ab3bf9f855c34a2f464fa1d221e550afa47d4e271b0aaf9d4184986ac516d0feb412fecd18772f
7
+ data.tar.gz: feb093da902ee218121c112471c2ea899540373c9ce74eff0db588f94ae5ab0992f7b39014bf6a3edb74a54e9b11e7f80d7c584f15dd08a56e4e4b37a63ee2c1
@@ -46,32 +46,3 @@ jobs:
46
46
  clang-tidy --version
47
47
  bundle exec rake lint
48
48
  touch .lint-passed
49
-
50
- docs:
51
- # Only run on push to main (not PRs)
52
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
53
- needs: test-ubuntu
54
- runs-on: ubuntu-latest
55
- permissions:
56
- contents: write
57
- steps:
58
- - uses: actions/checkout@v4
59
-
60
- - name: Set up Ruby
61
- uses: ruby/setup-ruby@v1
62
- with:
63
- ruby-version: '3.4'
64
- bundler-cache: true
65
-
66
- - name: Compile extension and generate documentation
67
- run: bundle exec rake compile docs
68
-
69
- - name: Disable Jekyll processing
70
- run: touch docs/.nojekyll
71
-
72
- - name: Deploy to GitHub Pages
73
- uses: peaceiris/actions-gh-pages@v3
74
- with:
75
- github_token: ${{ secrets.GITHUB_TOKEN }}
76
- publish_dir: ./docs
77
- allow_empty_commit: true
@@ -0,0 +1,51 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ workflow_dispatch:
7
+
8
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ # Allow only one concurrent deployment
15
+ concurrency:
16
+ group: "pages"
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: '3.4'
29
+ bundler-cache: true
30
+
31
+ - name: Compile extension and generate documentation
32
+ run: bundle exec rake compile docs
33
+
34
+ - name: Disable Jekyll processing
35
+ run: touch docs/.nojekyll
36
+
37
+ - name: Upload artifact
38
+ uses: actions/upload-pages-artifact@v3
39
+ with:
40
+ path: ./docs
41
+
42
+ deploy:
43
+ environment:
44
+ name: github-pages
45
+ url: ${{ steps.deployment.outputs.page_url }}
46
+ runs-on: ubuntu-latest
47
+ needs: build
48
+ steps:
49
+ - name: Deploy to GitHub Pages
50
+ id: deployment
51
+ uses: actions/deploy-pages@v4
data/.gitignore CHANGED
@@ -42,4 +42,6 @@ benchmarks/.benchmark_results/
42
42
 
43
43
  # Documentation (generated by YARD)
44
44
  docs/
45
+ !docs/files/
46
+ !docs/files/EXAMPLE.md
45
47
  .yardoc/
data/CHANGELOG.md CHANGED
@@ -1 +1,9 @@
1
+ ## [0.1.2] - 2025-11-11
2
+
3
+ - Fix segfault in merge
4
+
5
+ ## [0.1.1] - 2025-11-09
6
+
7
+ - Fix bugs with Stylesheet#merge resulting in wrong results (#11)
8
+
1
9
  ## [0.1.0] - 2025-11-09
data/Gemfile CHANGED
@@ -14,6 +14,7 @@ gem 'benchmark-ips', '~> 2.0'
14
14
  gem 'css_parser', '~> 1.0' # for benchmarking against
15
15
  gem 'minitest'
16
16
  gem 'minitest-spec'
17
+ gem 'nokogiri' # for docs
17
18
  gem 'simplecov', require: false
18
19
  gem 'simplecov-cobertura', require: false
19
20
  gem 'webmock', '~> 3.0' # for testing URL loading
data/README.md CHANGED
@@ -11,7 +11,7 @@ A performant CSS parser for accurate parsing of complex CSS structures.
11
11
  - **C Extension**: Performance-focused C implementation for parsing and serialization
12
12
  - **CSS2 Support**: Selectors, combinators, pseudo-classes, pseudo-elements, @media queries
13
13
  - **CSS3 Support**: Attribute selectors (`^=`, `$=`, `*=`)
14
- - **CSS Color Level 4**: Supports hex, rgb, hsl, hwb, oklab, oklch, and named colors with high precision
14
+ - **CSS Color Level 4**: Parses and preserves modern color formats (hex, rgb, hsl, hwb, oklab, oklch, lab, lch, named colors). Optional color conversion utility for transforming between formats.
15
15
  - **Specificity Calculation**: Automatic CSS specificity computation
16
16
  - **Media Query Filtering**: Query rules by media type
17
17
  - **Zero Runtime Dependencies**: Pure C extension with no runtime gem dependencies
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ begin
12
12
  require 'rake/extensiontask'
13
13
 
14
14
  # Configure the main extension
15
- Rake::ExtensionTask.new('cataract') do |ext|
15
+ Rake::ExtensionTask.new('native_extension') do |ext|
16
16
  ext.lib_dir = 'lib/cataract'
17
17
  ext.ext_dir = 'ext/cataract'
18
18
  end
@@ -26,7 +26,6 @@ end
26
26
 
27
27
  # Configure CLEAN to run before compilation
28
28
  # rake-compiler already adds: tmp/, lib/**/*.{so,bundle}, etc.
29
- # All C files are now hand-written (Ragel removed), so only clean build artifacts
30
29
  CLEAN.include('ext/**/Makefile', 'ext/**/*.o')
31
30
 
32
31
  Rake::TestTask.new(:test) do |t|
@@ -182,6 +181,8 @@ begin
182
181
  desc 'Generate example CSS analysis for documentation'
183
182
  task :generate_example do
184
183
  puts 'Generating GitHub CSS analysis example...'
184
+ require 'fileutils'
185
+ FileUtils.mkdir_p('docs')
185
186
  # Generate with file. prefix for YARD compatibility
186
187
  system('ruby examples/css_analyzer.rb https://github.com -o docs/file.github_analysis.html')
187
188
  end
@@ -0,0 +1,35 @@
1
+ # Live Example
2
+
3
+ This is a live example of Cataract analyzing GitHub.com's CSS.
4
+
5
+ **{file:github_analysis.html View GitHub.com CSS Analysis Report}**
6
+
7
+ This analysis was generated using:
8
+
9
+ ```bash
10
+ ruby examples/css_analyzer.rb https://github.com -o docs/github_analysis.html
11
+ ```
12
+
13
+ The example demonstrates:
14
+ - **Real-world CSS parsing performance** - Analyzing 16,000+ rules from GitHub.com
15
+ - **Property usage statistics** - Top properties and their frequency
16
+ - **Color palette extraction** - All colors used across the site
17
+ - **Specificity analysis** - Distribution of selector complexity
18
+ - **!important usage patterns** - How often declarations are marked important
19
+
20
+ The analysis is regenerated each time documentation is built with `rake docs`.
21
+
22
+ ## Running Your Own Analysis
23
+
24
+ You can analyze any website or CSS file:
25
+
26
+ ```bash
27
+ # Analyze a website
28
+ ruby examples/css_analyzer.rb https://example.com
29
+
30
+ # Analyze a CSS file
31
+ ruby examples/css_analyzer.rb path/to/styles.css
32
+
33
+ # Save to HTML report
34
+ ruby examples/css_analyzer.rb https://example.com -o report.html
35
+ ```
@@ -16,17 +16,10 @@ module CSSAnalyzer
16
16
  def initialize(source, options = {})
17
17
  @source = source
18
18
  @options = {
19
- top: 20,
20
- use_shim: false
19
+ top: 20
21
20
  }.merge(options)
22
21
  @timings = {}
23
22
 
24
- # Load shim if requested
25
- if @options[:use_shim]
26
- require_relative '../../lib/cataract/css_parser_compat'
27
- Cataract.mimic_CssParser!
28
- end
29
-
30
23
  # Load CSS based on source type
31
24
  @stylesheet = load_css(source)
32
25
  end
@@ -76,10 +69,9 @@ module CSSAnalyzer
76
69
 
77
70
  # Save parsed CSS to a file for debugging/comparison
78
71
  def save_parsed_css
79
- # Generate a unique filename based on source and shim usage
72
+ # Generate a unique filename based on source
80
73
  source_slug = @source.gsub(%r{[:/]}, '_').gsub(/[^a-zA-Z0-9_.-]/, '')
81
- shim_suffix = @options[:use_shim] ? '-shim' : '-direct'
82
- filename = "parsed-css-#{source_slug}#{shim_suffix}.css"
74
+ filename = "parsed-css-#{source_slug}.css"
83
75
 
84
76
  # Serialize stylesheet to CSS
85
77
  css_output = @stylesheet.to_s
@@ -120,27 +112,18 @@ module CSSAnalyzer
120
112
  premailer = Premailer.new(url, with_html_string: false)
121
113
  @timings[:fetch] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - fetch_start
122
114
 
123
- # Get CSS parser from Premailer
115
+ # Get CSS parser from Premailer and convert to string
124
116
  parser = premailer.instance_variable_get(:@css_parser)
117
+ parse_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
118
+ css_string = parser.to_s
119
+ @timings[:premailer_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - parse_start
125
120
 
126
- # If using Cataract shim, parser is already a Cataract::Stylesheet - use it directly
127
- if defined?(CssParser::CATARACT_SHIM) && CssParser::CATARACT_SHIM
128
- @timings[:premailer_parse] = 0 # Already parsed by Premailer/Cataract
129
- @timings[:cataract_parse] = 0 # No reparsing needed
130
- parser # Return the Cataract::Stylesheet directly
131
- else
132
- # Not using shim - parser is real css_parser, get CSS string and reparse
133
- parse_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
- css_string = parser.to_s
135
- @timings[:premailer_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - parse_start
121
+ # Parse it with Cataract
122
+ cataract_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
123
+ stylesheet = Cataract.parse_css(css_string)
124
+ @timings[:cataract_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - cataract_start
136
125
 
137
- # Parse it with Cataract
138
- cataract_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
139
- stylesheet = Cataract.parse_css(css_string)
140
- @timings[:cataract_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - cataract_start
141
-
142
- stylesheet
143
- end
126
+ stylesheet
144
127
  end
145
128
 
146
129
  def source_name
@@ -25,19 +25,12 @@ if __FILE__ == $PROGRAM_NAME
25
25
  options[:output] = file
26
26
  end
27
27
 
28
- opts.on('--use-shim', 'Use Cataract shim for css_parser (for Premailer)') do
29
- options[:use_shim] = true
30
- end
31
-
32
28
  opts.on('-h', '--help', 'Show this help message') do
33
29
  puts opts
34
30
  exit
35
31
  end
36
32
  end.parse!
37
33
 
38
- # Check for ENV var to enable shim
39
- options[:use_shim] = true if ENV['CATARACT_SHIM']
40
-
41
34
  # Check for required argument
42
35
  if ARGV.empty?
43
36
  warn 'Error: No URL or file specified'
@@ -228,22 +228,24 @@ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char
228
228
  rb_str_append(result, nested_selector);
229
229
  rb_str_cat2(result, " {\n");
230
230
 
231
- // Declarations on their own line (4-space indent)
232
- rb_str_cat2(result, indent);
233
- rb_str_cat2(result, " ");
234
- serialize_declarations(result, nested_declarations);
235
- rb_str_cat2(result, "\n");
231
+ // Declarations (one per line) with 4-space indent
232
+ VALUE nested_indent = rb_str_new_cstr(indent);
233
+ rb_str_cat2(nested_indent, " ");
234
+ const char *nested_indent_ptr = RSTRING_PTR(nested_indent);
235
+ serialize_declarations_formatted(result, nested_declarations, nested_indent_ptr);
236
+ RB_GC_GUARD(nested_indent);
236
237
 
237
238
  // Closing brace (2-space indent)
238
239
  rb_str_cat2(result, indent);
239
240
  rb_str_cat2(result, " }\n");
240
241
  }
241
242
  } else {
242
- // Serialize as declarations (e.g., @font-face)
243
- rb_str_cat2(result, indent);
244
- rb_str_cat2(result, " ");
245
- serialize_declarations(result, content);
246
- rb_str_cat2(result, "\n");
243
+ // Serialize as declarations (e.g., @font-face, one per line)
244
+ VALUE content_indent = rb_str_new_cstr(indent);
245
+ rb_str_cat2(content_indent, " ");
246
+ const char *content_indent_ptr = RSTRING_PTR(content_indent);
247
+ serialize_declarations_formatted(result, content, content_indent_ptr);
248
+ RB_GC_GUARD(content_indent);
247
249
  }
248
250
  }
249
251
 
@@ -268,11 +270,12 @@ static void serialize_rule_formatted(VALUE result, VALUE rule, const char *inden
268
270
  rb_str_append(result, selector);
269
271
  rb_str_cat2(result, " {\n");
270
272
 
271
- // Declarations on their own line with extra indentation
272
- rb_str_cat2(result, indent);
273
- rb_str_cat2(result, " ");
274
- serialize_declarations(result, declarations);
275
- rb_str_cat2(result, "\n");
273
+ // Declarations (one per line) with extra indentation
274
+ VALUE decl_indent = rb_str_new_cstr(indent);
275
+ rb_str_cat2(decl_indent, " ");
276
+ const char *decl_indent_ptr = RSTRING_PTR(decl_indent);
277
+ serialize_declarations_formatted(result, declarations, decl_indent_ptr);
278
+ RB_GC_GUARD(decl_indent);
276
279
 
277
280
  // Closing brace
278
281
  rb_str_cat2(result, indent);
@@ -987,7 +990,7 @@ static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
987
990
  // Ruby Module Initialization
988
991
  // ============================================================================
989
992
 
990
- void Init_cataract(void) {
993
+ void Init_native_extension(void) {
991
994
  // Get Cataract module (should be defined by main extension)
992
995
  VALUE mCataract = rb_define_module("Cataract");
993
996
 
@@ -110,6 +110,10 @@ static inline VALUE strip_string(const char *str, long len) {
110
110
  #define STR_NEW_CSTR(str) rb_str_new_cstr(str)
111
111
  #endif
112
112
 
113
+ // String comparison macro - check if Ruby string equals C string literal
114
+ #define STR_EQ(val, lit) (RSTRING_LEN(val) == strlen(lit) && \
115
+ memcmp(RSTRING_PTR(val), lit, strlen(lit)) == 0)
116
+
113
117
  // Safety limits
114
118
  #ifndef MAX_PARSE_DEPTH
115
119
  #define MAX_PARSE_DEPTH 10 // Max recursion depth for nested @media/@supports blocks and CSS nesting
@@ -45,4 +45,4 @@ else
45
45
  puts 'String buffer optimization: DISABLED'
46
46
  end
47
47
 
48
- create_makefile('cataract/cataract')
48
+ create_makefile('cataract/native_extension')