cataract 0.1.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.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-tidy +30 -0
  3. data/.github/workflows/ci-macos.yml +12 -0
  4. data/.github/workflows/ci.yml +77 -0
  5. data/.github/workflows/test.yml +76 -0
  6. data/.gitignore +45 -0
  7. data/.overcommit.yml +38 -0
  8. data/.rubocop.yml +83 -0
  9. data/BENCHMARKS.md +201 -0
  10. data/CHANGELOG.md +1 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +21 -0
  13. data/RAGEL_MIGRATION.md +60 -0
  14. data/README.md +292 -0
  15. data/Rakefile +209 -0
  16. data/benchmarks/benchmark_harness.rb +193 -0
  17. data/benchmarks/benchmark_merging.rb +121 -0
  18. data/benchmarks/benchmark_optimization_comparison.rb +168 -0
  19. data/benchmarks/benchmark_parsing.rb +153 -0
  20. data/benchmarks/benchmark_ragel_removal.rb +56 -0
  21. data/benchmarks/benchmark_runner.rb +70 -0
  22. data/benchmarks/benchmark_serialization.rb +180 -0
  23. data/benchmarks/benchmark_shorthand.rb +109 -0
  24. data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
  25. data/benchmarks/benchmark_specificity.rb +124 -0
  26. data/benchmarks/benchmark_string_allocation.rb +151 -0
  27. data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
  28. data/benchmarks/benchmark_to_s_cached.rb +55 -0
  29. data/benchmarks/benchmark_value_splitter.rb +54 -0
  30. data/benchmarks/benchmark_yjit.rb +158 -0
  31. data/benchmarks/benchmark_yjit_workers.rb +61 -0
  32. data/benchmarks/profile_to_s.rb +23 -0
  33. data/benchmarks/speedup_calculator.rb +83 -0
  34. data/benchmarks/system_metadata.rb +81 -0
  35. data/benchmarks/templates/benchmarks.md.erb +221 -0
  36. data/benchmarks/yjit_tests.rb +141 -0
  37. data/cataract.gemspec +34 -0
  38. data/cliff.toml +92 -0
  39. data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
  40. data/examples/color_conversion_visual_test/generate.rb +202 -0
  41. data/examples/color_conversion_visual_test/template.html.erb +259 -0
  42. data/examples/css_analyzer/analyzer.rb +164 -0
  43. data/examples/css_analyzer/analyzers/base.rb +33 -0
  44. data/examples/css_analyzer/analyzers/colors.rb +133 -0
  45. data/examples/css_analyzer/analyzers/important.rb +88 -0
  46. data/examples/css_analyzer/analyzers/properties.rb +61 -0
  47. data/examples/css_analyzer/analyzers/specificity.rb +68 -0
  48. data/examples/css_analyzer/templates/report.html.erb +575 -0
  49. data/examples/css_analyzer.rb +69 -0
  50. data/examples/github_analysis.html +5343 -0
  51. data/ext/cataract/cataract.c +1086 -0
  52. data/ext/cataract/cataract.h +174 -0
  53. data/ext/cataract/css_parser.c +1435 -0
  54. data/ext/cataract/extconf.rb +48 -0
  55. data/ext/cataract/import_scanner.c +174 -0
  56. data/ext/cataract/merge.c +973 -0
  57. data/ext/cataract/shorthand_expander.c +902 -0
  58. data/ext/cataract/specificity.c +213 -0
  59. data/ext/cataract/value_splitter.c +116 -0
  60. data/ext/cataract_color/cataract_color.c +16 -0
  61. data/ext/cataract_color/color_conversion.c +1687 -0
  62. data/ext/cataract_color/color_conversion.h +136 -0
  63. data/ext/cataract_color/color_conversion_lab.c +571 -0
  64. data/ext/cataract_color/color_conversion_named.c +259 -0
  65. data/ext/cataract_color/color_conversion_oklab.c +547 -0
  66. data/ext/cataract_color/extconf.rb +23 -0
  67. data/ext/cataract_old/cataract.c +393 -0
  68. data/ext/cataract_old/cataract.h +250 -0
  69. data/ext/cataract_old/css_parser.c +933 -0
  70. data/ext/cataract_old/extconf.rb +67 -0
  71. data/ext/cataract_old/import_scanner.c +174 -0
  72. data/ext/cataract_old/merge.c +776 -0
  73. data/ext/cataract_old/shorthand_expander.c +902 -0
  74. data/ext/cataract_old/specificity.c +213 -0
  75. data/ext/cataract_old/stylesheet.c +290 -0
  76. data/ext/cataract_old/value_splitter.c +116 -0
  77. data/lib/cataract/at_rule.rb +97 -0
  78. data/lib/cataract/color_conversion.rb +18 -0
  79. data/lib/cataract/declarations.rb +332 -0
  80. data/lib/cataract/import_resolver.rb +210 -0
  81. data/lib/cataract/rule.rb +131 -0
  82. data/lib/cataract/stylesheet.rb +716 -0
  83. data/lib/cataract/stylesheet_scope.rb +257 -0
  84. data/lib/cataract/version.rb +5 -0
  85. data/lib/cataract.rb +107 -0
  86. data/lib/tasks/gem.rake +158 -0
  87. data/scripts/fuzzer/run.rb +828 -0
  88. data/scripts/fuzzer/worker.rb +99 -0
  89. data/scripts/generate_benchmarks_md.rb +155 -0
  90. metadata +135 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: afbaeaaa174e69b056a175c1a0aed5da7f52e33fcbc4859448262aff427086f9
