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,173 @@
1
+ module Habaki
2
+ class MediaQueryExpression < Node
3
+ # @return [String]
4
+ attr_accessor :feature
5
+ # @return [Values]
6
+ attr_accessor :values
7
+
8
+ def initialize
9
+ @values = Values.new
10
+ end
11
+
12
+ # @return [Value]
13
+ def value
14
+ @values.first
15
+ end
16
+
17
+ # @param [Visitor::Media] media
18
+ # @return [Boolean]
19
+ def media_match?(media)
20
+ case @feature
21
+ when "min-width"
22
+ return true unless media.width
23
+ media.width >= value.to_px
24
+ when "max-width"
25
+ return true unless media.width
26
+ media.width <= value.to_px
27
+ when "min-height"
28
+ return true unless media.height
29
+ media.height >= value.to_px
30
+ when "max-height"
31
+ return true unless media.height
32
+ media.height <= value.to_px
33
+ end
34
+ end
35
+
36
+ # @param [Formatter::Base] format
37
+ # @return [String]
38
+ def string(format = Formatter::Base.new)
39
+ "(#{@feature}#{@values.any? ? ": #{@values.string(format)}" : ""})"
40
+ end
41
+
42
+ # @api private
43
+ # @param [Katana::MediaQueryExpression] exp
44
+ # @return [void]
45
+ def read_from_katana(exp)
46
+ @feature = exp.feature
47
+ if exp.values
48
+ @values = Values.read_from_katana(exp.values)
49
+ end
50
+ end
51
+ end
52
+
53
+ class MediaQuery < Node
54
+ # @return [String]
55
+ attr_accessor :type
56
+ # @return [Symbol]
57
+ attr_accessor :restrictor
58
+ # @return [Array<MediaQueryExpression>]
59
+ attr_accessor :expressions
60
+
61
+ def initialize
62
+ @expressions = []
63
+ end
64
+
65
+ def media_match_type?(mediatype = "all")
66
+ return true unless mediatype
67
+ case @restrictor
68
+ when :none, :only
69
+ @type == mediatype || @type == "all" || !mediatype
70
+ when :not
71
+ @type != mediatype
72
+ else
73
+ false
74
+ end
75
+ end
76
+
77
+ # @param [Visitor::Media] media
78
+ def media_match?(media)
79
+ return false unless media_match_type?(media.type)
80
+ @expressions.each do |exp|
81
+ return false unless exp.media_match?(media)
82
+ end
83
+ true
84
+ end
85
+
86
+ # @param [Formatter::Base] format
87
+ # @return [String]
88
+ def string(format = Formatter::Base.new)
89
+ str = (@restrictor != :none ? @restrictor.to_s + " " : "") + (@type ? @type : "")
90
+ if @expressions.any?
91
+ @expressions.each do |exp|
92
+ str += " and " if str != ""
93
+ str += exp.string(format)
94
+ end
95
+ end
96
+ str
97
+ end
98
+
99
+ # @api private
100
+ # @param [Katana::MediaQuery] med
101
+ # @return [void]
102
+ def read_from_katana(med)
103
+ @type = med.type
104
+ @restrictor = med.restrictor
105
+ med.expressions.each do |exp|
106
+ @expressions << MediaQueryExpression.read_from_katana(exp)
107
+ end
108
+ end
109
+ end
110
+
111
+ # Array of {MediaQuery}
112
+ class MediaQueries < NodeArray
113
+ # @param [Formatter::Base] format
114
+ # @return [String]
115
+ def string(format = Formatter::Base.new)
116
+ string_join(format, ",")
117
+ end
118
+
119
+ # @param [Visitor::Media] media
120
+ # @return [Boolean]
121
+ def media_match?(media)
122
+ inject(false) { |result, q| result ||= q.media_match?(media) }
123
+ end
124
+
125
+ # @api private
126
+ # @param [Katana::Array<Katana::MediaQuery>] meds
127
+ # @return [void]
128
+ def read_from_katana(meds)
129
+ meds.each do |med|
130
+ push MediaQuery.read_from_katana(med)
131
+ end
132
+ end
133
+ end
134
+
135
+ # Rule for @media
136
+ class MediaRule < Rule
137
+ # @return [MediaQueries]
138
+ attr_accessor :medias
139
+ # @return [Rules]
140
+ attr_accessor :rules
141
+
142
+ def initialize
143
+ @medias = MediaQueries.new
144
+ @rules = Rules.new
145
+ end
146
+
147
+ # does rule media match ?
148
+ # @param [Visitor::Media, String, NilClass] media use String (eg: "print") to check only media type, nil to match everything, or Visitor::Media for complex query
149
+ # @return [Boolean]
150
+ def media_match?(media)
151
+ case media
152
+ when ::String, NilClass
153
+ @medias.media_match?(Visitor::Media.new(media))
154
+ else
155
+ @medias.media_match?(media)
156
+ end
157
+ end
158
+
159
+ # @param [Formatter::Base] format
160
+ # @return [String]
161
+ def string(format = Formatter::Base.new)
162
+ "@media #{@medias.string(format)} {\n#{@rules.string(format + 1)}\n}"
163
+ end
164
+
165
+ # @api private
166
+ # @param [Katana::MediaRule] rule
167
+ # @return [void]
168
+ def read_from_katana(rule)
169
+ @medias = MediaQueries.read_from_katana(rule.medias)
170
+ @rules = Rules.read_from_katana(rule.rules)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,31 @@
1
+ module Habaki
2
+ # Rule for @namespace
3
+ # TODO: implement QualifiedName namespace resolution
4
+ class NamespaceRule < Rule
5
+ # @return [String]
6
+ attr_accessor :prefix
7
+ # @return [String]
8
+ attr_accessor :uri
9
+
10
+ # @param [String] prefix
11
+ # @param [String] uri
12
+ def initialize(prefix = nil, uri = nil)
13
+ @prefix = prefix
14
+ @uri = uri
15
+ end
16
+
17
+ # @param [Formatter::Base] format
18
+ # @return [String]
19
+ def string(format = Formatter::Base.new)
20
+ "@namespace #{@prefix.length > 0 ? "#{@prefix} " : ""}\"#{@uri}\";"
21
+ end
22
+
23
+ # @api private
24
+ # @param [Katana::NamespaceRule] rule
25
+ # @return [void]
26
+ def read_from_katana(rule)
27
+ @prefix = rule.prefix
28
+ @uri = rule.uri
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ module Habaki
2
+ module NodeReader
3
+ # read from low level Katana struct
4
+ def read_from_katana(low)
5
+ obj = self.new
6
+ obj.read_from_katana(low)
7
+ obj
8
+ end
9
+ end
10
+
11
+ class Node
12
+ extend NodeReader
13
+
14
+ # @param [Formatter::Base] format
15
+ # @return [::String]
16
+ def string(format = Formatter::Base.new)
17
+ ""
18
+ end
19
+
20
+ # @return [::String]
21
+ def to_s
22
+ string(Formatter::Flat.new)
23
+ end
24
+
25
+ # read from low level Katana struct
26
+ # @return [nil]
27
+ def read_from_katana(low) end
28
+ end
29
+
30
+ class NodeArray < Array
31
+ extend NodeReader
32
+
33
+ # @param [Formatter::Base] format
34
+ # @return [::String]
35
+ def string(format = Formatter::Base.new)
36
+ ""
37
+ end
38
+
39
+ def string_join(format, sep)
40
+ map do |node| node.string(format) end.join(sep)
41
+ end
42
+
43
+ # @return [::String]
44
+ def to_s
45
+ string(Formatter::Flat.new)
46
+ end
47
+
48
+ # read from low level Katana struct
49
+ # @return [nil]
50
+ def read_from_katana(low) end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ module Habaki
2
+ # page rule @page
3
+ class PageRule < 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
+ "@page {#{@declarations.string(format + 1)}}"
15
+ end
16
+
17
+ # @api private
18
+ # @param [Katana::PageRule] rule
19
+ # @return [void]
20
+ def read_from_katana(rule)
21
+ @declarations = Declarations.read_from_katana(rule.declarations)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Habaki
2
+ # name with optional ns prefix
3
+ class QualifiedName < Node
4
+ # @return [String]
5
+ attr_accessor :local
6
+ # @return [String]
7
+ attr_accessor :prefix
8
+
9
+ # @param [String] local
10
+ # @param [String] prefix
11
+ def initialize(local = nil, prefix = nil)
12
+ @local = local
13
+ @prefix = prefix
14
+ end
15
+
16
+ # @param [Formatter::Base] format
17
+ # @return [String]
18
+ def string(format = Formatter::Base.new)
19
+ @prefix ? "#{@prefix}|#{@local}" : @local
20
+ end
21
+
22
+ # @api private
23
+ # @param [Katana::QualifiedName] tag
24
+ def read_from_katana(tag)
25
+ @local = tag.local
26
+ @prefix = tag.prefix
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ module Habaki
2
+ # @abstract CSS rule
3
+ class Rule < Node
4
+ # @return [Stylesheet]
5
+ attr_accessor :stylesheet
6
+
7
+ # @return [Array, nil]
8
+ def selectors
9
+ nil
10
+ end
11
+
12
+ # traverse selector if selectors
13
+ # @yieldparam [Selector] selector
14
+ def each_selector(&block)
15
+ return unless selectors
16
+
17
+ selectors.each do |decl|
18
+ block.call decl
19
+ end
20
+ end
21
+
22
+ # @return [Array, nil]
23
+ def declarations
24
+ nil
25
+ end
26
+
27
+ # traverse declarations if declarations
28
+ # @yieldparam [Declaration] declaration
29
+ def each_declaration(&block)
30
+ return unless declarations
31
+
32
+ declarations.each do |decl|
33
+ block.call decl
34
+ end
35
+ end
36
+
37
+ # @return [Array, nil]
38
+ def rules
39
+ nil
40
+ end
41
+
42
+ # @param [Visitor::Element] element
43
+ # @return [Boolean]
44
+ def element_match?(element)
45
+ false
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,225 @@
1
+ module Habaki
2
+ # selector matcher helper
3
+ module SelectorMatcher
4
+ # small hash index on tag, class and id
5
+ def index_selectors!(*args)
6
+ @hash_tree[args] = {}
7
+
8
+ each_rule(*args) do |rule|
9
+ rule.each_selector do |selector|
10
+ sub_sels = selector.sub_selectors.last
11
+ tag_name = nil
12
+ class_name = nil
13
+ id_name = nil
14
+ sub_sels.each do |sub_sel|
15
+ case sub_sel.match
16
+ when :tag
17
+ tag_name = sub_sel.tag.local
18
+ when :class
19
+ class_name = sub_sel.value
20
+ when :id
21
+ id_name = sub_sel.value
22
+ end
23
+ end
24
+
25
+ class_or_id = nil
26
+ class_or_id = ".#{class_name}" if class_name
27
+ class_or_id = "##{id_name}" if id_name
28
+
29
+ @hash_tree[args][tag_name||"*"] ||= {}
30
+ @hash_tree[args][tag_name||"*"][class_or_id] ||= Set.new
31
+ @hash_tree[args][tag_name||"*"][class_or_id] << rule
32
+ end
33
+ end
34
+ end
35
+
36
+ def lookup_rules(args, tag_name, class_name, id_name, &block)
37
+ class_or_id = nil
38
+ class_or_id = ".#{class_name}" if class_name
39
+ class_or_id = "##{id_name}" if id_name
40
+
41
+ @hash_tree[args].dig(tag_name, nil)&.each do |rule|
42
+ block.call rule
43
+ end
44
+
45
+ @hash_tree[args].dig("*", class_or_id)&.each do |rule|
46
+ block.call rule
47
+ end
48
+
49
+ @hash_tree[args].dig(tag_name, class_or_id)&.each do |rule|
50
+ block.call rule
51
+ end
52
+ end
53
+
54
+ # traverse rules matching with {Visitor::Element}
55
+ # @param [Visitor::Element] element
56
+ # @yieldparam [Rule] rule
57
+ # @yieldparam [Selector] selector
58
+ # @yieldparam [Specificity] specificity
59
+ # @return [void]
60
+ def each_match(element, *args, &block)
61
+ @hash_tree ||= {}
62
+ index_selectors!(*args) unless @hash_tree[args]
63
+
64
+ lookup_rules(args, element.tag_name, element.class_name, element.id_name) do |rule|
65
+ rule.each_selector do |selector|
66
+ specificity = Specificity.new
67
+ if selector.element_match?(element, specificity)
68
+ block.call rule, selector, specificity
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # traverse rules matching with {Visitor::Element}
75
+ # @param [Visitor::Element] element
76
+ # @yieldparam [Rule] rule
77
+ # @return [void]
78
+ def each_matching_rule(element, *args, &block)
79
+ find_matching_rules(element, *args).each do |rule|
80
+ block.call rule
81
+ end
82
+ end
83
+
84
+ # rules matching with {Visitor::Element} ordered by specificity score (highest last)
85
+ # @param [Visitor::Element] element
86
+ # @return [Array<Rule>]
87
+ def find_matching_rules(element, *args)
88
+ results = []
89
+ each_match(element, *args) do |rule, selector, specificity|
90
+ results << [specificity.score, rule]
91
+ end
92
+ results.sort! { |a, b| a.first <=> b.first }
93
+ results.map(&:last)
94
+ end
95
+
96
+ # get cascaded declarations results for {Visitor::Element}
97
+ # @param [Visitor::Element] element
98
+ # @return [Declarations]
99
+ def find_matching_declarations(element, *args)
100
+ declarations = Declarations.new
101
+ each_matching_rule(element, *args) do |rule|
102
+ rule.each_declaration do |decl|
103
+ declarations.replace_important(decl)
104
+ end
105
+ end
106
+ declarations
107
+ end
108
+ end
109
+
110
+ class Rules < NodeArray
111
+ include SelectorMatcher
112
+
113
+ # @param [Class<Rule>] klass
114
+ # @return [Enumerator<Rule>]
115
+ def enum_with_class(klass)
116
+ Enumerator.new do |rules|
117
+ each do |rule|
118
+ rules << rule if rule.is_a?(klass)
119
+ end
120
+ end
121
+ end
122
+
123
+ # @return [CharsetRule, nil]
124
+ def charset
125
+ enum_with_class(CharsetRule).first
126
+ end
127
+
128
+ # @return [Enumerator<MediaRule>]
129
+ def medias
130
+ enum_with_class(MediaRule)
131
+ end
132
+
133
+ # @return [Enumerator<SupportsRule>]
134
+ def supports
135
+ enum_with_class(SupportsRule)
136
+ end
137
+
138
+ # @return [Enumerator<NamespaceRule>]
139
+ def namespaces
140
+ enum_with_class(NamespaceRule)
141
+ end
142
+
143
+ # @return [Enumerator<FontFaceRule>]
144
+ def font_faces
145
+ enum_with_class(FontFaceRule)
146
+ end
147
+
148
+ # @return [Enumerator<PageRule>]
149
+ def pages
150
+ enum_with_class(PageRule)
151
+ end
152
+
153
+ # @return [Enumerator<StyleRule>]
154
+ def styles
155
+ enum_with_class(StyleRule)
156
+ end
157
+
158
+ # add rule by selectors string
159
+ # @param [String] selector_str
160
+ # @return [StyleRule]
161
+ def add_by_selector(selector_str)
162
+ rule = StyleRule.new
163
+ rule.selectors = Selectors.parse(selector_str)
164
+ push rule
165
+ rule
166
+ end
167
+
168
+ # find rules from selector str
169
+ # @param [String] selector_str
170
+ # @return [Array<Rule>]
171
+ def find_by_selector(selector_str)
172
+ results = []
173
+ each do |rule|
174
+ rule.each_selector do |selector|
175
+ results << rule if selector_str == selector.to_s
176
+ end
177
+ end
178
+ results
179
+ end
180
+
181
+ def each_rule(&block)
182
+ each &block
183
+ end
184
+
185
+ # @param [Formatter::Base] format
186
+ # @return [String]
187
+ def string(format = Formatter::Base.new)
188
+ "#{format.rules_prefix}#{string_join(format, format.rules_join)}"
189
+ end
190
+
191
+ # @api private
192
+ # @param [Katana::Array] rules
193
+ # @return [void]
194
+ def read_from_katana(rules)
195
+ rules.each do |rule|
196
+ push read_rule(rule)
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ def read_rule(rul)
203
+ case rul
204
+ when Katana::ImportRule
205
+ ImportRule.read_from_katana(rul)
206
+ when Katana::CharsetRule
207
+ CharsetRule.read_from_katana(rul)
208
+ when Katana::MediaRule
209
+ MediaRule.read_from_katana(rul)
210
+ when Katana::FontFaceRule
211
+ FontFaceRule.read_from_katana(rul)
212
+ when Katana::PageRule
213
+ PageRule.read_from_katana(rul)
214
+ when Katana::NamespaceRule
215
+ NamespaceRule.read_from_katana(rul)
216
+ when Katana::StyleRule
217
+ StyleRule.read_from_katana(rul)
218
+ when Katana::SupportsRule
219
+ SupportsRule.read_from_katana(rul)
220
+ else
221
+ raise "Unsupported rule #{rul.class}"
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,98 @@
1
+ module Habaki
2
+ # CSS selector
3
+ class Selector < Node
4
+ # Array of {SubSelectors} group
5
+ # @return [Array<SubSelectors>]
6
+ attr_accessor :sub_selectors
7
+
8
+ def initialize
9
+ @sub_selectors = []
10
+ end
11
+
12
+ # does selector match {Visitor::Element} ?
13
+ # @param [Visitor::Element] element
14
+ # @param [Specificity, nil] specificity
15
+ # @return [Boolean]
16
+ def element_match?(element, specificity = nil)
17
+ return false if @sub_selectors.empty?
18
+
19
+ current_sub_selector = nil
20
+ @sub_selectors.reverse_each do |sub_selector|
21
+ if current_sub_selector
22
+ case current_sub_selector.relation
23
+ when :descendant
24
+ parent_element = element.parent
25
+ sub_match = false
26
+ while parent_element do
27
+ sub_match = sub_selector.element_match?(parent_element, specificity)
28
+ parent_element = parent_element.parent
29
+ break if sub_match
30
+ end
31
+ return false unless sub_match
32
+ when :child
33
+ parent_element = element.parent
34
+ return false unless parent_element
35
+ return false unless sub_selector.element_match?(parent_element, specificity)
36
+ when :direct_adjacent
37
+ previous_element = element.previous
38
+ return false unless previous_element
39
+ return false unless sub_selector.element_match?(previous_element, specificity)
40
+ when :indirect_adjacent
41
+ previous_element = element.previous
42
+ sub_match = false
43
+ while previous_element do
44
+ sub_match = sub_selector.element_match?(previous_element, specificity)
45
+ previous_element = previous_element.previous
46
+ break if sub_match
47
+ end
48
+ return false unless sub_match
49
+ else
50
+ # STDERR.puts "unknown relation #{current_sub_selector.relation}"
51
+ return false
52
+ end
53
+ else
54
+ return false unless sub_selector.element_match?(element, specificity)
55
+ end
56
+
57
+ current_sub_selector = sub_selector
58
+ end
59
+ true
60
+ end
61
+
62
+ # @param [Formatter::Base] format
63
+ # @return [String]
64
+ def string(format = Formatter::Base.new)
65
+ @sub_selectors.map do |sub_sel|
66
+ sub_sel.string(format)
67
+ end.join("")
68
+ end
69
+
70
+ # @api private
71
+ # @param [Katana::Selector] sel
72
+ def read_from_katana(sel)
73
+ @sub_selectors = rec_sub_sel(sel)
74
+ end
75
+
76
+ private
77
+
78
+ # parse sub selectors recursively
79
+ def rec_sub_sel(sel)
80
+ sub_sels = []
81
+ cur_sel = sel
82
+ cur_sub_sel = SubSelectors.new
83
+ while cur_sel do
84
+ cur_sub_sel << SubSelector.read_from_katana(cur_sel)
85
+ break if cur_sel.relation != :sub_selector || !cur_sel.tag_history
86
+ cur_sel = cur_sel.tag_history
87
+ end
88
+
89
+ cur_sub_sel.relation = cur_sel.relation if cur_sel.relation != :sub_selector
90
+ sub_sels << cur_sub_sel
91
+
92
+ if cur_sel.relation != :sub_selector
93
+ sub_sels = rec_sub_sel(cur_sel.tag_history) + sub_sels
94
+ end
95
+ sub_sels
96
+ end
97
+ end
98
+ end