cataract 0.3.0 → 0.4.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.
@@ -0,0 +1,30 @@
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 'import_statement'
23
+
24
+ require_relative 'native_extension'
25
+
26
+ # Load supporting Ruby files (used by both implementations)
27
+ require_relative 'stylesheet_scope'
28
+ require_relative 'stylesheet'
29
+ require_relative 'declarations'
30
+ 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