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.
@@ -29,6 +29,9 @@ module Cataract
29
29
  # @return [Array<MediaQuery>] Array of media query objects
30
30
  attr_reader :media_queries
31
31
 
32
+ # @return [Array<ConditionalGroup>] Array of conditional-group objects (@supports/@layer/@container/@scope)
33
+ attr_reader :conditional_groups
34
+
32
35
  # @return [Hash<Symbol, Array<Integer>>] Cached index mapping media query text to rule IDs
33
36
  # Lazily build and return media_index.
34
37
  # Only builds the index when first accessed, not eagerly during parse.
@@ -112,6 +115,13 @@ module Cataract
112
115
  # Type validation
113
116
  raise TypeError, "options must be a Hash, got #{options.class}" unless options.is_a?(Hash)
114
117
 
118
+ # Which backend (Cataract::Backends::Native / ::Pure) produced this
119
+ # stylesheet - defaults to whichever this process picked as active.
120
+ # Not part of the public options contract (used internally by
121
+ # Backends::Native.parse / Backends::Pure.parse), so it's popped off
122
+ # before @options is built.
123
+ @backend = options.delete(:backend) || Cataract::Backends.active
124
+
115
125
  # Support :imports as alias for :import (backwards compatibility)
116
126
  options[:import] = options.delete(:imports) if options.key?(:imports) && !options.key?(:import)
117
127
 
@@ -153,6 +163,8 @@ module Cataract
153
163
  @_next_selector_list_id = 0 # Counter for selector list IDs
154
164
  @_media_query_lists = {} # Hash: list_id => Array of MediaQuery IDs (for "screen, print" grouping)
155
165
  @_next_media_query_list_id = 0 # Counter for media query list IDs
166
+ @conditional_groups = [] # Array of ConditionalGroup objects (@supports/@layer/@container/@scope)
167
+ @_next_conditional_group_id = 0 # Counter for ConditionalGroup IDs
156
168
  @charset = nil
157
169
  @imports = [] # Array of ImportStatement objects
158
170
  @_has_nesting = nil # Set by parser (nil or boolean)
@@ -169,6 +181,7 @@ module Cataract
169
181
  # @param source [Stylesheet] Source stylesheet being copied
170
182
  def initialize_copy(source)
171
183
  super
184
+ @backend = source.backend
172
185
  @options = source.options.dup
173
186
  @rules = source.rules.dup
174
187
  @media_queries = source.media_queries.dup
@@ -179,6 +192,8 @@ module Cataract
179
192
  @_next_selector_list_id = source.next_selector_list_id
180
193
  @_media_query_lists = source.media_query_lists.transform_values(&:dup)
181
194
  @_next_media_query_list_id = source.next_media_query_list_id
195
+ @conditional_groups = source.conditional_groups.dup
196
+ @_next_conditional_group_id = source.next_conditional_group_id
182
197
  @parser_options = source.parser_options.dup
183
198
  clear_memoized_caches
184
199
  @_hash = nil # Clear cached hash
@@ -466,7 +481,8 @@ module Cataract
466
481
  # @example Filter to multiple media types
467
482
  # sheet.to_s(media: [:screen, :print]) # => "@media screen { ... } @media print { ... }"
468
483
  def to_s(media: :all)
469
- Cataract.stylesheet_to_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists)
484
+ @backend.stylesheet_to_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists,
485
+ @conditional_groups)
470
486
  end
471
487
  alias to_css to_s
472
488
 
@@ -492,7 +508,8 @@ module Cataract
492
508
  #
493
509
  # @see #to_s For compact single-line output
494
510
  def to_formatted_s(media: :all)
495
- Cataract.stylesheet_to_formatted_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists)
511
+ @backend.stylesheet_to_formatted_s(filter_rules_by_media(media), @charset, @_has_nesting || false, @_selector_lists, @media_queries, @_media_query_lists,
512
+ @conditional_groups)
496
513
  end
497
514
 
498
515
  # Get number of rules
@@ -718,7 +735,7 @@ module Cataract
718
735
  parse_options = build_parse_options(effective_base_uri, effective_absolute_paths)
719
736
 
720
737
  # Parse CSS first (this extracts @import statements into result[:imports])
721
- result = Cataract._parse_css(css, parse_options)
738
+ result = @backend.parse(css, parse_options)
722
739
 
