diff --git a/lib/oga.rb b/lib/oga.rb index 9553712..2be82d3 100644 --- a/lib/oga.rb +++ b/lib/oga.rb @@ -49,6 +49,8 @@ require 'oga/html/parser' require 'oga/html/sax_parser' require 'oga/html/entities' +require 'oga/ruby/node' + require 'oga/xpath/lexer' require 'oga/xpath/parser' require 'oga/xpath/evaluator' diff --git a/lib/oga/ruby/node.rb b/lib/oga/ruby/node.rb new file mode 100644 index 0000000..36f52d3 --- /dev/null +++ b/lib/oga/ruby/node.rb @@ -0,0 +1,147 @@ +module Oga + module Ruby + ## + # Class representing a single node in a Ruby AST. + # + # The setup of this class is roughly based on the "ast" Gem. The "ast" Gem + # is not used for this class as it provides too many methods that might + # conflict with this class' {#method_missing}. + # + # ASTs can be built by creating a node and then chaining various method + # calls together. For example, the following could be used to build an "if" + # statement: + # + # number1 = Node.new(:lit, %w{10}) + # number2 = Node.new(:lit, %w{20}) + # + # (number2 > number1).if_true do + # Node.new(:lit, %w{30}) + # end + # + # When serialized to Ruby this would roughly lead to the following code: + # + # if 20 > 10 + # 30 + # end + # + # @private + # + class Node < BasicObject + # @return [Symbol] + attr_reader :type + + # @param [Symbol] type + # @param [Array] children + def initialize(type, children = []) + @type = type.to_sym + @children = children + end + + # @return [Array] + def to_a + @children + end + + ## + # Returns an assignment node. + # + # @param [Oga::Ruby::Node] other + # @return [Oga::Ruby::Node] + # + def assign(other) + Node.new(:assign, [self, other]) + end + + ## + # Returns an equality expression node. + # + # @param [Oga::Ruby::Node] other + # @return [Oga::Ruby::Node] + # + def eq(other) + Node.new(:eq, [self, other]) + end + + ## + # Returns a boolean "and" node. + # + # @param [Oga::Ruby::Node] other + # @return [Oga::Ruby::Node] + # + def and(other) + Node.new(:and, [self, other]) + end + + ## + # Returns a boolean "or" node. + # + # @param [Oga::Ruby::Node] other + # @return [Oga::Ruby::Node] + # + def or(other) + Node.new(:or, [self, other]) + end + + ## + # Returns a node for Ruby's "is_a?" method. + # + # @param [Class] klass + # @return [Oga::Ruby::Node] + # + def is_a?(klass) + Node.new(:send, [self, 'is_a?', Node.new(:lit, [klass.to_s])]) + end + + ## + # Wraps the current node in a block. + # + # @param [Array] args Arguments (as Node instances) to pass to the block. + # @return [Oga::Ruby::Node] + # + def add_block(*args) + Node.new(:block, [self, args, yield]) + end + + ## + # Wraps the current node in an if statement node. + # + # The body of this statement is set to the return value of the supplied + # block. + # + # @return [Oga::Ruby::Node] + # + def if_true + Node.new(:if, [self, yield]) + end + + ## + # Chains two nodes together. + # + # @param [Oga::Ruby::Node] other + # @return [Oga::Ruby::Node] + # + def followed_by(other) + Node.new(:begin, [self, other]) + end + + ## + # Returns a node for a method call. + # + # @param [Symbol] name The name of the method to call. + # + # @param [Array] args Any arguments (as Node instances) to pass to the + # method. + # + # @return [Oga::Ruby::Node] + # + def method_missing(name, *args) + Node.new(:send, [self, name.to_s, *args]) + end + + # @return [String] + def inspect + "(#{type} #{@children.map(&:inspect).join(' ')})" + end + end # Node + end # Ruby +end # Oga diff --git a/spec/oga/ruby/node_spec.rb b/spec/oga/ruby/node_spec.rb new file mode 100644 index 0000000..744f3b7 --- /dev/null +++ b/spec/oga/ruby/node_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +describe Oga::Ruby::Node do + describe '#type' do + it 'returns the type of the node as a Symbol' do + node = described_class.new('foo') + + node.type.should == :foo + end + end + + describe '#to_a' do + it 'returns the children of the Node as an Array' do + node = described_class.new(:foo, %w{10}) + + node.to_a.should == %w{10} + end + end + + describe '#assign' do + it 'returns an assignment Node' do + left = described_class.new(:lit, %w{number}) + right = described_class.new(:lit, %w{10}) + node = left.assign(right) + + node.type.should == :assign + node.to_a.should == [left, right] + end + end + + describe '#eq' do + it 'returns an equality Node' do + left = described_class.new(:lit, %w{number}) + right = described_class.new(:lit, %w{10}) + node = left.eq(right) + + node.type.should == :eq + node.to_a.should == [left, right] + end + end + + describe '#and' do + it 'returns a boolean and Node' do + left = described_class.new(:lit, %w{number}) + right = described_class.new(:lit, %w{10}) + node = left.and(right) + + node.type.should == :and + node.to_a.should == [left, right] + end + end + + describe '#or' do + it 'returns a boolean or Node' do + left = described_class.new(:lit, %w{number}) + right = described_class.new(:lit, %w{10}) + node = left.or(right) + + node.type.should == :or + node.to_a.should == [left, right] + end + end + + describe '#is_a?' do + it 'returns a is_a? call Node' do + left = described_class.new(:lit, %w{number}) + node = left.is_a?(String) + + node.type.should == :send + + node.to_a[0].should == left + node.to_a[1].should == 'is_a?' + + node.to_a[2].type.should == :lit + node.to_a[2].to_a.should == %w{String} + end + end + + describe '#add_block' do + it 'returns a block Node' do + left = described_class.new(:lit, %w{number}) + arg = described_class.new(:lit, %w{10}) + body = described_class.new(:lit, %w{20}) + block = left.add_block(arg) { body } + + block.type.should == :block + block.to_a.should == [left, [arg], body] + end + end + + describe '#if_true' do + it 'returns an if-statement Node' do + condition = described_class.new(:lit, %w{number}) + body = described_class.new(:lit, %w{10}) + statement = condition.if_true { body } + + statement.type.should == :if + statement.to_a.should == [condition, body] + end + end + + describe '#followed_by' do + it 'returns a Node chaining two nodes together' do + node1 = described_class.new(:lit, %w{A}) + node2 = described_class.new(:lit, %w{B}) + joined = node1.followed_by(node2) + + joined.type.should == :begin + joined.to_a.should == [node1, node2] + end + end + + describe '#method_missing' do + it 'returns a send Node' do + receiver = described_class.new(:lit, %w{foo}) + arg = described_class.new(:lit, %w{10}) + call = receiver.foo(arg) + + call.type.should == :send + call.to_a.should == [receiver, 'foo', arg] + end + end + + describe '#inspect' do + it 'returns a String' do + node = described_class.new(:lit, %w{10}) + + node.inspect.should == '(lit "10")' + end + end +end