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.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/.yardopts +9 -0
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +17 -0
- data/ext/cataract/cataract.c +193 -111
- data/ext/cataract/cataract.h +13 -3
- data/ext/cataract/css_parser.c +298 -54
- data/ext/cataract/flatten.c +6 -2
- data/ext/cataract/specificity.c +13 -2
- data/lib/cataract/at_rule.rb +4 -2
- data/lib/cataract/conditional_group.rb +72 -0
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +31 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1203 -1182
- data/lib/cataract/pure/parser.rb +1953 -1715
- data/lib/cataract/pure/serializer.rb +653 -706
- data/lib/cataract/pure/specificity.rb +173 -144
- data/lib/cataract/pure.rb +74 -101
- data/lib/cataract/rule.rb +14 -7
- data/lib/cataract/stylesheet.rb +84 -25
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +5 -2
- data/lib/cataract/pure/helpers.rb +0 -35
data/ext/cataract/flatten.c
CHANGED
|
@@ -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
|
|
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
|
|
data/ext/cataract/specificity.c
CHANGED
|
@@ -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
|
|
data/lib/cataract/at_rule.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|