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,167 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
# Serializer based on CSS Syntax Module Level 4 §9 Serialization.
|
|
3
|
+
# https://drafts.csswg.org/css-syntax/#serialization
|
|
4
|
+
#
|
|
5
|
+
# The output is intended to round-trip: re-parsing it should yield an
|
|
6
|
+
# equivalent AST. Idents, strings, hashes, and dimensions are escaped
|
|
7
|
+
# following the spec rules.
|
|
8
|
+
module Serializer
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
INDENT = ' '.freeze
|
|
12
|
+
|
|
13
|
+
def serialize(node)
|
|
14
|
+
case node
|
|
15
|
+
when Nodes::Stylesheet then serialize_stylesheet(node)
|
|
16
|
+
when Nodes::AtRule then serialize_at_rule(node)
|
|
17
|
+
when Nodes::QualifiedRule then serialize_qualified_rule(node)
|
|
18
|
+
when Nodes::Block then serialize_block(node)
|
|
19
|
+
when Nodes::Declaration then serialize_declaration(node)
|
|
20
|
+
when Nodes::Function then serialize_function(node)
|
|
21
|
+
when Nodes::SimpleBlock then serialize_simple_block(node)
|
|
22
|
+
when Token then serialize_token(node)
|
|
23
|
+
when Selectors::Node then Selectors::Serializer.serialize(node)
|
|
24
|
+
when Array then node.map { serialize(it) }.join
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError, "cannot serialize #{node.class}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def serialize_stylesheet(ss)
|
|
33
|
+
ss.rules.map { serialize(it) }.join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serialize_at_rule(rule)
|
|
37
|
+
head = "@#{serialize_ident(rule.name)}"
|
|
38
|
+
|
|
39
|
+
prelude_str = serialize(rule.prelude)
|
|
40
|
+
head += " #{prelude_str}" unless prelude_str.empty?
|
|
41
|
+
|
|
42
|
+
rule.block ? "#{head} #{serialize_block(rule.block)}" : "#{head};"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def serialize_qualified_rule(rule)
|
|
46
|
+
"#{serialize(rule.prelude)} #{serialize_block(rule.block)}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def serialize_block(block)
|
|
50
|
+
return '{}' if block.items.empty?
|
|
51
|
+
|
|
52
|
+
inner = block.items.map { serialize(it) }.join("\n")
|
|
53
|
+
"{\n#{indent(inner)}\n}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def serialize_declaration(decl)
|
|
57
|
+
important = decl.important ? ' !important' : ''
|
|
58
|
+
"#{serialize_ident(decl.name)}: #{serialize(decl.value)}#{important};"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def serialize_function(fn)
|
|
62
|
+
"#{serialize_ident(fn.name)}(#{serialize(fn.value)})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def serialize_simple_block(block)
|
|
66
|
+
"#{block.open}#{serialize(block.value)}#{BRACKET_PAIRS.fetch(block.open)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# §9.3.
|
|
70
|
+
def serialize_token(t)
|
|
71
|
+
case t.type
|
|
72
|
+
when :ident then serialize_ident(t.value)
|
|
73
|
+
when :function then "#{serialize_ident(t.value)}("
|
|
74
|
+
when :at_keyword then "@#{serialize_ident(t.value)}"
|
|
75
|
+
when :hash then serialize_hash(t)
|
|
76
|
+
when :string then serialize_string(t.value)
|
|
77
|
+
when :url then "url(#{serialize_string(t.value)})"
|
|
78
|
+
when :bad_string, :bad_url then ''
|
|
79
|
+
when :delim then t.value
|
|
80
|
+
when :number then serialize_number(t.value, t.flag)
|
|
81
|
+
when :percentage then "#{serialize_number(t.value, :integer)}%"
|
|
82
|
+
when :dimension then serialize_dimension(t)
|
|
83
|
+
when :whitespace then ' '
|
|
84
|
+
when :comment then "/*#{t.value}*/"
|
|
85
|
+
when :cdo then '<!--'
|
|
86
|
+
when :cdc then '-->'
|
|
87
|
+
when :colon then ':'
|
|
88
|
+
when :semicolon then ';'
|
|
89
|
+
when :comma then ','
|
|
90
|
+
when :lbracket then '['
|
|
91
|
+
when :rbracket then ']'
|
|
92
|
+
when :lparen then '('
|
|
93
|
+
when :rparen then ')'
|
|
94
|
+
when :lbrace then '{'
|
|
95
|
+
when :rbrace then '}'
|
|
96
|
+
when :eof then ''
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def serialize_hash(t)
|
|
101
|
+
t.flag == :id ? "##{serialize_ident(t.value)}" : "##{serialize_name(t.value)}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def serialize_dimension(t)
|
|
105
|
+
"#{serialize_number(t.value, t.flag)}#{serialize_dimension_unit(t.unit)}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# If a unit starts with `e[+-]?<digit>`, the leading `e` would re-merge
|
|
109
|
+
# into the number's exponent on re-tokenization. Escape it.
|
|
110
|
+
def serialize_dimension_unit(unit)
|
|
111
|
+
if unit.match?(/\A[eE](?:[+-]?\d)/)
|
|
112
|
+
"\\#{format('%X', unit[0].ord)} #{unit[1..]}"
|
|
113
|
+
else
|
|
114
|
+
serialize_ident(unit)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# §9.3.6. Avoids E-notation entirely.
|
|
119
|
+
def serialize_number(value, flag)
|
|
120
|
+
case
|
|
121
|
+
when value.is_a?(Integer) && flag != :number
|
|
122
|
+
value.to_s
|
|
123
|
+
when value.is_a?(Integer)
|
|
124
|
+
"#{value}.0"
|
|
125
|
+
when value.finite?
|
|
126
|
+
format_finite_float(value)
|
|
127
|
+
else
|
|
128
|
+
'0'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def format_finite_float(f)
|
|
133
|
+
s = f.to_s
|
|
134
|
+
return s unless s.match?(/[eE]/)
|
|
135
|
+
|
|
136
|
+
sign = f.negative? ? '-' : ''
|
|
137
|
+
mantissa, e = s.sub(/\A-/, '').split(/[eE]/)
|
|
138
|
+
e = e.to_i
|
|
139
|
+
whole, frac = mantissa.split('.')
|
|
140
|
+
frac ||= ''
|
|
141
|
+
|
|
142
|
+
digits = whole + frac
|
|
143
|
+
decimal = whole.length + e
|
|
144
|
+
|
|
145
|
+
body =
|
|
146
|
+
if decimal <= 0
|
|
147
|
+
'0.' + ('0' * -decimal) + digits.sub(/0+\z/, '')
|
|
148
|
+
elsif decimal >= digits.length
|
|
149
|
+
digits + ('0' * (decimal - digits.length)) + '.0'
|
|
150
|
+
else
|
|
151
|
+
"#{digits[0, decimal]}.#{digits[decimal..]}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
"#{sign}#{body.sub(/(\.\d*?)0+\z/, '\1').sub(/\.\z/, '.0')}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def serialize_ident(s) = Escape.ident(s)
|
|
158
|
+
def serialize_name(s) = Escape.name(s)
|
|
159
|
+
def serialize_string(s) = Escape.string(s)
|
|
160
|
+
|
|
161
|
+
def indent(str)
|
|
162
|
+
str.lines.map { "#{INDENT}#{it}" }.join.then {
|
|
163
|
+
it.end_with?("\n") ? it.chomp : it
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/css/token.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
# Source location of a token within the preprocessed input. `offset` and
|
|
3
|
+
# `end_offset` are 0-based character indices; `line` and `column` are
|
|
4
|
+
# 1-based.
|
|
5
|
+
Position = Data.define(:line, :column, :offset, :end_offset) do
|
|
6
|
+
def to_s
|
|
7
|
+
"#{line}:#{column}"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Token
|
|
12
|
+
TYPES = %i[
|
|
13
|
+
ident function at_keyword hash string bad_string url bad_url
|
|
14
|
+
delim number percentage dimension whitespace cdo cdc comment
|
|
15
|
+
colon semicolon comma
|
|
16
|
+
lbracket rbracket lparen rparen lbrace rbrace
|
|
17
|
+
eof
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :type, :value, :flag, :unit, :position
|
|
21
|
+
|
|
22
|
+
def initialize(type, value = nil, flag: nil, unit: nil, position: nil)
|
|
23
|
+
raise ArgumentError, "unknown token type: #{type.inspect}" unless TYPES.include?(type)
|
|
24
|
+
|
|
25
|
+
@type = type
|
|
26
|
+
@value = value
|
|
27
|
+
@flag = flag
|
|
28
|
+
@unit = unit
|
|
29
|
+
@position = position
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Position is intentionally excluded from equality so that hand-built
|
|
33
|
+
# tokens compare equal to parsed tokens.
|
|
34
|
+
def ==(other)
|
|
35
|
+
other.is_a?(Token) &&
|
|
36
|
+
other.type == type &&
|
|
37
|
+
other.value == value &&
|
|
38
|
+
other.flag == flag &&
|
|
39
|
+
other.unit == unit
|
|
40
|
+
end
|
|
41
|
+
alias eql? ==
|
|
42
|
+
|
|
43
|
+
def hash
|
|
44
|
+
[type, value, flag, unit].hash
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def whitespace?
|
|
48
|
+
type == :whitespace
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def comment?
|
|
52
|
+
type == :comment
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# True for tokens that don't carry semantic content — used by the parser
|
|
56
|
+
# to skip insignificant tokens between meaningful ones.
|
|
57
|
+
def trivia?
|
|
58
|
+
type == :whitespace || type == :comment
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Mutating: assigns the token's source position and returns self. Used
|
|
62
|
+
# by the tokenizer so each token requires only a single allocation.
|
|
63
|
+
def assign_position!(pos)
|
|
64
|
+
@position = pos
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def inspect
|
|
69
|
+
parts = ["type=#{type.inspect}"]
|
|
70
|
+
parts << "value=#{value.inspect}" unless value.nil?
|
|
71
|
+
parts << "flag=#{flag.inspect}" unless flag.nil?
|
|
72
|
+
parts << "unit=#{unit.inspect}" unless unit.nil?
|
|
73
|
+
parts << "@#{position}" unless position.nil?
|
|
74
|
+
|
|
75
|
+
"#<CSS::Token #{parts.join(' ')}>"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
# Shared cursor used by the three parsers (`Selectors::Parser`,
|
|
3
|
+
# `Selectors::AnBParser::Impl`, `MediaQueries::Parser`). Each one walks
|
|
4
|
+
# an array of items with the same primitives — tokens for the selector
|
|
5
|
+
# parsers, mixed Token / SimpleBlock / Function items for the media-
|
|
6
|
+
# query parser. Predicates against the item's `.type` go through
|
|
7
|
+
# `peek_token`, which collapses non-Token items to EOF safely.
|
|
8
|
+
module TokenCursor
|
|
9
|
+
EOF_TOKEN = Token.new(:eof).freeze
|
|
10
|
+
|
|
11
|
+
def init_cursor(items)
|
|
12
|
+
@items = items
|
|
13
|
+
@pos = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def peek(offset = 0)
|
|
17
|
+
@items[@pos + offset] || EOF_TOKEN
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns peek unwrapped to a Token; non-Token items collapse to
|
|
21
|
+
# EOF. Lets media-query code do `.type == :colon` against streams
|
|
22
|
+
# that may also hold SimpleBlock / Function items.
|
|
23
|
+
def peek_token
|
|
24
|
+
item = peek
|
|
25
|
+
item.is_a?(Token) ? item : EOF_TOKEN
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def consume
|
|
29
|
+
item = @items[@pos] || EOF_TOKEN
|
|
30
|
+
@pos += 1
|
|
31
|
+
item
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def skip_whitespace
|
|
35
|
+
while (item = peek).is_a?(Token) && item.type == :whitespace
|
|
36
|
+
@pos += 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def eof?
|
|
41
|
+
@pos >= @items.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_error!(message)
|
|
45
|
+
pos = peek.respond_to?(:position) ? peek.position : nil
|
|
46
|
+
raise ParseError.new(message, position: pos)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|