4
+ data.tar.gz: dd190c243a6d494ed1e87c155cdeda2e6843bbc827e048e8df7eb992d2c8d09b
5
+ SHA512:
6
+ metadata.gz: '083570d10482fe637251a7d6fa912e6ec0ea3daa3d0283fa65b6f7aa54d798c693cb359fda1c6186bc5223504d9f31d91e6b359c170d718623279f2aac615186'
7
+ data.tar.gz: 5bd729068d06e069f625b2fc2d034dab769707f749ee175fbc420253cac953e52f8143194c1bca51a18bcd16d6190026e1823375b9eba3a564fd39a237e065ea
data/.clang-tidy ADDED
@@ -0,0 +1,30 @@
1
+ ---
2
+ # clang-tidy configuration for Cataract CSS parser
3
+ # Based on Ada URL parser's config: https://github.com/ada-url/ada
4
+
5
+ Checks: >
6
+ bugprone-*,
7
+ -bugprone-easily-swappable-parameters,
8
+ -bugprone-exception-escape,
9
+ -bugprone-implicit-widening-of-multiplication-result,
10
+ -bugprone-narrowing-conversions,
11
+ -bugprone-suspicious-include,
12
+ -bugprone-unhandled-exception-at-new,
13
+ clang-analyzer-*,
14
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
15
+
16
+ # Turn all the warnings from the checks above into errors.
17
+ WarningsAsErrors: '*'
18
+
19
+ # Check first-party headers only (our own code in ext/cataract/)
20
+ HeaderFilterRegex: 'ext/cataract/.*\.h$'
21
+
22
+ # Don't check system headers (Ruby C API headers)
23
+ SystemHeaders: false
24
+
25
+ # Ruby C extension specific suppressions
26
+ # (Add more as needed based on Ruby API conventions)
27
+ CheckOptions:
28
+ # Ruby API uses long parameter lists (e.g., rb_struct_define_under)
29
+ - key: bugprone-easily-swappable-parameters.MinimumLength
30
+ value: 4
@@ -0,0 +1,12 @@
1
+ name: CI - macOS (Manual)
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ test-macos:
8
+ uses: ./.github/workflows/test.yml
9
+ with:
10
+ os: macos-latest
11
+ secrets:
12
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,77 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test-ubuntu:
11
+ uses: ./.github/workflows/test.yml
12
+ with:
13
+ os: ubuntu-latest
14
+ secrets:
15
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
16
+
17
+ lint:
18
+ if: github.actor == 'jamescook'
19
+ needs: test-ubuntu
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Set up Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: '3.4'
28
+ bundler-cache: true
29
+
30
+ - name: Cache clang-tidy results
31
+ id: lint-cache
32
+ uses: actions/cache@v4
33
+ with:
34
+ path: .lint-passed
35
+ key: lint-${{ hashFiles('ext/**/*.c', 'ext/**/*.h', '.clang-tidy', 'Gemfile') }}
36
+
37
+ - name: Install clang-tidy
38
+ if: steps.lint-cache.outputs.cache-hit != 'true'
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y clang-tidy
42
+
43
+ - name: Run clang-tidy
44
+ if: steps.lint-cache.outputs.cache-hit != 'true'
45
+ run: |
46
+ clang-tidy --version
47
+ bundle exec rake lint
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,76 @@
1
+ name: Test Suite
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ os:
7
+ required: true
8
+ type: string
9
+ description: 'Operating system to run tests on'
10
+ secrets:
11
+ CODECOV_TOKEN:
12
+ required: false
13
+ description: 'Codecov upload token'
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ${{ inputs.os }}
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ ruby: ['3.1', '3.2', '3.3', '3.4']
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby }}
30
+ bundler-cache: true
31
+
32
+ - name: Display Ruby version
33
+ run: ruby --version
34
+
35
+ - name: Run tests
36
+ run: bundle exec rake compile test
37
+ env:
38
+ COVERAGE: 1
39
+
40
+ - name: Run debug test (Ruby 3.4 only)
41
+ if: matrix.ruby == '3.4'
42
+ run: |
43
+ bundle exec rake clean compile
44
+ bundle exec ruby -Ilib:test test/test_stylesheet.rb
45
+ env:
46
+ CONFIGURE_ARGS: --enable-debug
47
+
48
+ - name: Upload coverage to Codecov
49
+ if: runner.os == 'Linux' && matrix.ruby == '3.4'
50
+ uses: codecov/codecov-action@v5
51
+ with:
52
+ fail_ci_if_error: false
53
+ token: ${{ secrets.CODECOV_TOKEN }}
54
+ verbose: true
55
+
56
+ - name: Upload coverage report artifact
57
+ if: runner.os == 'Linux' && matrix.ruby == '3.4'
58
+ uses: actions/upload-artifact@v4
59
+ with:
60
+ name: coverage-report
61
+ path: coverage/
62
+ retention-days: 30
63
+
64
+ rubocop:
65
+ runs-on: ${{ inputs.os }}
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+
69
+ - name: Set up Ruby
70
+ uses: ruby/setup-ruby@v1
71
+ with:
72
+ ruby-version: '3.4'
73
+ bundler-cache: true
74
+
75
+ - name: Run rubocop
76
+ run: bundle exec rubocop --fail-level W
data/.gitignore ADDED
@@ -0,0 +1,45 @@
1
+ # Build artifacts
2
+ tmp/
3
+ *.gem
4
+ *.so
5
+ *.o
6
+
7
+ # Generated C code from Ragel
8
+ ext/**/cataract.c
9
+
10
+ # Compiled extensions
11
+ lib/**/*.so
12
+ lib/**/*.bundle
13
+
14
+ # Makefile generated by extconf.rb
15
+ ext/**/Makefile
16
+
17
+ # OS files
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Editor files
22
+ *.swp
23
+ *.swo
24
+ *~
25
+ .vscode/
26
+ .idea/
27
+
28
+ # Bundler
29
+ vendor/
30
+ .bundle/
31
+
32
+ # Test artifacts
33
+ coverage/
34
+ test/.benchmark_results/
35
+ test/fuzz_last_input.css
36
+ test/fuzz_crash_*.css
37
+ test/fuzz_crash_*.log
38
+
39
+ # Benchmark results
40
+ benchmarks/.results/
41
+ benchmarks/.benchmark_results/
42
+
43
+ # Documentation (generated by YARD)
44
+ docs/
45
+ .yardoc/
data/.overcommit.yml ADDED
@@ -0,0 +1,38 @@
1
+ # Use this file to configure the Overcommit hooks you wish to use.
2
+ # This will run before git commits and prevent commits if checks fail.
3
+
4
+ # Uncomment the following lines to make Overcommit install hooks on `bundle install`
5
+ gemfile: false
6
+
7
+ PreCommit:
8
+ RuboCop:
9
+ enabled: true
10
+ on_warn: fail # Treat warnings as failures
11
+ command: ['bundle', 'exec', 'rubocop']
12
+ flags: ['--force-exclusion']
13
+ include:
14
+ - '**/*.rb'
15
+ - '**/*.rake'
16
+ - '**/Rakefile'
17
+ - '**/Gemfile'
18
+
19
+ TrailingWhitespace:
20
+ enabled: true
21
+ exclude:
22
+ - '**/vendor/**/*'
23
+ - '**/docs/**/*'
24
+ - '**/*.html'
25
+ - '**/*.css'
26
+
27
+ YamlSyntax:
28
+ enabled: true
29
+
30
+ # Don't run checks on merge commits
31
+ CommitMsg:
32
+ ALL:
33
+ requires_files: false
34
+ quiet: false
35
+
36
+ PostCheckout:
37
+ ALL:
38
+ enabled: false
data/.rubocop.yml ADDED
@@ -0,0 +1,83 @@
1
+ plugins:
2
+ - rubocop-performance
3
+ - rubocop-minitest
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.1
7
+ NewCops: enable
8
+ SuggestExtensions: false
9
+ Exclude:
10
+ - 'tmp/**/*'
11
+ - 'vendor/**/*'
12
+ - 'bin/**/*'
13
+ - 'test/fixtures/**/*'
14
+ - '**/*.so'
15
+ - '**/*.bundle'
16
+
17
+ # extconf.rb uses standard Ruby C extension global variables ($CFLAGS, etc.)
18
+ Style/GlobalVars:
19
+ Exclude:
20
+ - 'ext/**/extconf.rb'
21
+
22
+ # Disable complexity metrics - too subjective and often leads to harder-to-read code
23
+ Metrics/AbcSize:
24
+ Enabled: false
25
+
26
+ Metrics/CyclomaticComplexity:
27
+ Enabled: false
28
+
29
+ Metrics/PerceivedComplexity:
30
+ Enabled: false
31
+
32
+ Metrics/MethodLength:
33
+ Enabled: false
34
+
35
+ Metrics/ClassLength:
36
+ Enabled: false
37
+
38
+ Metrics/ModuleLength:
39
+ Enabled: false
40
+
41
+ Metrics/BlockLength:
42
+ Enabled: false
43
+
44
+ Metrics/ParameterLists:
45
+ Enabled: false
46
+
47
+ # Keep line length for tests disabled - long test strings are fine
48
+ Layout/LineLength:
49
+ Exclude:
50
+ - 'benchmarks/**/*'
51
+ - 'test/**/*'
52
+ - 'scripts/**/*'
53
+ - '*.gemspec'
54
+ AllowedPatterns:
55
+ - '^\s*#' # Allow long comment lines
56
+
57
+ # Allow more assertions per test - strict limit is too restrictive
58
+ Minitest/MultipleAssertions:
59
+ Max: 10
60
+
61
+ Style/Documentation:
62
+ Enabled: true
63
+ Exclude:
64
+ - 'test/**/*'
65
+ - 'benchmarks/**/*'
66
+
67
+ # Disable modifier if/unless enforcement - use it when it's clearer, not because a cop says so
68
+ Style/IfUnlessModifier:
69
+ Enabled: false
70
+
71
+ Style/FrozenStringLiteralComment:
72
+ Enabled: true
73
+ Exclude:
74
+ - 'test/**/*'
75
+
76
+ # Allow numbers in test method names - they represent actual values being tested
77
+ Naming/VariableNumber:
78
+ Exclude:
79
+ - 'test/**/*'
80
+
81
+ Naming/PredicatePrefix:
82
+ Exclude:
83
+ - 'lib/**/*.rb'
data/BENCHMARKS.md ADDED
@@ -0,0 +1,201 @@
1
+ <!-- AUTO-GENERATED FILE - DO NOT EDIT -->
2
+ <!-- This file is automatically generated from benchmark results. -->
3
+ <!-- To regenerate: rake benchmark:generate_docs -->
4
+
5
+ # Performance Benchmarks
6
+
7
+ Comprehensive performance comparison between Cataract and css_parser gem.
8
+
9
+ ## Test Environment
10
+
11
+ - **Ruby**: ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin23]
12
+ - **CPU**: Apple M1 Pro
13
+ - **Memory**: 32GB
14
+ - **OS**: macOS 14.5
15
+ - **Generated**: 2025-10-30T16:01:15-05:00
16
+
17
+ <details>
18
+ <summary><h2>CSS Parsing</h2></summary>
19
+
20
+ Performance of parsing CSS into internal data structures.
21
+
22
+ Time to parse CSS into internal data structures
23
+
24
+ ### Small CSS (64 lines, 1.0KB)
25
+
26
+
27
+ | Parser | Speed | Time per operation |
28
+ |--------|-------|-------------------|
29
+ | css_parser | 6.16K i/s | 162.34 μs |
30
+ | **Cataract** | **63.79K i/s** | **15.68 μs** |
31
+ | **Speedup** | **10.36x faster** | |
32
+
33
+ ### Medium CSS with @media (139 lines, 1.6KB)
34
+
35
+
36
+ | Parser | Speed | Time per operation |
37
+ |--------|-------|-------------------|
38
+ | css_parser | 3.44K i/s | 290.64 μs |
39
+ | **Cataract** | **41.45K i/s** | **24.13 μs** |
40
+ | **Speedup** | **12.05x faster** | |
41
+
42
+
43
+ </details>
44
+
45
+ ---
46
+
47
+ <details>
48
+ <summary><h2>CSS Serialization (to_s)</h2></summary>
49
+
50
+ Performance of converting parsed CSS back to string format.
51
+
52
+ Time to convert parsed CSS back to string format
53
+
54
+ ### Full Serialization (Bootstrap CSS - 191KB)
55
+
56
+
57
+ | Parser | Speed | Time per operation |
58
+ |--------|-------|-------------------|
59
+ | css_parser | 34.0 i/s | 29.41 ms |
60
+ | **Cataract** | **714.8 i/s** | **1.4 ms** |
61
+ | **Speedup** | **21.02x faster** | |
62
+
63
+ ### Media Type Filtering (print only)
64
+
65
+
66
+ | Parser | Speed | Time per operation |
67
+ |--------|-------|-------------------|
68
+ | css_parser | 4.06K i/s | 246.56 μs |
69
+ | **Cataract** | **232.22K i/s** | **4.31 μs** |
70
+ | **Speedup** | **57.26x faster** | |
71
+
72
+
73
+ </details>
74
+
75
+ ---
76
+
77
+ <details>
78
+ <summary><h2>Specificity Calculation</h2></summary>
79
+
80
+ Performance of calculating CSS selector specificity values.
81
+
82
+ Time to calculate CSS selector specificity values
83
+
84
+ | Test Case | Speedup |
85
+ |-----------|---------|
86
+ | Simple Selectors | **22.03x faster** |
87
+ | Compound Selectors | **30.55x faster** |
88
+ | Combinators | **28.34x faster** |
89
+ | Pseudo-classes & Pseudo-elements | **46.06x faster** |
90
+ | :not() Pseudo-class (CSS3) | **23.64x faster** |
91
+ | Complex Real-world Selectors | **49.17x faster** |
92
+
93
+ **Summary:** 22.03x faster to 49.17x faster (avg 33.3x faster)
94
+
95
+ </details>
96
+
97
+ ---
98
+
99
+ <details>
100
+ <summary><h2>CSS Merging</h2></summary>
101
+
102
+ Performance of merging multiple CSS rule sets with the same selector.
103
+
104
+ Time to merge multiple CSS rule sets with same selector
105
+
106
+ | Test Case | Speedup |
107
+ |-----------|---------|
108
+ | No shorthand properties (large) | **4.14x faster** |
109
+ | Simple properties | **3.86x faster** |
110
+ | Cascade with specificity | **5.75x faster** |
111
+ | Important declarations | **6.1x faster** |
112
+ | Shorthand expansion | **4.16x faster** |
113
+ | Complex merging | **3.07x faster** |
114
+
115
+ **Summary:** 3.07x faster to 6.1x faster (avg 4.51x faster)
116
+
117
+ ### What's Being Tested
118
+ - Specificity-based CSS cascade (ID > class > element)
119
+ - `!important` declaration handling
120
+ - Shorthand property expansion (e.g., `margin` → `margin-top`, `margin-right`, etc.)
121
+ - Shorthand property creation from longhand properties
122
+
123
+ </details>
124
+
125
+ ---
126
+
127
+ <details>
128
+ <summary><h2>YJIT Impact</h2></summary>
129
+
130
+ Impact of Ruby's YJIT JIT compiler on Ruby-side operations. The C extension performance is the same regardless of YJIT.
131
+
132
+ Ruby-side operations with and without YJIT
133
+
134
+ ### Operations Per Second
135
+
136
+ | Operation | Without YJIT | With YJIT | YJIT Improvement |
137
+ |-----------|--------------|-----------|------------------|
138
+ | property access | 227.18K i/s | 322.32K i/s | **1.42x faster** (42% faster) |
139
+ | declaration merging | 204.26K i/s | 337.81K i/s | **1.65x faster** (65% faster) |
140
+ | to_s generation | 242.66K i/s | 391.16K i/s | **1.61x faster** (61% faster) |
141
+ | parse + iterate | 121.52K i/s | 142.77K i/s | **1.17x faster** (17% faster) |
142
+
143
+ ### Key Takeaways
144
+ - YJIT provides significant performance boost for Ruby-side operations
145
+ - Greatest impact on declaration merging
146
+ - Parse + iterate benefits least since most work is in C
147
+ - Recommended: Enable YJIT in production (`--yjit` flag or `RUBY_YJIT_ENABLE=1`)
148
+
149
+ </details>
150
+
151
+ ---
152
+
153
+ ## Summary
154
+
155
+ ### Performance Highlights
156
+
157
+ | Category | Min Speedup | Max Speedup | Avg Speedup |
158
+ |----------|-------------|-------------|-------------|
159
+ | **Parsing** | 10.36x faster | 12.05x faster | 11.2x faster |
160
+ | **Serialization** | 21.02x faster | 57.26x faster | 39.14x faster |
161
+ | **Specificity** | 22.03x faster | 49.17x faster | 33.3x faster |
162
+ | **Merging** | 3.07x faster | 6.1x faster | 4.51x faster |
163
+
164
+ ### Implementation Notes
165
+
166
+ 1. **C Extension**: Critical paths (parsing, specificity, merging, serialization) implemented in C
167
+ 2. **Efficient Data Structures**: Rules grouped by media query for O(1) lookups
168
+ 3. **Memory Efficient**: Pre-allocated string buffers, minimal Ruby object allocations
169
+ 4. **Optimized Algorithms**: Purpose-built CSS specificity calculator
170
+
171
+ ### Use Cases
172
+
173
+ - **Large CSS files**: Handles complex stylesheets efficiently
174
+ - **Specificity calculations**: Optimized for selector analysis
175
+ - **High-volume processing**: Reduced allocations minimize GC pressure
176
+ - **Production applications**: Tested with Bootstrap CSS and real-world stylesheets
177
+
178
+ ---
179
+
180
+ ## Running Benchmarks
181
+
182
+ ```bash
183
+ # All benchmarks
184
+ rake benchmark 2>&1 | tee benchmark_output.txt
185
+
186
+ # Individual benchmarks
187
+ rake benchmark:parsing
188
+ rake benchmark:serialization
189
+ rake benchmark:specificity
190
+ rake benchmark:merging
191
+ rake benchmark:yjit
192
+
193
+ # Generate documentation
194
+ rake benchmark:generate_docs
195
+ ```
196
+
197
+ ## Notes
198
+
199
+ - All benchmarks use benchmark-ips with 3s warmup and 5-10s measurement periods
200
+ - Measurements are median i/s (iterations per second) with standard deviation
201
+ - css_parser gem must be installed for comparison benchmarks
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ ## [0.1.0] - 2025-11-09
data/Gemfile ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in cataract.gemspec
6
+ gemspec
7
+
8
+ # Build dependencies
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rake-compiler', '~> 1.0'
11
+
12
+ # Development/benchmarking dependencies (not needed by gem users)
13
+ gem 'benchmark-ips', '~> 2.0'
14
+ gem 'css_parser', '~> 1.0' # for benchmarking against
15
+ gem 'minitest'
16
+ gem 'minitest-spec'
17
+ gem 'simplecov', require: false
18
+ gem 'simplecov-cobertura', require: false
19
+ gem 'webmock', '~> 3.0' # for testing URL loading
20
+
21
+ gem 'overcommit', '~> 0.64', group: :development
22
+ gem 'premailer'
23
+ gem 'rubocop', '~> 1.81', group: :development
24
+ gem 'rubocop-minitest', '~> 0.38.2', group: :development
25
+ gem 'rubocop-performance', '~> 1.26', group: :development
26
+
27
+ gem 'yard', '~> 0.9', group: :development
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 James Cook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # Ragel to Pure C Migration
2
+
3
+ ## Why We Switched
4
+
5
+ Started with Ragel as an experiment for the CSS parser, but quickly moved to hand-written pure C in October 2024.
6
+
7
+ ### Performance
8
+
9
+ Benchmarks showed the pure C implementation was **2.08x faster** than Ragel's default style (T0):
10
+
11
+ ```
12
+ Parsing bootstrap.css (10,000 iterations)
13
+ Ragel (T0): 1.234s
14
+ Pure C: 0.593s (2.08x faster)
15
+ ```
16
+
17
+ While Ragel's F0/F1 styles were faster than T0, they produced significantly larger binaries and had longer compile times.
18
+
19
+ ### Compilation Speed
20
+
21
+ - **Ragel**: 2-3 seconds to generate C code, then compile
22
+ - **Pure C**: Immediate compilation, no code generation step
23
+
24
+ Some Ragel styles (G0/G1/G2) never finished compiling - waited 10+ minutes before giving up.
25
+
26
+ ### Ragel Complexity Issues
27
+
28
+ Hit walls with non-determinism and state machine complexity:
29
+ - Adding `@charset` support caused compile times to explode
30
+ - Issues with entering/leaving characters in complex patterns
31
+ - State machine became too complex for Ragel to optimize efficiently
32
+
33
+ ### Binary Size
34
+
35
+ Hand-written C produces smaller binaries compared to Ragel's generated code, especially the faster F0/F1 styles.
36
+
37
+ ### Maintainability
38
+
39
+ - Pure C is more familiar to contributors (no Ragel DSL to learn)
40
+ - Easier to debug (no generated code indirection)
41
+ - Standard C tooling works out of the box (debuggers, profilers, linters)
42
+ - No build-time Ragel dependency for gem users
43
+
44
+ ### Build Simplicity
45
+
46
+ Removing Ragel eliminated:
47
+ - Build-time dependency on Ragel binary
48
+ - Separate code generation step in CI
49
+ - Platform-specific Ragel installation issues
50
+ - Complexity in the build toolchain
51
+
52
+ ## Trade-offs
53
+
54
+ Pure C is more verbose than Ragel's DSL, but the performance gains and simpler build process made it an easy choice.
55
+
56
+ ## Details
57
+
58
+ - Swapped in October 2024
59
+ - Files: `css_parser.c`, `specificity.c`, `value_splitter.c`
60
+ - API unchanged, all tests pass