saxxy 0.1.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +13 -0
  5. data/LICENSE +22 -0
  6. data/README.md +117 -0
  7. data/Rakefile +12 -0
  8. data/lib/saxxy.rb +2 -0
  9. data/lib/saxxy/activatable.rb +160 -0
  10. data/lib/saxxy/callbacks/libxml.rb +26 -0
  11. data/lib/saxxy/callbacks/nokogiri.rb +30 -0
  12. data/lib/saxxy/callbacks/ox.rb +66 -0
  13. data/lib/saxxy/callbacks/sax.rb +86 -0
  14. data/lib/saxxy/context.rb +88 -0
  15. data/lib/saxxy/context_tree.rb +85 -0
  16. data/lib/saxxy/event.rb +83 -0
  17. data/lib/saxxy/event_registry.rb +122 -0
  18. data/lib/saxxy/node_action.rb +59 -0
  19. data/lib/saxxy/node_rule.rb +90 -0
  20. data/lib/saxxy/parsers/base.rb +28 -0
  21. data/lib/saxxy/parsers/libxml.rb +52 -0
  22. data/lib/saxxy/parsers/nokogiri.rb +28 -0
  23. data/lib/saxxy/parsers/ox.rb +30 -0
  24. data/lib/saxxy/service.rb +47 -0
  25. data/lib/saxxy/utils/agent.rb +66 -0
  26. data/lib/saxxy/utils/callback_array.rb +27 -0
  27. data/lib/saxxy/utils/helpers.rb +13 -0
  28. data/lib/saxxy/version.rb +3 -0
  29. data/saxxy.gemspec +21 -0
  30. data/spec/saxxy/activatable_spec.rb +344 -0
  31. data/spec/saxxy/callbacks/sax_spec.rb +456 -0
  32. data/spec/saxxy/context_spec.rb +51 -0
  33. data/spec/saxxy/context_tree_spec.rb +68 -0
  34. data/spec/saxxy/event_registry_spec.rb +137 -0
  35. data/spec/saxxy/event_spec.rb +49 -0
  36. data/spec/saxxy/node_action_spec.rb +46 -0
  37. data/spec/saxxy/node_rule_spec.rb +99 -0
  38. data/spec/saxxy/parsers/libxml_spec.rb +104 -0
  39. data/spec/saxxy/parsers/nokogiri_spec.rb +200 -0
  40. data/spec/saxxy/parsers/ox_spec.rb +175 -0
  41. data/spec/saxxy/utils/agent_spec.rb +63 -0
  42. data/spec/spec_helper.rb +28 -0
  43. data/spec/support/agent_macros.rb +24 -0
  44. metadata +155 -0
