cataract 0.3.0 → 0.5.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.
@@ -644,12 +644,15 @@ static int flatten_selector_group_callback(VALUE group_key, VALUE group_indices,
644
644
 
645
645
  int new_rule_id = *ctx->rule_id_counter;
646
646
 
647
- // Extract media_query_id from first rule in group (all should have same media_query_id)
647
+ // Extract media_query_id/conditional_group_id from first rule in group
648
+ // (all should share both, same as selector_list_id above)
648
649
  VALUE media_query_id = Qnil;
650
+ VALUE conditional_group_id = Qnil;
649
651
  if (RARRAY_LEN(group_indices) > 0) {
650
652
  long first_rule_idx = FIX2LONG(RARRAY_AREF(group_indices, 0));
651
653
  VALUE first_rule = RARRAY_AREF(ctx->rules_array, first_rule_idx);
652
654
  media_query_id = rb_struct_aref(first_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
655
+ conditional_group_id = rb_struct_aref(first_rule, INT2FIX(RULE_CONDITIONAL_GROUP_ID));
653
656
  }
654
657
 
655
658
  // Track old rule IDs to new rule ID mapping (only for rules in media queries)
@@ -672,7 +675,8 @@ static int flatten_selector_group_callback(VALUE group_key, VALUE group_indices,
672
675
  Qnil, // parent_rule_id
673
676
  Qnil, // nesting_style
674
677
  selector_list_id, // Preserve selector_list_id if all rules in group share same ID
675
- media_query_id // Preserve media_query_id from source rules
678
+ media_query_id, // Preserve media_query_id from source rules
679
+ conditional_group_id // Preserve conditional_group_id from source rules
676
680
  );
677
681
  rb_ary_push(ctx->merged_rules, new_rule);
678
682
 
@@ -13,6 +13,8 @@
13
13
  * - :not() doesn't count itself, but its content does
14
14
  * - Legacy pseudo-elements with single colon (:before) count as pseudo-elements
15
15
  * - Universal selector (*) has zero specificity
16
+ * - A namespace prefix (ns|E, *|E, |E) contributes zero itself - only the
17
+ * local name after '|' counts, same as a bare "E" would
16
18
  */
17
19
 
18
20
  #include "cataract.h"
@@ -188,14 +190,23 @@ VALUE calculate_specificity(VALUE self, VALUE selector_string) {
188
190
  continue;
189
191
  }
190
192
 
191
- // Type selector (element name): div, span, etc.
193
+ // Type selector (element name): div, span, etc. - or a namespace
194
+ // prefix (css-namespaces-3 qname grammar: prefix? '|' ident). A
195
+ // prefix contributes nothing itself; only the local name after '|'
196
+ // is the real type selector, so don't count it here - just skip
197
+ // past it and the separator and let the next iteration count the
198
+ // local name instead.
192
199
  if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) {
193
- element_count++;
194
200
  // Skip the identifier
195
201
  while (p < pe && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
196
202
  (*p >= '0' && *p <= '9') || *p == '-' || *p == '_')) {
197
203
  p++;
198
204
  }
205
+ if (p < pe && *p == '|') {
206
+ p++; // skip namespace separator - prefix itself doesn't count
207
+ continue;
208
+ }
209
+ element_count++;
199
210
  continue;
200
211
  }
201
212
 
@@ -8,7 +8,6 @@ module Cataract
8
8
  # The content field varies by at-rule type:
9
9
  # - `@keyframes`: Array of Rule (keyframe percentage blocks like "0%", "100%")
10
10
  # - `@font-face`: Array of Declaration (font property declarations)
11
- # - `@supports`: Array of Rule (conditional rules)
12
11
  #
13
12
  # @example Parse @keyframes
14
13
  # css = "@keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; } }"
@@ -29,7 +28,10 @@ module Cataract
29
28
  # @attr [Array<Rule>, Array<Declaration>] content Nested rules or declarations
30
29
  # @attr [nil] specificity Always nil for at-rules (they don't have CSS specificity)
31
30
  # @attr [Integer, nil] media_query_id ID of MediaQuery if inside @media block, nil otherwise
