habaki 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/ext/katana/extconf.rb +20 -0
  4. data/ext/katana/rb_katana.c +280 -0
  5. data/ext/katana/rb_katana.h +102 -0
  6. data/ext/katana/rb_katana_array.c +144 -0
  7. data/ext/katana/rb_katana_declaration.c +389 -0
  8. data/ext/katana/rb_katana_rule.c +461 -0
  9. data/ext/katana/rb_katana_selector.c +559 -0
  10. data/ext/katana/src/foundation.c +237 -0
  11. data/ext/katana/src/foundation.h +120 -0
  12. data/ext/katana/src/katana.h +590 -0
  13. data/ext/katana/src/katana.lex.c +4104 -0
  14. data/ext/katana/src/katana.lex.h +592 -0
  15. data/ext/katana/src/katana.tab.c +4422 -0
  16. data/ext/katana/src/katana.tab.h +262 -0
  17. data/ext/katana/src/parser.c +1563 -0
  18. data/ext/katana/src/parser.h +237 -0
  19. data/ext/katana/src/selector.c +659 -0
  20. data/ext/katana/src/selector.h +54 -0
  21. data/ext/katana/src/tokenizer.c +300 -0
  22. data/ext/katana/src/tokenizer.h +41 -0
  23. data/lib/habaki/charset_rule.rb +25 -0
  24. data/lib/habaki/declaration.rb +53 -0
  25. data/lib/habaki/declarations.rb +346 -0
  26. data/lib/habaki/error.rb +43 -0
  27. data/lib/habaki/font_face_rule.rb +24 -0
  28. data/lib/habaki/formal_syntax.rb +464 -0
  29. data/lib/habaki/formatter.rb +99 -0
  30. data/lib/habaki/import_rule.rb +34 -0
  31. data/lib/habaki/media_rule.rb +173 -0
  32. data/lib/habaki/namespace_rule.rb +31 -0
  33. data/lib/habaki/node.rb +52 -0
  34. data/lib/habaki/page_rule.rb +24 -0
  35. data/lib/habaki/qualified_name.rb +29 -0
  36. data/lib/habaki/rule.rb +48 -0
  37. data/lib/habaki/rules.rb +225 -0
  38. data/lib/habaki/selector.rb +98 -0
  39. data/lib/habaki/selectors.rb +49 -0
  40. data/lib/habaki/style_rule.rb +35 -0
  41. data/lib/habaki/stylesheet.rb +158 -0
  42. data/lib/habaki/sub_selector.rb +234 -0
  43. data/lib/habaki/sub_selectors.rb +42 -0
  44. data/lib/habaki/supports_rule.rb +65 -0
  45. data/lib/habaki/value.rb +321 -0
  46. data/lib/habaki/values.rb +86 -0
  47. data/lib/habaki/visitor/element.rb +50 -0
  48. data/lib/habaki/visitor/media.rb +22 -0
  49. data/lib/habaki/visitor/nokogiri_element.rb +56 -0
  50. data/lib/habaki.rb +39 -0
  51. 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
@@ -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