Class: Cataract::Stylesheet
- Inherits:
-
Object
- Object
- Cataract::Stylesheet
- Includes:
- Enumerable
- Defined in:
- lib/cataract/stylesheet.rb,
ext/cataract/cataract.c
Overview
Represents a parsed CSS stylesheet with rule management and merging capabilities.
The Stylesheet class stores parsed CSS rules in a flat array structure that preserves insertion order. Media queries are tracked via an index that maps media query symbols to rule IDs, allowing efficient filtering and serialization.
Instance Attribute Summary collapse
-
#charset ⇒ String?
readonly
The @charset declaration if present.
-
#rules ⇒ Array<Rule>
readonly
Array of parsed CSS rules.
Class Method Summary collapse
-
.load_file(filename, base_dir = '.', **options) ⇒ Stylesheet
Load CSS from a file and return a new Stylesheet.
-
.load_uri(uri, **options) ⇒ Stylesheet
Load CSS from a URI and return a new Stylesheet.
-
.parse(css, **options) ⇒ Stylesheet
Parse CSS and return a new Stylesheet.
Instance Method Summary collapse
-
#add_block(css, fix_braces: false, media_types: nil) ⇒ self
Add CSS block to stylesheet.
-
#add_rule(selector:, declarations:, media_types: nil) ⇒ self
Add a single rule.
-
#base_only ⇒ StylesheetScope
Filter to only base rules (rules not inside any @media query).
-
#base_rules ⇒ Array<Rule>
Get all rules without media query (rules that apply to all media).
-
#clear! ⇒ self
Clear all rules.
-
#convert_colors! ⇒ Object
Add convert_colors! instance method to Stylesheet.
-
#each {|rule| ... } ⇒ Enumerator
Iterate over all rules (required by Enumerable).
-
#empty? ⇒ Boolean
Check if stylesheet is empty.
-
#initialize(options = {}) ⇒ Stylesheet
constructor
Create a new empty stylesheet.
- #inspect ⇒ Object
-
#load_file(filename, base_dir = '.', _media_types = :all) ⇒ self
Load CSS from a local file and add to this stylesheet.
-
#load_uri(uri, options = {}) ⇒ self
Load CSS from a URI and add to this stylesheet.
-
#media_queries ⇒ Array<Symbol>
Get all unique media query symbols.
-
#merge ⇒ Stylesheet
Merge all rules in this stylesheet according to CSS cascade rules.
-
#merge! ⇒ self
Merge rules in-place, mutating the receiver.
-
#remove_rules!(selector: nil, media_types: nil) ⇒ self
Remove rules matching criteria.
-
#selectors ⇒ Array<String>
Get all selectors.
-
#size ⇒ Integer
(also: #length, #rules_count)
Get number of rules.
-
#to_formatted_s(media: :all) ⇒ String
Serialize to formatted CSS string with indentation and newlines.
-
#to_h ⇒ Hash
Convert to hash.
-
#to_s(media: :all) ⇒ String
(also: #to_css)
Serialize to CSS string.
-
#with_at_rule_type(type) ⇒ StylesheetScope
Filter by at-rule type.
-
#with_important(property = nil) ⇒ StylesheetScope
Filter to rules with !important declarations.
-
#with_media(media) ⇒ StylesheetScope
Filter rules by media query symbol(s).
-
#with_property(property, value = nil) ⇒ StylesheetScope
Filter rules by CSS property name and optional value.
-
#with_selector(selector) ⇒ StylesheetScope
Filter rules by CSS selector.
-
#with_specificity(specificity) ⇒ StylesheetScope
Filter rules by CSS specificity.
Constructor Details
#initialize(options = {}) ⇒ Stylesheet
Create a new empty stylesheet.
42 43 44 45 46 47 48 49 50 51 |
# File 'lib/cataract/stylesheet.rb', line 42 def initialize( = {}) @options = { import: false, io_exceptions: true }.merge() @rules = [] # Flat array of Rule structs @_media_index = {} # Hash: Symbol => Array of rule IDs @charset = nil end |
Instance Attribute Details
#charset ⇒ String? (readonly)
Returns The @charset declaration if present.
22 23 24 |
# File 'lib/cataract/stylesheet.rb', line 22 def charset @charset end |
#rules ⇒ Array<Rule> (readonly)
Returns Array of parsed CSS rules.
22 23 24 |
# File 'lib/cataract/stylesheet.rb', line 22 def rules @rules end |
Class Method Details
.load_file(filename, base_dir = '.', **options) ⇒ Stylesheet
Load CSS from a file and return a new Stylesheet.
70 71 72 73 74 |
# File 'lib/cataract/stylesheet.rb', line 70 def self.load_file(filename, base_dir = '.', **) sheet = new() sheet.load_file(filename, base_dir) sheet end |
.load_uri(uri, **options) ⇒ Stylesheet
Load CSS from a URI and return a new Stylesheet.
81 82 83 84 85 |
# File 'lib/cataract/stylesheet.rb', line 81 def self.load_uri(uri, **) sheet = new() sheet.load_uri(uri, ) sheet end |
.parse(css, **options) ⇒ Stylesheet
Parse CSS and return a new Stylesheet
58 59 60 61 62 |
# File 'lib/cataract/stylesheet.rb', line 58 def self.parse(css, **) sheet = new() sheet.add_block(css) sheet end |
Instance Method Details
#add_block(css, fix_braces: false, media_types: nil) ⇒ self
Add CSS block to stylesheet
TODO: Move to C?
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 |
# File 'lib/cataract/stylesheet.rb', line 556 def add_block(css, fix_braces: false, media_types: nil) css += ' }' if fix_braces && !css.strip.end_with?('}') # Convenience wrapper: wrap in @media if media_types specified if media_types media_list = Array(media_types).join(', ') css = "@media #{media_list} { #{css} }" end # Resolve @import statements if configured in constructor css_to_parse = if @options[:import] ImportResolver.resolve(css, @options[:import]) else css end # Get current rule ID offset offset = @_last_rule_id || 0 # Parse CSS with C function (returns hash) result = Cataract._parse_css(css_to_parse) # Merge rules with offsetted IDs new_rules = result[:rules] new_rules.each do |rule| rule.id += offset @rules << rule end # Merge media_index with offsetted IDs result[:_media_index].each do |media_sym, rule_ids| offsetted_ids = rule_ids.map { |id| id + offset } if @_media_index[media_sym] @_media_index[media_sym].concat(offsetted_ids) else @_media_index[media_sym] = offsetted_ids end end # Update last rule ID @_last_rule_id = offset + new_rules.length # Set charset if not already set @charset ||= result[:charset] # Track if we have any nesting (for serialization optimization) @_has_nesting = result[:_has_nesting] self end |
#add_rule(selector:, declarations:, media_types: nil) ⇒ self
Add a single rule
613 614 615 616 617 |
# File 'lib/cataract/stylesheet.rb', line 613 def add_rule(selector:, declarations:, media_types: nil) # Wrap in CSS syntax and add as block css = "#{selector} { #{declarations} }" add_block(css, media_types: media_types) end |
#base_only ⇒ StylesheetScope
Filter to only base rules (rules not inside any @media query).
Returns a chainable StylesheetScope that can be further filtered.
200 201 202 |
# File 'lib/cataract/stylesheet.rb', line 200 def base_only StylesheetScope.new(self, base_only: true) end |
#base_rules ⇒ Array<Rule>
Get all rules without media query (rules that apply to all media)
245 246 247 248 249 |
# File 'lib/cataract/stylesheet.rb', line 245 def base_rules # Rules not in any media_index entry media_rule_ids = @_media_index.values.flatten.uniq @rules.select.with_index { |_rule, idx| !media_rule_ids.include?(idx) } end |
#clear! ⇒ self
Clear all rules
407 408 409 410 411 412 413 |
# File 'lib/cataract/stylesheet.rb', line 407 def clear! @rules.clear @_media_index.clear @charset = nil @selectors = nil # Clear memoized cache self end |
#convert_colors! ⇒ Object
Add convert_colors! instance method to Stylesheet
140 |
# File 'ext/cataract_color/color_conversion.c', line 140
VALUE rb_stylesheet_convert_colors(int argc, VALUE *argv, VALUE self);
|
#each {|rule| ... } ⇒ Enumerator
Iterate over all rules (required by Enumerable).
Yields both selector-based rules (Rule) and at-rules (AtRule). Use rule.selector? to filter for selector-based rules only.
101 102 103 104 105 |
# File 'lib/cataract/stylesheet.rb', line 101 def each(&) return enum_for(:each) unless block_given? @rules.each(&) end |
#empty? ⇒ Boolean
Check if stylesheet is empty
400 401 402 |
# File 'lib/cataract/stylesheet.rb', line 400 def empty? @rules.empty? end |
#inspect ⇒ Object
629 630 631 632 633 634 635 636 637 638 |
# File 'lib/cataract/stylesheet.rb', line 629 def inspect total_rules = size if total_rules.zero? '#<Cataract::Stylesheet empty>' else preview = @rules.first(3).map(&:selector).join(', ') more = total_rules > 3 ? ', ...' : '' "#<Cataract::Stylesheet #{total_rules} rules: #{preview}#{more}>" end end |
#load_file(filename, base_dir = '.', _media_types = :all) ⇒ self
Load CSS from a local file and add to this stylesheet.
420 421 422 423 424 425 426 427 |
# File 'lib/cataract/stylesheet.rb', line 420 def load_file(filename, base_dir = '.', _media_types = :all) # Normalize file path and convert to file:// URI file_path = File.(filename, base_dir) file_uri = "file://#{file_path}" # Delegate to load_uri which handles imports and base_path load_uri(file_uri) end |
#load_uri(uri, options = {}) ⇒ self
Load CSS from a URI and add to this stylesheet.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 |
# File 'lib/cataract/stylesheet.rb', line 434 def load_uri(uri, = {}) require 'uri' require 'net/http' uri_obj = URI(uri) css_content = nil file_path = nil case uri_obj.scheme when 'http', 'https' response = Net::HTTP.get_response(uri_obj) unless response.is_a?(Net::HTTPSuccess) raise IOError, "Failed to load URI: #{uri} (#{response.code} #{response.})" end css_content = response.body when 'file', nil # file:// URI or relative path path = uri_obj.scheme == 'file' ? uri_obj.path : uri # Handle base_uri if provided if [:base_uri] base = URI([:base_uri]) path = File.join(base.path, path) if base.scheme == 'file' || base.scheme.nil? end file_path = File.(path) # If imports are enabled and base_path not already set, set it for resolving relative imports if @options[:import].is_a?(Hash) && @options[:import][:base_path].nil? file_dir = File.dirname(file_path) @options[:import] = @options[:import].merge(base_path: file_dir) end css_content = File.read(file_path) else raise ArgumentError, "Unsupported URI scheme: #{uri_obj.scheme}" end add_block(css_content) self rescue Errno::ENOENT raise IOError, "File not found: #{uri}" if @options[:io_exceptions] self rescue StandardError => e raise IOError, "Error loading URI: #{uri} - #{e.}" if @options[:io_exceptions] self end |
#media_queries ⇒ Array<Symbol>
Get all unique media query symbols
254 255 256 |
# File 'lib/cataract/stylesheet.rb', line 254 def media_queries @_media_index.keys end |
#merge ⇒ Stylesheet
Merge all rules in this stylesheet according to CSS cascade rules
Applies specificity and !important precedence rules to compute the final set of declarations. Also recreates shorthand properties from longhand properties where possible.
647 648 649 650 |
# File 'lib/cataract/stylesheet.rb', line 647 def merge # C function handles everything - returns new Stylesheet Cataract.merge(self) end |
#merge! ⇒ self
Merge rules in-place, mutating the receiver.
This is a convenience method that updates the stylesheet’s internal rules and media_index with the merged result. The Stylesheet object itself is mutated (same object_id), but note that the C merge function still allocates new arrays internally.
660 661 662 663 664 665 666 |
# File 'lib/cataract/stylesheet.rb', line 660 def merge! merged = Cataract.merge(self) @rules = merged.instance_variable_get(:@rules) @_media_index = merged.instance_variable_get(:@_media_index) @_has_nesting = merged.instance_variable_get(:@_has_nesting) self end |
#remove_rules!(selector: nil, media_types: nil) ⇒ self
Remove rules matching criteria
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/cataract/stylesheet.rb', line 497 def remove_rules!(selector: nil, media_types: nil) # Normalize media_types to array filter_media = media_types ? Array(media_types).map(&:to_sym) : nil # Find rules to remove rules_to_remove = Set.new @rules.each_with_index do |rule, rule_id| # Check selector match next if selector && rule.selector != selector # Check media type match if filter_media rule_media_types = @_media_index.select { |_media, ids| ids.include?(rule_id) }.keys # Extract individual media types from complex queries individual_types = rule_media_types.flat_map { |key| Cataract.parse_media_types(key) }.uniq # If rule is not in any media query (base rule), skip if filtering by media if individual_types.empty? next unless filter_media.include?(:all) else # Check if rule's media types intersect with filter next unless individual_types.intersect?(filter_media) end end rules_to_remove << rule_id end # Remove rules and update media_index rules_to_remove.sort.reverse_each do |rule_id| @rules.delete_at(rule_id) # Remove from media_index and update IDs for rules after this one @_media_index.each_value do |ids| ids.delete(rule_id) # Decrement IDs greater than removed ID ids.map! { |id| id > rule_id ? id - 1 : id } end end # Clean up empty media_index entries @_media_index.delete_if { |_media, ids| ids.empty? } # Update rule IDs in remaining rules @rules.each_with_index { |rule, new_id| rule.id = new_id } # Clear memoized cache @selectors = nil self end |
#selectors ⇒ Array<String>
Get all selectors
261 262 263 |
# File 'lib/cataract/stylesheet.rb', line 261 def selectors @selectors ||= @rules.map(&:selector) end |
#size ⇒ Integer Also known as: length, rules_count
Get number of rules
391 392 393 |
# File 'lib/cataract/stylesheet.rb', line 391 def size @rules.length end |
#to_formatted_s(media: :all) ⇒ String
Serialize to formatted CSS string with indentation and newlines.
Converts the stylesheet to a human-readable CSS string with proper indentation. Rules are formatted with each declaration on its own line, and media queries are properly indented. Optionally filters output to specific media queries.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/cataract/stylesheet.rb', line 346 def to_formatted_s(media: :all) which_media = media # Normalize to array for consistent filtering which_media_array = which_media.is_a?(Array) ? which_media : [which_media] # If :all is present, return everything (no filtering) if which_media_array.include?(:all) Cataract._stylesheet_to_formatted_s(@rules, @_media_index, @charset, @_has_nesting || false) else # Collect all rule IDs that match the requested media types matching_rule_ids = Set.new # Include rules not in any media query (they apply to all media) media_rule_ids = @_media_index.values.flatten.uniq all_rule_ids = (0...@rules.length).to_a non_media_rule_ids = all_rule_ids - media_rule_ids matching_rule_ids.merge(non_media_rule_ids) # Include rules from requested media types which_media_array.each do |media_sym| if @_media_index[media_sym] matching_rule_ids.merge(@_media_index[media_sym]) end end # Build filtered rules array (keep original IDs, no recreation needed) filtered_rules = matching_rule_ids.sort.map! { |rule_id| @rules[rule_id] } # Build filtered media_index (keep original IDs, just filter to included rules) filtered_media_index = {} which_media_array.each do |media_sym| if @_media_index[media_sym] filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids.to_a end end # C serialization with filtered data # Note: Filtered rules might still contain nesting, so pass the flag Cataract._stylesheet_to_formatted_s(filtered_rules, filtered_media_index, @charset, @_has_nesting || false) end end |
#to_h ⇒ Hash
Convert to hash
622 623 624 625 626 627 |
# File 'lib/cataract/stylesheet.rb', line 622 def to_h { rules: @rules, charset: @charset } end |
#to_s(media: :all) ⇒ String Also known as: to_css
Serialize to CSS string
Converts the stylesheet to a CSS string. Optionally filters output to only include rules from specific media queries.
Important: When filtering to specific media types, base rules (rules not inside any @media block) are NOT included. Only rules explicitly inside the requested @media queries are output. Use :all to include base rules.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/cataract/stylesheet.rb', line 290 def to_s(media: :all) which_media = media # Normalize to array for consistent filtering which_media_array = which_media.is_a?(Array) ? which_media : [which_media] # If :all is present, return everything (no filtering) if which_media_array.include?(:all) Cataract._stylesheet_to_s(@rules, @_media_index, @charset, @_has_nesting || false) else # Collect all rule IDs that match the requested media types matching_rule_ids = Set.new which_media_array.each do |media_sym| if @_media_index[media_sym] matching_rule_ids.merge(@_media_index[media_sym]) end end # Build filtered rules array (keep original IDs, no recreation needed) filtered_rules = matching_rule_ids.sort.map! { |rule_id| @rules[rule_id] } # Build filtered media_index (keep original IDs, just filter to included rules) filtered_media_index = {} which_media_array.each do |media_sym| if @_media_index[media_sym] filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids.to_a end end # C serialization with filtered data # Note: Filtered rules might still contain nesting, so pass the flag Cataract._stylesheet_to_s(filtered_rules, filtered_media_index, @charset, @_has_nesting || false) end end |
#with_at_rule_type(type) ⇒ StylesheetScope
Filter by at-rule type.
Returns a chainable StylesheetScope that can be further filtered.
219 220 221 |
# File 'lib/cataract/stylesheet.rb', line 219 def with_at_rule_type(type) StylesheetScope.new(self, at_rule_type: type) end |
#with_important(property = nil) ⇒ StylesheetScope
Filter to rules with !important declarations.
Returns a chainable StylesheetScope that can be further filtered.
238 239 240 |
# File 'lib/cataract/stylesheet.rb', line 238 def with_important(property = nil) StylesheetScope.new(self, important: true, important_property: property) end |
#with_media(media) ⇒ StylesheetScope
Filter rules by media query symbol(s).
Returns a chainable StylesheetScope that can be further filtered.
123 124 125 |
# File 'lib/cataract/stylesheet.rb', line 123 def with_media(media) StylesheetScope.new(self, media: media) end |
#with_property(property, value = nil) ⇒ StylesheetScope
Filter rules by CSS property name and optional value.
Returns a chainable StylesheetScope that can be further filtered.
185 186 187 |
# File 'lib/cataract/stylesheet.rb', line 185 def with_property(property, value = nil) StylesheetScope.new(self, property: property, property_value: value) end |
#with_selector(selector) ⇒ StylesheetScope
Filter rules by CSS selector.
Returns a chainable StylesheetScope that can be further filtered. Supports both exact string matching and regular expression patterns.
165 166 167 |
# File 'lib/cataract/stylesheet.rb', line 165 def with_selector(selector) StylesheetScope.new(self, selector: selector) end |
#with_specificity(specificity) ⇒ StylesheetScope
Filter rules by CSS specificity.
Returns a chainable StylesheetScope that can be further filtered.
142 143 144 |
# File 'lib/cataract/stylesheet.rb', line 142 def with_specificity(specificity) StylesheetScope.new(self, specificity: specificity) end |