723
740
  merge_parsed_block!(result, effective_base_uri, effective_base_dir)
724
741
 
@@ -761,8 +778,9 @@ module Cataract
761
778
  # Compare stylesheets for equality.
762
779
  #
763
780
  # Two stylesheets are equal if they have the same rules in the same order
764
- # and the same media queries. Rule equality uses shorthand-aware comparison.
765
- # Order matters because CSS cascade depends on rule order.
781
+ # and the same media queries and conditional groups. Rule equality uses
782
+ # shorthand-aware comparison. Order matters because CSS cascade depends
783
+ # on rule order.
766
784
  #
767
785
  # Charset is ignored since it's file encoding metadata, not semantic content.
768
786
  #
@@ -772,6 +790,7 @@ module Cataract
772
790
  return false unless other.is_a?(Stylesheet)
773
791
  return false unless rules == other.rules
774
792
  return false unless @media_queries == other.media_queries
793
+ return false unless @conditional_groups == other.conditional_groups
775
794
 
776
795
  true
777
796
  end
@@ -779,11 +798,12 @@ module Cataract
779
798
 
780
799
  # Generate hash code for this stylesheet.
781
800
  #
782
- # Hash is based on rules and media_queries to match equality semantics.
801
+ # Hash is based on rules, media_queries, and conditional_groups to match
802
+ # equality semantics.
783
803
  #
784
804
  # @return [Integer] hash code
785
805
  def hash
786
- @_hash ||= [self.class, rules, @media_queries].hash # rubocop:disable Naming/MemoizedInstanceVariableName
806
+ @_hash ||= [self.class, rules, @media_queries, @conditional_groups].hash # rubocop:disable Naming/MemoizedInstanceVariableName
787
807
  end
788
808
 
789
809
  # Flatten all rules in this stylesheet according to CSS cascade rules.
@@ -794,7 +814,9 @@ module Cataract
794
814
  #
795
815
  # @return [Stylesheet] New stylesheet with cascade applied
796
816
  def flatten
797
- Cataract.flatten(self)
817
+ result = @backend.flatten(self)
818
+ result.backend = @backend
819
+ result
798
820
  end
799
821
  alias cascade flatten
800
822
 
@@ -813,7 +835,7 @@ module Cataract
813
835
  #
814
836
  # @return [self] Returns self for method chaining
815
837
  def flatten!
816
- flattened = Cataract.flatten(self)
838
+ flattened = @backend.flatten(self)
817
839
  @rules = flattened.rules
818
840
  @media_index = flattened.media_index_cache
819
841
  @_has_nesting = flattened.has_nesting
@@ -842,11 +864,13 @@ module Cataract
842
864
  list_id_offset = merge_selector_lists!(other.selector_lists, rule_id_offset: offset)
843
865
  mq_id_offset = merge_media_queries!(other.media_queries)
844
866
  merge_media_query_lists!(other.media_query_lists, mq_id_offset: mq_id_offset)
867
+ cg_id_offset = merge_conditional_groups!(other.conditional_groups)
845
868
 
846
869
  # Add rules with updated IDs and cross-references
847
870
  other.rules.each do |rule|
848
871
  new_rule = rule.dup
849
- rebase_rule!(new_rule, rule_id_offset: offset, list_id_offset: list_id_offset, mq_id_offset: mq_id_offset)
872
+ rebase_rule!(new_rule, rule_id_offset: offset, list_id_offset: list_id_offset, mq_id_offset: mq_id_offset,
873
+ cg_id_offset: cg_id_offset)
850
874
  @rules << new_rule
851
875
  end
852
876
 
@@ -930,11 +954,16 @@ module Cataract
930
954
  # raw cache these need).
931
955
 
932
956
  attr_reader :options, :parser_options
957
+ attr_accessor :backend
933
958
 
934
959
  def next_media_query_id
935
960
  @_next_media_query_id
936
961
  end
937
962
 
963
+ def next_conditional_group_id
964
+ @_next_conditional_group_id
965
+ end
966
+
938
967
  def next_selector_list_id
939
968
  @_next_selector_list_id
940
969
  end
@@ -987,6 +1016,28 @@ module Cataract
987
1016
  mq_id_offset
988
1017
  end
989
1018
 
