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.
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