gammo 0.2.0 → 0.3.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 +4 -4
- data/.github/workflows/test.yml +32 -0
- data/Gemfile.lock +6 -6
- data/README.md +334 -10
- data/Rakefile +5 -1
- data/lib/gammo/attributes.rb +5 -0
- data/lib/gammo/css_selector/ast/combinator.rb +92 -0
- data/lib/gammo/css_selector/ast/selector/attrib_selector.rb +86 -0
- data/lib/gammo/css_selector/ast/selector/class_selector.rb +19 -0
- data/lib/gammo/css_selector/ast/selector/id_selector.rb +18 -0
- data/lib/gammo/css_selector/ast/selector/negation.rb +21 -0
- data/lib/gammo/css_selector/ast/selector/pseudo_class.rb +92 -0
- data/lib/gammo/css_selector/ast/selector.rb +100 -0
- data/lib/gammo/css_selector/context.rb +17 -0
- data/lib/gammo/css_selector/errors.rb +6 -0
- data/lib/gammo/css_selector/node_set.rb +44 -0
- data/lib/gammo/css_selector/parser.rb +790 -0
- data/lib/gammo/css_selector/parser.y +321 -0
- data/lib/gammo/css_selector.rb +33 -0
- data/lib/gammo/modules/subclassify.rb +31 -0
- data/lib/gammo/node.rb +2 -0
- data/lib/gammo/parser/foreign.rb +3 -3
- data/lib/gammo/parser/insertion_mode/after_after_body.rb +1 -1
- data/lib/gammo/parser/insertion_mode/after_after_frameset.rb +1 -1
- data/lib/gammo/parser/insertion_mode/after_body.rb +1 -1
- data/lib/gammo/parser/insertion_mode/after_frameset.rb +1 -1
- data/lib/gammo/parser/insertion_mode/after_head.rb +1 -1
- data/lib/gammo/parser/insertion_mode/before_head.rb +1 -1
- data/lib/gammo/parser/insertion_mode/before_html.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_body.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_column_group.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_frameset.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_head.rb +3 -2
- data/lib/gammo/parser/insertion_mode/in_head_noscript.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_select.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_table.rb +1 -1
- data/lib/gammo/parser/insertion_mode/in_template.rb +1 -1
- data/lib/gammo/parser/insertion_mode/initial.rb +1 -1
- data/lib/gammo/parser/insertion_mode/text.rb +1 -1
- data/lib/gammo/parser/insertion_mode.rb +1 -1
- data/lib/gammo/tokenizer/tokens.rb +10 -1
- data/lib/gammo/tokenizer.rb +10 -10
- data/lib/gammo/version.rb +1 -1
- data/lib/gammo/xpath/ast/axis.rb +1 -1
- data/lib/gammo/xpath/ast/expression.rb +2 -0
- data/lib/gammo/xpath/ast/function.rb +1 -1
- data/lib/gammo/xpath/ast/node_test.rb +1 -1
- data/lib/gammo/xpath/ast/path.rb +1 -0
- data/lib/gammo/xpath.rb +4 -5
- metadata +17 -4
- data/.travis.yml +0 -6
- data/lib/gammo/xpath/ast/subclassify.rb +0 -35
@@ -0,0 +1,19 @@
|
|
1
|
+
module Gammo
|
2
|
+
module CSSSelector
|
3
|
+
module AST
|
4
|
+
module Selector
|
5
|
+
class Class
|
6
|
+
def initialize(class_name)
|
7
|
+
@class_name = class_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def match?(context)
|
11
|
+
# TODO: prefer using class_name
|
12
|
+
return false unless val = context.node.attributes[:class]
|
13
|
+
val == @class_name || (val.split(/\s/).include?(@class_name))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Gammo
|
2
|
+
module CSSSelector
|
3
|
+
module AST
|
4
|
+
module Selector
|
5
|
+
class ID
|
6
|
+
def initialize(id)
|
7
|
+
@id = id
|
8
|
+
end
|
9
|
+
|
10
|
+
def match?(context)
|
11
|
+
return false unless val = context.node.attributes[:id]
|
12
|
+
val == @id || (val.split(/\s/).include?(@id))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Gammo
|
2
|
+
module CSSSelector
|
3
|
+
module AST
|
4
|
+
module Selector
|
5
|
+
class Negation
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
extend Subclassify
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
@arguments = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def match?(context)
|
15
|
+
!@arguments[0].match?(context)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Gammo
|
2
|
+
module CSSSelector
|
3
|
+
module AST
|
4
|
+
module Selector
|
5
|
+
class Pseudo
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
extend Subclassify
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
@arguments = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def match?(context)
|
15
|
+
raise NotImplemented, "#match? must be implemented by sub class"
|
16
|
+
end
|
17
|
+
|
18
|
+
class Enabled < Pseudo
|
19
|
+
declare :enabled
|
20
|
+
|
21
|
+
def match?(context)
|
22
|
+
# Return true if attributes do not have the key, or value is not
|
23
|
+
# nil or the same with the key.
|
24
|
+
!context.node.attributes.key?(:disabled) || (!context.node.attributes[:disabled].nil? &&
|
25
|
+
context.node.attributes[:disabled] != 'disabled')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Disabled < Pseudo
|
30
|
+
declare :disabled
|
31
|
+
|
32
|
+
def match?(context)
|
33
|
+
# Return true if attributes have the key but nil, or value is the
|
34
|
+
# same with the key.
|
35
|
+
(context.node.attributes.key?(:disabled) && context.node.attributes[:disabled].nil?) ||
|
36
|
+
context.node.attributes[:disabled] == 'disabled'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Checked < Pseudo
|
41
|
+
declare :checked
|
42
|
+
|
43
|
+
def match?(context)
|
44
|
+
# Return true if attributes have the key but nil, or value is the
|
45
|
+
# same with the key.
|
46
|
+
(context.node.attributes.key?(:checked) && context.node.attributes[:checked].nil?) ||
|
47
|
+
context.node.attributes[:checked] == 'checked'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Root < Pseudo
|
52
|
+
declare :root
|
53
|
+
|
54
|
+
def match?(context)
|
55
|
+
# TODO:
|
56
|
+
context.node.tag == Tags::Html
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class NthChild < Pseudo
|
61
|
+
declare :'nth-child'
|
62
|
+
|
63
|
+
InvalidExpression = Class.new(ArgumentError)
|
64
|
+
|
65
|
+
CONVERT_TABLE = {
|
66
|
+
'odd' => ['2n+1'], #'2n+1',
|
67
|
+
'even' => ['2n']
|
68
|
+
}.freeze
|
69
|
+
|
70
|
+
# TODO: AST-style
|
71
|
+
def match?(context)
|
72
|
+
exprs = @arguments[0]
|
73
|
+
exprs = CONVERT_TABLE[exprs[0]] if CONVERT_TABLE[exprs[0]]
|
74
|
+
|
75
|
+
case value = exprs.join
|
76
|
+
when /\A\s*([\+\-])?([0-9]+)?\s*\z/ then
|
77
|
+
# Raises an error if given value is not integer, but basically unreachable.
|
78
|
+
context.position == Integer(value)
|
79
|
+
when match = /\A\s*([\-\+])?([0-9]+)?(#{Parser::N})(?:\s*([\-\+])\s*([0-9]+))?\s*\z/
|
80
|
+
d = (context.position - "#{$6}#{$7}".to_f) / "#{$1}#{$2 || 1}".to_f
|
81
|
+
# Converts the value into integer in order to ignore float numbers.
|
82
|
+
d >= 0 && d == d.to_i
|
83
|
+
else
|
84
|
+
raise InvalidExpression, 'invalid expression = %s' % value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'delegate'
|
3
|
+
require 'gammo/modules/subclassify'
|
4
|
+
require 'gammo/css_selector/node_set'
|
5
|
+
require 'gammo/css_selector/ast/selector/id_selector'
|
6
|
+
require 'gammo/css_selector/ast/selector/attrib_selector'
|
7
|
+
require 'gammo/css_selector/ast/selector/class_selector'
|
8
|
+
require 'gammo/css_selector/ast/selector/pseudo_class'
|
9
|
+
require 'gammo/css_selector/ast/selector/negation'
|
10
|
+
|
11
|
+
module Gammo
|
12
|
+
module CSSSelector
|
13
|
+
module AST
|
14
|
+
# Class for representing selectors group defined in the CSS selector specification.
|
15
|
+
# @!visibility private
|
16
|
+
class SelectorsGroup < DelegateClass(Array)
|
17
|
+
def initialize
|
18
|
+
super([])
|
19
|
+
end
|
20
|
+
|
21
|
+
def evaluate(context)
|
22
|
+
map { |selector| selector.evaluate(context.dup) }.inject(:+)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @!visibility private
|
27
|
+
module Selector
|
28
|
+
class Base
|
29
|
+
attr_accessor :selectors
|
30
|
+
|
31
|
+
def initialize(namespace_prefix: nil, selectors: [])
|
32
|
+
@namespace_prefix = namespace_prefix
|
33
|
+
@selectors = selectors
|
34
|
+
@combinations = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def evaluate(context)
|
38
|
+
node_set = NodeSet.new
|
39
|
+
search_descendant(context, node_set)
|
40
|
+
|
41
|
+
@combinations.inject(node_set) do |ns, combination|
|
42
|
+
duplicates = Set.new
|
43
|
+
ns.each_with_object(NodeSet.new) do |node, ret|
|
44
|
+
context.node = node
|
45
|
+
# TODO: #concat
|
46
|
+
combination.evaluate(context).each do |matched|
|
47
|
+
ret << matched if duplicates.add?(matched)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def search_descendant(context, node_set)
|
54
|
+
queue = [context]
|
55
|
+
until queue.empty?
|
56
|
+
current_context = queue.shift
|
57
|
+
node_set << current_context.node if match?(current_context)
|
58
|
+
current_context.node.children.inject(0) do |i, child|
|
59
|
+
next i unless child.kind_of?(Node::Element)
|
60
|
+
i += 1
|
61
|
+
queue << Context.new(node: child, position: i)
|
62
|
+
i
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def combine(selector)
|
68
|
+
@combinations << selector
|
69
|
+
end
|
70
|
+
|
71
|
+
def match?(context)
|
72
|
+
@selectors.all? { |matcher| matcher.match?(context) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Universal < Base
|
77
|
+
def initialize(**opts)
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
def match?(context)
|
82
|
+
super && context.node.kind_of?(Gammo::Node::Element)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Type < Base
|
87
|
+
def initialize(element_name:, **opts)
|
88
|
+
@element_name = element_name
|
89
|
+
super(**opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
def match?(context)
|
93
|
+
return false if @element_name != context.node.tag
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gammo
|
2
|
+
module CSSSelector
|
3
|
+
# Class for representing a context at traversing DOM.
|
4
|
+
# @!visibility private
|
5
|
+
class Context
|
6
|
+
# Defines context node, context position and context size.
|
7
|
+
attr_accessor :node, :position, :size
|
8
|
+
|
9
|
+
# @param [Gammo::Node] node
|
10
|
+
# @param [Integer] position
|
11
|
+
def initialize(node:, position: 1)
|
12
|
+
@node = node
|
13
|
+
@position = position
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Gammo
|
4
|
+
module CSSSelector
|
5
|
+
# Class for representing node set
|
6
|
+
# Especially this class will be used for expressing the result of evaluation
|
7
|
+
# of a given CSS selector.
|
8
|
+
class NodeSet
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :@nodes, :<<, :each, :each_with_object, :each_with_index, :length, :size,
|
11
|
+
:map, :[], :first, :last, :concat, :all?, :any?, :empty?
|
12
|
+
|
13
|
+
attr_reader :nodes
|
14
|
+
|
15
|
+
attr_accessor :disjoint
|
16
|
+
|
17
|
+
# Constructs a new instance of Gammo::CSSSelector::NodeSet.
|
18
|
+
# @return [Gammo::CSSSelector::NodeSet]
|
19
|
+
def initialize
|
20
|
+
@nodes = []
|
21
|
+
@disjoint = false
|
22
|
+
end
|
23
|
+
|
24
|
+
# Replaces self nodes with an other node set destructively.
|
25
|
+
# @param [Gammo::CSSSelector::NodeSet] other
|
26
|
+
# @return [Gammo::CSSSelector::NodeSet]
|
27
|
+
# @!visibility private
|
28
|
+
def replace(other)
|
29
|
+
@nodes.replace(other.nodes)
|
30
|
+
end
|
31
|
+
|
32
|
+
def +(other)
|
33
|
+
ns = NodeSet.new
|
34
|
+
ns.nodes.concat(@nodes | other.nodes)
|
35
|
+
ns
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!visibility private
|
39
|
+
def to_s
|
40
|
+
first.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|