p_css 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/lib/css/cascade.rb +168 -0
- data/lib/css/code_points.rb +36 -0
- data/lib/css/escape.rb +82 -0
- data/lib/css/media_queries/context.rb +60 -0
- data/lib/css/media_queries/evaluator.rb +157 -0
- data/lib/css/media_queries/nodes.rb +41 -0
- data/lib/css/media_queries/parser.rb +374 -0
- data/lib/css/media_queries.rb +9 -0
- data/lib/css/nesting.rb +229 -0
- data/lib/css/nodes.rb +42 -0
- data/lib/css/parser.rb +430 -0
- data/lib/css/selectors/anb_parser.rb +174 -0
- data/lib/css/selectors/matcher.rb +449 -0
- data/lib/css/selectors/nodes.rb +61 -0
- data/lib/css/selectors/parser.rb +395 -0
- data/lib/css/selectors/serializer.rb +102 -0
- data/lib/css/selectors/specificity.rb +81 -0
- data/lib/css/selectors.rb +11 -0
- data/lib/css/serializer.rb +167 -0
- data/lib/css/token.rb +78 -0
- data/lib/css/token_cursor.rb +49 -0
- data/lib/css/tokenizer.rb +441 -0
- data/lib/css/urange.rb +45 -0
- data/lib/css/version.rb +3 -0
- data/lib/css.rb +73 -0
- data/lib/p_css.rb +1 -0
- metadata +73 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module MediaQueries
|
|
3
|
+
# Evaluates a MediaQueryList against a Context, returning true if at
|
|
4
|
+
# least one media-query in the list matches.
|
|
5
|
+
module Evaluator
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Length conversion to CSS px assumes 1em = 1rem = 16px. Per Media
|
|
9
|
+
# Queries Level 4 §1.3 this is the conventional fallback when the
|
|
10
|
+
# font-size of the root is unknown.
|
|
11
|
+
EM_PX = 16.0
|
|
12
|
+
|
|
13
|
+
LENGTH_UNITS_PX = {
|
|
14
|
+
'px' => 1.0,
|
|
15
|
+
'em' => EM_PX,
|
|
16
|
+
'rem' => EM_PX,
|
|
17
|
+
'ex' => EM_PX * 0.5,
|
|
18
|
+
'ch' => EM_PX * 0.5,
|
|
19
|
+
'pt' => 96.0 / 72,
|
|
20
|
+
'pc' => 16.0,
|
|
21
|
+
'in' => 96.0,
|
|
22
|
+
'cm' => 96.0 / 2.54,
|
|
23
|
+
'mm' => 96.0 / 25.4,
|
|
24
|
+
'q' => 96.0 / 25.4 / 4
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
RESOLUTION_UNITS_DPPX = {
|
|
28
|
+
'dppx' => 1.0,
|
|
29
|
+
'x' => 1.0,
|
|
30
|
+
'dpi' => 1.0 / 96,
|
|
31
|
+
'dpcm' => 2.54 / 96
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
RESOLUTION_FEATURES = %w[resolution].freeze
|
|
35
|
+
|
|
36
|
+
INVERSE_OP = {lt: :gt, le: :ge, gt: :lt, ge: :le, eq: :eq}.freeze
|
|
37
|
+
|
|
38
|
+
PREFIX_OP = {min: :ge, max: :le}.freeze
|
|
39
|
+
|
|
40
|
+
def evaluate(query_list, context)
|
|
41
|
+
query_list.queries.any? { evaluate_query(it, context) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def evaluate_query(query, context)
|
|
47
|
+
result = evaluate_query_main(query, context)
|
|
48
|
+
query.modifier == :not ? !result : result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def evaluate_query_main(query, context)
|
|
52
|
+
if query.type
|
|
53
|
+
return false unless type_matches?(query.type, context.media_type)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return true if query.condition.nil?
|
|
57
|
+
|
|
58
|
+
evaluate_condition(query.condition, context)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def type_matches?(type, ctx_type)
|
|
62
|
+
type == 'all' || type == ctx_type.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def evaluate_condition(node, context)
|
|
66
|
+
case node
|
|
67
|
+
when MediaNot then !evaluate_condition(node.operand, context)
|
|
68
|
+
when MediaAnd then node.operands.all? { evaluate_condition(it, context) }
|
|
69
|
+
when MediaOr then node.operands.any? { evaluate_condition(it, context) }
|
|
70
|
+
when MediaFeature then evaluate_feature(node, context)
|
|
71
|
+
when GeneralEnclosed then false
|
|
72
|
+
else false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def evaluate_feature(feature, context)
|
|
77
|
+
ctx_name, prefix = strip_prefix(feature.name)
|
|
78
|
+
ctx_value = context[ctx_name]
|
|
79
|
+
|
|
80
|
+
return evaluate_boolean(ctx_value) if feature.op.nil?
|
|
81
|
+
|
|
82
|
+
compare(prefix, feature.op, ctx_value, feature.value, ctx_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def evaluate_boolean(ctx_value)
|
|
86
|
+
return false if ctx_value.nil?
|
|
87
|
+
return false if ctx_value == 0 || ctx_value == false || ctx_value == '' || ctx_value == 'none'
|
|
88
|
+
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def strip_prefix(name)
|
|
93
|
+
case name
|
|
94
|
+
when /\Amin-(.+)/ then [$1, :min]
|
|
95
|
+
when /\Amax-(.+)/ then [$1, :max]
|
|
96
|
+
else [name, nil]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def compare(prefix, op, ctx_value, feature_value, ctx_name)
|
|
101
|
+
op = PREFIX_OP[prefix] || op
|
|
102
|
+
|
|
103
|
+
return string_op_apply(op, ctx_value.to_s, feature_value.value.to_s) if ident_compare?(feature_value)
|
|
104
|
+
|
|
105
|
+
a = numeric_for(ctx_name, ctx_value)
|
|
106
|
+
b = numeric_for(ctx_name, feature_value)
|
|
107
|
+
|
|
108
|
+
return false if a.nil? || b.nil?
|
|
109
|
+
|
|
110
|
+
numeric_op_apply(op, a, b)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ident_compare?(feature_value)
|
|
114
|
+
feature_value.is_a?(Token) && feature_value.type == :ident
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def string_op_apply(op, a, b)
|
|
118
|
+
op == :eq && a.casecmp?(b)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def numeric_op_apply(op, a, b)
|
|
122
|
+
case op
|
|
123
|
+
when :eq then a == b
|
|
124
|
+
when :lt then a < b
|
|
125
|
+
when :le then a <= b
|
|
126
|
+
when :gt then a > b
|
|
127
|
+
when :ge then a >= b
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Converts both context value and feature value to a comparable
|
|
132
|
+
# numeric in the canonical unit for the named feature.
|
|
133
|
+
def numeric_for(ctx_name, value)
|
|
134
|
+
case value
|
|
135
|
+
when Numeric then value.to_f
|
|
136
|
+
when Ratio then value.to_f
|
|
137
|
+
when Token
|
|
138
|
+
case value.type
|
|
139
|
+
when :number then value.value.to_f
|
|
140
|
+
when :percentage then value.value.to_f / 100
|
|
141
|
+
when :dimension then dimension_to_canonical(value, ctx_name)
|
|
142
|
+
else nil
|
|
143
|
+
end
|
|
144
|
+
else nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def dimension_to_canonical(token, ctx_name)
|
|
149
|
+
unit = token.unit.downcase
|
|
150
|
+
table = RESOLUTION_FEATURES.include?(ctx_name) ? RESOLUTION_UNITS_DPPX : LENGTH_UNITS_PX
|
|
151
|
+
|
|
152
|
+
factor = table[unit]
|
|
153
|
+
factor && token.value.to_f * factor
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module MediaQueries
|
|
3
|
+
# Marker module for media-query AST nodes; lets the main serializer
|
|
4
|
+
# dispatch into MediaQueries::Serializer when it ever exists.
|
|
5
|
+
module Node; end
|
|
6
|
+
|
|
7
|
+
MediaQueryList = Data.define(:queries) do
|
|
8
|
+
include Node
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# `modifier` is `nil`, `:not`, or `:only`.
|
|
12
|
+
# `type` is `nil` or a downcased string ('screen', 'print', 'all', ...).
|
|
13
|
+
# `condition` is `nil` or a media-condition node.
|
|
14
|
+
MediaQuery = Data.define(:modifier, :type, :condition) do
|
|
15
|
+
include Node
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
MediaNot = Data.define(:operand) { include Node }
|
|
19
|
+
MediaAnd = Data.define(:operands) { include Node }
|
|
20
|
+
MediaOr = Data.define(:operands) { include Node }
|
|
21
|
+
|
|
22
|
+
# `op` is `nil` (boolean form, e.g. `(color)`), `:eq` (plain form,
|
|
23
|
+
# `(min-width: 600px)`, or range `=`), `:lt`, `:le`, `:gt`, or `:ge`.
|
|
24
|
+
# `value` is `nil` (boolean), a Token, or a Ratio.
|
|
25
|
+
MediaFeature = Data.define(:name, :op, :value) do
|
|
26
|
+
include Node
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Catch-all for `(...)` content the parser couldn't recognize as a
|
|
30
|
+
# feature or condition. Preserved so downstream tools can still see it.
|
|
31
|
+
GeneralEnclosed = Data.define(:tokens) do
|
|
32
|
+
include Node
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Numeric ratio used in `aspect-ratio` / `device-aspect-ratio` features.
|
|
36
|
+
Ratio = Data.define(:numerator, :denominator) do
|
|
37
|
+
include Node
|
|
38
|
+
def to_f = numerator.to_f / denominator
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module MediaQueries
|
|
3
|
+
# Parser for `<media-query-list>` per Media Queries Level 4 §3.
|
|
4
|
+
# https://drafts.csswg.org/mediaqueries-4/
|
|
5
|
+
#
|
|
6
|
+
# Accepts either a String (which is tokenized and re-component-valued
|
|
7
|
+
# so `(...)` becomes a `SimpleBlock`) or an Array of component values
|
|
8
|
+
# (for use against an `@media` rule's prelude from the main parser).
|
|
9
|
+
class Parser
|
|
10
|
+
include CSS::TokenCursor
|
|
11
|
+
|
|
12
|
+
MODIFIER_KEYWORDS = %w[not only].freeze
|
|
13
|
+
LOGICAL_KEYWORDS = %w[and or not].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def parse(input)
|
|
17
|
+
new(items_from(input)).parse_media_query_list
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def items_from(input)
|
|
23
|
+
input.is_a?(String) ? CSS::Parser.parse_component_values(input) : input.to_a
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(items)
|
|
28
|
+
init_cursor(items)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_media_query_list
|
|
32
|
+
skip_whitespace
|
|
33
|
+
|
|
34
|
+
queries = [parse_media_query]
|
|
35
|
+
|
|
36
|
+
loop do
|
|
37
|
+
skip_whitespace
|
|
38
|
+
break unless peek_token.type == :comma
|
|
39
|
+
|
|
40
|
+
consume
|
|
41
|
+
queries << parse_media_query
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
skip_whitespace
|
|
45
|
+
|
|
46
|
+
unless eof?
|
|
47
|
+
parse_error!("trailing tokens after media query list: #{describe(peek)}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
MediaQueryList.new(queries:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_media_query
|
|
54
|
+
skip_whitespace
|
|
55
|
+
|
|
56
|
+
saved = @pos
|
|
57
|
+
|
|
58
|
+
# Try the `[not | only]? <media-type> [and <condition-without-or>]?` form.
|
|
59
|
+
modifier = consume_modifier
|
|
60
|
+
skip_whitespace if modifier
|
|
61
|
+
|
|
62
|
+
if (item = peek).is_a?(Token) && item.type == :ident && !LOGICAL_KEYWORDS.include?(item.value.downcase)
|
|
63
|
+
type = consume.value.downcase
|
|
64
|
+
skip_whitespace
|
|
65
|
+
|
|
66
|
+
condition = nil
|
|
67
|
+
|
|
68
|
+
if keyword?('and')
|
|
69
|
+
consume
|
|
70
|
+
skip_whitespace
|
|
71
|
+
condition = parse_media_condition(allow_or: false)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return MediaQuery.new(modifier:, type:, condition:)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Otherwise this is a pure media-condition (no type / modifier).
|
|
78
|
+
@pos = saved
|
|
79
|
+
|
|
80
|
+
MediaQuery.new(modifier: nil, type: nil, condition: parse_media_condition(allow_or: true))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def parse_media_condition(allow_or:)
|
|
86
|
+
skip_whitespace
|
|
87
|
+
|
|
88
|
+
if keyword?('not')
|
|
89
|
+
consume
|
|
90
|
+
skip_whitespace
|
|
91
|
+
return MediaNot.new(operand: parse_media_in_parens)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
first = parse_media_in_parens
|
|
95
|
+
|
|
96
|
+
skip_whitespace
|
|
97
|
+
|
|
98
|
+
if keyword?('and')
|
|
99
|
+
operands = [first]
|
|
100
|
+
while keyword?('and')
|
|
101
|
+
consume
|
|
102
|
+
skip_whitespace
|
|
103
|
+
operands << parse_media_in_parens
|
|
104
|
+
skip_whitespace
|
|
105
|
+
end
|
|
106
|
+
MediaAnd.new(operands:)
|
|
107
|
+
elsif allow_or && keyword?('or')
|
|
108
|
+
operands = [first]
|
|
109
|
+
while keyword?('or')
|
|
110
|
+
consume
|
|
111
|
+
skip_whitespace
|
|
112
|
+
operands << parse_media_in_parens
|
|
113
|
+
skip_whitespace
|
|
114
|
+
end
|
|
115
|
+
MediaOr.new(operands:)
|
|
116
|
+
else
|
|
117
|
+
first
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_media_in_parens
|
|
122
|
+
item = peek
|
|
123
|
+
|
|
124
|
+
unless item.is_a?(Nodes::SimpleBlock) && item.parenthesized?
|
|
125
|
+
parse_error!("expected '(', got #{describe(item)}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
consume
|
|
129
|
+
inner = self.class.new(item.value)
|
|
130
|
+
inner.parse_in_parens_contents
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
protected
|
|
134
|
+
|
|
135
|
+
# Called on a sub-parser whose @items is the contents inside `(...)`.
|
|
136
|
+
# Returns a media-condition or a feature.
|
|
137
|
+
def parse_in_parens_contents
|
|
138
|
+
skip_whitespace
|
|
139
|
+
|
|
140
|
+
return GeneralEnclosed.new(tokens: []) if eof?
|
|
141
|
+
|
|
142
|
+
# Nested `(condition)`?
|
|
143
|
+
first = peek
|
|
144
|
+
|
|
145
|
+
if first.is_a?(Nodes::SimpleBlock) && first.parenthesized?
|
|
146
|
+
cond = parse_media_condition(allow_or: true)
|
|
147
|
+
skip_whitespace
|
|
148
|
+
|
|
149
|
+
unless eof?
|
|
150
|
+
return GeneralEnclosed.new(tokens: @items)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return cond
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
result = try_parse_feature
|
|
157
|
+
|
|
158
|
+
return result if result
|
|
159
|
+
|
|
160
|
+
GeneralEnclosed.new(tokens: @items)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def try_parse_feature
|
|
166
|
+
saved = @pos
|
|
167
|
+
|
|
168
|
+
starting_token = peek
|
|
169
|
+
|
|
170
|
+
if starting_token.is_a?(Token) && starting_token.type == :ident
|
|
171
|
+
feature = try_parse_feature_starting_with_ident
|
|
172
|
+
|
|
173
|
+
return feature if feature
|
|
174
|
+
|
|
175
|
+
@pos = saved
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if value_starts?(starting_token)
|
|
179
|
+
feature = try_parse_feature_starting_with_value
|
|
180
|
+
|
|
181
|
+
return feature if feature
|
|
182
|
+
|
|
183
|
+
@pos = saved
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def try_parse_feature_starting_with_ident
|
|
190
|
+
name = consume.value.downcase
|
|
191
|
+
|
|
192
|
+
skip_whitespace
|
|
193
|
+
|
|
194
|
+
if eof?
|
|
195
|
+
return MediaFeature.new(name:, op: nil, value: nil)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if peek_token.type == :colon
|
|
199
|
+
consume
|
|
200
|
+
skip_whitespace
|
|
201
|
+
value = parse_mf_value
|
|
202
|
+
|
|
203
|
+
return nil if value.nil?
|
|
204
|
+
|
|
205
|
+
skip_whitespace
|
|
206
|
+
|
|
207
|
+
return nil unless eof?
|
|
208
|
+
|
|
209
|
+
return MediaFeature.new(name:, op: :eq, value:)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if (op = consume_comparison)
|
|
213
|
+
skip_whitespace
|
|
214
|
+
value = parse_mf_value
|
|
215
|
+
|
|
216
|
+
return nil if value.nil?
|
|
217
|
+
|
|
218
|
+
skip_whitespace
|
|
219
|
+
|
|
220
|
+
if eof?
|
|
221
|
+
return MediaFeature.new(name:, op:, value:)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Bounded form: `<name> <op> <value> ... <op> <value>`
|
|
225
|
+
# Per spec, bounded form has the name in the middle, not here.
|
|
226
|
+
# Reject.
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def try_parse_feature_starting_with_value
|
|
234
|
+
first_value = parse_mf_value
|
|
235
|
+
|
|
236
|
+
return nil if first_value.nil?
|
|
237
|
+
|
|
238
|
+
skip_whitespace
|
|
239
|
+
|
|
240
|
+
first_op = consume_comparison
|
|
241
|
+
|
|
242
|
+
return nil unless first_op
|
|
243
|
+
|
|
244
|
+
skip_whitespace
|
|
245
|
+
|
|
246
|
+
return nil unless peek_token.type == :ident
|
|
247
|
+
|
|
248
|
+
name = consume.value.downcase
|
|
249
|
+
|
|
250
|
+
skip_whitespace
|
|
251
|
+
|
|
252
|
+
if eof?
|
|
253
|
+
return MediaFeature.new(name:, op: invert_op(first_op), value: first_value)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Bounded form: <value> <op1> <name> <op2> <value>
|
|
257
|
+
second_op = consume_comparison
|
|
258
|
+
|
|
259
|
+
return nil unless second_op
|
|
260
|
+
|
|
261
|
+
skip_whitespace
|
|
262
|
+
|
|
263
|
+
second_value = parse_mf_value
|
|
264
|
+
|
|
265
|
+
return nil if second_value.nil?
|
|
266
|
+
|
|
267
|
+
skip_whitespace
|
|
268
|
+
|
|
269
|
+
return nil unless eof?
|
|
270
|
+
|
|
271
|
+
# Decompose into MediaAnd of two normalized features.
|
|
272
|
+
MediaAnd.new(operands: [
|
|
273
|
+
MediaFeature.new(name:, op: invert_op(first_op), value: first_value),
|
|
274
|
+
MediaFeature.new(name:, op: second_op, value: second_value)
|
|
275
|
+
])
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# `value op name` swaps to `name (inverted op) value`.
|
|
279
|
+
INVERSE_OP = {lt: :gt, le: :ge, gt: :lt, ge: :le, eq: :eq}.freeze
|
|
280
|
+
|
|
281
|
+
def invert_op(op) = INVERSE_OP.fetch(op, op)
|
|
282
|
+
|
|
283
|
+
def parse_mf_value
|
|
284
|
+
item = peek
|
|
285
|
+
|
|
286
|
+
return nil unless item.is_a?(Token)
|
|
287
|
+
|
|
288
|
+
case item.type
|
|
289
|
+
when :number
|
|
290
|
+
consume
|
|
291
|
+
|
|
292
|
+
if peek_token.type == :delim && peek_token.value == '/'
|
|
293
|
+
consume
|
|
294
|
+
skip_whitespace
|
|
295
|
+
denom = peek
|
|
296
|
+
|
|
297
|
+
return nil unless denom.is_a?(Token) && denom.type == :number
|
|
298
|
+
|
|
299
|
+
consume
|
|
300
|
+
return Ratio.new(numerator: item.value, denominator: denom.value)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
item
|
|
304
|
+
when :dimension, :percentage, :ident, :string
|
|
305
|
+
consume
|
|
306
|
+
item
|
|
307
|
+
else
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def consume_comparison
|
|
313
|
+
item = peek
|
|
314
|
+
|
|
315
|
+
return nil unless item.is_a?(Token) && item.type == :delim
|
|
316
|
+
|
|
317
|
+
case item.value
|
|
318
|
+
when '='
|
|
319
|
+
consume
|
|
320
|
+
:eq
|
|
321
|
+
when '<'
|
|
322
|
+
consume
|
|
323
|
+
|
|
324
|
+
if peek_token.type == :delim && peek_token.value == '='
|
|
325
|
+
consume
|
|
326
|
+
:le
|
|
327
|
+
else
|
|
328
|
+
:lt
|
|
329
|
+
end
|
|
330
|
+
when '>'
|
|
331
|
+
consume
|
|
332
|
+
|
|
333
|
+
if peek_token.type == :delim && peek_token.value == '='
|
|
334
|
+
consume
|
|
335
|
+
:ge
|
|
336
|
+
else
|
|
337
|
+
:gt
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def value_starts?(item)
|
|
343
|
+
item.is_a?(Token) && %i[number dimension percentage].include?(item.type)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def consume_modifier
|
|
347
|
+
item = peek
|
|
348
|
+
|
|
349
|
+
return nil unless item.is_a?(Token) && item.type == :ident
|
|
350
|
+
|
|
351
|
+
kw = item.value.downcase
|
|
352
|
+
|
|
353
|
+
return nil unless MODIFIER_KEYWORDS.include?(kw)
|
|
354
|
+
|
|
355
|
+
consume
|
|
356
|
+
kw.to_sym
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def keyword?(kw)
|
|
360
|
+
item = peek
|
|
361
|
+
item.is_a?(Token) && item.type == :ident && item.value.downcase == kw
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def describe(item)
|
|
365
|
+
case item
|
|
366
|
+
when Token then item.type
|
|
367
|
+
when Nodes::SimpleBlock then "#{item.open}-block"
|
|
368
|
+
when Nodes::Function then "#{item.name}()"
|
|
369
|
+
else item.class.name
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|