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