habaki 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,346 @@
|
|
1
|
+
module Habaki
|
2
|
+
# partially adapted from css_parser https://github.com/premailer/css_parser/blob/master/lib/css_parser/rule_set.rb
|
3
|
+
module Shorthand
|
4
|
+
BORDER_PROPERTIES = %w[border border-left border-right border-top border-bottom].freeze
|
5
|
+
|
6
|
+
DIMENSIONS = [
|
7
|
+
['margin', %w[margin-top margin-right margin-bottom margin-left]],
|
8
|
+
['padding', %w[padding-top padding-right padding-bottom padding-left]],
|
9
|
+
['border-color', %w[border-top-color border-right-color border-bottom-color border-left-color]],
|
10
|
+
['border-style', %w[border-top-style border-right-style border-bottom-style border-left-style]],
|
11
|
+
['border-width', %w[border-top-width border-right-width border-bottom-width border-left-width]]
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
# Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
|
15
|
+
def expand_shorthand!
|
16
|
+
# border must be expanded before dimensions
|
17
|
+
expand_border_shorthand!
|
18
|
+
expand_dimensions_shorthand!
|
19
|
+
expand_font_shorthand!
|
20
|
+
expand_background_shorthand!
|
21
|
+
expand_list_style_shorthand!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
|
25
|
+
# Additional splitting happens in expand_dimensions_shorthand!
|
26
|
+
def expand_border_shorthand! # :nodoc:
|
27
|
+
BORDER_PROPERTIES.each do |k|
|
28
|
+
expand_shorthand_properties!(k)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
|
33
|
+
# into their constituent parts. Handles margin, padding, border-color, border-style and border-width.
|
34
|
+
def expand_dimensions_shorthand! # :nodoc:
|
35
|
+
DIMENSIONS.each do |property, (top, right, bottom, left)|
|
36
|
+
next unless (declaration = find_by_property(property))
|
37
|
+
|
38
|
+
case declaration.values.length
|
39
|
+
when 1
|
40
|
+
values = declaration.values * 4
|
41
|
+
when 2
|
42
|
+
values = declaration.values * 2
|
43
|
+
when 3
|
44
|
+
values = declaration.values
|
45
|
+
values << declaration.values[1] # left = right
|
46
|
+
when 4
|
47
|
+
values = declaration.values
|
48
|
+
else
|
49
|
+
raise ArgumentError, "Cannot parse #{declaration.values}"
|
50
|
+
end
|
51
|
+
|
52
|
+
replacement = [top, right, bottom, left].zip(values).to_h
|
53
|
+
|
54
|
+
position = find_by_property(property)&.position
|
55
|
+
remove_by_property(property)
|
56
|
+
|
57
|
+
replacement.each do |short_prop, value|
|
58
|
+
decl = add_by_property(short_prop, value)
|
59
|
+
decl.position = position
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
|
65
|
+
# into their constituent parts.
|
66
|
+
#
|
67
|
+
# See http://www.w3.org/TR/CSS21/colors.html#propdef-background
|
68
|
+
def expand_background_shorthand! # :nodoc:
|
69
|
+
expand_shorthand_properties!("background")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
|
73
|
+
# into their constituent parts.
|
74
|
+
def expand_font_shorthand! # :nodoc:
|
75
|
+
expand_shorthand_properties!("font")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
|
79
|
+
# into their constituent parts.
|
80
|
+
#
|
81
|
+
# See http://www.w3.org/TR/CSS21/generate.html#lists
|
82
|
+
def expand_list_style_shorthand! # :nodoc:
|
83
|
+
expand_shorthand_properties!("list-style")
|
84
|
+
end
|
85
|
+
|
86
|
+
def expand_shorthand_properties!(property)
|
87
|
+
return unless (declaration = find_by_property(property))
|
88
|
+
|
89
|
+
tmp_decl = Declaration.new("--shorthand-"+declaration.property, declaration.important)
|
90
|
+
tmp_decl.values = declaration.values
|
91
|
+
matcher = FormalSyntax::Matcher.new(tmp_decl)
|
92
|
+
return unless matcher.match?
|
93
|
+
|
94
|
+
props = {}
|
95
|
+
matcher.matches.each do |match|
|
96
|
+
next if match.value == Operator.new("/") # font-size/line-height
|
97
|
+
props[match.reference] ||= Values.new
|
98
|
+
props[match.reference] << match.value
|
99
|
+
end
|
100
|
+
|
101
|
+
props.each do |k, v|
|
102
|
+
new_decl = add_by_property(k, Values.new([v].flatten))
|
103
|
+
new_decl.position = declaration.position
|
104
|
+
end
|
105
|
+
|
106
|
+
remove_by_property(property)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
|
110
|
+
def create_shorthand!
|
111
|
+
create_background_shorthand!
|
112
|
+
create_dimensions_shorthand!
|
113
|
+
# border must be shortened after dimensions
|
114
|
+
create_border_shorthand!
|
115
|
+
create_font_shorthand!
|
116
|
+
create_list_style_shorthand!
|
117
|
+
end
|
118
|
+
|
119
|
+
# Combine border-color, border-style and border-width into border
|
120
|
+
# Should be run after create_dimensions_shorthand!
|
121
|
+
def create_border_shorthand! # :nodoc:
|
122
|
+
border_style_properties = %w[border-width border-style border-color]
|
123
|
+
|
124
|
+
border_style_properties.each do |prop|
|
125
|
+
create_shorthand_properties! prop unless find_by_property(prop)
|
126
|
+
end
|
127
|
+
|
128
|
+
create_shorthand_properties! 'border' if border_style_properties.map{|prop| find_by_property(prop)}.all?
|
129
|
+
end
|
130
|
+
|
131
|
+
# Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
|
132
|
+
# converts them into a shorthand CSS <tt>background</tt> property.
|
133
|
+
#
|
134
|
+
# Leaves properties declared !important alone.
|
135
|
+
def create_background_shorthand! # :nodoc:
|
136
|
+
# When we have a background-size property we must separate it and distinguish it from
|
137
|
+
# background-position by preceding it with a backslash. In this case we also need to
|
138
|
+
# have a background-position property, so we set it if it's missing.
|
139
|
+
# http://www.w3schools.com/cssref/css3_pr_background.asp
|
140
|
+
if (declaration = find_by_property('background-size')) && !declaration.important
|
141
|
+
add_by_property('background-position', Values.new([Percentage.new(0), Percentage.new(0)]))
|
142
|
+
end
|
143
|
+
|
144
|
+
create_shorthand_properties! 'background'
|
145
|
+
end
|
146
|
+
|
147
|
+
# Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
|
148
|
+
# tries to convert them into a shorthand CSS <tt>font</tt> property. All
|
149
|
+
# font properties must be present in order to create a shorthand declaration.
|
150
|
+
def create_font_shorthand! # :nodoc:
|
151
|
+
create_shorthand_properties!("font", true)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
|
155
|
+
# converts them into a shorthand CSS <tt>list-style</tt> property.
|
156
|
+
#
|
157
|
+
# Leaves properties declared !important alone.
|
158
|
+
def create_list_style_shorthand! # :nodoc:
|
159
|
+
create_shorthand_properties! 'list-style'
|
160
|
+
end
|
161
|
+
|
162
|
+
# Combine several properties into a shorthand one
|
163
|
+
def create_shorthand_properties!(shorthand_property, need_all = false)
|
164
|
+
properties_to_delete = []
|
165
|
+
|
166
|
+
new_values = []
|
167
|
+
FormalSyntax::Tree.tree.property("--shorthand-"+shorthand_property).traverse do |node|
|
168
|
+
case node.type
|
169
|
+
when :ref
|
170
|
+
decl = find_by_property(node.value)
|
171
|
+
if decl
|
172
|
+
properties_to_delete << decl.property
|
173
|
+
new_values += decl.values
|
174
|
+
else
|
175
|
+
return if need_all
|
176
|
+
end
|
177
|
+
when :token
|
178
|
+
# only if next node property is present (line-height, background-size)
|
179
|
+
new_values << Operator.new(node.value) if node.parent&.children&.last&.value && find_by_property(node.parent.children.last.value)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
return if new_values.empty?
|
184
|
+
|
185
|
+
first_position = find_by_property(properties_to_delete.first)&.position
|
186
|
+
properties_to_delete.each do |property|
|
187
|
+
remove_by_property(property)
|
188
|
+
end
|
189
|
+
|
190
|
+
new_decl = add_by_property(shorthand_property, new_values)
|
191
|
+
new_decl.position = first_position
|
192
|
+
end
|
193
|
+
|
194
|
+
# Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
|
195
|
+
# and converts them into shorthand CSS properties.
|
196
|
+
def create_dimensions_shorthand! # :nodoc:
|
197
|
+
return if length < 4
|
198
|
+
|
199
|
+
DIMENSIONS.each do |property, dimensions|
|
200
|
+
values = [:top, :right, :bottom, :left].each_with_index.with_object({}) do |(side, index), result|
|
201
|
+
next unless (declaration = find_by_property(dimensions[index]))
|
202
|
+
result[side] = declaration.value
|
203
|
+
end
|
204
|
+
|
205
|
+
# All four dimensions must be present
|
206
|
+
next if values.length != dimensions.length
|
207
|
+
|
208
|
+
new_values = Values.new(values.values_at(*compute_dimensions_shorthand(values)))
|
209
|
+
unless new_values.empty?
|
210
|
+
first_position = find_by_property(dimensions.first)&.position
|
211
|
+
decl = add_by_property(property, new_values)
|
212
|
+
decl.position = first_position
|
213
|
+
end
|
214
|
+
|
215
|
+
# Delete the longhand values
|
216
|
+
dimensions.each do |prop|
|
217
|
+
remove_by_property(prop)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def compute_dimensions_shorthand(values)
|
223
|
+
# All four sides are equal, returning single value
|
224
|
+
return [:top] if values.values.uniq.count == 1
|
225
|
+
|
226
|
+
# `/* top | right | bottom | left */`
|
227
|
+
return [:top, :right, :bottom, :left] if values[:left] != values[:right]
|
228
|
+
|
229
|
+
# Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
|
230
|
+
return [:top, :left] if values[:top] == values[:bottom]
|
231
|
+
|
232
|
+
[:top, :left, :bottom]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Array of {Declaration}
|
237
|
+
class Declarations < NodeArray
|
238
|
+
include Shorthand
|
239
|
+
|
240
|
+
def initialize(*args)
|
241
|
+
super(*args)
|
242
|
+
@hash = {}
|
243
|
+
end
|
244
|
+
|
245
|
+
# Parse inline declarations
|
246
|
+
# @param [String] data
|
247
|
+
# @return [Declarations]
|
248
|
+
def self.parse(data)
|
249
|
+
decls = self.new
|
250
|
+
decls.parse!(data)
|
251
|
+
decls
|
252
|
+
end
|
253
|
+
|
254
|
+
def push_declaration(decl)
|
255
|
+
@hash[decl.property] = decl
|
256
|
+
push decl
|
257
|
+
end
|
258
|
+
|
259
|
+
# Parse inline declarations and append to current declarations
|
260
|
+
# @param [String] data
|
261
|
+
# @return [void]
|
262
|
+
def parse!(data)
|
263
|
+
return unless data
|
264
|
+
|
265
|
+
out = Katana.parse_inline(data)
|
266
|
+
if out.declarations
|
267
|
+
read_from_katana(out.declarations)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Find declaration with property
|
272
|
+
# @param [String] property
|
273
|
+
# @return [Declaration]
|
274
|
+
def find_by_property(property)
|
275
|
+
@hash[property]
|
276
|
+
end
|
277
|
+
|
278
|
+
# Remove declaration with property
|
279
|
+
# @param [String] property
|
280
|
+
# @return [void]
|
281
|
+
def remove_by_property(property)
|
282
|
+
@hash.delete(property)
|
283
|
+
reject! { |decl| decl.property == property }
|
284
|
+
end
|
285
|
+
|
286
|
+
# Add declaration
|
287
|
+
# @param [String] property
|
288
|
+
# @param [Value, Values, Array<Value>] value
|
289
|
+
# @param [Boolean] important
|
290
|
+
# @return [Declaration]
|
291
|
+
def add_by_property(property, value = [], important = false)
|
292
|
+
decl = Habaki::Declaration.new(property, important)
|
293
|
+
decl.values = Values.new([value].flatten)
|
294
|
+
push_declaration decl
|
295
|
+
decl
|
296
|
+
end
|
297
|
+
|
298
|
+
# Add declaration or replace if more important
|
299
|
+
# @param [Declaration] decl
|
300
|
+
# @return [Declaration]
|
301
|
+
def replace_important(decl)
|
302
|
+
previous_decl = find_by_property(decl.property)
|
303
|
+
if previous_decl
|
304
|
+
if decl.important || !previous_decl.important
|
305
|
+
#remove_by_property(decl.property)
|
306
|
+
delete(previous_decl)
|
307
|
+
push_declaration decl
|
308
|
+
end
|
309
|
+
else
|
310
|
+
push_declaration decl
|
311
|
+
end
|
312
|
+
decl
|
313
|
+
end
|
314
|
+
|
315
|
+
# at position or shortcut for find_by_property
|
316
|
+
# @param [Integer, String] prop index or property name
|
317
|
+
# @return [Declaration, nil]
|
318
|
+
def [](prop)
|
319
|
+
case prop
|
320
|
+
when Integer
|
321
|
+
at(prop)
|
322
|
+
when ::String
|
323
|
+
find_by_property(prop)
|
324
|
+
else
|
325
|
+
raise TypeError, "invalid type #{prop.class}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# @param [Formatter::Base] format
|
330
|
+
# @return [String]
|
331
|
+
def string(format = Formatter::Base.new)
|
332
|
+
map do |decl|
|
333
|
+
decl.string(format) + ";"
|
334
|
+
end.join(format.declarations_join)
|
335
|
+
end
|
336
|
+
|
337
|
+
# @api private
|
338
|
+
# @param [Katana::Array<Katana::Declaration>] decls
|
339
|
+
# @return [void]
|
340
|
+
def read_from_katana(decls)
|
341
|
+
decls.each do |decl|
|
342
|
+
push_declaration Declaration.read_from_katana(decl)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
data/lib/habaki/error.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Habaki
|
2
|
+
class SourcePosition
|
3
|
+
# @return [Integer]
|
4
|
+
attr_accessor :line
|
5
|
+
# @return [Integer]
|
6
|
+
attr_accessor :column
|
7
|
+
|
8
|
+
def initialize(line = 0, column = 0)
|
9
|
+
@line = line
|
10
|
+
@column = column
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# syntax error
|
15
|
+
class Error < Node
|
16
|
+
# @return [SourcePosition]
|
17
|
+
attr_accessor :position
|
18
|
+
# @return [String]
|
19
|
+
attr_accessor :message
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@position = SourcePosition.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Integer]
|
26
|
+
def line
|
27
|
+
@position.line
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Integer]
|
31
|
+
def column
|
32
|
+
@position.column
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
# @param [Katana::Error] err
|
37
|
+
# @return [void]
|
38
|
+
def read_from_katana(err)
|
39
|
+
@position = SourcePosition.new(err.first_line, err.first_column)
|
40
|
+
@message = err.message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Habaki
|
2
|
+
# Rule for @font-face
|
3
|
+
class FontFaceRule < Rule
|
4
|
+
# @return [Declarations]
|
5
|
+
attr_accessor :declarations
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@declarations = Declarations.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param [Formatter::Base] format
|
12
|
+
# @return [String]
|
13
|
+
def string(format = Formatter::Base.new)
|
14
|
+
"@font-face {#{@declarations.string(format)}}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
# @param [Katana::FontFaceRule] rule
|
19
|
+
# @return [void]
|
20
|
+
def read_from_katana(rule)
|
21
|
+
@declarations = Declarations.read_from_katana(rule.declarations)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|