32
- AtRule = Struct.new(:id, :selector, :content, :specificity, :media_query_id) unless const_defined?(:AtRule)
31
+ # @attr [Integer, nil] conditional_group_id ID of ConditionalGroup if inside @supports/@layer/@container/@scope, nil otherwise
32
+ unless const_defined?(:AtRule)
33
+ AtRule = Struct.new(:id, :selector, :content, :specificity, :media_query_id, :conditional_group_id)
34
+ end
33
35
 
34
36
  class AtRule
35
37
  # Check if this is a selector-based rule (vs an at-rule like @keyframes).
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cataract
4
+ # ConditionalGroup represents a CSS "conditional group" at-rule wrapper -
5
+ # @supports, @layer, @container, or @scope.
6
+ #
7
+ # Like MediaQuery, these are stored in the Stylesheet and referenced by
8
+ # Rules/AtRules via conditional_group_id, so the rules they wrap stay flat
9
+ # and queryable in Stylesheet#rules while the wrapper's own name/condition
10
+ # (and nesting inside other conditional groups, via parent_id) survive for
11
+ # round-trip serialization. Cataract never evaluates the condition/name
12
+ # (no feature-query or container-query matching) - it's kept as opaque
13
+ # text.
14
+ #
15
+ # @example Access conditional-group properties
16
+ # group = ConditionalGroup.new(0, :supports, nil, '(display: grid)', nil)
17
+ # group.id #=> 0
18
+ # group.type #=> :supports
19
+ # group.condition #=> "(display: grid)"
20
+ #
21
+ # @attr [Integer] id Unique identifier for this conditional group within the stylesheet
22
+ # @attr [Symbol] type At-rule kind (:supports, :layer, :container, :scope)
23
+ # @attr [String, nil] name Named form (e.g. `@layer utilities`, `@container sidebar`), nil if unnamed
24
+ # @attr [String, nil] condition Opaque condition/prelude text (feature query, container query, scope root/limit), nil if none
25
+ # @attr [Integer, nil] parent_id Id of the enclosing ConditionalGroup, or nil if not nested inside another one
26
+ ConditionalGroup = Struct.new(:id, :type, :name, :condition, :parent_id) do
27
+ # Create a ConditionalGroup with keyword arguments for readability.
28
+ #
29
+ # @param id [Integer] Unique ID for this conditional group
30
+ # @param type [Symbol] At-rule kind (:supports, :layer, :container, :scope)
31
+ # @param name [String, nil] Named form, if any
32
+ # @param condition [String, nil] Opaque condition/prelude text, if any
33
+ # @param parent_id [Integer, nil] Enclosing ConditionalGroup's id, if nested
34
+ # @return [ConditionalGroup] New conditional group instance
35
+ #
36
+ # @example
37
+ # ConditionalGroup.make(id: 0, type: :supports, condition: '(display: grid)')
38
+ def self.make(id:, type:, name: nil, condition: nil, parent_id: nil)
39
+ new(id, type, name, condition, parent_id)
40
+ end
41
+
42
+ # Compare conditional groups for equality based on type, name, condition,
43
+ # and nesting. IDs are not considered since they're internal identifiers.
44
+ #
45
+ # @param other [Object] Object to compare with
46
+ # @return [Boolean] true if conditional groups match
47
+ def ==(other)
48
+ case other
49
+ when ConditionalGroup
50
+ type == other.type && name == other.name && condition == other.condition
51
+ else
52
+ false
53
+ end
54
+ end
55
+ alias_method :eql?, :==
56
+
57
+ # Generate hash code for this conditional group.
58
+ #
59
+ # @return [Integer] hash code
60
+ def hash
61
+ [self.class, type, name, condition].hash
62
+ end
63
+
64
+ # Get detailed inspection string.
65
+ #
66
+ # @return [String] Inspection string
67
+ def inspect
68
+ "#<ConditionalGroup id=#{id} type=#{type.inspect} name=#{name.inspect} " \
69
+ "condition=#{condition.inspect} parent_id=#{parent_id.inspect}>"
70
+ end
71
+ end
72
+ end
@@ -198,12 +198,22 @@ module Cataract
198
198
 
199
199
  # Convert declarations to CSS string.
200
200
  #
201
- # Implemented in C for performance.
202
- #
203
201
  # @return [String] CSS declaration block string
