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.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +9 -0
- data/ext/cataract/cataract.c +72 -103
- data/ext/cataract/cataract.h +0 -1
- data/ext/cataract/css_parser.c +0 -18
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1197 -1182
- data/lib/cataract/pure/parser.rb +1725 -1729
- data/lib/cataract/pure/serializer.rb +575 -715
- data/lib/cataract/pure/specificity.rb +165 -144
- data/lib/cataract/pure.rb +73 -101
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +19 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +3 -2
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
# Comment characters
|
|
28
|
+
BYTE_SLASH = 47 # '/'
|
|
29
|
+
BYTE_STAR = 42 # '*'
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
# Quote characters
|
|
32
|
+
BYTE_SQUOTE = 39 # "'"
|
|
33
|
+
BYTE_DQUOTE = 34 # '"'
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
# Digit range (0-9)
|
|
79
|
+
BYTE_DIGIT_0 = 48 # '0'
|
|
80
|
+
BYTE_DIGIT_9 = 57 # '9'
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|