1019
+ # Offset and append ConditionalGroup objects into @conditional_groups,
1020
+ # without mutating the source objects. Unlike merge_media_queries!,
1021
+ # each group's own parent_id (pointing at another ConditionalGroup, for
1022
+ # nesting) must also be rebased by the same offset.
1023
+ #
1024
+ # @param source_groups [Array<ConditionalGroup>, nil]
1025
+ # @return [Integer] id offset applied to each merged ConditionalGroup
1026
+ def merge_conditional_groups!(source_groups)
1027
+ cg_id_offset = @_next_conditional_group_id
1028
+ return cg_id_offset if source_groups.nil? || source_groups.empty?
1029
+
1030
+ source_groups.each do |group|
1031
+ new_group = group.dup
1032
+ new_group.id += cg_id_offset
1033
+ new_group.parent_id += cg_id_offset if new_group.parent_id
1034
+ @conditional_groups << new_group
1035
+ end
1036
+ @_next_conditional_group_id += source_groups.size
1037
+
1038
+ cg_id_offset
1039
+ end
1040
+
990
1041
  # Offset and merge a selector_lists hash (list_id => [rule_ids]) into
991
1042
  # @_selector_lists. List ids are always rebased onto the next available
992
1043
  # id; rule ids are rebased by rule_id_offset (0 when the caller defers
@@ -1045,18 +1096,21 @@ module Cataract
1045
1096
  end
1046
1097
 
1047
1098
  # Rebase a rule/at-rule's own id and its cross-references (Rule's
1048
- # selector_list_id, and media_query_id on both Rule and AtRule) by the
1049
- # given offsets, mutating it in place. Callers merging from a live
1050
- # Stylesheet they don't own (e.g. concat) must dup the rule first.
1099
+ # selector_list_id, and media_query_id/conditional_group_id on both Rule
1100
+ # and AtRule) by the given offsets, mutating it in place. Callers
1101
+ # merging from a live Stylesheet they don't own (e.g. concat) must dup
1102
+ # the rule first.
1051
1103
  #
1052
1104
  # @param rule [Rule, AtRule]
1053
1105
  # @param rule_id_offset [Integer]
1054
1106
  # @param list_id_offset [Integer]
1055
1107
  # @param mq_id_offset [Integer]
1108
+ # @param cg_id_offset [Integer]
1056
1109
  # @return [void]
1057
- def rebase_rule!(rule, rule_id_offset:, list_id_offset: 0, mq_id_offset: 0)
1110
+ def rebase_rule!(rule, rule_id_offset:, list_id_offset: 0, mq_id_offset: 0, cg_id_offset: 0)
1058
1111
  rule.id += rule_id_offset
1059
1112
  rule.media_query_id += mq_id_offset if rule.media_query_id
1113
+ rule.conditional_group_id += cg_id_offset if rule.conditional_group_id
1060
1114
  return unless rule.is_a?(Rule)
1061
1115
 
1062
1116
  rule.selector_list_id += list_id_offset if rule.selector_list_id
@@ -1080,7 +1134,7 @@ module Cataract
1080
1134
  # rules, media queries, selector/media-query lists, and index, then
1081
1135
  # resolve any @import statements it contained.
1082
1136
  #
1083
- # @param result [Hash] Return value of Cataract._parse_css
1137
+ # @param result [Hash] Return value of the backend's parse
1084
1138
  # @return [void]
1085
1139
  def merge_parsed_block!(result, effective_base_uri, effective_base_dir)
1086
1140
  offset = @_last_rule_id || 0
@@ -1088,10 +1142,12 @@ module Cataract
1088
1142
  list_id_offset = merge_selector_lists!(result[:_selector_lists], rule_id_offset: offset)
1089
1143
  mq_id_offset = merge_media_queries!(result[:media_queries])
1090
1144
  merge_media_query_lists!(result[:_media_query_lists], mq_id_offset: mq_id_offset)
1145
+ cg_id_offset = merge_conditional_groups!(result[:conditional_groups])
1091
1146
 
1092
1147
  new_rules = result[:rules]
1093
1148
  new_rules.each do |rule|