204
202
  #
205
203
  # @example
206
204
  # decls.to_s #=> "color: red; margin: 10px !important;"
205
+ def to_s
206
+ result = +''
207
+ @values.each_with_index do |decl, i|
208
+ result << decl.property
209
+ result << ': '
210
+ result << decl.value
211
+ result << ' !important' if decl.important
212
+ result << ';'
213
+ result << ' ' if i < @values.length - 1 # Add space after semicolon except for last
214
+ end
215
+ result
216
+ end
207
217
 
208
218
  # Enable implicit string conversion for comparisons
209
219
  alias to_str to_s
@@ -290,7 +300,7 @@ module Cataract
290
300
  # @param source [Declarations] Source Declarations being copied
291
301
  def initialize_copy(source)
292
302
  super
293
- @values = source.instance_variable_get(:@values).dup
303
+ @values = source.to_a.dup
294
304
  end
295
305
 
296
306
  # Compare this Declarations with another object.
@@ -331,7 +341,7 @@ module Cataract
331
341
 
332
342
  # Parse "color: red; background: blue" string into array of Declaration structs
333
343
  def parse_declaration_string(str)
334
- Cataract.parse_declarations(str)
344
+ Cataract::Backends.active.parse_declarations(str)
335
345
  end
336
346
  end
337
347
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Native (C extension) backend for Cataract.
4
+ #
5
+ # Load this instead of the pure Ruby version by NOT setting CATARACT_PURE
6
+ # before requiring 'cataract' (not by requiring this file directly -
7
+ # lib/cataract.rb is what sets up Cataract::Backends.active and the
8
+ # top-level identity constants like IMPLEMENTATION; this file alone does
9
+ # not):
10
+ # require 'cataract'
11
+
12
+ require_relative 'error'
13
+ require_relative 'version'
14
+ require_relative 'constants'
15
+
16
+ # Load struct definitions first (before the C extension) - the extension's
17
+ # Init function looks these up and raises if they're missing.
18
+ require_relative 'declaration'
19
+ require_relative 'rule'
20
+ require_relative 'at_rule'
21
+ require_relative 'media_query'
22
+ require_relative 'conditional_group'
23
+ require_relative 'import_statement'
24
+
25
+ require_relative 'native_extension'
26
+
27
+ # Load supporting Ruby files (used by both implementations)
28
+ require_relative 'stylesheet_scope'
29
+ require_relative 'stylesheet'
30
+ require_relative 'declarations'
31
+ require_relative 'import_resolver'
@@ -4,80 +4,84 @@
4
4
  # Using getbyte() instead of String#[] to avoid allocating millions of string objects
5
5
 
6
6
  module Cataract
7
- # Whitespace bytes
8
- BYTE_SPACE = 32 # ' '
9
- BYTE_TAB = 9 # '\t'
10
- BYTE_NEWLINE = 10 # '\n'
11
- BYTE_CR = 13 # '\r'
7
+ module Backends
8
+ class PureImpl
9
+ # Whitespace bytes
10
+ BYTE_SPACE = 32 # ' '
11
+ BYTE_TAB = 9 # '\t'
12
+ BYTE_NEWLINE = 10 # '\n'
13
+ BYTE_CR = 13 # '\r'
12
14
 
13
- # CSS structural characters
14
- BYTE_AT = 64 # '@'
15
- BYTE_LBRACE = 123 # '{'
16
- BYTE_RBRACE = 125 # '}'
17
- BYTE_LPAREN = 40 # '('
18
- BYTE_RPAREN = 41 # ')'
19
- BYTE_LBRACKET = 91 # '['
20
- BYTE_RBRACKET = 93 # ']'
21
- BYTE_SEMICOLON = 59 # ';'
22
- BYTE_COLON = 58 # ':'
23
- BYTE_COMMA = 44 # ','
15
+ # CSS structural characters
16
+ BYTE_AT = 64 # '@'
17
+ BYTE_LBRACE = 123 # '{'
18
+ BYTE_RBRACE = 125 # '}'
19
+ BYTE_LPAREN = 40 # '('
20
+ BYTE_RPAREN = 41 # ')'
21
+ BYTE_LBRACKET = 91 # '['
22
+ BYTE_RBRACKET = 93 # ']'
23
+ BYTE_SEMICOLON = 59 # ';'
24
+ BYTE_COLON = 58 # ':'
25
+ BYTE_COMMA = 44 # ','
24
26
 
