p_css 0.1.1 → 0.1.3
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/lib/css/cascade.rb +38 -32
- data/lib/css/selectors/matcher.rb +1 -5
- data/lib/css/tokenizer.rb +42 -17
- data/lib/css/version.rb +1 -1
- data/sig/css/cascade.rbs +22 -0
- data/sig/css/media_queries.rbs +107 -0
- data/sig/css/nodes.rbs +76 -0
- data/sig/css/selectors.rbs +161 -0
- data/sig/css/token.rbs +33 -0
- data/sig/css.rbs +94 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df1cd693075fe04da6a0c9ce4c65c9c4ef5f85c5b84bb82bd0136cf62fe52552
|
|
4
|
+
data.tar.gz: 9a9fc875c3872c49396c5b6c753a099ba3cc344efa7aa97745958d277630c7cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28c76784dac592aa39cfaa5100b69e4765861ec2ab1ed536fb95dfb963481de2bd64d6b6b954f71b14b17ea4a328cc65af6f7a63df662f2083c2ef5ec623e892
|
|
7
|
+
data.tar.gz: 291d12b999205c032e7cebaa5da4b69aff717cb9f7b9743c3c54ce99539b59ec49612b87e9bcf105d374cc79871d8955fc57f18200b8ba76c396894db14f349f
|
data/lib/css/cascade.rb
CHANGED
|
@@ -115,9 +115,8 @@ module CSS
|
|
|
115
115
|
# Index
|
|
116
116
|
# ----------------------------------------------------------------
|
|
117
117
|
|
|
118
|
-
Index
|
|
119
|
-
|
|
120
|
-
EMPTY_INDEXES = [].freeze
|
|
118
|
+
Index = Data.define(:by_id, :by_class, :by_tag, :universal)
|
|
119
|
+
AnchorKey = Data.define(:kind, :name)
|
|
121
120
|
|
|
122
121
|
def build_index(entries)
|
|
123
122
|
by_id = {}
|
|
@@ -126,19 +125,19 @@ module CSS
|
|
|
126
125
|
universal = []
|
|
127
126
|
|
|
128
127
|
entries.each_with_index do |entry, idx|
|
|
129
|
-
|
|
128
|
+
seen = Set.new
|
|
130
129
|
|
|
131
130
|
entry.selector_pairs.each do |sel, _spec|
|
|
132
131
|
key = anchor_key(sel)
|
|
133
132
|
|
|
134
|
-
next if
|
|
133
|
+
next if seen.include?(key)
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
seen << key
|
|
137
136
|
|
|
138
|
-
case key.
|
|
139
|
-
when :id then (by_id[key.
|
|
140
|
-
when :class then (by_class[key.
|
|
141
|
-
when :tag then (by_tag[key.
|
|
137
|
+
case key.kind
|
|
138
|
+
when :id then (by_id[key.name] ||= []) << idx
|
|
139
|
+
when :class then (by_class[key.name] ||= []) << idx
|
|
140
|
+
when :tag then (by_tag[key.name] ||= []) << idx
|
|
142
141
|
when :universal then universal << idx
|
|
143
142
|
end
|
|
144
143
|
end
|
|
@@ -158,44 +157,51 @@ module CSS
|
|
|
158
157
|
# they will be tested against every element, but real-world
|
|
159
158
|
# stylesheets rarely have many such rules.
|
|
160
159
|
def anchor_key(complex_selector)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return [:tag, c.name.downcase] if c.is_a?(Selectors::TypeSelector)
|
|
160
|
+
class_name = nil
|
|
161
|
+
tag_name = nil
|
|
162
|
+
|
|
163
|
+
complex_selector.compounds.last.components.each do |c|
|
|
164
|
+
case c
|
|
165
|
+
when Selectors::IdSelector then return AnchorKey.new(kind: :id, name: c.name)
|
|
166
|
+
when Selectors::ClassSelector then class_name ||= c.name
|
|
167
|
+
when Selectors::TypeSelector then tag_name ||= c.name.downcase
|
|
168
|
+
end
|
|
171
169
|
end
|
|
172
170
|
|
|
173
|
-
|
|
171
|
+
return AnchorKey.new(kind: :class, name: class_name) if class_name
|
|
172
|
+
return AnchorKey.new(kind: :tag, name: tag_name) if tag_name
|
|
173
|
+
|
|
174
|
+
AnchorKey.new(kind: :universal, name: nil)
|
|
174
175
|
end
|
|
175
176
|
|
|
176
177
|
# Resolve helpers
|
|
177
178
|
# ----------------------------------------------------------------
|
|
178
179
|
|
|
180
|
+
# Buckets are appended in source-order at compile time, so each is
|
|
181
|
+
# already sorted ascending and unique. Concatenating them and using
|
|
182
|
+
# `sort! + uniq!` is cheaper than going through a `Set`: integers
|
|
183
|
+
# sort in C, and `uniq!` on a sorted array only removes adjacent
|
|
184
|
+
# duplicates.
|
|
179
185
|
def collect_candidate_indexes(element, cache)
|
|
180
|
-
|
|
186
|
+
out = []
|
|
181
187
|
|
|
182
|
-
el_id
|
|
188
|
+
el_id = Selectors::Matcher.id_of(element, cache)
|
|
189
|
+
bucket = @index.by_id[el_id] if el_id
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
seen.merge(bucket)
|
|
186
|
-
end
|
|
191
|
+
out.concat(bucket) if bucket
|
|
187
192
|
|
|
188
193
|
Selectors::Matcher.classes_of(element, cache).each do |cls|
|
|
189
194
|
bucket = @index.by_class[cls]
|
|
190
|
-
|
|
195
|
+
out.concat(bucket) if bucket
|
|
191
196
|
end
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
bucket = @index.by_tag[Selectors::Matcher.tag_of(element, cache)]
|
|
199
|
+
out.concat(bucket) if bucket
|
|
195
200
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
out.concat(@index.universal)
|
|
202
|
+
out.sort!
|
|
203
|
+
out.uniq!
|
|
204
|
+
out
|
|
199
205
|
end
|
|
200
206
|
|
|
201
207
|
def best_matching_specificity(element, selector_pairs, cache)
|
|
@@ -30,7 +30,7 @@ module CSS
|
|
|
30
30
|
# for every selector in a hot loop (e.g. `Cascade#resolve` against
|
|
31
31
|
# hundreds of rules). Keyed by `Object#object_id`; only valid for
|
|
32
32
|
# the duration of a single matcher invocation.
|
|
33
|
-
Context =
|
|
33
|
+
Context = Data.define(:tag, :id, :classes)
|
|
34
34
|
|
|
35
35
|
EMPTY_CLASS_SET = Set.new.freeze
|
|
36
36
|
|
|
@@ -425,10 +425,6 @@ module CSS
|
|
|
425
425
|
element[lower]
|
|
426
426
|
end
|
|
427
427
|
|
|
428
|
-
def class_list(element)
|
|
429
|
-
attr(element, 'class').to_s.split(/\s+/)
|
|
430
|
-
end
|
|
431
|
-
|
|
432
428
|
def parent_element(element)
|
|
433
429
|
p = element.respond_to?(:parent) ? element.parent : nil
|
|
434
430
|
|
data/lib/css/tokenizer.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module CSS
|
|
2
2
|
# Tokenizer based on CSS Syntax Module Level 3/4 §4.
|
|
3
3
|
# https://www.w3.org/TR/css-syntax-3/#tokenization
|
|
4
|
+
#
|
|
5
|
+
# Not thread-safe: an instance carries mutable cursors (`@pos`,
|
|
6
|
+
# `@newline_cursor`) that advance over the input. Allocate one
|
|
7
|
+
# tokenizer per thread.
|
|
4
8
|
class Tokenizer
|
|
5
9
|
include CodePoints
|
|
6
10
|
|
|
@@ -21,9 +25,10 @@ module CSS
|
|
|
21
25
|
PREPROCESS_RE = /\r\n?|\f|\0/.freeze
|
|
22
26
|
|
|
23
27
|
def initialize(input, preserve_comments: false)
|
|
24
|
-
@
|
|
28
|
+
@chars = preprocess(input)
|
|
25
29
|
@pos = 0
|
|
26
|
-
@newlines = collect_newline_offsets(@
|
|
30
|
+
@newlines = collect_newline_offsets(@chars)
|
|
31
|
+
@newline_cursor = 0
|
|
27
32
|
@preserve_comments = preserve_comments
|
|
28
33
|
end
|
|
29
34
|
|
|
@@ -43,7 +48,7 @@ module CSS
|
|
|
43
48
|
def next_token
|
|
44
49
|
consume_comments unless @preserve_comments
|
|
45
50
|
|
|
46
|
-
return Token.new(:eof) if @pos >= @
|
|
51
|
+
return Token.new(:eof) if @pos >= @chars.length
|
|
47
52
|
|
|
48
53
|
start_offset = @pos
|
|
49
54
|
tok = consume_one_token
|
|
@@ -127,18 +132,25 @@ module CSS
|
|
|
127
132
|
end
|
|
128
133
|
end
|
|
129
134
|
|
|
135
|
+
# Random access on a non-ascii-only UTF-8 String is O(distance from
|
|
136
|
+
# the cached character index), and the peek-ahead pattern (`peek`,
|
|
137
|
+
# `peek(1)`, `peek(2)`) defeats the cache — empirically ~200× slower
|
|
138
|
+
# than indexing a flat Array. Splitting into `chars` once amortizes
|
|
139
|
+
# the UTF-8 walk and gives us O(1) random access for the rest of
|
|
140
|
+
# tokenization.
|
|
130
141
|
def preprocess(input)
|
|
131
|
-
input
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
input
|
|
143
|
+
.encode('UTF-8')
|
|
144
|
+
.gsub(PREPROCESS_RE) { $~[0] == "\0" ? CodePoints::REPLACEMENT : "\n" }
|
|
145
|
+
.chars
|
|
134
146
|
end
|
|
135
147
|
|
|
136
148
|
def peek(offset = 0)
|
|
137
|
-
@
|
|
149
|
+
@chars[@pos + offset]
|
|
138
150
|
end
|
|
139
151
|
|
|
140
152
|
def consume
|
|
141
|
-
c = @
|
|
153
|
+
c = @chars[@pos]
|
|
142
154
|
return nil if c.nil?
|
|
143
155
|
|
|
144
156
|
@pos += 1
|
|
@@ -149,21 +161,34 @@ module CSS
|
|
|
149
161
|
@pos -= 1
|
|
150
162
|
end
|
|
151
163
|
|
|
152
|
-
def collect_newline_offsets(
|
|
164
|
+
def collect_newline_offsets(chars)
|
|
153
165
|
offsets = []
|
|
154
|
-
i =
|
|
166
|
+
i = 0
|
|
167
|
+
n = chars.length
|
|
168
|
+
|
|
169
|
+
while i < n
|
|
170
|
+
offsets << i if chars[i] == "\n"
|
|
171
|
+
i += 1
|
|
172
|
+
end
|
|
155
173
|
|
|
156
|
-
offsets << i while (i = input.index("\n", i + 1))
|
|
157
174
|
offsets
|
|
158
175
|
end
|
|
159
176
|
|
|
160
|
-
# Newline characters themselves are reported as belonging to the
|
|
161
|
-
# they terminate (col = offset + 1 on line 1, etc).
|
|
177
|
+
# Newline characters themselves are reported as belonging to the
|
|
178
|
+
# line they terminate (col = offset + 1 on line 1, etc).
|
|
179
|
+
#
|
|
180
|
+
# Tokens are emitted in order, so the offsets passed in are
|
|
181
|
+
# monotonically non-decreasing. We keep a running cursor into
|
|
182
|
+
# `@newlines` and advance linearly — amortized O(1) per call,
|
|
183
|
+
# vs. O(log n) per call with a fresh `bsearch`.
|
|
162
184
|
def line_column_at(offset)
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
while @newline_cursor < @newlines.size && @newlines[@newline_cursor] < offset
|
|
186
|
+
@newline_cursor += 1
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
prev_nl = @newline_cursor.zero? ? -1 : @newlines[@newline_cursor - 1]
|
|
165
190
|
|
|
166
|
-
[
|
|
191
|
+
[@newline_cursor + 1, offset - prev_nl]
|
|
167
192
|
end
|
|
168
193
|
|
|
169
194
|
def whitespace?(c)
|
|
@@ -242,7 +267,7 @@ module CSS
|
|
|
242
267
|
end
|
|
243
268
|
|
|
244
269
|
def eof?
|
|
245
|
-
@pos >= @
|
|
270
|
+
@pos >= @chars.length
|
|
246
271
|
end
|
|
247
272
|
|
|
248
273
|
def consume_whitespace
|
data/lib/css/version.rb
CHANGED
data/sig/css/cascade.rbs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
# See `CSS.cascade` for the typical entry point.
|
|
3
|
+
class Cascade
|
|
4
|
+
type inline_style = String | Nodes::Block | Array[Nodes::Declaration]
|
|
5
|
+
|
|
6
|
+
class Match < Data
|
|
7
|
+
attr_reader declaration: Nodes::Declaration
|
|
8
|
+
attr_reader specificity: Selectors::Specificity
|
|
9
|
+
attr_reader inline: bool
|
|
10
|
+
attr_reader order: Integer
|
|
11
|
+
|
|
12
|
+
def self.new: (declaration: Nodes::Declaration, specificity: Selectors::Specificity, inline: bool, order: Integer) -> Match
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize: (Nodes::Stylesheet stylesheet, ?context: MediaQueries::Context) -> void
|
|
16
|
+
|
|
17
|
+
# Returns Hash<String, Declaration> of winning declarations after
|
|
18
|
+
# !important > inline > stylesheet > specificity > source-order
|
|
19
|
+
# sorting.
|
|
20
|
+
def resolve: (untyped element, ?inline_style: inline_style?) -> Hash[String, Nodes::Declaration]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module MediaQueries
|
|
3
|
+
module Node
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
type modifier = :not | :only
|
|
7
|
+
|
|
8
|
+
type comparison_op = :eq | :lt | :le | :gt | :ge
|
|
9
|
+
|
|
10
|
+
type media_condition = MediaNot | MediaAnd | MediaOr | MediaFeature | GeneralEnclosed
|
|
11
|
+
|
|
12
|
+
type feature_value = Token | Ratio
|
|
13
|
+
|
|
14
|
+
class MediaQueryList < Data
|
|
15
|
+
include Node
|
|
16
|
+
|
|
17
|
+
attr_reader queries: Array[MediaQuery]
|
|
18
|
+
|
|
19
|
+
def self.new: (queries: Array[MediaQuery]) -> MediaQueryList
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class MediaQuery < Data
|
|
23
|
+
include Node
|
|
24
|
+
|
|
25
|
+
attr_reader modifier: modifier?
|
|
26
|
+
attr_reader type: String?
|
|
27
|
+
attr_reader condition: media_condition?
|
|
28
|
+
|
|
29
|
+
def self.new: (modifier: modifier?, type: String?, condition: media_condition?) -> MediaQuery
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class MediaNot < Data
|
|
33
|
+
include Node
|
|
34
|
+
|
|
35
|
+
attr_reader operand: media_condition
|
|
36
|
+
|
|
37
|
+
def self.new: (operand: media_condition) -> MediaNot
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class MediaAnd < Data
|
|
41
|
+
include Node
|
|
42
|
+
|
|
43
|
+
attr_reader operands: Array[media_condition]
|
|
44
|
+
|
|
45
|
+
def self.new: (operands: Array[media_condition]) -> MediaAnd
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class MediaOr < Data
|
|
49
|
+
include Node
|
|
50
|
+
|
|
51
|
+
attr_reader operands: Array[media_condition]
|
|
52
|
+
|
|
53
|
+
def self.new: (operands: Array[media_condition]) -> MediaOr
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `op` is `nil` for boolean form (`(color)`), `:eq` for plain
|
|
57
|
+
# (`(min-width: 600px)`) and explicit `=`, otherwise a comparison.
|
|
58
|
+
# `value` mirrors that — `nil` for boolean form.
|
|
59
|
+
class MediaFeature < Data
|
|
60
|
+
include Node
|
|
61
|
+
|
|
62
|
+
attr_reader name: String
|
|
63
|
+
attr_reader op: comparison_op?
|
|
64
|
+
attr_reader value: feature_value?
|
|
65
|
+
|
|
66
|
+
def self.new: (name: String, op: comparison_op?, value: feature_value?) -> MediaFeature
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class GeneralEnclosed < Data
|
|
70
|
+
include Node
|
|
71
|
+
|
|
72
|
+
attr_reader tokens: Array[component_value]
|
|
73
|
+
|
|
74
|
+
def self.new: (tokens: Array[component_value]) -> GeneralEnclosed
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class Ratio < Data
|
|
78
|
+
include Node
|
|
79
|
+
|
|
80
|
+
attr_reader numerator: Numeric
|
|
81
|
+
attr_reader denominator: Numeric
|
|
82
|
+
|
|
83
|
+
def self.new: (numerator: Numeric, denominator: Numeric) -> Ratio
|
|
84
|
+
|
|
85
|
+
def to_f: () -> Float
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# User-agent context against which a `MediaQueryList` is evaluated.
|
|
89
|
+
# `features` is keyed by feature name (no `min-`/`max-` prefix);
|
|
90
|
+
# values follow Media Queries 4 conventions (lengths in CSS px,
|
|
91
|
+
# resolution in `dppx`, identifiers as Strings, booleans as 1/0).
|
|
92
|
+
class Context < Data
|
|
93
|
+
DEFAULTS: Hash[String, untyped]
|
|
94
|
+
|
|
95
|
+
attr_reader features: Hash[String, untyped]
|
|
96
|
+
|
|
97
|
+
def self.new: (features: Hash[String, untyped]) -> Context
|
|
98
|
+
|
|
99
|
+
# `Context.default('width' => 1200, 'prefers-color-scheme' => 'dark')`
|
|
100
|
+
def self.default: (**untyped overrides) -> Context
|
|
101
|
+
|
|
102
|
+
def []: ((String | Symbol) name) -> untyped
|
|
103
|
+
def media_type: () -> String
|
|
104
|
+
def with: (**untyped overrides) -> Context
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/sig/css/nodes.rbs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module Nodes
|
|
3
|
+
# `Token` here covers preserved comments when the stylesheet was
|
|
4
|
+
# parsed with `preserve_comments: true`.
|
|
5
|
+
type rule = AtRule | QualifiedRule | Token
|
|
6
|
+
|
|
7
|
+
type block_item = Declaration | QualifiedRule | AtRule | Token
|
|
8
|
+
|
|
9
|
+
class Stylesheet < Data
|
|
10
|
+
attr_reader rules: Array[rule]
|
|
11
|
+
|
|
12
|
+
def self.new: (rules: Array[rule]) -> Stylesheet
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# `prelude` is the list of component values before `{` or `;`;
|
|
16
|
+
# `block` is `nil` for at-rules ending with `;` (e.g. `@charset "UTF-8";`).
|
|
17
|
+
class AtRule < Data
|
|
18
|
+
attr_reader name: String
|
|
19
|
+
attr_reader prelude: Array[component_value]
|
|
20
|
+
attr_reader block: Block?
|
|
21
|
+
|
|
22
|
+
def self.new: (name: String, prelude: Array[component_value], block: Block?) -> AtRule
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class QualifiedRule < Data
|
|
26
|
+
attr_reader prelude: Array[component_value]
|
|
27
|
+
attr_reader block: Block
|
|
28
|
+
|
|
29
|
+
def self.new: (prelude: Array[component_value], block: Block) -> QualifiedRule
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Block < Data
|
|
33
|
+
attr_reader items: Array[block_item]
|
|
34
|
+
|
|
35
|
+
def self.new: (items: Array[block_item]) -> Block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Declaration < Data
|
|
39
|
+
attr_reader name: String
|
|
40
|
+
attr_reader value: Array[component_value]
|
|
41
|
+
attr_reader important: bool
|
|
42
|
+
|
|
43
|
+
def self.new: (name: String, value: Array[component_value], important: bool) -> Declaration
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Function < Data
|
|
47
|
+
attr_reader name: String
|
|
48
|
+
attr_reader value: Array[component_value]
|
|
49
|
+
|
|
50
|
+
def self.new: (name: String, value: Array[component_value]) -> Function
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `open` is one of `(`, `[`, `{`.
|
|
54
|
+
class SimpleBlock < Data
|
|
55
|
+
attr_reader open: String
|
|
56
|
+
attr_reader value: Array[component_value]
|
|
57
|
+
|
|
58
|
+
def self.new: (open: String, value: Array[component_value]) -> SimpleBlock
|
|
59
|
+
|
|
60
|
+
def braced?: () -> bool
|
|
61
|
+
def bracketed?: () -> bool
|
|
62
|
+
def parenthesized?: () -> bool
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Inclusive code-point range produced by `CSS.parse_urange`.
|
|
66
|
+
class UnicodeRange < Data
|
|
67
|
+
attr_reader first: Integer
|
|
68
|
+
attr_reader last: Integer
|
|
69
|
+
|
|
70
|
+
def self.new: (first: Integer, last: Integer) -> UnicodeRange
|
|
71
|
+
|
|
72
|
+
def cover?: (Integer cp) -> bool
|
|
73
|
+
def to_s: () -> String
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module Selectors
|
|
3
|
+
# Marker module included by every Selector AST node so callers can
|
|
4
|
+
# discriminate `Selectors::Node` from raw component values.
|
|
5
|
+
module Node
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
type combinator = :descendant | :child | :next_sibling | :subsequent_sibling
|
|
9
|
+
|
|
10
|
+
type simple_selector =
|
|
11
|
+
TypeSelector | UniversalSelector | NestingSelector
|
|
12
|
+
| IdSelector | ClassSelector | AttributeSelector
|
|
13
|
+
| PseudoClass | PseudoElement
|
|
14
|
+
|
|
15
|
+
type attribute_matcher = :exact | :includes | :dash | :prefix | :suffix | :substring
|
|
16
|
+
|
|
17
|
+
type case_flag = :i | :s
|
|
18
|
+
|
|
19
|
+
# `argument` is `nil` for plain `:hover` etc., a `SelectorList` for
|
|
20
|
+
# `:not / :is / :where / :matches`, an `AnB` for `:nth-*`, or an
|
|
21
|
+
# opaque component-value array for unrecognized functional pseudos
|
|
22
|
+
# (including `:has`, which is intentionally not parsed as a selector
|
|
23
|
+
# list yet).
|
|
24
|
+
type pseudo_argument = nil | SelectorList | AnB | Array[component_value]
|
|
25
|
+
|
|
26
|
+
class SelectorList < Data
|
|
27
|
+
include Node
|
|
28
|
+
|
|
29
|
+
attr_reader selectors: Array[ComplexSelector]
|
|
30
|
+
|
|
31
|
+
def self.new: (selectors: Array[ComplexSelector]) -> SelectorList
|
|
32
|
+
|
|
33
|
+
def to_s: () -> String
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `compounds.size == combinators.size + 1`. `combinators[i]` joins
|
|
37
|
+
# `compounds[i]` to `compounds[i + 1]`.
|
|
38
|
+
class ComplexSelector < Data
|
|
39
|
+
include Node
|
|
40
|
+
|
|
41
|
+
attr_reader compounds: Array[CompoundSelector]
|
|
42
|
+
attr_reader combinators: Array[combinator]
|
|
43
|
+
|
|
44
|
+
def self.new: (compounds: Array[CompoundSelector], combinators: Array[combinator]) -> ComplexSelector
|
|
45
|
+
|
|
46
|
+
def to_s: () -> String
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class CompoundSelector < Data
|
|
50
|
+
include Node
|
|
51
|
+
|
|
52
|
+
attr_reader components: Array[simple_selector]
|
|
53
|
+
|
|
54
|
+
def self.new: (components: Array[simple_selector]) -> CompoundSelector
|
|
55
|
+
|
|
56
|
+
def to_s: () -> String
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class TypeSelector < Data
|
|
60
|
+
include Node
|
|
61
|
+
|
|
62
|
+
attr_reader name: String
|
|
63
|
+
|
|
64
|
+
def self.new: (name: String) -> TypeSelector
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class UniversalSelector < Data
|
|
68
|
+
include Node
|
|
69
|
+
|
|
70
|
+
def self.new: () -> UniversalSelector
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class NestingSelector < Data
|
|
74
|
+
include Node
|
|
75
|
+
|
|
76
|
+
def self.new: () -> NestingSelector
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class IdSelector < Data
|
|
80
|
+
include Node
|
|
81
|
+
|
|
82
|
+
attr_reader name: String
|
|
83
|
+
|
|
84
|
+
def self.new: (name: String) -> IdSelector
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class ClassSelector < Data
|
|
88
|
+
include Node
|
|
89
|
+
|
|
90
|
+
attr_reader name: String
|
|
91
|
+
|
|
92
|
+
def self.new: (name: String) -> ClassSelector
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class AttributeSelector < Data
|
|
96
|
+
include Node
|
|
97
|
+
|
|
98
|
+
attr_reader name: String
|
|
99
|
+
attr_reader matcher: attribute_matcher?
|
|
100
|
+
attr_reader value: String?
|
|
101
|
+
attr_reader case_flag: case_flag?
|
|
102
|
+
|
|
103
|
+
def self.new: (name: String, matcher: attribute_matcher?, value: String?, case_flag: case_flag?) -> AttributeSelector
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class PseudoClass < Data
|
|
107
|
+
include Node
|
|
108
|
+
|
|
109
|
+
attr_reader name: String
|
|
110
|
+
attr_reader argument: pseudo_argument
|
|
111
|
+
|
|
112
|
+
def self.new: (name: String, argument: pseudo_argument) -> PseudoClass
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class PseudoElement < Data
|
|
116
|
+
include Node
|
|
117
|
+
|
|
118
|
+
attr_reader name: String
|
|
119
|
+
attr_reader argument: pseudo_argument
|
|
120
|
+
|
|
121
|
+
def self.new: (name: String, argument: pseudo_argument) -> PseudoElement
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class AnB < Data
|
|
125
|
+
include Node
|
|
126
|
+
|
|
127
|
+
attr_reader step: Integer
|
|
128
|
+
attr_reader offset: Integer
|
|
129
|
+
|
|
130
|
+
def self.new: (step: Integer, offset: Integer) -> AnB
|
|
131
|
+
|
|
132
|
+
def to_s: () -> String
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Selectors §16 specificity tuple `(a, b, c)` — id / class+attr+pseudo /
|
|
136
|
+
# type+pseudo-element counts.
|
|
137
|
+
class Specificity < Data
|
|
138
|
+
include Comparable
|
|
139
|
+
|
|
140
|
+
ZERO: Specificity
|
|
141
|
+
|
|
142
|
+
attr_reader a: Integer
|
|
143
|
+
attr_reader b: Integer
|
|
144
|
+
attr_reader c: Integer
|
|
145
|
+
|
|
146
|
+
def self.new: (a: Integer, b: Integer, c: Integer) -> Specificity
|
|
147
|
+
|
|
148
|
+
def <=>: (untyped other) -> Integer?
|
|
149
|
+
def +: (Specificity other) -> Specificity
|
|
150
|
+
def to_s: () -> String
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
module Matcher
|
|
154
|
+
def self.matches?: (untyped element, untyped selector, ?cache: Hash[Integer, untyped]) -> bool
|
|
155
|
+
|
|
156
|
+
def self.tag_of: (untyped element, ?Hash[Integer, untyped]?) -> String
|
|
157
|
+
def self.id_of: (untyped element, ?Hash[Integer, untyped]?) -> String?
|
|
158
|
+
def self.classes_of: (untyped element, ?Hash[Integer, untyped]?) -> Set[String]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
data/sig/css/token.rbs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
class Token
|
|
3
|
+
TYPES: Array[Symbol]
|
|
4
|
+
|
|
5
|
+
type token_type =
|
|
6
|
+
:ident | :function | :at_keyword | :hash | :string | :bad_string
|
|
7
|
+
| :url | :bad_url | :delim | :number | :percentage | :dimension
|
|
8
|
+
| :whitespace | :cdo | :cdc | :comment
|
|
9
|
+
| :colon | :semicolon | :comma
|
|
10
|
+
| :lbracket | :rbracket | :lparen | :rparen | :lbrace | :rbrace
|
|
11
|
+
| :eof
|
|
12
|
+
|
|
13
|
+
attr_reader type: token_type
|
|
14
|
+
attr_reader value: untyped
|
|
15
|
+
attr_reader flag: Symbol?
|
|
16
|
+
attr_reader unit: String?
|
|
17
|
+
attr_reader position: Position?
|
|
18
|
+
|
|
19
|
+
def initialize: (token_type type, ?untyped value, ?flag: Symbol?, ?unit: String?, ?position: Position?) -> void
|
|
20
|
+
|
|
21
|
+
def whitespace?: () -> bool
|
|
22
|
+
def comment?: () -> bool
|
|
23
|
+
def trivia?: () -> bool
|
|
24
|
+
|
|
25
|
+
def assign_position!: (Position pos) -> self
|
|
26
|
+
|
|
27
|
+
def ==: (untyped other) -> bool
|
|
28
|
+
def eql?: (untyped other) -> bool
|
|
29
|
+
def hash: () -> Integer
|
|
30
|
+
|
|
31
|
+
def inspect: () -> String
|
|
32
|
+
end
|
|
33
|
+
end
|
data/sig/css.rbs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# CSS Syntax Level 4 toolkit — top-level entry points and shared types.
|
|
2
|
+
#
|
|
3
|
+
# Public API surface lives directly on `CSS`; AST nodes live under
|
|
4
|
+
# `CSS::Nodes`, `CSS::Selectors`, and `CSS::MediaQueries`. Per-token
|
|
5
|
+
# source positions are `CSS::Position`.
|
|
6
|
+
module CSS
|
|
7
|
+
# Inputs that carry CSS source. `String` is tokenized internally; an
|
|
8
|
+
# array of component values (as produced by `CSS.parse_component_values`
|
|
9
|
+
# or by reading a rule's `prelude`) is consumed structurally.
|
|
10
|
+
type input = String | Array[component_value]
|
|
11
|
+
|
|
12
|
+
# The output of `CSS.parse_component_value` and the element type of a
|
|
13
|
+
# rule's `prelude` / declaration's `value`.
|
|
14
|
+
type component_value = Token | Nodes::Function | Nodes::SimpleBlock
|
|
15
|
+
|
|
16
|
+
# Bracket-pair information used by both the parser (open token type →
|
|
17
|
+
# open char, close token type) and the serializer.
|
|
18
|
+
BRACKET_OPEN_CHAR: Hash[Symbol, String]
|
|
19
|
+
BRACKET_CLOSE_TYPE: Hash[Symbol, Symbol]
|
|
20
|
+
BRACKET_PAIRS: Hash[String, String]
|
|
21
|
+
|
|
22
|
+
class ParseError < StandardError
|
|
23
|
+
attr_reader position: Position?
|
|
24
|
+
|
|
25
|
+
def initialize: (String message, ?position: Position?) -> void
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 1-based line / column with 0-based character offsets into the
|
|
29
|
+
# preprocessed input.
|
|
30
|
+
class Position < Data
|
|
31
|
+
attr_reader line: Integer
|
|
32
|
+
attr_reader column: Integer
|
|
33
|
+
attr_reader offset: Integer
|
|
34
|
+
attr_reader end_offset: Integer
|
|
35
|
+
|
|
36
|
+
def self.new: (line: Integer, column: Integer, offset: Integer, end_offset: Integer) -> Position
|
|
37
|
+
|
|
38
|
+
def to_s: () -> String
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Tokenization (Syntax 4 §4)
|
|
42
|
+
# ---------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def self.tokenize: (String input, ?preserve_comments: bool) -> Array[Token]
|
|
45
|
+
|
|
46
|
+
# Parsing (Syntax 4 §5.3)
|
|
47
|
+
# ---------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def self.parse_stylesheet: (input input, ?preserve_comments: bool) -> Nodes::Stylesheet
|
|
50
|
+
def self.parse: (input input, ?preserve_comments: bool) -> Nodes::Stylesheet
|
|
51
|
+
|
|
52
|
+
def self.parse_rule: (input input, ?preserve_comments: bool) -> (Nodes::AtRule | Nodes::QualifiedRule)
|
|
53
|
+
def self.parse_declaration: (input input, ?preserve_comments: bool) -> Nodes::Declaration
|
|
54
|
+
def self.parse_block_contents: (input input, ?preserve_comments: bool) -> Nodes::Block
|
|
55
|
+
|
|
56
|
+
def self.parse_component_value: (input input, ?preserve_comments: bool) -> component_value
|
|
57
|
+
def self.parse_component_values: (input input, ?preserve_comments: bool) -> Array[component_value]
|
|
58
|
+
def self.parse_comma_separated_values: (input input, ?preserve_comments: bool) -> Array[Array[component_value]]
|
|
59
|
+
|
|
60
|
+
# Selectors (Selectors 4)
|
|
61
|
+
# ---------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def self.parse_selector_list: (String | Array[component_value] input) -> Selectors::SelectorList
|
|
64
|
+
def self.parse_selector: (String | Array[component_value] input) -> Selectors::ComplexSelector
|
|
65
|
+
def self.parse_anb: (String | Array[Token] input) -> Selectors::AnB
|
|
66
|
+
|
|
67
|
+
def self.specificity: (Selectors::Node selector) -> Selectors::Specificity
|
|
68
|
+
|
|
69
|
+
type selector = Selectors::SelectorList | Selectors::ComplexSelector | Selectors::CompoundSelector
|
|
70
|
+
|
|
71
|
+
def self.matches?: (untyped element, String | selector selector, ?cache: Hash[Integer, untyped]) -> bool
|
|
72
|
+
|
|
73
|
+
# Media queries (Media Queries 4)
|
|
74
|
+
# ---------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def self.parse_media_query_list: (String | Array[component_value] input) -> MediaQueries::MediaQueryList
|
|
77
|
+
|
|
78
|
+
type media_context_input = MediaQueries::Context | Hash[String, untyped]
|
|
79
|
+
|
|
80
|
+
def self.media_matches?: (String | MediaQueries::MediaQueryList query_list, media_context_input context) -> bool
|
|
81
|
+
|
|
82
|
+
# Other transformations
|
|
83
|
+
# ---------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def self.parse_urange: (_ToS input) -> Nodes::UnicodeRange
|
|
86
|
+
|
|
87
|
+
def self.desugar: (Nodes::Stylesheet stylesheet) -> Nodes::Stylesheet
|
|
88
|
+
|
|
89
|
+
def self.cascade: (Nodes::Stylesheet stylesheet, ?context: MediaQueries::Context) -> Cascade
|
|
90
|
+
|
|
91
|
+
def self.serialize: (untyped node) -> String
|
|
92
|
+
|
|
93
|
+
VERSION: String
|
|
94
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: p_css
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
@@ -45,6 +45,12 @@ files:
|
|
|
45
45
|
- lib/css/urange.rb
|
|
46
46
|
- lib/css/version.rb
|
|
47
47
|
- lib/p_css.rb
|
|
48
|
+
- sig/css.rbs
|
|
49
|
+
- sig/css/cascade.rbs
|
|
50
|
+
- sig/css/media_queries.rbs
|
|
51
|
+
- sig/css/nodes.rbs
|
|
52
|
+
- sig/css/selectors.rbs
|
|
53
|
+
- sig/css/token.rbs
|
|
48
54
|
homepage: https://github.com/ursm/p_css
|
|
49
55
|
licenses:
|
|
50
56
|
- MIT
|