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.
- checksums.yaml +7 -0
- data/.clang-tidy +30 -0
- data/.github/workflows/ci-macos.yml +12 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/test.yml +76 -0
- data/.gitignore +45 -0
- data/.overcommit.yml +38 -0
- data/.rubocop.yml +83 -0
- data/BENCHMARKS.md +201 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +27 -0
- data/LICENSE +21 -0
- data/RAGEL_MIGRATION.md +60 -0
- data/README.md +292 -0
- data/Rakefile +209 -0
- data/benchmarks/benchmark_harness.rb +193 -0
- data/benchmarks/benchmark_merging.rb +121 -0
- data/benchmarks/benchmark_optimization_comparison.rb +168 -0
- data/benchmarks/benchmark_parsing.rb +153 -0
- data/benchmarks/benchmark_ragel_removal.rb +56 -0
- data/benchmarks/benchmark_runner.rb +70 -0
- data/benchmarks/benchmark_serialization.rb +180 -0
- data/benchmarks/benchmark_shorthand.rb +109 -0
- data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
- data/benchmarks/benchmark_specificity.rb +124 -0
- data/benchmarks/benchmark_string_allocation.rb +151 -0
- data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
- data/benchmarks/benchmark_to_s_cached.rb +55 -0
- data/benchmarks/benchmark_value_splitter.rb +54 -0
- data/benchmarks/benchmark_yjit.rb +158 -0
- data/benchmarks/benchmark_yjit_workers.rb +61 -0
- data/benchmarks/profile_to_s.rb +23 -0
- data/benchmarks/speedup_calculator.rb +83 -0
- data/benchmarks/system_metadata.rb +81 -0
- data/benchmarks/templates/benchmarks.md.erb +221 -0
- data/benchmarks/yjit_tests.rb +141 -0
- data/cataract.gemspec +34 -0
- data/cliff.toml +92 -0
- data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
- data/examples/color_conversion_visual_test/generate.rb +202 -0
- data/examples/color_conversion_visual_test/template.html.erb +259 -0
- data/examples/css_analyzer/analyzer.rb +164 -0
- data/examples/css_analyzer/analyzers/base.rb +33 -0
- data/examples/css_analyzer/analyzers/colors.rb +133 -0
- data/examples/css_analyzer/analyzers/important.rb +88 -0
- data/examples/css_analyzer/analyzers/properties.rb +61 -0
- data/examples/css_analyzer/analyzers/specificity.rb +68 -0
- data/examples/css_analyzer/templates/report.html.erb +575 -0
- data/examples/css_analyzer.rb +69 -0
- data/examples/github_analysis.html +5343 -0
- data/ext/cataract/cataract.c +1086 -0
- data/ext/cataract/cataract.h +174 -0
- data/ext/cataract/css_parser.c +1435 -0
- data/ext/cataract/extconf.rb +48 -0
- data/ext/cataract/import_scanner.c +174 -0
- data/ext/cataract/merge.c +973 -0
- data/ext/cataract/shorthand_expander.c +902 -0
- data/ext/cataract/specificity.c +213 -0
- data/ext/cataract/value_splitter.c +116 -0
- data/ext/cataract_color/cataract_color.c +16 -0
- data/ext/cataract_color/color_conversion.c +1687 -0
- data/ext/cataract_color/color_conversion.h +136 -0
- data/ext/cataract_color/color_conversion_lab.c +571 -0
- data/ext/cataract_color/color_conversion_named.c +259 -0
- data/ext/cataract_color/color_conversion_oklab.c +547 -0
- data/ext/cataract_color/extconf.rb +23 -0
- data/ext/cataract_old/cataract.c +393 -0
- data/ext/cataract_old/cataract.h +250 -0
- data/ext/cataract_old/css_parser.c +933 -0
- data/ext/cataract_old/extconf.rb +67 -0
- data/ext/cataract_old/import_scanner.c +174 -0
- data/ext/cataract_old/merge.c +776 -0
- data/ext/cataract_old/shorthand_expander.c +902 -0
- data/ext/cataract_old/specificity.c +213 -0
- data/ext/cataract_old/stylesheet.c +290 -0
- data/ext/cataract_old/value_splitter.c +116 -0
- data/lib/cataract/at_rule.rb +97 -0
- data/lib/cataract/color_conversion.rb +18 -0
- data/lib/cataract/declarations.rb +332 -0
- data/lib/cataract/import_resolver.rb +210 -0
- data/lib/cataract/rule.rb +131 -0
- data/lib/cataract/stylesheet.rb +716 -0
- data/lib/cataract/stylesheet_scope.rb +257 -0
- data/lib/cataract/version.rb +5 -0
- data/lib/cataract.rb +107 -0
- data/lib/tasks/gem.rake +158 -0
- data/scripts/fuzzer/run.rb +828 -0
- data/scripts/fuzzer/worker.rb +99 -0
- data/scripts/generate_benchmarks_md.rb +155 -0
- 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,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.
|
data/RAGEL_MIGRATION.md
ADDED
|
@@ -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
|