25
- # Comment characters
26
- BYTE_SLASH = 47 # '/'
27
- BYTE_STAR = 42 # '*'
27
+ # Comment characters
28
+ BYTE_SLASH = 47 # '/'
29
+ BYTE_STAR = 42 # '*'
28
30
 
29
- # Quote characters
30
- BYTE_SQUOTE = 39 # "'"
31
- BYTE_DQUOTE = 34 # '"'
31
+ # Quote characters
32
+ BYTE_SQUOTE = 39 # "'"
33
+ BYTE_DQUOTE = 34 # '"'
32
34
 
33
- # Selector characters
34
- BYTE_HASH = 35 # '#'
35
- BYTE_DOT = 46 # '.'
36
- BYTE_GT = 62 # '>'
37
- BYTE_PLUS = 43 # '+'
38
- BYTE_TILDE = 126 # '~'
39
- BYTE_ASTERISK = 42 # '*'
40
- BYTE_AMPERSAND = 38 # '&'
35
+ # Selector characters
36
+ BYTE_HASH = 35 # '#'
37
+ BYTE_DOT = 46 # '.'
38
+ BYTE_GT = 62 # '>'
39
+ BYTE_PLUS = 43 # '+'
40
+ BYTE_TILDE = 126 # '~'
41
+ BYTE_ASTERISK = 42 # '*'
42
+ BYTE_AMPERSAND = 38 # '&'
41
43
 
42
- # Other
43
- BYTE_HYPHEN = 45 # '-'
44
- BYTE_UNDERSCORE = 95 # '_'
45
- BYTE_BACKSLASH = 92 # '\\'
46
- BYTE_BANG = 33 # '!'
47
- BYTE_PERCENT = 37 # '%'
48
- BYTE_EQUALS = 61 # '='
49
- BYTE_CARET = 94 # '^'
50
- BYTE_DOLLAR = 36 # '$'
51
- BYTE_PIPE = 124 # '|'
44
+ # Other
45
+ BYTE_HYPHEN = 45 # '-'
46
+ BYTE_UNDERSCORE = 95 # '_'
47
+ BYTE_BACKSLASH = 92 # '\\'
48
+ BYTE_BANG = 33 # '!'
49
+ BYTE_PERCENT = 37 # '%'
50
+ BYTE_EQUALS = 61 # '='
51
+ BYTE_CARET = 94 # '^'
52
+ BYTE_DOLLAR = 36 # '$'
53
+ BYTE_PIPE = 124 # '|'
52
54
 
53
- # Specific lowercase letters (for keyword matching)
54
- BYTE_LOWER_U = 117 # 'u'
55
- BYTE_LOWER_R = 114 # 'r'
56
- BYTE_LOWER_L = 108 # 'l'
57
- BYTE_LOWER_D = 100 # 'd'
58
- BYTE_LOWER_T = 116 # 't'
59
- BYTE_LOWER_N = 110 # 'n'
55
+ # Specific lowercase letters (for keyword matching)
56
+ BYTE_LOWER_U = 117 # 'u'
57
+ BYTE_LOWER_R = 114 # 'r'
58
+ BYTE_LOWER_L = 108 # 'l'
59
+ BYTE_LOWER_D = 100 # 'd'
60
+ BYTE_LOWER_T = 116 # 't'
61
+ BYTE_LOWER_N = 110 # 'n'
60
62
 
61
- # Specific uppercase letters (for case-insensitive matching)
62
- BYTE_UPPER_U = 85 # 'U'
63
- BYTE_UPPER_R = 82 # 'R'
64
- BYTE_UPPER_L = 76 # 'L'
65
- BYTE_UPPER_D = 68 # 'D'
66
- BYTE_UPPER_T = 84 # 'T'
67
- BYTE_UPPER_N = 78 # 'N'
63
+ # Specific uppercase letters (for case-insensitive matching)
64
+ BYTE_UPPER_U = 85 # 'U'
65
+ BYTE_UPPER_R = 82 # 'R'
66
+ BYTE_UPPER_L = 76 # 'L'
67
+ BYTE_UPPER_D = 68 # 'D'
68
+ BYTE_UPPER_T = 84 # 'T'
69
+ BYTE_UPPER_N = 78 # 'N'
68
70
 
