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,49 @@
1
+ module Habaki
2
+ # Array of {Selectors}
3
+ class Selectors < NodeArray
4
+ # parse selectors from string
5
+ # @param [String] data
6
+ # @return [Selectors]
7
+ def self.parse(data)
8
+ sels = self.new
9
+ sels.parse!(data)
10
+ sels
11
+ end
12
+
13
+ # parse selectors from string and append to current selectors
14
+ # @param [String] data
15
+ # @return [void]
16
+ def parse!(data)
17
+ return unless data
18
+
19
+ out = Katana.parse_selectors(data)
20
+ if out.selectors
21
+ read_from_katana(out.selectors)
22
+ end
23
+ end
24
+
25
+ # does one of theses selectors match {Visitor::Element} ?
26
+ # @param [Visitor::Element] element
27
+ # @return [Boolean]
28
+ def element_match?(element)
29
+ each do |selector|
30
+ return true if selector.element_match?(element)
31
+ end
32
+ false
33
+ end
34
+
35
+ # @param [Formatter::Base] format
36
+ # @return [String]
37
+ def string(format = Formatter::Base.new)
38
+ string_join(format, ",")
39
+ end
40
+
41
+ # @api private
42
+ # @param [Katana::Array<Katana::Selector>] sels
43
+ def read_from_katana(sels)
44
+ sels.each do |sel|
45
+ push Selector.read_from_katana(sel)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ module Habaki
2
+ # CSS style rule selectors + declarations
3
+ class StyleRule < Rule
4
+ # @return [Selectors]
5
+ attr_accessor :selectors
6
+ # @return [Declarations]
7
+ attr_accessor :declarations
8
+
9
+ def initialize
10
+ @selectors = Selectors.new
11
+ @declarations = Declarations.new
12
+ end
13
+
14
+ # does rule match {Visitor::Element} ?
15
+ # @param [Visitor::Element] element
16
+ # @return [Boolean]
17
+ def element_match?(element)
18
+ selectors.element_match?(element)
19
+ end
20
+
21
+ # @param [Formatter::Base] format
22
+ # @return [String]
23
+ def string(format = Formatter::Base.new)
24
+ "#{@selectors.string(format)} {#{format.declarations_prefix}#{@declarations.string(format+1)}#{format.declarations_suffix}#{format.rules_prefix}}"
25
+ end
26
+
27
+ # @api private
28
+ # @param [Katana::StyleRule] rule
29
+ # @return [void]
30
+ def read_from_katana(rule)
31
+ @selectors = Selectors.read_from_katana(rule.selectors)
32
+ @declarations = Declarations.read_from_katana(rule.declarations)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,158 @@
1
+ module Habaki
2
+ # main structure
3
+ class Stylesheet < Node
4
+ include SelectorMatcher
5
+
6
+ # @return [Rules]
7
+ attr_accessor :rules
8
+ # @return [Array<Error>]
9
+ attr_accessor :errors
10
+
11
+ def initialize
12
+ @rules = Rules.new
13
+ @errors = []
14
+ end
15
+
16
+ # traverse rules
17
+ # @param [Visitor::Media, String, NilClass] media
18
+ # @yieldparam [Rules] rules
19
+ # @return [void]
20
+ def each_rules(media = nil, &block)
21
+ block.call @rules
22
+ @rules.each do |rule|
23
+ next unless rule.rules
24
+ next if rule.is_a?(MediaRule) && !rule.media_match?(media)
25
+ block.call rule.rules
26
+ end
27
+ end
28
+
29
+ # traverse all rules (including all media & supports)
30
+ # @param [Visitor::Media, String, NilClass] media
31
+ # @yieldparam [Rule] rule
32
+ # @return [void]
33
+ def each_rule(media = nil, &block)
34
+ each_rules(media) do |rules|
35
+ rules.each do |rule|
36
+ block.call rule
37
+ end
38
+ end
39
+ end
40
+
41
+ # does selector exists ?
42
+ # @param [String] selector_str
43
+ # @param [Visitor::Media, String, NilClass] media
44
+ # @return [Boolean]
45
+ def has_selector?(selector_str, media = nil)
46
+ each_rule do |rule|
47
+ rule.each_selector do |selector|
48
+ return true if selector_str == selector.to_s
49
+ end
50
+ end
51
+ false
52
+ end
53
+
54
+ # find rule from selector str
55
+ # @param [String] selector_str
56
+ # @param [Visitor::Media, String, NilClass] media
57
+ # @return [Array<Rule>]
58
+ def find_by_selector(selector_str, media = nil)
59
+ results = []
60
+ each_rule(media) do |rule|
61
+ rule.each_selector do |selector|
62
+ results << rule if selector_str == selector.to_s
63
+ end
64
+ end
65
+ results
66
+ end
67
+
68
+ # find declarations from selector str
69
+ # @param [String] selector_str
70
+ # @param [Visitor::Media, String, NilClass] media
71
+ # @return [Array<Declarations>]
72
+ def find_declarations_by_selector(selector_str, media = nil)
73
+ results = []
74
+ each_rule(media) do |rule|
75
+ next unless rule.selectors
76
+ results << rule.declarations if rule.selectors.map(&:to_s).include?(selector_str)
77
+ end
78
+ results
79
+ end
80
+
81
+ # remove rules with no declaration
82
+ def compact!
83
+ @rules.reject! { |rule| rule.declarations&.empty? || false }
84
+ @rules.each do |rule|
85
+ if rule.rules
86
+ rule.rules.reject! { |emb_rule| emb_rule.declarations&.empty? || false }
87
+ end
88
+ end
89
+ @rules.reject! { |rule| rule.rules&.empty? || false }
90
+ end
91
+
92
+ # instanciate and parse from data
93
+ # @param [String] data
94
+ # @return [Stylesheet]
95
+ def self.parse(data)
96
+ stylesheet = self.new
97
+ stylesheet.parse!(data)
98
+ stylesheet
99
+ end
100
+
101
+ # instanciate and parse from file
102
+ # @param [String] filename
103
+ # @return [Stylesheet]
104
+ def self.parse_file(filename)
105
+ parse(File.read(filename))
106
+ end
107
+
108
+ # parse from data and append to current stylesheet
109
+ # @param [String] data
110
+ # @return [void]
111
+ def parse!(data)
112
+ return unless data
113
+
114
+ read_from_katana(Katana.parse(data))
115
+ end
116
+
117
+ # parse from file and append to current stylesheet
118
+ # @param [String] filename
119
+ # @return [void]
120
+ def parse_file!(filename)
121
+ parse(File.read(filename))
122
+ end
123
+
124
+ # @param [Formatter::Base] format
125
+ # @return [String]
126
+ def string(format = Formatter::Base.new)
127
+ @rules.string(format)
128
+ end
129
+
130
+ # @api private
131
+ # @param [Katana::Output] out
132
+ # @return [void]
133
+ def read_from_katana(out)
134
+ @rules.read_from_katana(out.stylesheet.imports)
135
+ @rules.read_from_katana(out.stylesheet.rules)
136
+
137
+ # keep reference to this stylesheet in each rule
138
+ each_rule do |rule|
139
+ rule.stylesheet = self
140
+ end
141
+
142
+ out.errors.each do |err|
143
+ @errors << Error.read_from_katana(err)
144
+ end
145
+ end
146
+ end
147
+
148
+ # group of stylesheets to match through multiple {Stylesheet}
149
+ class Stylesheets < Array
150
+ include SelectorMatcher
151
+
152
+ def each_rule(media = nil, &block)
153
+ each do |stylesheet|
154
+ stylesheet.each_rule(media, &block)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,234 @@
1
+ module Habaki
2
+ # CSS specificity score
3
+ class Specificity
4
+ attr_accessor :score
5
+
6
+ def initialize
7
+ @score = 0
8
+ end
9
+ end
10
+
11
+ # part of selector (eg in p.t, p is a tag subselector and .t a class subselector)
12
+ class SubSelector < Node
13
+ # @return [Symbol]
14
+ attr_accessor :match
15
+ # @return [String]
16
+ attr_accessor :tag
17
+ # @return [Symbol]
18
+ attr_accessor :pseudo
19
+
20
+ # @return [String]
21
+ attr_accessor :attribute
22
+ # @return [String]
23
+ attr_accessor :value
24
+ # @return [String]
25
+ attr_accessor :argument
26
+ # @return [Selectors]
27
+ attr_accessor :selectors
28
+
29
+ # @return [SourcePosition]
30
+ attr_accessor :position
31
+
32
+ # is this selector on attribute ?
33
+ # @return [Boolean]
34
+ def attribute_selector?
35
+ @match.to_s.start_with?("attribute_")
36
+ end
37
+
38
+ # does sub selector match {Visitor::Element} ?
39
+ # @param [Visitor::Element] element
40
+ # @param [Specificity, nil] specificity
41
+ # @return [Boolean]
42
+ def element_match?(element, specificity = nil)
43
+ case @match
44
+ when :tag
45
+ tag_match?(element.tag_name, specificity)
46
+ when :class
47
+ class_match?(element.class_name, specificity)
48
+ when :id
49
+ id_match?(element.id_name, specificity)
50
+ when :pseudo_class
51
+ pseudo_class_match?(element, specificity)
52
+ when :pseudo_element
53
+ # TODO
54
+ false
55
+ else
56
+ if attribute_selector?
57
+ element_attr = element.attr(@attribute.local)
58
+ (element_attr ? attribute_value_match?(element_attr, specificity) : false)
59
+ else
60
+ false
61
+ end
62
+ end
63
+ end
64
+
65
+ # does selector match tag ?
66
+ # @param [String] name
67
+ # @param [Specificity, nil] specificity
68
+ # @return [Boolean]
69
+ def tag_match?(name, specificity = nil)
70
+ match_with_specificity(@tag.local == name, specificity, 1) || @tag.local == "*"
71
+ end
72
+
73
+ # does selector match class ?
74
+ # @param [String] name
75
+ # @param [Specificity, nil] specificity
76
+ # @return [Boolean]
77
+ def class_match?(name, specificity = nil)
78
+ match_with_specificity(@value == name, specificity, 10)
79
+ end
80
+
81
+ # does selector match id ?
82
+ # @param [String] name
83
+ # @param [Specificity, nil] specificity
84
+ # @return [Boolean]
85
+ def id_match?(name, specificity = nil)
86
+ match_with_specificity(@value == name, specificity, 100)
87
+ end
88
+
89
+ # does selector match attribute value ?
90
+ # @param [String] val
91
+ # @param [Specificity, nil] specificity
92
+ # @return [Boolean]
93
+ def attribute_value_match?(val, specificity = nil)
94
+ match_with_specificity(
95
+ case @match
96
+ when :attribute_exact
97
+ val == @value
98
+ when :attribute_begin
99
+ val.start_with?(@value)
100
+ when :attribute_end
101
+ val.end_with?(@value)
102
+ when :attribute_contain
103
+ val.include?(@value)
104
+ when :attribute_hyphen
105
+ val.start_with?("#{@value}-")
106
+ else
107
+ false
108
+ end, specificity, 10)
109
+ end
110
+
111
+ # does selector pseudo class match {Visitor::Element} ?
112
+ # @param [Visitor::Element] element
113
+ # @param [Specificity, nil] specificity
114
+ # @return [Boolean]
115
+ def pseudo_class_match?(element, specificity = nil)
116
+ match_with_specificity(
117
+ case @pseudo
118
+ when :root
119
+ element.tag_name == "html"
120
+ when :empty
121
+ element.children.empty?
122
+ when :first_child
123
+ parent_element = element.parent
124
+ parent_element&.children.first == element
125
+ when :last_child
126
+ parent_element = element.parent
127
+ parent_element&.children.last == element
128
+ when :only_child
129
+ parent_element = element.parent
130
+ parent_element&.children.length == 1 && parent_element&.children.first == element
131
+ when :nth_child
132
+ parent_element = element.parent
133
+ arg = @argument.split("+")
134
+ case arg[0]
135
+ when "odd"
136
+ ((parent_element&.children.index(element) + 1) % 2 == 1)
137
+ when "even"
138
+ ((parent_element&.children.index(element) + 1) % 2 == 0)
139
+ when /^\d+$/
140
+ parent_element&.children[@argument.to_i - 1] == element
141
+ when "n"
142
+ ((parent_element&.children.index(element) + 1) % 1) == (arg[1]&.to_i || 0)
143
+ when /n$/
144
+ ((parent_element&.children.index(element) + 1) % arg[0].sub("n", "").to_i) == (arg[1]&.to_i || 0)
145
+ else
146
+ # TODO "of type"
147
+ false
148
+ end
149
+ when :not
150
+ !@selectors.element_match?(element)
151
+ when :not_parsed, :unknown
152
+ true
153
+ else
154
+ # STDERR.puts "unsupported pseudo #{sub_sel.pseudo}"
155
+ false
156
+ end, specificity, 10)
157
+ end
158
+
159
+ # @param [Formatter::Base] format
160
+ # @return [String]
161
+ def string(format = Formatter::Base.new)
162
+ str = ""
163
+ if attribute_selector?
164
+ str += "[#{@attribute.string(format)}"
165
+ case @match
166
+ when :attribute_exact
167
+ str += "="
168
+ when :attribute_set
169
+ str += "]"
170
+ when :attribute_list
171
+ str += "~="
172
+ when :attribute_hyphen
173
+ str += "|="
174
+ when :attribute_begin
175
+ str += "^="
176
+ when :attribute_end
177
+ str += "$="
178
+ when :attribute_contain
179
+ str += "*="
180
+ end
181
+ str += "\"#{@value}\"]" if @match != :attribute_set
182
+ else
183
+ case @match
184
+ when :tag
185
+ str += @tag.string(format)
186
+ when :class
187
+ str += ".#{@value}"
188
+ when :id
189
+ str += "##{@value}"
190
+ when :pseudo_class, :pseudo_page_class
191
+ str += ":#{@value}"
192
+ case @pseudo
193
+ when :any, :not, :host, :host_context
194
+ str += @selectors.string(format)
195
+ str += ")"
196
+ when :lang, :nth_child, :nth_last_child, :nth_of_type, :nth_last_of_type
197
+ str += "#{@argument})"
198
+ end
199
+ when :pseudo_element
200
+ str += "::#{@value}"
201
+ end
202
+ end
203
+ str
204
+ end
205
+
206
+ # @api private
207
+ # @param [Katana::Selector] sel
208
+ def read_from_katana(sel)
209
+ @match = sel.match
210
+ @tag = QualifiedName.read_from_katana(sel.tag) if sel.tag
211
+ @pseudo = sel.pseudo
212
+
213
+ @attribute = QualifiedName.read_from_katana(sel.data.attribute) if sel.data.attribute
214
+ @value = sel.data.value
215
+ @argument = sel.data.argument
216
+
217
+ @selectors = Selectors.read_from_katana(sel.data.selectors) if sel.data.selectors
218
+
219
+ @position = SourcePosition.new(sel.position.line, sel.position.column)
220
+ end
221
+
222
+ private
223
+
224
+ # @param [Boolean] comp
225
+ # @param [Specificity, nil] specificity
226
+ # @param [Integer] score
227
+ # @return [Boolean]
228
+ def match_with_specificity(comp, specificity, score)
229
+ specificity.score += score if specificity && comp
230
+ comp
231
+ end
232
+
233
+ end
234
+ end
@@ -0,0 +1,42 @@
1
+ module Habaki
2
+ # Array of {SubSelector}
3
+ class SubSelectors < NodeArray
4
+ # @return [Symbol]
5
+ attr_accessor :relation
6
+
7
+ # does every sub selectors match {Visitor::Element} ?
8
+ # @param [Visitor::Element] element
9
+ # @param [Specificity, nil] specificity
10
+ # @return [Boolean]
11
+ def element_match?(element, specificity = nil)
12
+ each do |sub_sel|
13
+ return false unless sub_sel.element_match?(element, specificity)
14
+ end
15
+ true
16
+ end
17
+
18
+ # @param [Formatter::Base] format
19
+ # @return [String]
20
+ def string(format = Formatter::Base.new)
21
+ "#{string_relation}#{string_join(format, "")}"
22
+ end
23
+
24
+ private
25
+
26
+ def string_relation
27
+ case @relation
28
+ when :descendant
29
+ " "
30
+ when :child
31
+ " > "
32
+ when :direct_adjacent
33
+ " + "
34
+ when :indirect_adjacent
35
+ " ~ "
36
+ else
37
+ ""
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ module Habaki
2
+ class SupportsExpression < Node
3
+ # @return [Symbol]
4
+ attr_accessor :operation
5
+ # @return [Array<SupportsExpression>]
6
+ attr_accessor :expressions
7
+ # @return [Declaration]
8
+ attr_accessor :declaration
9
+
10
+ def initialize
11
+ @expressions = []
12
+ end
13
+
14
+ # @param [Formatter::Base] format
15
+ # @return [String]
16
+ def string(format = Formatter::Base.new)
17
+ str = ""
18
+ case @expressions.length
19
+ when 0
20
+ if @declaration
21
+ str += "(#{@declaration.string(format)})"
22
+ end
23
+ when 1
24
+ str += "(#{@operation == :not ? "not " : ""}" + @expressions[0].string(format) + ")"
25
+ when 2
26
+ str += "#{@expressions[0].string(format)} #{@operation} #{@expressions[1].string(format)}"
27
+ end
28
+ str
29
+ end
30
+
31
+ # @api private
32
+ def read_from_katana(exp)
33
+ @operation = exp.operation
34
+ exp.expressions.each do |sub_exp|
35
+ @expressions << SupportsExpression.read_from_katana(sub_exp)
36
+ end
37
+ @declaration = Declaration.read_from_katana(exp.declaration) if exp.declaration
38
+ end
39
+ end
40
+
41
+ # supports rule @supports
42
+ class SupportsRule < Rule
43
+ # @return [SupportsExpression]
44
+ attr_accessor :expression
45
+ # @return [Rules]
46
+ attr_accessor :rules
47
+
48
+ def initialize
49
+ @rules = Rules.new
50
+ end
51
+
52
+ # @return [String]
53
+ def string(format = Formatter::Base.new)
54
+ "@supports #{@expression.string(format)} {\n#{@rules.string(format + 1)}\n}"
55
+ end
56
+
57
+ # @api private
58
+ # @param [Katana::SupportsRule] rule
59
+ # @return [void]
60
+ def read_from_katana(rule)
61
+ @expression = SupportsExpression.read_from_katana(rule.expression)
62
+ @rules = Rules.read_from_katana(rule.rules)
63
+ end
64
+ end
65
+ end