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