69
- # Letter ranges (a-z, A-Z)
70
- BYTE_LOWER_A = 97 # 'a'
71
- BYTE_LOWER_Z = 122 # 'z'
72
- BYTE_UPPER_A = 65 # 'A'
73
- BYTE_UPPER_Z = 90 # 'Z'
74
- BYTE_CASE_DIFF = 32 # Difference between lowercase and uppercase ('a' - 'A')
71
+ # Letter ranges (a-z, A-Z)
72
+ BYTE_LOWER_A = 97 # 'a'
73
+ BYTE_LOWER_Z = 122 # 'z'
74
+ BYTE_UPPER_A = 65 # 'A'
75
+ BYTE_UPPER_Z = 90 # 'Z'
76
+ BYTE_CASE_DIFF = 32 # Difference between lowercase and uppercase ('a' - 'A')
75
77
 
76
- # Digit range (0-9)
77
- BYTE_DIGIT_0 = 48 # '0'
78
- BYTE_DIGIT_9 = 57 # '9'
78
+ # Digit range (0-9)
79
+ BYTE_DIGIT_0 = 48 # '0'
80
+ BYTE_DIGIT_9 = 57 # '9'
79
81
 
80
- # Nesting styles (for CSS nesting support)
81
- NESTING_STYLE_IMPLICIT = 0 # Implicit nesting: .parent { .child { ... } } => .parent .child
82
- NESTING_STYLE_EXPLICIT = 1 # Explicit nesting: .parent { &:hover { ... } } => .parent:hover
82
+ # Nesting styles (for CSS nesting support)
83
+ NESTING_STYLE_IMPLICIT = 0 # Implicit nesting: .parent { .child { ... } } => .parent .child
84
+ NESTING_STYLE_EXPLICIT = 1 # Explicit nesting: .parent { &:hover { ... } } => .parent:hover
85
+ end
86
+ end
83
87
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pure Ruby CSS parser - standalone declaration-list string parsing
4
+ # NO REGEXP ALLOWED - char-by-char parsing only
5
+ #
6
+ # Backs Declarations.new(some_string), NOT the main parser's declaration
7
+ # handling - the two intentionally disagree in a few ways:
8
+ # - Custom properties (--foo) are downcased here too (the main parser
9
+ # preserves their case, since they're case-sensitive per spec)
10
+ # - No relative-URL conversion (there's no base_uri to convert against)
11
+ # - A missing colon stops parsing entirely, rather than recovering at the
12
+ # next semicolon like the main parser does
13
+ # Mirrors ext/cataract/cataract.c's new_parse_declarations_string exactly,
14
+ # byte for byte, since both backends must agree here.
15
+
16
+ module Cataract
17
+ module Backends
18
+ class PureImpl
19
+ # Parse a standalone CSS declaration-list string ("color: red; margin: 10px")
20
+ # into an array of Declaration structs.
21
+ #
22
+ # @param str [String] CSS declarations, optionally wrapped in { ... }
23
+ # @return [Array<Declaration>] Parsed declarations
24
+ def parse_declarations(str)
25
+ fin = str.bytesize
26
+ pos = 0
27
+
28
+ # Strip outer braces and whitespace (css_parser compatibility)
29
+ pos += 1 while pos < fin && (decl_whitespace?(str.getbyte(pos)) || str.getbyte(pos) == BYTE_LBRACE)
30
+ fin -= 1 while fin > pos && (decl_whitespace?(str.getbyte(fin - 1)) || str.getbyte(fin - 1) == BYTE_RBRACE)
31
+
32
+ declarations = []
33
+
34
+ while pos < fin
35
+ pos += 1 while pos < fin && (decl_whitespace?(str.getbyte(pos)) || str.getbyte(pos) == BYTE_SEMICOLON)
36
+ break if pos >= fin
37
+
38
+ span = scan_one_declaration(str, pos, fin)
39
+ break unless span # No colon found - stop parsing entirely
40
+
41
+ pos = span[:next_pos]
42
+ next unless span[:val_end] > span[:val_start] # Skip if value is empty
43
+
44
+ property = str.byteslice(span[:prop_start], span[:prop_end] - span[:prop_start])
45
+ .force_encoding(Encoding::US_ASCII).downcase
46
+ value = str.byteslice(span[:val_start], span[:val_end] - span[:val_start]).force_encoding(Encoding::UTF_8)
47
+
48
+ declarations << Declaration.new(property, value, span[:important])
49
+ end
50
+
51
+ declarations
52
+ end
53
+
54
+ # Scans one "prop: value" declaration starting at pos, stopping at fin
55
+ # (never reads past it). On success, returns a span hash and next_pos is
56
+ # the position just past the terminating ';' (or fin/'}' if there wasn't
57
+ # one). On failure - no ':' found - returns nil, leaving the caller to
58
+ # decide how to recover (this parser stops entirely).
59
+ #
60
+ # The value scan tracks paren depth, so a ';' inside url(...) or rgba(...)
61
+ # doesn't end the value early, and always stops at an unguarded '}'.
62
+ def scan_one_declaration(str, pos, fin)
63
+ prop_start = pos
64
+ pos += 1 while pos < fin && str.getbyte(pos) != BYTE_COLON
65
+ return nil if pos >= fin
66
+
67
+ prop_end = pos
68
+ prop_end -= 1 while prop_end > prop_start && decl_whitespace?(str.getbyte(prop_end - 1))
69
+ prop_start += 1 while prop_start < prop_end && decl_whitespace?(str.getbyte(prop_start))
70
+
71
+ pos += 1 # Skip ':'
72
+ pos += 1 while pos < fin && decl_whitespace?(str.getbyte(pos))
73
+
74
+ val_start = pos
75
+ paren_depth = 0
76
+ while pos < fin && str.getbyte(pos) != BYTE_RBRACE
77
+ byte = str.getbyte(pos)
78
+ if byte == BYTE_LPAREN
79
+ paren_depth += 1
80
+ elsif byte == BYTE_RPAREN
81
+ paren_depth -= 1
82
+ elsif byte == BYTE_SEMICOLON && paren_depth == 0
83
+ break
84
+ end
85
+ pos += 1
86
+ end
87
+ val_end = pos
88
+ val_end -= 1 while val_end > val_start && decl_whitespace?(str.getbyte(val_end - 1))
89
+
90
+ important, val_end = extract_important(str, val_start, val_end)
91
+ val_end -= 1 while val_end > val_start && decl_whitespace?(str.getbyte(val_end - 1))
92
+
93
+ pos += 1 if pos < fin && str.getbyte(pos) == BYTE_SEMICOLON
94
+
95
+ { prop_start: prop_start, prop_end: prop_end, val_start: val_start, val_end: val_end,
96
+ important: important, next_pos: pos }
97
+ end
98
+ private :scan_one_declaration
99
+
100
+ # Detects and strips a trailing '!important' marker from [val_start, val_end).
101
+ # Assumes trailing whitespace has already been trimmed once. Zero or more
102
+ # whitespace tokens are allowed between '!' and 'important' per the CSS2.1
103
+ # grammar. Returns [important?, new_val_end].
104
+ def extract_important(str, val_start, val_end)
105
+ check = val_end
106
+ return [false, val_end] if check - val_start < 10 # strlen("!important") == 10
107
+
108
+ check -= 1 while check > val_start && decl_whitespace?(str.getbyte(check - 1))
109
+ return [false, val_end] if check - val_start < 9 || str.byteslice(check - 9, 9) != 'important'
110
+
111
+ check -= 9
112
+ check -= 1 while check > val_start && decl_whitespace?(str.getbyte(check - 1))
113
+ return [false, val_end] if check <= val_start || str.getbyte(check - 1) != BYTE_BANG
114
+
115
+ [true, check - 1]
116
+ end
117
+ private :extract_important
118
+
119
+ def decl_whitespace?(byte)
120
+ byte == BYTE_SPACE || byte == BYTE_TAB || byte == BYTE_NEWLINE || byte == BYTE_CR
121
+ end
122
+ private :decl_whitespace?
123
+ end
124
+ end
125
+ end