1094
- rebase_rule!(rule, rule_id_offset: offset, list_id_offset: list_id_offset, mq_id_offset: mq_id_offset)
1149
+ rebase_rule!(rule, rule_id_offset: offset, list_id_offset: list_id_offset, mq_id_offset: mq_id_offset,
1150
+ cg_id_offset: cg_id_offset)
1095
1151
  @rules << rule
1096
1152
  end
1097
1153
  @_last_rule_id = offset + new_rules.length
@@ -1263,7 +1319,8 @@ module Cataract
1263
1319
  # Build parse options for imported CSS
1264
1320
  parse_opts = {
1265
1321
  import: opts.merge(imported_urls: imported_urls_copy, depth: depth + 1, base_uri: imported_base_uri),
1266
- parser: @parser_options.dup # Inherit parent's parser options (including selector_lists)
1322
+ parser: @parser_options.dup, # Inherit parent's parser options (including selector_lists)
1323
+ backend: @backend # Imported stylesheets are produced by the same backend as their parent
1267
1324
  }
1268
1325
 
1269
1326
  # If URL conversion is enabled (base_uri present), enable it for imported files too
@@ -1301,7 +1358,8 @@ module Cataract
1301
1358
  # @return [void]
1302
1359
  def merge_imported_sheet!(imported_sheet, import)
1303
1360
  mq_id_offset = merge_media_queries!(imported_sheet.media_queries)
1304
- rebase_imported_rules_media_query!(imported_sheet.rules, mq_id_offset, import.media_query_id)
1361
+ cg_id_offset = merge_conditional_groups!(imported_sheet.conditional_groups)
1362
+ rebase_imported_rules!(imported_sheet.rules, mq_id_offset, cg_id_offset, import.media_query_id)
1305
1363
 
1306
1364
  insert_position = import.id
1307
1365
  imported_sheet.rules.each_with_index do |rule, idx|
@@ -1312,15 +1370,16 @@ module Cataract
1312
1370
  merge_media_query_lists!(imported_sheet.media_query_lists, mq_id_offset: mq_id_offset)
1313
1371
  end
1314
1372
 
1315
- # Rebase every imported rule/at-rule's media_query_id onto this
1316
- # stylesheet's own @media_queries (now that the imported sheet's own
1317
- # media queries have been merged in), then, if the @import statement
1318
- # itself had a media qualifier, combine it with (or assign it to) each
1319
- # rule's media context.
1373
+ # Rebase every imported rule/at-rule's media_query_id and
1374
+ # conditional_group_id onto this stylesheet's own @media_queries/
1375
+ # @conditional_groups (now that the imported sheet's own copies have
1376
+ # been merged in), then, if the @import statement itself had a media
1377
+ # qualifier, combine it with (or assign it to) each rule's media
1378
+ # context.
1320
1379
  #
1321
1380
  # @return [void]
1322
- def rebase_imported_rules_media_query!(imported_rules, mq_id_offset, import_media_query_id)
1323
- imported_rules.each { |rule| rebase_rule!(rule, rule_id_offset: 0, mq_id_offset: mq_id_offset) }
1381
+ def rebase_imported_rules!(imported_rules, mq_id_offset, cg_id_offset, import_media_query_id)
1382
+ imported_rules.each { |rule| rebase_rule!(rule, rule_id_offset: 0, mq_id_offset: mq_id_offset, cg_id_offset: cg_id_offset) }
1324
1383
 
1325
1384
  return unless import_media_query_id
1326
1385
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cataract
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/cataract.rb CHANGED
@@ -1,34 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'cataract/version'
4
- require_relative 'cataract/error'
5
- require_relative 'cataract/constants'
6
-
7
- # Load struct definitions first (before C extension or pure Ruby)
8
- require_relative 'cataract/declaration'
9
- require_relative 'cataract/rule'
10
- require_relative 'cataract/at_rule'
11
- require_relative 'cataract/media_query'
12
- require_relative 'cataract/import_statement'
13
-
14
- # Load pure Ruby or C extension based on ENV var
15
- if %w[1 true].include?(ENV.fetch('CATARACT_PURE', nil)) || RUBY_ENGINE == 'jruby'
16
- require_relative 'cataract/pure'
17
- else
18
- require_relative 'cataract/native_extension'
19
- end
20
-
21
- # Load supporting Ruby files (used by both implementations)
22
- require_relative 'cataract/stylesheet_scope'
23
- require_relative 'cataract/stylesheet'
24
- require_relative 'cataract/declarations'
25
- require_relative 'cataract/import_resolver'
26
-
27
3
  # Cataract is a high-performance CSS parser written in C with a Ruby interface.
