cataract 0.1.0 → 0.1.2
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/ci.yml +0 -29
- data/.github/workflows/docs.yml +51 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +1 -0
- data/README.md +1 -1
- data/Rakefile +3 -2
- data/cataract.gemspec +4 -4
- data/docs/files/EXAMPLE.md +35 -0
- data/examples/css_analyzer/analyzer.rb +12 -29
- data/examples/css_analyzer.rb +0 -7
- data/ext/cataract/cataract.c +19 -16
- data/ext/cataract/cataract.h +4 -0
- data/ext/cataract/extconf.rb +1 -1
- data/ext/cataract/merge.c +731 -59
- data/ext/cataract/shorthand_expander.c +152 -39
- data/ext/cataract_color/color_conversion.c +59 -28
- data/ext/cataract_color/color_conversion_named.c +10 -0
- data/lib/cataract/declarations.rb +1 -2
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +3 -3
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b98cea191d8f92ffa56c46ad4600a8f16594fcbf1d6fe66b5c5317c161a1599
|
|
4
|
+
data.tar.gz: 7501de6b3fb3aba7d7319332932008003a181b4eaa67ed50b0b23aaa0063ff5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45b860f18d838ac6dab2d775ce4f73e38624cce23ed700bcd40c91aeb5713f6ac19b120a7647a4a838e050a86605bf252a7b83f4c4d20335eb9e18436db64dab
|
|
7
|
+
data.tar.gz: 8625e832be70e0d463a3bd90116132d6e871cbe51eff1ec61fe0e7b7d1a290a2265ee94e7731fccf16b3d46380d39d07d49e703febbfe2d7137c5aec8c46f99f
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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
data/CHANGELOG.md
CHANGED
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**:
|
|
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('
|
|
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
|
data/cataract.gemspec
CHANGED
|
@@ -5,11 +5,11 @@ require_relative 'lib/cataract/version'
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'cataract'
|
|
7
7
|
spec.version = Cataract::VERSION
|
|
8
|
-
spec.authors = ['
|
|
9
|
-
spec.email = ['
|
|
8
|
+
spec.authors = ['James Cook']
|
|
9
|
+
spec.email = ['jcook.rubyist@gmail.com']
|
|
10
10
|
|
|
11
|
-
spec.summary = 'CSS parser
|
|
12
|
-
spec.description = 'A
|
|
11
|
+
spec.summary = 'High-performance CSS parser with C extensions'
|
|
12
|
+
spec.description = 'A performant CSS parser with C extensions for accurate parsing of complex CSS structures including media queries, nested selectors, and CSS Color Level 4'
|
|
13
13
|
spec.homepage = 'https://github.com/jamescook/cataract'
|
|
14
14
|
spec.license = 'MIT'
|
|
15
15
|
spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
|
|
@@ -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
|
|
72
|
+
# Generate a unique filename based on source
|
|
80
73
|
source_slug = @source.gsub(%r{[:/]}, '_').gsub(/[^a-zA-Z0-9_.-]/, '')
|
|
81
|
-
|
|
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
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
data/examples/css_analyzer.rb
CHANGED
|
@@ -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'
|
data/ext/cataract/cataract.c
CHANGED
|
@@ -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
|
|
232
|
-
|
|
233
|
-
rb_str_cat2(
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
244
|
-
rb_str_cat2(
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
rb_str_cat2(
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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
|
|
data/ext/cataract/cataract.h
CHANGED
|
@@ -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
|
data/ext/cataract/extconf.rb
CHANGED