habaki 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 +7 -0
- data/Gemfile +3 -0
- data/ext/katana/extconf.rb +20 -0
- data/ext/katana/rb_katana.c +280 -0
- data/ext/katana/rb_katana.h +102 -0
- data/ext/katana/rb_katana_array.c +144 -0
- data/ext/katana/rb_katana_declaration.c +389 -0
- data/ext/katana/rb_katana_rule.c +461 -0
- data/ext/katana/rb_katana_selector.c +559 -0
- data/ext/katana/src/foundation.c +237 -0
- data/ext/katana/src/foundation.h +120 -0
- data/ext/katana/src/katana.h +590 -0
- data/ext/katana/src/katana.lex.c +4104 -0
- data/ext/katana/src/katana.lex.h +592 -0
- data/ext/katana/src/katana.tab.c +4422 -0
- data/ext/katana/src/katana.tab.h +262 -0
- data/ext/katana/src/parser.c +1563 -0
- data/ext/katana/src/parser.h +237 -0
- data/ext/katana/src/selector.c +659 -0
- data/ext/katana/src/selector.h +54 -0
- data/ext/katana/src/tokenizer.c +300 -0
- data/ext/katana/src/tokenizer.h +41 -0
- data/lib/habaki/charset_rule.rb +25 -0
- data/lib/habaki/declaration.rb +53 -0
- data/lib/habaki/declarations.rb +346 -0
- data/lib/habaki/error.rb +43 -0
- data/lib/habaki/font_face_rule.rb +24 -0
- data/lib/habaki/formal_syntax.rb +464 -0
- data/lib/habaki/formatter.rb +99 -0
- data/lib/habaki/import_rule.rb +34 -0
- data/lib/habaki/media_rule.rb +173 -0
- data/lib/habaki/namespace_rule.rb +31 -0
- data/lib/habaki/node.rb +52 -0
- data/lib/habaki/page_rule.rb +24 -0
- data/lib/habaki/qualified_name.rb +29 -0
- data/lib/habaki/rule.rb +48 -0
- data/lib/habaki/rules.rb +225 -0
- data/lib/habaki/selector.rb +98 -0
- data/lib/habaki/selectors.rb +49 -0
- data/lib/habaki/style_rule.rb +35 -0
- data/lib/habaki/stylesheet.rb +158 -0
- data/lib/habaki/sub_selector.rb +234 -0
- data/lib/habaki/sub_selectors.rb +42 -0
- data/lib/habaki/supports_rule.rb +65 -0
- data/lib/habaki/value.rb +321 -0
- data/lib/habaki/values.rb +86 -0
- data/lib/habaki/visitor/element.rb +50 -0
- data/lib/habaki/visitor/media.rb +22 -0
- data/lib/habaki/visitor/nokogiri_element.rb +56 -0
- data/lib/habaki.rb +39 -0
- metadata +190 -0
data/lib/habaki/value.rb
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
module Habaki
|
2
|
+
# abstract value type
|
3
|
+
class Value < Node
|
4
|
+
# @return [::String, Float]
|
5
|
+
attr_accessor :data
|
6
|
+
|
7
|
+
def initialize(data = nil)
|
8
|
+
@data = data
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
to_s == other.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def eql?(other)
|
16
|
+
to_s.eql?(other.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def hash
|
20
|
+
to_s.hash
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [Formatter::Base] format
|
24
|
+
# @return [String]
|
25
|
+
def string(format = Formatter::Base.new)
|
26
|
+
"#{@data}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
# @param [Katana::Value] val
|
31
|
+
# @return [void]
|
32
|
+
def read_from_katana(val)
|
33
|
+
@data = val.value
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def data_i_or_f
|
39
|
+
return 0 unless @data
|
40
|
+
@data.round == @data ? @data.round : @data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# <length> value type in px, pt etc
|
45
|
+
class Length < Value
|
46
|
+
# @return [Symbol]
|
47
|
+
attr_accessor :unit
|
48
|
+
|
49
|
+
def initialize(data = nil, unit = nil)
|
50
|
+
@data = data
|
51
|
+
@unit = unit
|
52
|
+
end
|
53
|
+
|
54
|
+
# is dimension absolute ?
|
55
|
+
# @return [Boolean]
|
56
|
+
def absolute?
|
57
|
+
[:px, :cm, :mm, :in, :pt, :pc].include?(@unit)
|
58
|
+
end
|
59
|
+
|
60
|
+
# is dimension relative ?
|
61
|
+
# @return [Boolean]
|
62
|
+
def relative?
|
63
|
+
[:em, :ex, :ch, :rem, :vw, :vh, :vmin, :vmax].include?(@unit)
|
64
|
+
end
|
65
|
+
|
66
|
+
# absolute value to pixel
|
67
|
+
# @return [Float, nil]
|
68
|
+
def to_px(ppi = 96)
|
69
|
+
case @unit
|
70
|
+
when :px
|
71
|
+
@data
|
72
|
+
when :cm
|
73
|
+
(@data / 2.54) * ppi
|
74
|
+
when :mm
|
75
|
+
((@data / 10.0) / 2.54) * ppi
|
76
|
+
when :in
|
77
|
+
@data * ppi
|
78
|
+
when :pt
|
79
|
+
@data / (72.0 / ppi)
|
80
|
+
when :pc
|
81
|
+
(@data * 12.0) / (72.0 / ppi)
|
82
|
+
else
|
83
|
+
# relative
|
84
|
+
raise TypeError, "cannot convert relative #{to_s} to px"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# absolute value to em
|
89
|
+
# @return [Float, nil]
|
90
|
+
def to_em(default_px = 16.0)
|
91
|
+
return 0.0 if default_px == 0.0
|
92
|
+
if absolute?
|
93
|
+
to_px / default_px
|
94
|
+
else
|
95
|
+
if @unit == :em
|
96
|
+
@data
|
97
|
+
else
|
98
|
+
raise TypeError, "cannot convert #{to_s} to em"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Float]
|
104
|
+
def to_f
|
105
|
+
@data.is_a?(Float) ? @data : 0.0
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Length]
|
109
|
+
def +(other)
|
110
|
+
case other
|
111
|
+
when Length
|
112
|
+
raise ArgumentError, "cannot addition with different units" unless @unit == other.unit
|
113
|
+
Length.new(@data + other.data, @unit)
|
114
|
+
else
|
115
|
+
raise ArgumentError, "cannot addition #{self.class} with #{other.class}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Length]
|
120
|
+
def -(other)
|
121
|
+
case other
|
122
|
+
when Length
|
123
|
+
raise ArgumentError, "cannot substract with different units" unless @unit == other.unit
|
124
|
+
Length.new(@data - other.data, @unit)
|
125
|
+
else
|
126
|
+
raise ArgumentError, "cannot substract #{self.class} with #{other.class}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Length]
|
131
|
+
def *(other)
|
132
|
+
case other
|
133
|
+
when Integer, Float
|
134
|
+
Length.new((@data * other).round(3), @unit)
|
135
|
+
when Percentage
|
136
|
+
Length.new((@data * other.data / 100.0).round(3), @unit)
|
137
|
+
else
|
138
|
+
raise ArgumentError, "cannot multiply #{self.class} with #{other.class}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [Length]
|
143
|
+
def /(other)
|
144
|
+
case other
|
145
|
+
when Integer, Float
|
146
|
+
Length.new((@data / other).round(3), @unit)
|
147
|
+
else
|
148
|
+
raise ArgumentError, "cannot divide #{self.class} with #{other.class}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
include Comparable
|
153
|
+
|
154
|
+
# @return [Integer]
|
155
|
+
def <=>(other)
|
156
|
+
raise ArgumentError, "cannot compare #{self.class} with #{other.class}" unless other.is_a?(Length)
|
157
|
+
if @unit == other.unit
|
158
|
+
@data <=> other.data
|
159
|
+
elsif absolute? && other.absolute?
|
160
|
+
to_px <=> other.to_px
|
161
|
+
else
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return [Boolean]
|
167
|
+
def ==(other)
|
168
|
+
return false unless other.is_a?(Length)
|
169
|
+
if @unit == other.unit
|
170
|
+
@data == other.data
|
171
|
+
elsif absolute? && other.absolute?
|
172
|
+
to_px == other.to_px
|
173
|
+
else
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# @param [Formatter::Base] format
|
179
|
+
# @return [String]
|
180
|
+
def string(format = Formatter::Base.new)
|
181
|
+
@unit ? "#{data_i_or_f}#{@unit}" : @data
|
182
|
+
end
|
183
|
+
|
184
|
+
# @api private
|
185
|
+
# @return [void]
|
186
|
+
def read_from_katana(val)
|
187
|
+
@data = val.value
|
188
|
+
@unit = val.unit
|
189
|
+
@unit = nil if @unit == :dimension
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# <angle> value type in deg, rad
|
194
|
+
class Angle < Value
|
195
|
+
# @return [Symbol]
|
196
|
+
attr_accessor :unit
|
197
|
+
|
198
|
+
def initialize(data = nil, unit = nil)
|
199
|
+
@data = data
|
200
|
+
@unit = unit
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param [Formatter::Base] format
|
204
|
+
# @return [String]
|
205
|
+
def string(format = Formatter::Base.new)
|
206
|
+
@unit ? "#{data_i_or_f}#{@unit}" : @data
|
207
|
+
end
|
208
|
+
|
209
|
+
# @api private
|
210
|
+
# @return [void]
|
211
|
+
def read_from_katana(val)
|
212
|
+
@data = val.value
|
213
|
+
@unit = val.unit
|
214
|
+
@unit = nil if @unit == :dimension
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# <percentage> value type
|
219
|
+
class Percentage < Value
|
220
|
+
# @return [Float]
|
221
|
+
def to_f
|
222
|
+
@data
|
223
|
+
end
|
224
|
+
|
225
|
+
# @param [Formatter::Base] format
|
226
|
+
# @return [String]
|
227
|
+
def string(format = Formatter::Base.new)
|
228
|
+
"#{data_i_or_f}%"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# <number> or <integer> value type
|
233
|
+
class Number < Value
|
234
|
+
# @return [Float]
|
235
|
+
def to_f
|
236
|
+
@data
|
237
|
+
end
|
238
|
+
|
239
|
+
# @return [Integer]
|
240
|
+
def to_i
|
241
|
+
@data.to_i
|
242
|
+
end
|
243
|
+
|
244
|
+
# @param [Formatter::Base] format
|
245
|
+
# @return [String]
|
246
|
+
def string(format = Formatter::Base.new)
|
247
|
+
"#{data_i_or_f}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# <ident> value type
|
252
|
+
class Ident < Value
|
253
|
+
end
|
254
|
+
|
255
|
+
# <string> value type
|
256
|
+
class String < Value
|
257
|
+
# @param [Formatter::Base] format
|
258
|
+
# @return [String]
|
259
|
+
def string(format = Formatter::Base.new)
|
260
|
+
"#{format.quote}#{@data}#{format.quote}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# operator , or /
|
265
|
+
class Operator < Value
|
266
|
+
end
|
267
|
+
|
268
|
+
# <hex-color> value type
|
269
|
+
class HexColor < Value
|
270
|
+
# @param [Formatter::Base] format
|
271
|
+
# @return [String]
|
272
|
+
def string(format = Formatter::Base.new)
|
273
|
+
"##{@data}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
class Function < Value
|
278
|
+
# @return [Values]
|
279
|
+
attr_accessor :args
|
280
|
+
|
281
|
+
# @param [Formatter::Base] format
|
282
|
+
# @return [String]
|
283
|
+
def string(format = Formatter::Base.new)
|
284
|
+
"#{@data}(#{@args.string(format)})"
|
285
|
+
end
|
286
|
+
|
287
|
+
# @api private
|
288
|
+
# @param [Katana::Value] val
|
289
|
+
# @return [void]
|
290
|
+
def read_from_katana(val)
|
291
|
+
@data = val.value.name.sub("(", "")
|
292
|
+
@args = Values.new
|
293
|
+
if val.value.args
|
294
|
+
@args = Values.read_from_katana(val.value.args)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# <url>/<uri> value type
|
300
|
+
class Url < Value
|
301
|
+
# is url of data type ?
|
302
|
+
# @return [Boolean]
|
303
|
+
def data_uri?
|
304
|
+
@data.start_with?("data:")
|
305
|
+
end
|
306
|
+
|
307
|
+
# @return [String]
|
308
|
+
def uri
|
309
|
+
@data
|
310
|
+
end
|
311
|
+
|
312
|
+
# @param [Formatter::Base] format
|
313
|
+
# @return [String]
|
314
|
+
def string(format = Formatter::Base.new)
|
315
|
+
"url(#{@data.include?(" ") ? "#{format.quote}#{@data}#{format.quote}" : @data})"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
class UnicodeRange < Value
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Habaki
|
2
|
+
# Array of {Values}
|
3
|
+
class Values < NodeArray
|
4
|
+
# traverse values with optional class type
|
5
|
+
# @param [Class] klass
|
6
|
+
# @return [void]
|
7
|
+
def each_value(klass = nil, &block)
|
8
|
+
each do |value|
|
9
|
+
block.call value if !klass || value.is_a?(klass)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# remove value taking care of operator in list
|
14
|
+
# @param [Value] value
|
15
|
+
# @return [void]
|
16
|
+
def remove_value(value)
|
17
|
+
idx = index(value)
|
18
|
+
prev_val = at(idx - 1)
|
19
|
+
next_val = at(idx + 1)
|
20
|
+
if prev_val&.is_a?(Operator)
|
21
|
+
delete_at(idx)
|
22
|
+
delete_at(idx - 1)
|
23
|
+
elsif next_val&.is_a?(Operator)
|
24
|
+
delete_at(idx + 1)
|
25
|
+
delete_at(idx)
|
26
|
+
else
|
27
|
+
delete_at(idx)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [Formatter::Base] format
|
32
|
+
# @return [String]
|
33
|
+
def string(format = Formatter::Base.new)
|
34
|
+
str = ""
|
35
|
+
each_cons(2) do |val|
|
36
|
+
str += val[0].string(format)
|
37
|
+
str += val[1].is_a?(Operator) || val[0].is_a?(Operator) ? "" : " "
|
38
|
+
end
|
39
|
+
str += last.string(format) if last
|
40
|
+
str
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
# @param [Katana::Array<Katana::Value>] vals
|
45
|
+
# @return [void]
|
46
|
+
def read_from_katana(vals)
|
47
|
+
vals.each do |val|
|
48
|
+
push read_from_unit(val)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def read_from_unit(val)
|
55
|
+
case val.unit
|
56
|
+
when :px, :pt, :ex, :em, :mm, :cm, :in, :pc, :rem, :ch, :turn,
|
57
|
+
:vw, :vh, :vmin, :vmax, :dppx, :dpi, :dpcm, :fr, :dimension
|
58
|
+
Length.read_from_katana(val)
|
59
|
+
when :deg, :rad, :grad
|
60
|
+
Angle.read_from_katana(val)
|
61
|
+
when :percentage
|
62
|
+
Percentage.read_from_katana(val)
|
63
|
+
when :number
|
64
|
+
Number.read_from_katana(val)
|
65
|
+
when :ident
|
66
|
+
Ident.read_from_katana(val)
|
67
|
+
when :string
|
68
|
+
String.read_from_katana(val)
|
69
|
+
when :parser_hexcolor
|
70
|
+
HexColor.read_from_katana(val)
|
71
|
+
when :parser_function
|
72
|
+
Function.read_from_katana(val)
|
73
|
+
when :parser_operator
|
74
|
+
Operator.read_from_katana(val)
|
75
|
+
when :uri
|
76
|
+
Url.read_from_katana(val)
|
77
|
+
when :unicode_range
|
78
|
+
UnicodeRange.read_from_katana(val)
|
79
|
+
else
|
80
|
+
# fallback
|
81
|
+
Value.read_from_katana(val)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Habaki
|
2
|
+
module Visitor
|
3
|
+
# @abstract CSS selector element visitor
|
4
|
+
class Element
|
5
|
+
# element tag name
|
6
|
+
# @return [String]
|
7
|
+
def tag_name
|
8
|
+
end
|
9
|
+
|
10
|
+
# element class name
|
11
|
+
# @return [String]
|
12
|
+
def class_name
|
13
|
+
end
|
14
|
+
|
15
|
+
# element id name
|
16
|
+
# @return [String]
|
17
|
+
def id_name
|
18
|
+
end
|
19
|
+
|
20
|
+
# element attribute
|
21
|
+
# @param [String] key
|
22
|
+
# @return [String]
|
23
|
+
def attr(key) end
|
24
|
+
|
25
|
+
# inner text
|
26
|
+
# @return [String]
|
27
|
+
def text
|
28
|
+
end
|
29
|
+
|
30
|
+
# element parent
|
31
|
+
# @return [Visitor::Element]
|
32
|
+
def parent
|
33
|
+
end
|
34
|
+
|
35
|
+
# element previous
|
36
|
+
# @return [Visitor::Element]
|
37
|
+
def previous
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Array<Visitor::Element>]
|
41
|
+
def children
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
|
45
|
+
# traverse elements
|
46
|
+
def traverse &block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Habaki
|
2
|
+
module Visitor
|
3
|
+
# media query visitor
|
4
|
+
class Media
|
5
|
+
# eg "print", "screen"
|
6
|
+
# @return [String]
|
7
|
+
attr_accessor :type
|
8
|
+
# width, in pixels
|
9
|
+
# @return [Integer]
|
10
|
+
attr_accessor :width
|
11
|
+
# height, in pixels
|
12
|
+
# @return [Integer]
|
13
|
+
attr_accessor :height
|
14
|
+
|
15
|
+
def initialize(type = "all", width = nil, height = nil)
|
16
|
+
@type = type
|
17
|
+
@width = width
|
18
|
+
@height = height
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Habaki
|
2
|
+
module Visitor
|
3
|
+
class NokogiriElement < Element
|
4
|
+
attr_accessor :element
|
5
|
+
|
6
|
+
def initialize(element)
|
7
|
+
@element = element
|
8
|
+
end
|
9
|
+
|
10
|
+
def tag_name
|
11
|
+
@element.name
|
12
|
+
end
|
13
|
+
|
14
|
+
def class_name
|
15
|
+
@element["class"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def id_name
|
19
|
+
@element["id"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def attr(key)
|
23
|
+
@element[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def text
|
27
|
+
@element.text
|
28
|
+
end
|
29
|
+
|
30
|
+
def parent
|
31
|
+
Visitor::NokogiriElement.new(@element.parent) if @element.respond_to?(:parent)
|
32
|
+
end
|
33
|
+
|
34
|
+
def previous
|
35
|
+
Visitor::NokogiriElement.new(@element.previous_element) if @element.respond_to?(:previous_element) && @element.previous_element
|
36
|
+
end
|
37
|
+
|
38
|
+
def children
|
39
|
+
@element.children.map do |child|
|
40
|
+
child.is_a?(Nokogiri::XML::Element) ? Visitor::NokogiriElement.new(child) : nil
|
41
|
+
end.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
def traverse &block
|
45
|
+
@element.traverse do |el|
|
46
|
+
next unless el.is_a?(Nokogiri::XML::Element)
|
47
|
+
block.call Visitor::NokogiriElement.new(el)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
@element == other.element
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/habaki.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'katana/katana'
|
3
|
+
|
4
|
+
require 'habaki/formal_syntax'
|
5
|
+
|
6
|
+
require 'habaki/formatter'
|
7
|
+
require 'habaki/node'
|
8
|
+
|
9
|
+
require 'habaki/value'
|
10
|
+
require 'habaki/values'
|
11
|
+
|
12
|
+
require 'habaki/declaration'
|
13
|
+
require 'habaki/declarations'
|
14
|
+
|
15
|
+
require 'habaki/qualified_name'
|
16
|
+
|
17
|
+
require 'habaki/visitor/element'
|
18
|
+
require 'habaki/visitor/nokogiri_element'
|
19
|
+
|
20
|
+
require 'habaki/sub_selector'
|
21
|
+
require 'habaki/sub_selectors'
|
22
|
+
require 'habaki/selector'
|
23
|
+
require 'habaki/selectors'
|
24
|
+
|
25
|
+
require 'habaki/visitor/media'
|
26
|
+
|
27
|
+
require 'habaki/rule'
|
28
|
+
require 'habaki/charset_rule'
|
29
|
+
require 'habaki/namespace_rule'
|
30
|
+
require 'habaki/import_rule'
|
31
|
+
require 'habaki/font_face_rule'
|
32
|
+
require 'habaki/page_rule'
|
33
|
+
require 'habaki/style_rule'
|
34
|
+
require 'habaki/media_rule'
|
35
|
+
require 'habaki/supports_rule'
|
36
|
+
require 'habaki/rules'
|
37
|
+
|
38
|
+
require 'habaki/error'
|
39
|
+
require 'habaki/stylesheet'
|