@@ -0,0 +1,86 @@
1
+ require "forwardable"
2
+ require "saxxy/utils/callback_array"
3
+ require "saxxy/event_registry"
4
+
5
+
6
+ module Saxxy
7
+ module Callbacks
8
+
9
+ module SAX
10
+ def self.included(base)
11
+ base.extend Forwardable
12
+ base.def_delegators :@event_registry, :activate_events_on, :deactivate_events_on,
13
+ :push_text, :register_event_from_action, :remove_actions!
14
+ end
15
+
16
+ def initialize(context)
17
+ initialize_state
18
+ @active_pool << context
19
+ end
20
+
21
+ def on_start_element(name, attrs = {})
22
+ register_and_activate_events_on(name, attrs)
23
+ activate_contexts_on(name, attrs)
24
+ end
25
+
26
+ def on_characters(string)
27
+ push_text(string || "")
28
+ end
29
+
30
+ def on_end_element(name)
31
+ deactivate_events_on(name)
32
+ deactivate_contexts_on(name)
33
+ end
34
+
35
+ def on_end_document
36
+ @active_pool.clear
37
+ @inactive_pool.clear
38
+ @event_registry.clear
39
+ end
40
+
41
+ private
42
+ def initialize_state
43
+ @event_registry = EventRegistry.new
44
+ @inactive_pool = CallbackArray.new.on_add(&method(:on_add_to_inactive_pool))
45
+ @active_pool = CallbackArray.new.on_add(&method(:on_add_to_active_pool)).on_remove(&method(:on_remove_from_active_pool))
46
+ end
47
+
48
+ def register_and_activate_events_on(name, attrs)
49
+ actions.select { |a| a.matches(name, attrs) }.each do |action|
50
+ register_event_from_action(action, name, attrs)
51
+ end
52
+ activate_events_on(name, attrs)
53
+ end
54
+
55
+ def activate_contexts_on(name, attrs)
56
+ @inactive_pool.each { |context| context.activate_on(name, attrs) }
57
+ end
58
+
59
+ def deactivate_contexts_on(name)
60
+ @active_pool.each { |context| context.deactivate_on(name) }
61
+ end
62
+
63
+ def on_add_to_inactive_pool(context)
64
+ context.on_activation do |context|
65
+ @inactive_pool >> context
66
+ @active_pool << context
67
+ end
68
+ end
69
+
70
+ def on_add_to_active_pool(context)
71
+ context.on_deactivation { |context| @active_pool >> context }
72
+ context.child_contexts.each { |context| @inactive_pool << context }
73
+ end
74
+
75
+ def on_remove_from_active_pool(context)
76
+ @inactive_pool << context if @active_pool.member?(context.parent_context)
77
+ remove_actions!(*context.actions)
78
+ end
79
+
80
+ def actions
81
+ @active_pool.flat_map(&:actions)
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,88 @@
1
+ require "saxxy/activatable"
2
+ require "saxxy/node_action"
3
+
4
+
5
+ module Saxxy
6
+
7
+ ##
8
+ # @author rubymaniac
9
+ #
10
+ # Context describes, semantically, an XML tag-context.
11
+ # For example an XML tag-context: <div>This is a context</div>
12
+ #
13
+ # Whether a tag-context is described by a Context object
14
+ # depends on whether the Context's activation rule matches
15
+ # this tag-context. Because a Context can be activated
16
+ # on a tag-context that matches it's activation rule
17
+ # it includes the "Activatable" module.
18
+ #
19
+ # A context can belong to a parent context and thus it
20
+ # may have a `parent_context` attribute that points to
21
+ # it's parent. A context may also have `child_contexts`.
22
+ #
23
+ # @!attribute [r] activation_rule
24
+ # @return [NodeRule] this context's activation rule
25
+ #
26
+ # @!attribute [r|w] parent_context
27
+ # @return [Context] this context's parent
28
+ #
29
+ # @!attribute [r] child_contexts
30
+ # @return [Array<Context>] this context's immediate descendants
31
+ ##
32
+ class Context
33
+ include Activatable
34
+
35
+ attr_accessor :parent_context
36
+ attr_reader :child_contexts, :actions
37
+
38
+ # Initializes a context with an `activation_rule` (defaults to `nil`).
39
+ #
40
+ # @param activation_rule [NodeRule] an instance of NodeRule or nil to
41
+ # declare that this context is automatically active.
42
+ #
43
+ def initialize(activation_rule = nil)
44
+ @child_contexts = []
45
+ @actions = []
46
+ initialize_activatable(activation_rule)
47
+ end
48
+
49
+ # Registers either a Context as a `child_context` by setting
50
+ # it's `parent_context` attribute to itself and appending it to
51
+ # the `child_contexts` array, or a NodeAction by appending it to
52
+ # the `actions` array.
53
+ #
54
+ # @param obj [Context|NodeAction] An instance of Context or
55
+ # an instance of NodeAction.
56
+ #
57
+ # @return [Context] self, i.e. the context
58
+ #
59
+ def register(obj)
60
+ case obj
61
+ when Context
62
+ obj.parent_context = self
63
+ @child_contexts << obj
64
+ when NodeAction
65
+ @actions << obj
66
+ end
67
+ self
68
+ end
69
+
70
+ # Checks whether this context has a parent.
71
+ #
72
+ # @return [Boolean] true if it has a `parent_context`, false otherwise
73
+ #
74
+ def has_parent?
75
+ !parent_context.nil?
76
+ end
77
+
78
+ # Checks whether this context is a root context,
79
+ # i.e. it has no `parent_context`
80
+ #
81
+ # @return [Boolean] false if it has a `parent_context`, true otherwise
82
+ #
83
+ def root?
84
+ !has_parent?
85
+ end
86
+ end
87
+
88
+ end
@@ -0,0 +1,85 @@
1
+ require "saxxy/context"
2
+ require "saxxy/node_action"
3
+ require "saxxy/node_rule"
4
+
5
+
6
+ module Saxxy
7
+
8
+ ##
9
+ # @author rubymaniac
10
+ #
11
+ # ContextTree describes the tree of contexts that the user
12
+ # eventually constructs by constraining NodeActions to be
13
+ # active under some Context.
14
+ #
15
+ #
16
+ # @!attribute [r|w] root
17
+ # @return [Context] the root context of the tree
18
+ #
19
+ ##
20
+ class ContextTree
21
+ attr_accessor :root
22
+
23
+ # Initializes a ContextTree by passing an optional context to be used
24
+ # by the actions in order to execute their action block and a block that
25
+ # will be evaluated in order to create the intenal tree structure.
26
+ #
27
+ # @param ctx [Object] an object to be used by the NodeActions
28
+ # in order to evaluate their block
29
+ #
30
+ # @param block [Proc] a block that will get evaluated and create
31
+ # the context tree structure
32
+ #
33
+ def initialize(ctx = nil, &block)
34
+ self.root = Saxxy::Context.new
35
+ @ctx = ctx || eval("self", block.binding)
36
+ eval_subtree!(&block)
37
+ end
38
+
39
+ # Creates a Context and uses the arguments to create its activation rule. After
40
+ # creating the context it registers it under the current root context and returns it.
41
+ #
42
+ # @param regexp_or_string [String|Regexp] the activation rule's name
43
+ # @param attributes [Hash] the activation rule's attributes
44
+ # @param block [Proc] a block that will get evaluated and register
45
+ # the child contexts and the actions
46
+ #
47
+ # @return [Context] the registered Context
48
+ def under(regexp_or_string, attributes = {}, &block)
49
+ Saxxy::Context.new(Saxxy::NodeRule.new(regexp_or_string, attributes)).tap do |context|
50
+ __register_context(context, &block)
51
+ end
52
+ end
53
+
54
+ # Creates a NodeAction and uses the arguments to create its activation rule and registers
55
+ # it under the current root context.
56
+ #
57
+ # @param regexp_or_string [String|Regexp] the activation rule's name
58
+ # @param attributes [Hash] the activation rule's attributes
59
+ # @param block [Proc] the NodeAction's action block that will get
60
+ # evaluated on the passed context at construction
61
+ #
62
+ # @return [NodeAction] the registered NodeAction
63
+ def on(regexp_or_string, attributes = {}, &block)
64
+ Saxxy::NodeAction.new(Saxxy::NodeRule.new(regexp_or_string, attributes), @ctx, &block).tap do |action|
65
+ __register_action(action)
66
+ end
67
+ end
68
+
69
+ private
70
+ def eval_subtree!(&block)
71
+ instance_eval(&block) if block_given?
72
+ self.root = root.parent_context if root.has_parent?
73
+ end
74
+
75
+ def __register_action(action)
76
+ root.register(action)
77
+ end
78
+
79
+ def __register_context(context, &block)
80
+ root.register(self.root = context)
81
+ eval_subtree!(&block)
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,83 @@
1
+ require "saxxy/activatable"
2
+
3
+
4
+ module Saxxy
5
+
6
+ ##
7
+ # @author rubymaniac
8
+ #
9
+ # An Event refers to a specific NodeAction and is what
10
+ # is registered when a NodeAction matches a specific node.
11
+ # Because a NodeAction may match more than one node many
12
+ # events, under this action, should get registered.
13
+ #
14
+ #
15
+ # @!attribute [r] text
16
+ # @return [String] the text under the matching node
17
+ #
18
+ # @!attribute [r] attributes
19
+ # @return [Hash] the attributes of the matching node
20
+ #
21
+ # @!attribute [r] element_name
22
+ # @return [Hash] the name of the matching node
23
+ #
24
+ # @!attribute [r] action
25
+ # @return [NodeAction] the underlying NodeAction
26
+ ##
27
+ class Event
28
+ include Activatable
29
+
30
+ attr_reader :text, :attributes, :element_name, :action
31
+
32
+ # Initializes an Event with an associated NodeAction
33
+ #
34
+ # @param action [NodeAction] this event's action
35
+ #
36
+ def initialize(action)
37
+ @action = action
38
+ initialize_options
39
+ initialize_activatable(action.activation_rule)
40
+ end
41
+
42
+ # Appends the argument to the event's text attribute
43
+ #
44
+ # @param text [NodeAction] the text to append
45
+ #
46
+ # @return text [String] the event's text
47
+ def append_text(text)
48
+ @text += text
49
+ @text
50
+ end
51
+
52
+ # Merges the argument hash to the event's attributes
53
+ #
54
+ # @param attrs [Hash] the attributes to merge
55
+ #
56
+ # @return attributes [Hash] the event's attributes
57
+ def merge_attributes(attrs)
58
+ @attributes.merge!(attrs)
59
+ end
60
+
61
+ # Changes the element_name
62
+ #
63
+ # @param name [String] the new element_name
64
+ #
65
+ # @return element_name [String] the event's element_name
66
+ def set_element_name(name)
67
+ @element_name = name
68
+ end
69
+
70
+ # Calls the action with the text, element_name, attributes
71
+ def fire
72
+ action.call(text, element_name, attributes)
73
+ end
74
+
75
+ private
76
+ def initialize_options
77
+ @text = ""
78
+ @element_name = nil
79
+ @attributes = {}
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,122 @@
1
+ require "saxxy/event"
2
+
3
+
4
+ module Saxxy
5
+
6
+ ##
7
+ # @author rubymaniac
8
+ #
9
+ # The Event Registry is in charge of registering new events
10
+ # and firing the event callback whenever a specific event gets
11
+ # deactivated.
12
+ #
13
+ # The registry has an @actions instance variable that holds
14
+ #
15
+ ##
16
+ class EventRegistry
17
+
18
+ # Initializes an empty Event Registry
19
+ #
20
+ def initialize
21
+ clear
22
+ end
23
+
24
+ # Registers an event into the registry by initializing it and setting
25
+ # its element_name and attributes accordingly.
26
+ #
27
+ # @param action [NodeAction] the action under which the event is registered
28
+ # @param name [String] the element_name for the event
29
+ # @param attributes [Hash] the attributes for the event
30
+ #
31
+ # @return event [Event] the registered event
32
+ #
33
+ def register_event_from_action(action, name = nil, attributes = {})
34
+ new_event_for(action).tap do |e|
35
+ e.set_element_name(name)
36
+ e.merge_attributes(attributes)
37
+ self[action] << e
38
+ end
39
+ end
40
+
41
+ # Loops through the active actions (those registered) and takes the last,
42
+ # i.e. active, event.
43
+ #
44
+ # @return events [Array] all the active events
45
+ #
46
+ def events
47
+ @actions.values.map(&:last)
48
+ end
49
+
50
+ # Appends the text on every active event
51
+ #
52
+ # @param text [String] the text to append
53
+ #
54
+ # @return events [Array] all the active events
55
+ #
56
+ def push_text(text)
57
+ send_on_each_event(:append_text, text)
58
+ end
59
+
60
+ # Deactivate the active events on a specific node
61
+ #
62
+ # @param element_name [String] the element name
63
+ #
64
+ # @return events [Array] all the active events
65
+ #
66
+ def deactivate_events_on(element_name)
67
+ send_on_each_event(:deactivate_on, element_name)
68
+ end
69
+
70
+ # Activate the active events on a specific node. This is done
71
+ # in order to increase the events' internal counter.
72
+ #
73
+ # @param element_name [String] nodes' element name
74
+ # @param attributes [Hash] nodes' attributes
75
+ #
76
+ # @return events [Array] all the active events
77
+ #
78
+ def activate_events_on(element_name, attributes)
79
+ send_on_each_event(:activate_on, element_name, attributes)
80
+ end
81
+
82
+ # Deletes the provided actions from the @actions without
83
+ # firing any callbacks.
84
+ #
85
+ # @param actions [Array] actions to be removed
86
+ #
87
+ # @return actions [Hash] the registered actions
88
+ #
89
+ def remove_actions!(*actions)
90
+ actions.each { |a| @actions.delete(a) }
91
+ @actions
92
+ end
93
+
94
+ # Clears all registered actions
95
+ #
96
+ def clear
97
+ @actions = {}
98
+ end
99
+
100
+ private
101
+ def new_event_for(action)
102
+ Saxxy::Event.new(action).on_deactivation do |ev|
103
+ ev.fire
104
+ unregister_event(action, ev)
105
+ end
106
+ end
107
+
108
+ def unregister_event(action, event)
109
+ self[action].delete(event)
110
+ self[action].last ? self[action].last.append_text(event.text) : @actions.delete(action)
111
+ end
112
+
113
+ def [](action)
114
+ @actions[action] ||= []
115
+ end
116
+
117
+ def send_on_each_event(method, *args)
118
+ events.each { |e| e.public_send(method, *args) }
119
+ end
120
+ end
121
+
122
+ end