habaki 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|