Module: Cataract
- Defined in:
- lib/cataract.rb,
lib/cataract/pure.rb,
lib/cataract/pure.rb,
lib/cataract/rule.rb,
lib/cataract/error.rb,
lib/cataract/at_rule.rb,
lib/cataract/version.rb,
lib/cataract/constants.rb,
lib/cataract/stylesheet.rb,
lib/cataract/declaration.rb,
lib/cataract/media_query.rb,
lib/cataract/pure/parser.rb,
lib/cataract/declarations.rb,
lib/cataract/pure/flatten.rb,
lib/cataract/pure/helpers.rb,
lib/cataract/import_resolver.rb,
lib/cataract/pure/serializer.rb,
lib/cataract/import_statement.rb,
lib/cataract/pure/specificity.rb,
lib/cataract/stylesheet_scope.rb,
lib/cataract/pure/byte_constants.rb,
ext/cataract/cataract.c
Overview
Pure Ruby CSS parser - Byte constants for fast parsing Using getbyte() instead of String#[] to avoid allocating millions of string objects
Defined Under Namespace
Modules: Flatten, ImportResolver Classes: AtRule, ColorConversionError, Declaration, Declarations, DepthError, Error, ImportError, ImportStatement, MediaQuery, ParseError, Parser, Rule, SizeError, Stylesheet, StylesheetScope
Constant Summary collapse
- PURE_RUBY_LOADED =
Flag to indicate pure Ruby version is loaded
true- IMPLEMENTATION =
Implementation type constant
ID2SYM(rb_intern("native"))
- COMPILE_FLAGS =
Compile flags (mimic C version)
compile_flags- VERSION =
'0.3.0'- DEFAULT_URI_RESOLVER =
Default URI resolver proc for converting relative URLs to absolute Uses Ruby's URI stdlib to merge base and relative URIs
lambda { |base, relative| require 'uri' URI.parse(base).merge(relative).to_s }.freeze
- PSEUDO_ELEMENT_KEYWORDS =
Legacy CSS2 pseudo-elements written with a single colon (e.g. :before) that must still be counted as pseudo-elements, not pseudo-classes.
%w[before after first-line first-letter selection].freeze
- BYTE_SPACE =
Whitespace bytes
32- BYTE_TAB =
' '
9- BYTE_NEWLINE =
'\t'
10- BYTE_CR =
'\n'
13- BYTE_AT =
CSS structural characters
64- BYTE_LBRACE =
'@'
123- BYTE_RBRACE =
'{'
125- BYTE_LPAREN =
'}'
40- BYTE_RPAREN =
'('
41- BYTE_LBRACKET =
')'
91- BYTE_RBRACKET =
'['
93- BYTE_SEMICOLON =
']'
59- BYTE_COLON =
';'
58- BYTE_COMMA =
':'
44- BYTE_SLASH =
Comment characters
47- BYTE_STAR =
'/'
42- BYTE_SQUOTE =
Quote characters
39- BYTE_DQUOTE =
"'"
34- BYTE_HASH =
Selector characters
35- BYTE_DOT =
'#'
46- BYTE_GT =
'.'
62- BYTE_PLUS =
'>'
43- BYTE_TILDE =
'+'
126- BYTE_ASTERISK =
'~'
42- BYTE_AMPERSAND =
'*'
38- BYTE_HYPHEN =
Other
45- BYTE_UNDERSCORE =
'-'
95- BYTE_BACKSLASH =
'_'
92- BYTE_BANG =
'\'
33- BYTE_PERCENT =
'!'
37- BYTE_EQUALS =
'%'
61- BYTE_CARET =
'='
94- BYTE_DOLLAR =
'^'
36- BYTE_PIPE =
'$'
124- BYTE_LOWER_U =
Specific lowercase letters (for keyword matching)
117- BYTE_LOWER_R =
'u'
114- BYTE_LOWER_L =
'r'
108- BYTE_LOWER_D =
'l'
100- BYTE_LOWER_T =
'd'
116- BYTE_LOWER_N =
't'
110- BYTE_UPPER_U =
Specific uppercase letters (for case-insensitive matching)
85- BYTE_UPPER_R =
'U'
82- BYTE_UPPER_L =
'R'
76- BYTE_UPPER_D =
'L'
68- BYTE_UPPER_T =
'D'
84- BYTE_UPPER_N =
'T'
78- BYTE_LOWER_A =
Letter ranges (a-z, A-Z)
97- BYTE_LOWER_Z =
'a'
122- BYTE_UPPER_A =
'z'
65- BYTE_UPPER_Z =
'A'
90- BYTE_CASE_DIFF =
'Z'
32- BYTE_DIGIT_0 =
Digit range (0-9)
48- BYTE_DIGIT_9 =
'0'
57- NESTING_STYLE_IMPLICIT =
Nesting styles (for CSS nesting support)
0- NESTING_STYLE_EXPLICIT =
Implicit nesting: .parent { .child { ... } } => .parent .child
1- NATIVE_EXTENSION_LOADED =
Flag to indicate native extension is loaded (for pure Ruby fallback detection)
Qtrue
Class Method Summary collapse
-
._parse_css(*args) ⇒ Hash
Parse CSS and return hash with parsed data This matches the old parse_css API.
-
._rule_parent_id(rule) ⇒ Integer?
private
Helper: read a rule's parent_rule_id, guarding against AtRule (which has no such member - only Rule participates in CSS nesting, AtRule never is nested via '&' nesting).
-
._serialize_children(result, parent_selector, children, rule_children, media_queries, parent_has_declarations) ⇒ Object
private
Helper: recursively serialize a rule's nested children.
-
._serialize_children_formatted(result, parent_selector, children, rule_children, media_queries, indent) ⇒ Object
private
Helper: recursively serialize a rule's nested children with indentation.
-
.calculate_specificity(selector) ⇒ Object
Deprecated alias for backwards compatibility.
-
.digit?(byte) ⇒ Boolean
Check if byte is a digit (0-9).
-
.expand_shorthand(decl) ⇒ Array<Declaration>
private
Expand a single shorthand declaration into longhand declarations.
-
.flatten(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied).
-
.flatten!(stylesheet) ⇒ Stylesheet
Flatten stylesheet rules in-place (mutates receiver).
-
.ident_char?(byte) ⇒ Boolean
Check if byte is alphanumeric, hyphen, or underscore (CSS identifier char).
-
.is_whitespace?(byte) ⇒ Boolean
Check if a byte is whitespace (space, tab, newline, CR).
-
.letter?(byte) ⇒ Boolean
Check if byte is a letter (a-z, A-Z).
-
.merge(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied).
-
.merge!(stylesheet) ⇒ Object
Deprecated: Use flatten! instead.
-
.parse_css(css, **options) ⇒ Object
NOTE: Copied from cataract.rb Need to untangle this eventually.
-
.parse_declarations(declarations_string) ⇒ Array<Declaration>
Ruby-facing wrapper for new_parse_declarations.
- .parse_media_types ⇒ Object
-
.stylesheet_to_formatted_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
Formatted version with indentation and newlines (with nesting support).
-
.stylesheet_to_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
New stylesheet serialization entry point - checks for nesting and delegates.
Instance Method Summary collapse
Class Method Details
._parse_css(*args) ⇒ Hash
Parse CSS and return hash with parsed data This matches the old parse_css API
91 92 93 94 |
# File 'lib/cataract/pure.rb', line 91 def self._parse_css(css_string, = {}) parser = Parser.new(css_string, parser_options: ) parser.parse end |
._rule_parent_id(rule) ⇒ Integer?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Helper: read a rule's parent_rule_id, guarding against AtRule (which has no such member - only Rule participates in CSS nesting, AtRule never is nested via '&' nesting). Calling .parent_rule_id directly on an AtRule raises NoMethodError.
66 67 68 |
# File 'lib/cataract/pure/serializer.rb', line 66 def self._rule_parent_id(rule) rule.is_a?(Rule) ? rule.parent_rule_id : nil end |
._serialize_children(result, parent_selector, children, rule_children, media_queries, parent_has_declarations) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Helper: recursively serialize a rule's nested children. CSS nesting can go as deep as the parser allows (MAX_PARSE_DEPTH), so this recurses rather than hand-unrolling a fixed number of levels - a nested rule's own nested rules are found the same way its parent's were, via rule_children.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/cataract/pure/serializer.rb', line 206 def self._serialize_children(result, parent_selector, children, rule_children, media_queries, parent_has_declarations) children.each_with_index do |child, index| # Add space before nested content # - Always add space if the parent had declarations # - Add space between nested rules (not before first if no declarations) if parent_has_declarations || index > 0 result << ' ' end # Check if this child has @media nesting (parent_rule_id present but nesting_style is nil) if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id] # This is a nested @media rule mq = media_queries[child.media_query_id] media_query_string = _build_media_query_string(mq, nil, nil, media_queries) result << "@media #{media_query_string} { " _serialize_declarations(result, child.declarations) # Serialize any children of this @media rule media_children = rule_children[child.id] || [] media_children.each_with_index do |media_child, media_idx| result << ' ' if media_idx > 0 || !child.declarations.empty? nested_media_selector = _reconstruct_nested_selector( child.selector, media_child.selector, media_child.nesting_style ) result << "#{nested_media_selector} { " _serialize_declarations(result, media_child.declarations) result << ' }' end result << ' }' else # Regular nested selector - determine if we need to reconstruct it with & nested_selector = _reconstruct_nested_selector(parent_selector, child.selector, child.nesting_style) result << "#{nested_selector} { " _serialize_declarations(result, child.declarations) # Recurse into this child's own nested children, however deep they go _serialize_children(result, child.selector, rule_children[child.id] || [], rule_children, media_queries, !child.declarations.empty?) result << ' }' end end end |
._serialize_children_formatted(result, parent_selector, children, rule_children, media_queries, indent) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Helper: recursively serialize a rule's nested children with indentation. CSS nesting can go as deep as the parser allows (MAX_PARSE_DEPTH), so this recurses rather than hand-unrolling a fixed number of levels - a nested rule's own nested rules are found the same way its parent's were, via rule_children, with the indent growing by one level each call.
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
# File 'lib/cataract/pure/serializer.rb', line 683 def self._serialize_children_formatted(result, parent_selector, children, rule_children, media_queries, indent) children.each do |child| if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id] # Nested @media mq = media_queries[child.media_query_id] media_query_string = _build_media_query_string(mq, nil, nil, media_queries) result << indent result << "@media #{media_query_string} {\n" unless child.declarations.empty? _serialize_declarations_formatted(result, child.declarations, "#{indent} ") end # Handle nested media children media_children = rule_children[child.id] || [] media_children.each do |media_child| nested_media_selector = _reconstruct_nested_selector( child.selector, media_child.selector, media_child.nesting_style ) result << indent result << " #{nested_media_selector} {\n" unless media_child.declarations.empty? _serialize_declarations_formatted(result, media_child.declarations, "#{indent} ") end result << indent result << " }\n" end result << indent result << "}\n" else # Regular nested selector nested_selector = _reconstruct_nested_selector(parent_selector, child.selector, child.nesting_style) result << indent result << "#{nested_selector} {\n" unless child.declarations.empty? _serialize_declarations_formatted(result, child.declarations, "#{indent} ") end # Recurse into this child's own nested children, however deep they go _serialize_children_formatted(result, child.selector, rule_children[child.id] || [], rule_children, media_queries, "#{indent} ") result << indent result << "}\n" end end end |
.calculate_specificity(selector) ⇒ Object
Deprecated alias for backwards compatibility
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 |
# File 'lib/cataract/pure/specificity.rb', line 20 def self.calculate_specificity(selector) return 0 if selector.nil? || selector.empty? # Counters for specificity components id_count = 0 class_count = 0 attr_count = 0 pseudo_class_count = 0 pseudo_element_count = 0 element_count = 0 i = 0 len = selector.length while i < len byte = selector.getbyte(i) if byte == BYTE_HASH id_count += 1 i = skip_identifier(selector, i + 1, len) elsif byte == BYTE_DOT class_count += 1 i = skip_identifier(selector, i + 1, len) elsif byte == BYTE_LBRACKET attr_count += 1 i = skip_attribute_selector(selector, i, len) elsif byte == BYTE_COLON i, is_not, not_content, counts_as_element = parse_pseudo(selector, i, len) if not_content # :not() doesn't count itself, but its content does not_specificity = calculate_specificity(not_content) id_count += not_specificity / 100 class_count += (not_specificity % 100) / 10 element_count += not_specificity % 10 elsif !is_not if counts_as_element pseudo_element_count += 1 else pseudo_class_count += 1 end end elsif letter?(byte) element_count += 1 i = skip_identifier(selector, i + 1, len) else # Whitespace, combinators, and the universal selector (*) all have # zero specificity - just skip a single byte. i += 1 end end # Calculate specificity using W3C formula (id_count * 100) + ((class_count + attr_count + pseudo_class_count) * 10) + ((element_count + pseudo_element_count) * 1) end |
.digit?(byte) ⇒ Boolean
Check if byte is a digit (0-9)
25 26 27 |
# File 'lib/cataract/pure/helpers.rb', line 25 def self.digit?(byte) byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9 end |
.expand_shorthand(decl) ⇒ Array<Declaration>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Expand a single shorthand declaration into longhand declarations. Underscore prefix indicates semi-private API - use with caution.
136 137 138 |
# File 'lib/cataract/pure.rb', line 136 def self.(decl) Flatten.(decl) end |
.flatten(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied)
106 107 108 |
# File 'lib/cataract/pure.rb', line 106 def self.flatten(stylesheet) Flatten.flatten(stylesheet, mutate: false) end |
.flatten!(stylesheet) ⇒ Stylesheet
Flatten stylesheet rules in-place (mutates receiver)
114 115 116 |
# File 'lib/cataract/pure.rb', line 114 def self.flatten!(stylesheet) Flatten.flatten(stylesheet, mutate: true) end |
.ident_char?(byte) ⇒ Boolean
Check if byte is alphanumeric, hyphen, or underscore (CSS identifier char)
32 33 34 |
# File 'lib/cataract/pure/helpers.rb', line 32 def self.ident_char?(byte) letter?(byte) || digit?(byte) || byte == BYTE_HYPHEN || byte == BYTE_UNDERSCORE end |
.is_whitespace?(byte) ⇒ Boolean
Check if a byte is whitespace (space, tab, newline, CR)
10 11 12 |
# File 'lib/cataract/pure/helpers.rb', line 10 def self.is_whitespace?(byte) byte == BYTE_SPACE || byte == BYTE_TAB || byte == BYTE_NEWLINE || byte == BYTE_CR end |
.letter?(byte) ⇒ Boolean
Check if byte is a letter (a-z, A-Z)
17 18 19 20 |
# File 'lib/cataract/pure/helpers.rb', line 17 def self.letter?(byte) (byte >= BYTE_LOWER_A && byte <= BYTE_LOWER_Z) || (byte >= BYTE_UPPER_A && byte <= BYTE_UPPER_Z) end |
.merge(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied)
119 120 121 122 |
# File 'lib/cataract/pure.rb', line 119 def self.merge(stylesheet) warn 'Cataract.merge is deprecated, use Cataract.flatten instead', uplevel: 1 flatten(stylesheet) end |
.merge!(stylesheet) ⇒ Object
Deprecated: Use flatten! instead
125 126 127 128 |
# File 'lib/cataract/pure.rb', line 125 def self.merge!(stylesheet) warn 'Cataract.merge! is deprecated, use Cataract.flatten! instead', uplevel: 1 flatten!(stylesheet) end |
.parse_css(css, **options) ⇒ Object
NOTE: Copied from cataract.rb Need to untangle this eventually
98 99 100 |
# File 'lib/cataract/pure.rb', line 98 def self.parse_css(css, **) Stylesheet.parse(css, **) end |
.parse_declarations(declarations_string) ⇒ Array<Declaration>
Ruby-facing wrapper for new_parse_declarations
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 |
# File 'ext/cataract/cataract.c', line 1158
static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
Check_Type(declarations_string, T_STRING);
const char *input = RSTRING_PTR(declarations_string);
long input_len = RSTRING_LEN(declarations_string);
// Strip outer braces and whitespace (css_parser compatibility)
const char *start = input;
const char *end = input + input_len;
while (start < end && (IS_WHITESPACE(*start) || *start == '{')) start++;
while (end > start && (IS_WHITESPACE(*(end-1)) || *(end-1) == '}')) end--;
VALUE result = new_parse_declarations_string(start, end);
RB_GC_GUARD(result);
return result;
}
|
.parse_media_types ⇒ Object
.stylesheet_to_formatted_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
Formatted version with indentation and newlines (with nesting support)
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 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 |
# File 'lib/cataract/pure/serializer.rb', line 549 def self.stylesheet_to_formatted_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {}) result = +'' # Add @charset if present unless charset.nil? result << "@charset \"#{charset}\";\n" end # Fast path: no nesting - use simple algorithm unless has_nesting return _stylesheet_to_formatted_s_without_nesting(rules, result, selector_lists, media_queries, media_query_lists) end # Build parent-child relationships rule_children = {} rules.each do |rule| parent_rule_id = _rule_parent_id(rule) next unless parent_rule_id parent_id = parent_rule_id.is_a?(Integer) ? parent_rule_id : parent_rule_id.to_i rule_children[parent_id] ||= [] rule_children[parent_id] << rule end # Build reverse map: media_query_id => list_id 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 # Serialize top-level rules only current_media_query_list_id = nil current_media_query = nil in_media_block = false rules.each do |rule| next if _rule_parent_id(rule) rule_media_query_id = rule.media_query_id rule_media_query = rule_media_query_id ? media_queries[rule_media_query_id] : nil rule_media_query_list_id = rule_media_query_id ? mq_id_to_list_id[rule_media_query_id] : nil if rule_media_query.nil? if in_media_block result << "}\n" in_media_block = false current_media_query = nil current_media_query_list_id = nil end # Add blank line before base rule if we just closed a media block (ends with "}\n") result << "\n" if result.length > 1 && result.getbyte(-1) == BYTE_NEWLINE && result.getbyte(-2) == BYTE_RBRACE _serialize_rule_with_nesting_formatted(result, rule, rule_children, '', media_queries) else # For lists: compare list_id # For single queries: compare by content (type + conditions) needs_new_block = _needs_new_media_block?(current_media_query, current_media_query_list_id, rule_media_query, rule_media_query_list_id) if needs_new_block if in_media_block result << "}\n" elsif result.length > 0 result << "\n" end current_media_query = rule_media_query current_media_query_list_id = rule_media_query_list_id # Serialize the media query (or list) media_query_string = _build_media_query_string(rule_media_query, rule_media_query_list_id, media_query_lists, media_queries) result << "@media #{media_query_string} {\n" in_media_block = true end _serialize_rule_with_nesting_formatted(result, rule, rule_children, ' ', media_queries) end end if in_media_block result << "}\n" end result end |
.stylesheet_to_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
New stylesheet serialization entry point - checks for nesting and delegates
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 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 |
# File 'lib/cataract/pure/serializer.rb', line 79 def self.stylesheet_to_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {}) result = +'' # Add @charset if present unless charset.nil? result << "@charset \"#{charset}\";\n" end # Fast path: no nesting - use simple algorithm unless has_nesting return _stylesheet_to_s_without_nesting(rules, result, selector_lists, media_queries, media_query_lists) end # Build parent-child relationships rule_children = {} rules.each do |rule| parent_rule_id = _rule_parent_id(rule) next unless parent_rule_id parent_id = parent_rule_id.is_a?(Integer) ? parent_rule_id : parent_rule_id.to_i rule_children[parent_id] ||= [] rule_children[parent_id] << rule end # Build reverse map: media_query_id => list_id 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 # Serialize top-level rules only (those without parent_rule_id) current_media_query_list_id = nil current_media_query = nil in_media_block = false rules.each do |rule| # Skip rules that have a parent (they'll be serialized as nested) next if _rule_parent_id(rule) rule_media_query_id = rule.media_query_id rule_media_query = rule_media_query_id ? media_queries[rule_media_query_id] : nil rule_media_query_list_id = rule_media_query_id ? mq_id_to_list_id[rule_media_query_id] : nil if rule_media_query.nil? # Close any open media block if in_media_block result << "}\n" in_media_block = false current_media_query = nil current_media_query_list_id = nil end else # Check if we need to open a new media block # For lists: compare list_id # For single queries: compare by content (type + conditions) needs_new_block = _needs_new_media_block?(current_media_query, current_media_query_list_id, rule_media_query, rule_media_query_list_id) if needs_new_block if in_media_block result << "}\n" end current_media_query = rule_media_query current_media_query_list_id = rule_media_query_list_id # Serialize the media query (or list) media_query_string = _build_media_query_string(rule_media_query, rule_media_query_list_id, media_query_lists, media_queries) result << "@media #{media_query_string} {\n" in_media_block = true end end _serialize_rule_with_nesting(result, rule, rule_children, media_queries) end if in_media_block result << "}\n" end result end |
Instance Method Details
#parse_css(css, **options) ⇒ Object
79 80 81 |
# File 'lib/cataract.rb', line 79 def parse_css(css, **) Stylesheet.parse(css, **) end |