28
4
  #
29
5
  # It provides fast CSS parsing, rule querying, cascade merging, and serialization.
30
6
  # Designed for performance-critical applications that need to process large amounts of CSS.
31
7
  #
8
+ # Pure Ruby and native each live entirely under their own Backends::Pure /
9
+ # Backends::Native namespace (no shared method/constant is ever defined by
10
+ # a backend directly on Cataract or on a shared value type), so both can be
11
+ # required in the same process - e.g. to compare their output directly.
12
+ # Backends.active is the one this process picked by default (below); it's
13
+ # what shared value types (Stylesheet, Declarations, Rule) fall back to
14
+ # when they aren't otherwise told which backend produced them.
15
+ #
32
16
  # @example Basic usage
33
17
  # require 'cataract'
34
18
  #
@@ -44,6 +28,33 @@ require_relative 'cataract/import_resolver'
44
28
  # @see Stylesheet Main class for working with parsed CSS
45
29
  # @see Rule Represents individual CSS rules
46
30
  module Cataract
31
+ module Backends
32
+ class << self
33
+ attr_accessor :active
34
+ end
35
+ end
36
+ end
37
+
38
+ if %w[1 true].include?(ENV.fetch('CATARACT_PURE', nil)) || RUBY_ENGINE == 'jruby'
39
+ require_relative 'cataract/pure'
40
+ Cataract::Backends.active = Cataract::Backends::Pure
41
+ else
42
+ require_relative 'cataract/native'
43
+ Cataract::Backends.active = Cataract::Backends::Native
44
+ end
45
+
46
+ module Cataract
47
+ # Mirror the active backend's identity constants at the top level.
48
+ #
49
+ # Native is a Module (IMPLEMENTATION/COMPILE_FLAGS live on it directly);
50
+ # Pure is a frozen instance (they live on its class instead), so reach
51
+ # through .class only when active isn't already a Module itself.
52
+ backend_const_holder = Backends.active.is_a?(Module) ? Backends.active : Backends.active.class
53
+ IMPLEMENTATION = backend_const_holder::IMPLEMENTATION
54
+ COMPILE_FLAGS = backend_const_holder::COMPILE_FLAGS
55
+ NATIVE_EXTENSION_LOADED = true if defined?(Backends::Native) && Backends.active == Backends::Native
56
+ PURE_RUBY_LOADED = true if defined?(Backends::Pure) && Backends.active == Backends::Pure
57
+
47
58
  class << self
48
59
  # Parse a CSS string into a Stylesheet object.
49
60
  #
@@ -75,10 +86,8 @@ module Cataract
75
86
  #
76
87
  # @see Stylesheet#parse
77
88
  # @see Stylesheet.parse
78
- unless method_defined?(:parse_css)
79
- def parse_css(css, **options)
80
- Stylesheet.parse(css, **options)
81
- end
89
+ def parse_css(css, **options)
90
+ Stylesheet.parse(css, **options)
82
91
  end
83
92
 
84
93
  # Flatten CSS rules according to CSS cascade rules.
@@ -113,9 +122,11 @@ module Cataract
113
122
  #
114
123
  # @note This is a module-level convenience method. The same functionality is available
115
124
  # as an instance method: `stylesheet.flatten`
116
- # @note Implemented in C (see ext/cataract/flatten.c)
117
125
  #
118
126
  # @see Stylesheet#flatten
119
- # Cataract.flatten is defined in C via rb_define_module_function
127
+ def flatten(stylesheet_or_css)
128
+ stylesheet_or_css = Stylesheet.parse(stylesheet_or_css) if stylesheet_or_css.is_a?(String)
129
+ stylesheet_or_css.flatten
130
+ end
120
131
  end
121
132
  end
@@ -35,7 +35,7 @@ namespace :profile do
35
35
  puts "CSS size: #{css_content.bytesize} bytes"
36
36
  puts
37
37
 
38
- require_relative '../../lib/cataract/pure'
38
+ require_relative '../../lib/cataract'
39
39
  require 'json'
40
40
 
41
41
  # Use higher sampling rate (interval in microseconds, default is 1000)
