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