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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +32 -0
  3. data/Gemfile.lock +6 -6
  4. data/README.md +334 -10
  5. data/Rakefile +5 -1
  6. data/lib/gammo/attributes.rb +5 -0
  7. data/lib/gammo/css_selector/ast/combinator.rb +92 -0
  8. data/lib/gammo/css_selector/ast/selector/attrib_selector.rb +86 -0
  9. data/lib/gammo/css_selector/ast/selector/class_selector.rb +19 -0
  10. data/lib/gammo/css_selector/ast/selector/id_selector.rb +18 -0
  11. data/lib/gammo/css_selector/ast/selector/negation.rb +21 -0
  12. data/lib/gammo/css_selector/ast/selector/pseudo_class.rb +92 -0
  13. data/lib/gammo/css_selector/ast/selector.rb +100 -0
  14. data/lib/gammo/css_selector/context.rb +17 -0
  15. data/lib/gammo/css_selector/errors.rb +6 -0
  16. data/lib/gammo/css_selector/node_set.rb +44 -0
  17. data/lib/gammo/css_selector/parser.rb +790 -0
  18. data/lib/gammo/css_selector/parser.y +321 -0
  19. data/lib/gammo/css_selector.rb +33 -0
  20. data/lib/gammo/modules/subclassify.rb +31 -0
  21. data/lib/gammo/node.rb +2 -0
  22. data/lib/gammo/parser/foreign.rb +3 -3
  23. data/lib/gammo/parser/insertion_mode/after_after_body.rb +1 -1
  24. data/lib/gammo/parser/insertion_mode/after_after_frameset.rb +1 -1
  25. data/lib/gammo/parser/insertion_mode/after_body.rb +1 -1
  26. data/lib/gammo/parser/insertion_mode/after_frameset.rb +1 -1
  27. data/lib/gammo/parser/insertion_mode/after_head.rb +1 -1
  28. data/lib/gammo/parser/insertion_mode/before_head.rb +1 -1
  29. data/lib/gammo/parser/insertion_mode/before_html.rb +1 -1
  30. data/lib/gammo/parser/insertion_mode/in_body.rb +1 -1
  31. data/lib/gammo/parser/insertion_mode/in_column_group.rb +1 -1
  32. data/lib/gammo/parser/insertion_mode/in_frameset.rb +1 -1
  33. data/lib/gammo/parser/insertion_mode/in_head.rb +3 -2
  34. data/lib/gammo/parser/insertion_mode/in_head_noscript.rb +1 -1
  35. data/lib/gammo/parser/insertion_mode/in_select.rb +1 -1
  36. data/lib/gammo/parser/insertion_mode/in_table.rb +1 -1
  37. data/lib/gammo/parser/insertion_mode/in_template.rb +1 -1
  38. data/lib/gammo/parser/insertion_mode/initial.rb +1 -1
  39. data/lib/gammo/parser/insertion_mode/text.rb +1 -1
  40. data/lib/gammo/parser/insertion_mode.rb +1 -1
  41. data/lib/gammo/tokenizer/tokens.rb +10 -1
  42. data/lib/gammo/tokenizer.rb +10 -10
  43. data/lib/gammo/version.rb +1 -1
  44. data/lib/gammo/xpath/ast/axis.rb +1 -1
  45. data/lib/gammo/xpath/ast/expression.rb +2 -0
  46. data/lib/gammo/xpath/ast/function.rb +1 -1
  47. data/lib/gammo/xpath/ast/node_test.rb +1 -1
  48. data/lib/gammo/xpath/ast/path.rb +1 -0
  49. data/lib/gammo/xpath.rb +4 -5
  50. metadata +17 -4
  51. data/.travis.yml +0 -6
  52. 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,6 @@
1
+ module Gammo
2
+ module CSSSelector
3
+ Error = Class.new(StandardError)
4
+ ParseError = Class.new(Error)
5
+ end
6
+ 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