@@ -74,6 +74,9 @@ namespace :profile do
74
74
  abort('stackprof gem not found. Install with: gem install stackprof')
75
75
  end
76
76
 
77
+ # Ensure we're using pure Ruby implementation
78
+ ENV['CATARACT_PURE'] = '1'
79
+
77
80
  fixture_path = File.expand_path('../../test/fixtures/bootstrap.css', __dir__)
78
81
  unless File.exist?(fixture_path)
79
82
  abort("Fixture not found: #{fixture_path}")
@@ -94,7 +97,7 @@ namespace :profile do
94
97
  puts "CSS size: #{css_content.bytesize} bytes"
95
98
  puts
96
99
 
97
- require_relative '../../lib/cataract/pure'
100
+ require_relative '../../lib/cataract'
98
101
  require 'json'
99
102
 
100
103
  # Parse once outside profiling to get stylesheet
@@ -162,7 +165,7 @@ namespace :profile do
162
165
  puts "CSS size: #{css_content.bytesize} bytes"
163
166
  puts
164
167
 
165
- require_relative '../../lib/cataract/pure'
168
+ require_relative '../../lib/cataract'
166
169
  require 'json'
167
170
 
168
171
  # Parse once outside profiling to get stylesheet
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cataract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -29,6 +29,7 @@ files:
29
29
  - ".overcommit.yml"
30
30
  - ".rubocop.yml"
31
31
  - ".rubocop_todo.yml"
32
+ - ".yardopts"
32
33
  - BENCHMARKS.md
33
34
  - CHANGELOG.md
34
35
  - Gemfile
@@ -69,6 +70,7 @@ files:
69
70
  - lib/cataract.rb
70
71
  - lib/cataract/at_rule.rb
71
72
  - lib/cataract/color_conversion.rb
73
+ - lib/cataract/conditional_group.rb
72
74
  - lib/cataract/constants.rb
73
75
  - lib/cataract/declaration.rb
74
76
  - lib/cataract/declarations.rb
@@ -76,10 +78,11 @@ files:
76
78
  - lib/cataract/import_resolver.rb
77
79
  - lib/cataract/import_statement.rb
78
80
  - lib/cataract/media_query.rb
81
+ - lib/cataract/native.rb
79
82
  - lib/cataract/pure.rb
80
83
  - lib/cataract/pure/byte_constants.rb
84
+ - lib/cataract/pure/declarations.rb
81
85
  - lib/cataract/pure/flatten.rb
82
- - lib/cataract/pure/helpers.rb
83
86
  - lib/cataract/pure/parser.rb
84
87
  - lib/cataract/pure/serializer.rb
85
88
  - lib/cataract/pure/specificity.rb
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Pure Ruby CSS parser - Helper methods
4
- # NO REGEXP ALLOWED - char-by-char parsing only
5
-
6
- module Cataract
7
- # Check if a byte is whitespace (space, tab, newline, CR)
8
- # @param byte [Integer] Byte value from String#getbyte
9
- # @return [Boolean] true if whitespace
10
- def self.is_whitespace?(byte)
11
- byte == BYTE_SPACE || byte == BYTE_TAB || byte == BYTE_NEWLINE || byte == BYTE_CR
12
- end
13
-
14
- # Check if byte is a letter (a-z, A-Z)
15
- # @param byte [Integer] Byte value from String#getbyte
16
- # @return [Boolean] true if letter
17
- def self.letter?(byte)
18
- (byte >= BYTE_LOWER_A && byte <= BYTE_LOWER_Z) ||
19
- (byte >= BYTE_UPPER_A && byte <= BYTE_UPPER_Z)
20
- end
21
-
22
- # Check if byte is a digit (0-9)
23
- # @param byte [Integer] Byte value from String#getbyte
24
- # @return [Boolean] true if digit
25
- def self.digit?(byte)
26
- byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9
27
- end
28
-
29
- # Check if byte is alphanumeric, hyphen, or underscore (CSS identifier char)
30
- # @param byte [Integer] Byte value from String#getbyte
31
- # @return [Boolean] true if valid identifier character
32
- def self.ident_char?(byte)
33
- letter?(byte) || digit?(byte) || byte == BYTE_HYPHEN || byte == BYTE_UNDERSCORE
34
- end
35
- end