cataract 0.2.4 → 0.3.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/.github/workflows/test.yml +6 -6
- data/.gitignore +17 -3
- data/.rubocop.yml +1 -0
- data/BENCHMARKS.md +24 -6
- data/CHANGELOG.md +29 -1
- data/Gemfile +2 -2
- data/examples/css_analyzer/analyzer.rb +9 -3
- data/examples/css_analyzer/analyzers/base.rb +1 -1
- data/ext/cataract/cataract.c +251 -458
- data/ext/cataract/cataract.h +127 -16
- data/ext/cataract/css_parser.c +1029 -700
- data/ext/cataract/extconf.rb +1 -2
- data/ext/cataract/flatten.c +279 -991
- data/ext/cataract/shorthand_expander.c +80 -102
- data/ext/cataract_color/color_conversion.c +1 -1
- data/lib/cataract/declarations.rb +13 -7
- data/lib/cataract/error.rb +49 -0
- data/lib/cataract/import_resolver.rb +36 -16
- data/lib/cataract/pure/byte_constants.rb +4 -1
- data/lib/cataract/pure/flatten.rb +143 -145
- data/lib/cataract/pure/parser.rb +623 -388
- data/lib/cataract/pure/serializer.rb +109 -104
- data/lib/cataract/pure/specificity.rb +112 -156
- data/lib/cataract/pure.rb +1 -6
- data/lib/cataract/stylesheet.rb +480 -410
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +1 -0
- metadata +3 -13
- data/ext/cataract/import_scanner.c +0 -174
- data/ext/cataract_old/cataract.c +0 -393
- data/ext/cataract_old/cataract.h +0 -250
- data/ext/cataract_old/css_parser.c +0 -933
- data/ext/cataract_old/extconf.rb +0 -67
- data/ext/cataract_old/import_scanner.c +0 -174
- data/ext/cataract_old/merge.c +0 -776
- data/ext/cataract_old/shorthand_expander.c +0 -902
- data/ext/cataract_old/specificity.c +0 -213
- data/ext/cataract_old/stylesheet.c +0 -290
- data/ext/cataract_old/value_splitter.c +0 -116
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a6424d26e77e0dd4e0167324dc6190f03a9ca041e51fa9e61a78163f6fe312b
|
|
4
|
+
data.tar.gz: 7c538f41ebd8e93a53caed4a5f066eea076277c26174bff7d22837730f03d90d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2297f4139e8025ac39b9467eaa4bd0541683af7e4870813159c557df813f411d389b59f5b28adb9c46f5390aba73405b937e946d9097b943e0ba70f6cb37963
|
|
7
|
+
data.tar.gz: 0b4d9b6ec564fd674ad8558b7b4f309494c0ca12628947a8501b17ccb3567e1a2667dd0ea89f88ad82f688ac580122ec7e9505b8149433504ef40d923228b75b
|
data/.github/workflows/test.yml
CHANGED
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
strategy:
|
|
19
19
|
fail-fast: false
|
|
20
20
|
matrix:
|
|
21
|
-
ruby: ['3.1', '3.2', '3.3', '3.4']
|
|
21
|
+
ruby: ['3.1', '3.2', '3.3', '3.4', '4.0']
|
|
22
22
|
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v4
|
|
@@ -37,8 +37,8 @@ jobs:
|
|
|
37
37
|
env:
|
|
38
38
|
COVERAGE: 1
|
|
39
39
|
|
|
40
|
-
- name: Run debug test (Ruby
|
|
41
|
-
if: matrix.ruby == '
|
|
40
|
+
- name: Run debug test (Ruby 4.0 only)
|
|
41
|
+
if: matrix.ruby == '4.0'
|
|
42
42
|
run: |
|
|
43
43
|
bundle exec rake clean compile
|
|
44
44
|
bundle exec ruby -Ilib:test test/test_stylesheet.rb
|
|
@@ -46,7 +46,7 @@ jobs:
|
|
|
46
46
|
CONFIGURE_ARGS: --enable-debug
|
|
47
47
|
|
|
48
48
|
- name: Upload coverage to Codecov
|
|
49
|
-
if: runner.os == 'Linux' && matrix.ruby == '
|
|
49
|
+
if: runner.os == 'Linux' && matrix.ruby == '4.0'
|
|
50
50
|
uses: codecov/codecov-action@v5
|
|
51
51
|
with:
|
|
52
52
|
fail_ci_if_error: false
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
verbose: true
|
|
55
55
|
|
|
56
56
|
- name: Upload coverage report artifact
|
|
57
|
-
if: runner.os == 'Linux' && matrix.ruby == '
|
|
57
|
+
if: runner.os == 'Linux' && matrix.ruby == '4.0'
|
|
58
58
|
uses: actions/upload-artifact@v4
|
|
59
59
|
with:
|
|
60
60
|
name: coverage-report
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
- name: Set up Ruby
|
|
70
70
|
uses: ruby/setup-ruby@v1
|
|
71
71
|
with:
|
|
72
|
-
ruby-version: '
|
|
72
|
+
ruby-version: '4.0'
|
|
73
73
|
bundler-cache: true
|
|
74
74
|
|
|
75
75
|
- name: Run rubocop
|
data/.gitignore
CHANGED
|
@@ -10,6 +10,8 @@ ext/**/cataract.c
|
|
|
10
10
|
# Compiled extensions
|
|
11
11
|
lib/**/*.so
|
|
12
12
|
lib/**/*.bundle
|
|
13
|
+
ext/**/*.bundle
|
|
14
|
+
ext/**/*.dSYM/
|
|
13
15
|
|
|
14
16
|
# Makefile generated by extconf.rb
|
|
15
17
|
ext/**/Makefile
|
|
@@ -29,19 +31,31 @@ Thumbs.db
|
|
|
29
31
|
vendor/
|
|
30
32
|
.bundle/
|
|
31
33
|
|
|
34
|
+
# Node (this is a Ruby gem; no JS toolchain is tracked, but guard against stray npm installs)
|
|
35
|
+
node_modules/
|
|
36
|
+
|
|
32
37
|
# Test artifacts
|
|
33
38
|
coverage/
|
|
34
39
|
test/.benchmark_results/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
|
|
41
|
+
# Fuzzer output (crash inputs, logs, recent-input dumps)
|
|
42
|
+
scripts/fuzzer/output/
|
|
38
43
|
|
|
39
44
|
# Benchmark results
|
|
40
45
|
benchmarks/.results/
|
|
41
46
|
benchmarks/.benchmark_results/
|
|
42
47
|
|
|
48
|
+
# CSS analyzer example debug output
|
|
49
|
+
examples/css_analyzer/output/
|
|
50
|
+
|
|
43
51
|
# Documentation (generated by YARD)
|
|
44
52
|
docs/
|
|
45
53
|
!docs/files/
|
|
46
54
|
!docs/files/EXAMPLE.md
|
|
47
55
|
.yardoc/
|
|
56
|
+
|
|
57
|
+
# Claude Code local project instructions (personal, not shared)
|
|
58
|
+
.claude/CLAUDE.md
|
|
59
|
+
|
|
60
|
+
.cache/
|
|
61
|
+
.cocci/
|
data/.rubocop.yml
CHANGED
|
@@ -178,4 +178,5 @@ Cataract/BanAssertIncludes:
|
|
|
178
178
|
- 'test/color/**/*.rb'
|
|
179
179
|
- 'test/test_benchmark_doc_generator.rb'
|
|
180
180
|
- 'test/test_speedup_calculator.rb'
|
|
181
|
+
- 'test/test_parse_errors.rb' # Parse error tests check error messages with assert_match
|
|
181
182
|
- 'test/support/**/*' # Support files define assert_contains which uses assert_includes internally
|
data/BENCHMARKS.md
CHANGED
|
@@ -20,17 +20,35 @@ Time to parse CSS into internal data structures
|
|
|
20
20
|
|
|
21
21
|
| Test Case | Native | Pure (no YJIT) | Pure (YJIT) |
|
|
22
22
|
|-----------|--------|----------------|-------------|
|
|
23
|
-
| Small CSS (64 lines, 1.0KB) | 37.
|
|
24
|
-
| Medium CSS with @media (139 lines, 1.6KB) | 34.
|
|
25
|
-
| Selector lists (3500 lines, 62.5KB, 500 lists) |
|
|
23
|
+
| Small CSS (64 lines, 1.0KB) | 37.27K i/s | 3.32K i/s | 13.48K i/s |
|
|
24
|
+
| Medium CSS with @media (139 lines, 1.6KB) | 34.66K i/s | 2.06K i/s | 8.94K i/s |
|
|
25
|
+
| Selector lists (3500 lines, 62.5KB, 500 lists) | 485.7 i/s | 55.7 i/s | 213.6 i/s |
|
|
26
26
|
|
|
27
27
|
### Speedups
|
|
28
28
|
|
|
29
29
|
| Comparison | Speedup |
|
|
30
30
|
|------------|---------|
|
|
31
|
-
| Native vs Pure (no YJIT) |
|
|
32
|
-
| Native vs Pure (YJIT) |
|
|
33
|
-
| YJIT impact on Pure Ruby | 4.
|
|
31
|
+
| Native vs Pure (no YJIT) | 14.4x faster (avg) |
|
|
32
|
+
| Native vs Pure (YJIT) | 3.38x faster (avg) |
|
|
33
|
+
| YJIT impact on Pure Ruby | 4.23x faster (avg) |
|
|
34
|
+
|
|
35
|
+
### Parse Error Checking Overhead
|
|
36
|
+
|
|
37
|
+
Parse error detection can be enabled with `raise_parse_errors: true`. This compares performance impact:
|
|
38
|
+
|
|
39
|
+
| Configuration | Native | Pure (no YJIT) | Pure (YJIT) |
|
|
40
|
+
|---------------|--------|----------------|-------------|
|
|
41
|
+
| Medium CSS (139 lines) - no error checking | 35.18K i/s | 2.07K i/s | 9.05K i/s |
|
|
42
|
+
| Medium CSS (139 lines) - with error checking | 34.84K i/s | 1.9K i/s | 8.19K i/s |
|
|
43
|
+
|
|
44
|
+
**Overhead Analysis:**
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
| Implementation | Overhead |
|
|
48
|
+
|----------------|----------|
|
|
49
|
+
| Native | ~0% (within noise) |
|
|
50
|
+
| Pure (no YJIT) | 8.9% slower |
|
|
51
|
+
| Pure (YJIT) | 10.4% slower |
|
|
34
52
|
|
|
35
53
|
---
|
|
36
54
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.3.0] - 2026-07-06
|
|
2
|
+
|
|
3
|
+
- Security/Breaking: `Stylesheet#load_uri` and `#load_file` now reject `file://` paths under `/etc/`, `/proc/`, `/sys/`, and `/dev/` by default (override with `dangerous_path_prefixes: []`). Previously these methods used a separate, unvalidated fetch implementation; they now reuse `ImportResolver` (the same validation and fetching `@import` resolution already uses), which also fixes `load_uri` not following HTTP redirects.
|
|
4
|
+
- Fix: rules nested 3+ levels deep, or nested inside a top-level `@media`/`@supports`/`@container`/`@scope` block, could serialize incorrectly (silently flattened, or printed as an unrelated top-level rule) in the pure Ruby backend.
|
|
5
|
+
- Fix: a bare type selector nested without `&` (invalid CSS nesting) no longer corrupts serialized output with unbalanced braces, in both the C and pure Ruby parsers.
|
|
6
|
+
- Fix: `@keyframes` parsing no longer pollutes the outer rule list with its own inner selectors.
|
|
7
|
+
- Fix: at-rules (`@keyframes`, `@font-face`, etc.) nested inside `@media` blocks now correctly keep their media context across parsing, merging (`add_block`, `concat`/`+`), `@import` resolution, and serialization - previously this could be lost, misattributed, or (combined with CSS nesting) raise `NoMethodError` in the pure Ruby serializer.
|
|
8
|
+
- Fix: `remove_rules!` and the `-` operator no longer desync media query ids from the rules that reference them.
|
|
9
|
+
- Fix: resolving `@import`s no longer leaves duplicate/colliding rule ids.
|
|
10
|
+
- Fix: a plain `@import "file.css";` (no media qualifier) no longer loses that file's own `@media` rules.
|
|
11
|
+
- Fix: `Stylesheet#concat`/`+` now merges the other stylesheet's media queries and selector-list groupings too - previously this silently dropped `h1, h2`-style grouping and corrupted media attribution.
|
|
12
|
+
- Fix: `@import` media queries with a type keyword (e.g. `"screen and (min-width: 500px)"`) no longer keep a stray `"and"` in the parsed conditions.
|
|
13
|
+
- Fix: `to_s`/`to_formatted_s` media filtering is now consistent between the two - both include base (non-media) rules when filtering to a specific media type; previously `to_s` excluded them while `to_formatted_s` included them.
|
|
14
|
+
- Fix: values containing `url(...;...)` no longer lose everything after the first semicolon when the rule also uses CSS nesting.
|
|
15
|
+
- Fix: `!important` now parses correctly with whitespace between `!` and `important`, per the CSS grammar.
|
|
16
|
+
- Fix: expanding `border`, `border-{top,right,bottom,left}`, `font`, `background`, and `list-style` shorthands (used when comparing rules for equality) no longer silently drops `!important`.
|
|
17
|
+
- Fix: `Declarations`/`Stylesheet` `#dup`/`#clone` now produce fully independent copies; previously mutating a copy could mutate the original.
|
|
18
|
+
- Performance: CSS selector specificity calculation is ~30% faster (no longer re-allocates an internal lookup array on every call).
|
|
19
|
+
- Performance: native (C extension) CSS flattening is ~1-5% faster (fewer string allocations when recreating shorthand properties).
|
|
20
|
+
|
|
21
|
+
## [0.2.5 - 2025-11-25]
|
|
22
|
+
|
|
23
|
+
- Feature: Parse error detection with `raise_parse_errors` option - validates CSS structure and raises `ParseError` exceptions for malformed input with line/column tracking
|
|
24
|
+
- Feature: Granular error control - enable specific checks (empty values, malformed declarations, invalid selectors, invalid selector syntax, malformed at-rules, unclosed blocks)
|
|
25
|
+
- Feature: Type safety validation for C extension - `Stylesheet.parse` and `Stylesheet.new` now validate argument types and raise clear `TypeError` instead of segfaulting
|
|
26
|
+
- Feature: Selector syntax validation using whitelist approach - catches invalid characters and sequences like `..class`, `##id`, `???`
|
|
27
|
+
- Fix: `add_block` with multiple `@import` statements now correctly tracks media type for each import instead of reusing the first import's media context
|
|
28
|
+
- Performance: Parse error checking adds minimal overhead (effectively zero for C/Pure Ruby, ~5% for Pure Ruby with YJIT)
|
|
29
|
+
- Testing: Fuzzer corpus enhanced with invalid CSS patterns for crash testing
|
|
2
30
|
|
|
3
31
|
## [0.2.4 - 2025-11-23]
|
|
4
32
|
- MediaQuery first-class objects: Refactored media queries from simple symbols to proper structs with id, type, and conditions, enabling accurate
|
data/Gemfile
CHANGED
|
@@ -16,10 +16,10 @@ gem 'css_parser', '~> 1.0' # for benchmarking against
|
|
|
16
16
|
gem 'minitest'
|
|
17
17
|
gem 'minitest-spec'
|
|
18
18
|
gem 'nokogiri' # for docs
|
|
19
|
-
gem 'ruby-prof', require: false # for profiling
|
|
19
|
+
gem 'ruby-prof', require: false, platforms: :ruby # for profiling (MRI only)
|
|
20
20
|
gem 'simplecov', require: false
|
|
21
21
|
gem 'simplecov-cobertura', require: false
|
|
22
|
-
gem 'stackprof', require: false # for profiling
|
|
22
|
+
gem 'stackprof', require: false, platforms: :ruby # for profiling (MRI only)
|
|
23
23
|
gem 'webmock', '~> 3.0' # for testing URL loading
|
|
24
24
|
|
|
25
25
|
gem 'overcommit', '~> 0.64', group: :development
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
4
5
|
require 'uri'
|
|
5
6
|
require_relative '../../lib/cataract'
|
|
6
7
|
require_relative 'analyzers/properties'
|
|
@@ -71,13 +72,18 @@ module CSSAnalyzer
|
|
|
71
72
|
def save_parsed_css
|
|
72
73
|
# Generate a unique filename based on source
|
|
73
74
|
source_slug = @source.gsub(%r{[:/]}, '_').gsub(/[^a-zA-Z0-9_.-]/, '')
|
|
74
|
-
|
|
75
|
+
|
|
76
|
+
# Write next to this script (not the caller's cwd) so running the
|
|
77
|
+
# analyzer from repo root doesn't scatter debug output there.
|
|
78
|
+
output_dir = File.join(__dir__, 'output')
|
|
79
|
+
FileUtils.mkdir_p(output_dir)
|
|
80
|
+
path = File.join(output_dir, "parsed-css-#{source_slug}.css")
|
|
75
81
|
|
|
76
82
|
# Serialize stylesheet to CSS
|
|
77
83
|
css_output = @stylesheet.to_s
|
|
78
84
|
|
|
79
|
-
File.write(
|
|
80
|
-
warn "Parsed CSS saved to #{
|
|
85
|
+
File.write(path, css_output)
|
|
86
|
+
warn "Parsed CSS saved to #{path} (#{@stylesheet.size} rules, #{css_output.length} bytes)"
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
private
|
|
@@ -26,7 +26,7 @@ module CSSAnalyzer
|
|
|
26
26
|
# @param rule [Cataract::Rule] The rule to check
|
|
27
27
|
# @return [Array<Symbol>] Array of media query symbols this rule appears in
|
|
28
28
|
def media_queries_for_rule(rule)
|
|
29
|
-
stylesheet.
|
|
29
|
+
stylesheet.media_index.select { |_media, ids| ids.include?(rule.id) }.keys
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|