Class: Cataract::Stylesheet
- Inherits:
-
Object
- Object
- Cataract::Stylesheet
- Includes:
- Enumerable
- Defined in:
- lib/cataract/pure.rb,
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.
-
#imports ⇒ Array<ImportStatement>
readonly
Array of @import statements.
-
#media_queries ⇒ Array<MediaQuery>
readonly
Array of media query objects.
-
#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
-
#+(other) ⇒ Stylesheet
Combine two stylesheets into a new one and apply cascade.
-
#-(other) ⇒ Stylesheet
Remove matching rules from this stylesheet.
-
#==(other) ⇒ Boolean
(also: #eql?)
Compare stylesheets for equality.
- #[](offset) ⇒ Object
-
#add_block(css, fix_braces: false, media_types: nil, base_uri: nil, base_dir: nil, absolute_paths: 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.
-
#concat(other) ⇒ self
Concatenate another stylesheet's rules into this one and apply cascade.
-
#convert_colors!(*_args) ⇒ Object
Add convert_colors! instance method to Stylesheet.
-
#custom_properties(media: nil) ⇒ Hash{Symbol => Hash{String => String}}
Get all custom property (CSS variable) definitions organized by media context.
-
#each {|rule| ... } ⇒ Enumerator
Iterate over all rules (required by Enumerable).
-
#empty? ⇒ Boolean
Check if stylesheet is empty.
-
#flatten ⇒ Stylesheet
(also: #cascade)
Flatten all rules in this stylesheet according to CSS cascade rules.
-
#flatten! ⇒ self
(also: #cascade!)
Flatten rules in-place, mutating the receiver.
-
#hash ⇒ Integer
Generate hash code for this stylesheet.
-
#initialize(options = {}) ⇒ Stylesheet
constructor
Create a new empty stylesheet.
-
#initialize_copy(source) ⇒ Object
Initialize copy for proper deep duplication.
- #inspect ⇒ Object
-
#load_file(filename, base_dir = '.', options = {}) ⇒ 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_index ⇒ Hash<Symbol, Array<Integer>>, Hash{Symbol => Array<Integer>}
Lazily build and return media_index.
-
#merge ⇒ Object
Deprecated: Use flatten instead.
-
#merge! ⇒ Object
Deprecated: Use flatten! instead.
-
#remove_rules!(rules_or_css, media_types: nil) ⇒ self
Remove rules from the stylesheet.
-
#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, prefix_match: false) ⇒ 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.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/cataract/stylesheet.rb', line 111 def initialize( = {}) # Type validation raise TypeError, "options must be a Hash, got #{.class}" unless .is_a?(Hash) # Support :imports as alias for :import (backwards compatibility) [:import] = .delete(:imports) if .key?(:imports) && !.key?(:import) @options = { import: false, io_exceptions: true, base_uri: nil, base_dir: nil, absolute_paths: false, uri_resolver: nil, parser: {}, raise_parse_errors: false }.merge() # Type validation for specific options if @options[:import_fetcher] && !@options[:import_fetcher].respond_to?(:call) raise TypeError, "import_fetcher must be a Proc or callable, got #{@options[:import_fetcher].class}" end if @options[:base_uri] && !@options[:base_uri].is_a?(String) raise TypeError, "base_uri must be a String, got #{@options[:base_uri].class}" end if @options[:uri_resolver] && !@options[:uri_resolver].respond_to?(:call) raise TypeError, "uri_resolver must be a Proc or callable, got #{@options[:uri_resolver].class}" end # Parser options with defaults (stored for passing to parser) @parser_options = { selector_lists: true, raise_parse_errors: @options[:raise_parse_errors] }.merge(@options[:parser] || {}) @rules = [] # Flat array of Rule structs @media_queries = [] # Array of MediaQuery objects @_next_media_query_id = 0 # Counter for MediaQuery IDs @media_index = {} # Hash: Symbol => Array of rule IDs (cached index, can be rebuilt from rules) @_selector_lists = {} # Hash: list_id => Array of rule IDs (for "h1, h2" grouping) @_next_selector_list_id = 0 # Counter for selector list IDs @_media_query_lists = {} # Hash: list_id => Array of MediaQuery IDs (for "screen, print" grouping) @_next_media_query_list_id = 0 # Counter for media query list IDs @charset = nil @imports = [] # Array of ImportStatement objects @_has_nesting = nil # Set by parser (nil or boolean) @_last_rule_id = nil # Tracks next rule ID for add_block @selectors = nil # Memoized cache of selectors @_custom_properties = nil # Memoized cache of custom properties end |
Instance Attribute Details
#charset ⇒ String? (readonly)
Returns The @charset declaration if present.
23 24 25 |
# File 'lib/cataract/stylesheet.rb', line 23 def charset @charset end |
#imports ⇒ Array<ImportStatement> (readonly)
Returns Array of @import statements.
23 24 25 |
# File 'lib/cataract/stylesheet.rb', line 23 def imports @imports end |
#media_queries ⇒ Array<MediaQuery> (readonly)
Returns Array of media query objects.
30 31 32 |
# File 'lib/cataract/stylesheet.rb', line 30 def media_queries @media_queries end |
#rules ⇒ Array<Rule> (readonly)
Returns Array of parsed CSS rules.
23 24 25 |
# File 'lib/cataract/stylesheet.rb', line 23 def rules @rules end |
Class Method Details
.load_file(filename, base_dir = '.', **options) ⇒ Stylesheet
Load CSS from a file and return a new Stylesheet.
205 206 207 208 209 |
# File 'lib/cataract/stylesheet.rb', line 205 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.
216 217 218 219 220 |
# File 'lib/cataract/stylesheet.rb', line 216 def self.load_uri(uri, **) sheet = new() sheet.load_uri(uri, ) sheet end |
.parse(css, **options) ⇒ Stylesheet
Parse CSS and return a new Stylesheet
192 193 194 195 196 |
# File 'lib/cataract/stylesheet.rb', line 192 def self.parse(css, **) sheet = new() sheet.add_block(css) sheet end |
Instance Method Details
#+(other) ⇒ Stylesheet
Combine two stylesheets into a new one and apply cascade.
Creates a new stylesheet containing rules from both stylesheets, then applies CSS cascade to resolve conflicts.
871 872 873 874 875 |
# File 'lib/cataract/stylesheet.rb', line 871 def +(other) result = dup result.concat(other) result end |
#-(other) ⇒ Stylesheet
Remove matching rules from this stylesheet.
Creates a new stylesheet with rules that don't match any rules in the other stylesheet. Uses Rule#== for matching (shorthand-aware). Does NOT apply cascade to the result.
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 |
# File 'lib/cataract/stylesheet.rb', line 885 def -(other) raise ArgumentError, 'Argument must be a Stylesheet' unless other.is_a?(Stylesheet) result = dup # Remove matching rules using Rule#== rules_to_remove_ids = [] result.rules.each_with_index do |rule, idx| rules_to_remove_ids << idx if other.rules.include?(rule) end # Remove in reverse order to maintain indices rules_to_remove_ids.reverse_each do |idx| result.rules.delete_at(idx) # Update media_index: remove this rule ID and decrement higher IDs result.media_index_cache.each_value do |ids| ids.delete(idx) ids.map! { |id| id > idx ? id - 1 : id } end end # Re-index remaining rules result.rules.each_with_index { |rule, new_id| rule.id = new_id } # Clean up empty media_index entries result.media_index_cache.delete_if { |_media, ids| ids.empty? } result.send(:compact_media_queries!) # Clear memoized cache result.send(:clear_memoized_caches) result._hash = nil result end |
#==(other) ⇒ Boolean Also known as: eql?
Compare stylesheets for equality.
Two stylesheets are equal if they have the same rules in the same order and the same media queries. Rule equality uses shorthand-aware comparison. Order matters because CSS cascade depends on rule order.
Charset is ignored since it's file encoding metadata, not semantic content.
771 772 773 774 775 776 777 |
# File 'lib/cataract/stylesheet.rb', line 771 def ==(other) return false unless other.is_a?(Stylesheet) return false unless rules == other.rules return false unless @media_queries == other.media_queries true end |
#[](offset) ⇒ Object
242 243 244 245 246 |
# File 'lib/cataract/stylesheet.rb', line 242 def [](offset) return unless @rules @rules[offset] end |
#add_block(css, fix_braces: false, media_types: nil, base_uri: nil, base_dir: nil, absolute_paths: nil) ⇒ self
Add CSS block to stylesheet
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 |
# File 'lib/cataract/stylesheet.rb', line 704 def add_block(css, fix_braces: false, media_types: nil, base_uri: nil, base_dir: nil, absolute_paths: 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 # Determine effective options (per-call overrides or constructor defaults) effective_base_uri = base_uri || @options[:base_uri] effective_base_dir = base_dir || @options[:base_dir] effective_absolute_paths = absolute_paths.nil? ? @options[:absolute_paths] : absolute_paths = (effective_base_uri, effective_absolute_paths) # Parse CSS first (this extracts @import statements into result[:imports]) result = Cataract._parse_css(css, ) merge_parsed_block!(result, effective_base_uri, effective_base_dir) self end |
#add_rule(selector:, declarations:, media_types: nil) ⇒ self
Add a single rule
734 735 736 737 738 |
# File 'lib/cataract/stylesheet.rb', line 734 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.
345 346 347 |
# File 'lib/cataract/stylesheet.rb', line 345 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)
390 391 392 393 394 |
# File 'lib/cataract/stylesheet.rb', line 390 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
517 518 519 520 521 522 523 |
# File 'lib/cataract/stylesheet.rb', line 517 def clear! @rules.clear @media_index.clear @charset = nil clear_memoized_caches self end |
#concat(other) ⇒ self
Concatenate another stylesheet's rules into this one and apply cascade.
Adds all rules from the other stylesheet to this one, then applies CSS cascade to resolve conflicts. Media queries are merged.
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 |
# File 'lib/cataract/stylesheet.rb', line 837 def concat(other) raise ArgumentError, 'Argument must be a Stylesheet' unless other.is_a?(Stylesheet) # Get the current offset for rule/media-query/selector-list IDs offset = @rules.length list_id_offset = merge_selector_lists!(other.selector_lists, rule_id_offset: offset) mq_id_offset = merge_media_queries!(other.media_queries) merge_media_query_lists!(other.media_query_lists, mq_id_offset: mq_id_offset) # Add rules with updated IDs and cross-references other.rules.each do |rule| new_rule = rule.dup rebase_rule!(new_rule, rule_id_offset: offset, list_id_offset: list_id_offset, mq_id_offset: mq_id_offset) @rules << new_rule end merge_media_index!(other.media_index_cache, rule_id_offset: offset) # Update nesting flag if other has nesting @_has_nesting = true if other.has_nesting clear_memoized_caches # Apply cascade in-place flatten! end |
#convert_colors!(*_args) ⇒ Object
Add convert_colors! instance method to Stylesheet
145 146 147 |
# File 'lib/cataract/pure.rb', line 145 def convert_colors!(*_args) raise NotImplementedError, 'convert_colors! is only available in the native C extension' end |
#custom_properties(media: nil) ⇒ Hash{Symbol => Hash{String => String}}
Get all custom property (CSS variable) definitions organized by media context.
Returns a hash mapping media contexts to custom property hashes. Custom properties are CSS variables that start with -- (e.g., --primary-color). The :root key contains base-level properties (not inside any @media block). When the same custom property is defined multiple times within the same context, the last definition in source order is used.
434 435 436 437 438 439 440 441 |
# File 'lib/cataract/stylesheet.rb', line 434 def custom_properties(media: nil) @_custom_properties ||= build_custom_properties return @_custom_properties if media.nil? # Filter by media if requested media_array = media.is_a?(Array) ? media : [media] @_custom_properties.slice(*media_array) end |
#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.
236 237 238 239 240 |
# File 'lib/cataract/stylesheet.rb', line 236 def each(&) return enum_for(:each) unless block_given? @rules.each(&) end |
#empty? ⇒ Boolean
Check if stylesheet is empty
510 511 512 |
# File 'lib/cataract/stylesheet.rb', line 510 def empty? @rules.empty? end |
#flatten ⇒ Stylesheet Also known as: cascade
Flatten 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.
796 797 798 |
# File 'lib/cataract/stylesheet.rb', line 796 def flatten Cataract.flatten(self) end |
#flatten! ⇒ self Also known as: cascade!
Flatten rules in-place, mutating the receiver.
This is a convenience method that updates the stylesheet's internal rules and media_index with the flattened result. The Stylesheet object itself is mutated (same object_id), but note that the C flatten function still allocates new arrays internally.
815 816 817 818 819 820 821 |
# File 'lib/cataract/stylesheet.rb', line 815 def flatten! flattened = Cataract.flatten(self) @rules = flattened.rules @media_index = flattened.media_index_cache @_has_nesting = flattened.has_nesting self end |
#hash ⇒ Integer
Generate hash code for this stylesheet.
Hash is based on rules and media_queries to match equality semantics.
785 786 787 |
# File 'lib/cataract/stylesheet.rb', line 785 def hash @_hash ||= [self.class, rules, @media_queries].hash # rubocop:disable Naming/MemoizedInstanceVariableName end |
#initialize_copy(source) ⇒ Object
Initialize copy for proper deep duplication.
Ensures that dup/clone creates a proper deep copy of the stylesheet, duplicating internal arrays and hashes so mutations don't affect the original.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/cataract/stylesheet.rb', line 170 def initialize_copy(source) super @options = source..dup @rules = source.rules.dup @media_queries = source.media_queries.dup @_next_media_query_id = source.next_media_query_id @media_index = source.media_index_cache.transform_values(&:dup) @imports = source.imports.dup @_selector_lists = source.selector_lists.transform_values(&:dup) @_next_selector_list_id = source.next_selector_list_id @_media_query_lists = source.media_query_lists.transform_values(&:dup) @_next_media_query_list_id = source.next_media_query_list_id @parser_options = source..dup clear_memoized_caches @_hash = nil # Clear cached hash end |
#inspect ⇒ Object
750 751 752 753 754 755 756 757 758 759 |
# File 'lib/cataract/stylesheet.rb', line 750 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 = '.', options = {}) ⇒ self
Load CSS from a local file and add to this stylesheet.
531 532 533 534 535 536 537 538 |
# File 'lib/cataract/stylesheet.rb', line 531 def load_file(filename, base_dir = '.', = {}) # 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.
545 546 547 548 549 550 551 552 553 554 555 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 |
# File 'lib/cataract/stylesheet.rb', line 545 def load_uri(uri, = {}) require 'uri' uri_obj = URI(uri) # Reuse the same validation and fetch collaborator @import resolution # uses (ImportResolver, via DefaultFetcher/open-uri) instead of a # second, separate Net::HTTP implementation with no validation at all - # that duplicate implementation also didn't follow redirects, unlike # this one. LOAD_DEFAULTS (rather than SAFE_DEFAULTS) reflects that the # caller chose this exact URI themselves, so http/any-extension are # allowed by default; dangerous_path_prefixes still applies unless the # caller overrides it (e.g. dangerous_path_prefixes: []). opts = ImportResolver.(, defaults: ImportResolver::LOAD_DEFAULTS) fetcher = opts[:fetcher] || @options[:import_fetcher] || ImportResolver::DefaultFetcher.new css_content = nil file_path = nil case uri_obj.scheme when 'http', 'https' ImportResolver.validate_url(uri, opts) css_content = fetcher.call(uri, opts) 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 file_uri = "file://#{file_path}" ImportResolver.validate_url(file_uri, opts) css_content = fetcher.call(file_uri, opts) 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_index ⇒ Hash<Symbol, Array<Integer>>, Hash{Symbol => Array<Integer>}
Lazily build and return media_index. Only builds the index when first accessed, not eagerly during parse.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/cataract/stylesheet.rb', line 37 def media_index # If media_index is empty but we have rules with media_query_id, build it if @media_index.empty? && @rules.any? { |r| r.respond_to?(:media_query_id) && r.media_query_id } @media_index = {} # First, build a reverse lookup: media_query_id => list_id (if in a list) mq_id_to_list_id = {} @_media_query_lists.each do |list_id, mq_ids| mq_ids.each { |mq_id| mq_id_to_list_id[mq_id] = list_id } end @rules.each do |rule| next unless rule.media_query_id # Check if this rule's media_query_id is part of a list list_id = mq_id_to_list_id[rule.media_query_id] if list_id # This rule is in a compound media query (e.g., "@media screen, print") # Index it under ALL media types in the list mq_ids = @_media_query_lists[list_id] mq_ids.each do |mq_id| mq = @media_queries[mq_id] next unless mq media_type = mq.type @media_index[media_type] ||= [] @media_index[media_type] << rule.id end else # Single media query - index under its type mq = @media_queries[rule.media_query_id] next unless mq media_type = mq.type @media_index[media_type] ||= [] @media_index[media_type] << rule.id end end # Deduplicate arrays once at the end @media_index.each_value(&:uniq!) end @media_index end |
#merge ⇒ Object
Deprecated: Use flatten instead
802 803 804 805 |
# File 'lib/cataract/stylesheet.rb', line 802 def merge warn 'Stylesheet#merge is deprecated, use #flatten instead', uplevel: 1 flatten end |
#merge! ⇒ Object
Deprecated: Use flatten! instead
825 826 827 828 |
# File 'lib/cataract/stylesheet.rb', line 825 def merge! warn 'Stylesheet#merge! is deprecated, use #flatten! instead', uplevel: 1 flatten! end |
#remove_rules!(rules_or_css, media_types: nil) ⇒ self
Remove rules from the stylesheet
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 |
# File 'lib/cataract/stylesheet.rb', line 623 def remove_rules!(rules_or_css, media_types: nil) # Determine if we're matching by selector (CSS string) or by object identity (rule objects) if rules_or_css.is_a?(String) # Parse CSS string and extract selectors for matching parsed = Stylesheet.parse(rules_or_css) selectors_to_remove = parsed.rules.filter_map(&:selector).to_set match_by_selector = true else # Use rule objects directly rules_to_remove = rules_or_css.is_a?(Array) ? rules_or_css : [rules_or_css] return self if rules_to_remove.empty? match_by_selector = false end # Normalize media_types to array filter_media = media_types ? Array(media_types).map(&:to_sym) : nil # Find rule IDs to remove rule_ids_to_remove = [] @rules.each_with_index do |rule, rule_id| # Check if this rule matches matches = if match_by_selector # Match by selector for CSS string input selectors_to_remove.include?(rule.selector) else # Match by object equality for rule collection input rules_to_remove.any?(rule) end next unless matches # Check media type match if filter is specified if filter_media rule_media_types = media_index.select { |_media, ids| ids.include?(rule_id) }.keys # If rule is not in any media query (base rule), skip unless :all is specified if rule_media_types.empty? next unless filter_media.include?(:all) else # Check if rule's media types intersect with filter next unless rule_media_types.intersect?(filter_media) end end rule_ids_to_remove << rule_id end # Remove rules and update media_index (sort in reverse to maintain indices during deletion) rule_ids_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? } compact_media_queries! # Update rule IDs in remaining rules @rules.each_with_index { |rule, new_id| rule.id = new_id } clear_memoized_caches self end |
#selectors ⇒ Array<String>
Get all selectors
399 400 401 |
# File 'lib/cataract/stylesheet.rb', line 399 def selectors @selectors ||= @rules.map(&:selector) end |
#size ⇒ Integer Also known as: length, rules_count
Get number of rules
501 502 503 |
# File 'lib/cataract/stylesheet.rb', line 501 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.
494 495 496 |
# File 'lib/cataract/stylesheet.rb', line 494 def to_formatted_s(media: :all) Cataract.stylesheet_to_formatted_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists) end |
#to_h ⇒ Hash
Convert to hash
743 744 745 746 747 748 |
# File 'lib/cataract/stylesheet.rb', line 743 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 always included, since they apply regardless of media context. Only rules from OTHER @media queries are excluded.
468 469 470 |
# File 'lib/cataract/stylesheet.rb', line 468 def to_s(media: :all) Cataract.stylesheet_to_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists) end |
#with_at_rule_type(type) ⇒ StylesheetScope
Filter by at-rule type.
Returns a chainable StylesheetScope that can be further filtered.
364 365 366 |
# File 'lib/cataract/stylesheet.rb', line 364 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.
383 384 385 |
# File 'lib/cataract/stylesheet.rb', line 383 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.
264 265 266 |
# File 'lib/cataract/stylesheet.rb', line 264 def with_media(media) StylesheetScope.new(self, media: media) end |
#with_property(property, value = nil, prefix_match: false) ⇒ StylesheetScope
Filter rules by CSS property name and optional value.
Returns a chainable StylesheetScope that can be further filtered.
330 331 332 |
# File 'lib/cataract/stylesheet.rb', line 330 def with_property(property, value = nil, prefix_match: false) StylesheetScope.new(self, property: property, property_value: value, property_prefix_match: prefix_match) 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.
306 307 308 |
# File 'lib/cataract/stylesheet.rb', line 306 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.
283 284 285 |
# File 'lib/cataract/stylesheet.rb', line 283 def with_specificity(specificity) StylesheetScope.new(self, specificity: specificity) end |