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,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
|
data/lib/habaki/node.rb
ADDED
@@ -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
|
data/lib/habaki/rule.rb
ADDED
@@ -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
|
data/lib/habaki/rules.rb
ADDED
@@ -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
|