Added Oga::Ruby::Node

This class will be used for building Ruby ASTs that will be generated
based on XPath expressions.
This commit is contained in:
Yorick Peterse 2015-07-02 22:52:07 +02:00
parent c25879f18e
commit 6673f176d8
3 changed files with 280 additions and 0 deletions

View File

@ -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'

147
lib/oga/ruby/node.rb Normal file
View File

@ -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

131
spec/oga/ruby/node_spec.rb Normal file
View File

@ -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