diff --git a/lib/oga/xml/tree_builder.rb b/lib/oga/xml/tree_builder.rb index 1da6d52..32dfad7 100644 --- a/lib/oga/xml/tree_builder.rb +++ b/lib/oga/xml/tree_builder.rb @@ -1,14 +1,21 @@ module Oga module XML ## - # Class for building a DOM tree of XML/HTML nodes. + # The TreeBuilder class turns an AST into a DOM tree. This DOM tree can be + # traversed by requesting child elements, parent elements, etc. # - # @!attribute [r] ast - # @return [Oga::AST::Node] + # Basic usage: + # + # builder = Oga::XML::TreeBuilder.new + # ast = s(:element, ...) + # + # builder.process(ast) # => # # class TreeBuilder < ::AST::Processor - attr_reader :ast - + ## + # @param [Oga::AST::Node] node + # @return [Oga::XML::Document] + # def on_document(node) document = Document.new document.children = process_all(node) @@ -20,10 +27,23 @@ module Oga return document end + ## + # @param [Oga::AST::Node] node + # @return [Oga::XML::Comment] + # def on_comment(node) return Comment.new(:text => node.children[0]) end + ## + # Processes an `(element)` node and its child elements. + # + # An element can have a parent, previous and next element as well as a + # number of child elements. + # + # @param [Oga::AST::Node] node + # @return [Oga::XML::Element] + # def on_element(node) ns, name, attr, *children = *node @@ -32,7 +52,7 @@ module Oga end if children - children = process_all(children) + children = process_all(children.compact) end element = Element.new( @@ -42,38 +62,73 @@ module Oga :children => children ) - element.children.each_with_index do |child, index| - if index > 0 - child.previous = element.children[index - 1] - end - - if index + 1 <= element.children.length - child.next = element.children[index + 1] - end - - child.parent = element - end + process_children(element) return element end + ## + # @param [Oga::AST::Node] node + # @return [Oga::XML::Text] + # def on_text(node) return Text.new(:text => node.children[0]) end + ## + # @param [Oga::AST::Node] node + # @return [Oga::XML::Cdata] + # def on_cdata(node) return Cdata.new(:text => node.children[0]) end + ## + # Converts a `(attributes)` node into a Hash. + # + # @param [Oga::AST::Node] node + # @return [Hash] + # def on_attributes(node) pairs = process_all(node) return Hash[pairs] end + ## + # @param [Oga::AST::Node] node + # @return [Array] + # def on_attribute(node) return *node end + + private + + ## + # Iterates over the child elements of an element and assigns the parent, + # previous and next elements. The supplied object is modified in place. + # + # @param [Oga::XML::Element] element + # + def process_children(element) + amount = element.children.length + + element.children.each_with_index do |child, index| + prev_index = index - 1 + next_index = index + 1 + + if index > 0 + child.previous = element.children[prev_index] + end + + if next_index <= amount + child.next = element.children[next_index] + end + + child.parent = element + end + end end # TreeBuilder end # XML end # Oga diff --git a/spec/oga/xml/tree_builder_spec.rb b/spec/oga/xml/tree_builder_spec.rb new file mode 100644 index 0000000..6b7d66d --- /dev/null +++ b/spec/oga/xml/tree_builder_spec.rb @@ -0,0 +1,174 @@ +require 'spec_helper' + +describe Oga::XML::TreeBuilder do + before do + @builder = described_class.new + end + + context '#on_element' do + context 'simple elements' do + before do + node = s(:element, 'foo', 'p', nil, nil) + @tag = @builder.process(node) + end + + example 'return a Element node' do + @tag.is_a?(Oga::XML::Element).should == true + end + + example 'include the name of the element' do + @tag.name.should == 'p' + end + + example 'include the namespace of the element' do + @tag.namespace.should == 'foo' + end + end + + context 'elements with attributes' do + before do + node = s( + :element, + nil, + 'p', + s(:attributes, s(:attribute, 'key', 'value')), + nil + ) + + @tag = @builder.process(node) + end + + example 'include the name of the element' do + @tag.name.should == 'p' + end + + example 'include the attributes' do + @tag.attributes.should == {'key' => 'value'} + end + end + + context 'elements with parent elements' do + before do + node = s(:element, nil, 'p', nil, s(:element, nil, 'span', nil, nil)) + @tag = @builder.process(node) + end + + example 'set the parent element' do + @tag.children[0].parent.should == @tag + end + end + + context 'elements with next elements' do + before do + node = s( + :element, + nil, + 'p', + nil, + s(:element, nil, 'a', nil, nil), + s(:element, nil, 'span', nil, nil) + ) + + @tag = @builder.process(node) + end + + example 'set the next element' do + @tag.children[0].next.should == @tag.children[1] + end + + example 'do not set the next element for the last element' do + @tag.children[1].next.should == nil + end + end + + context 'elements with previous elements' do + before do + node = s( + :element, + nil, + 'p', + nil, + s(:element, nil, 'a', nil, nil), + s(:element, nil, 'span', nil, nil) + ) + + @tag = @builder.process(node) + end + + example 'set the previous element' do + @tag.children[1].previous.should == @tag.children[0] + end + + example 'do not set the previous element for the first element' do + @tag.children[0].previous.should == nil + end + end + + context 'elements with child elements' do + before do + node = s(:element, nil, 'p', nil, s(:element, nil, 'span', nil, nil)) + @tag = @builder.process(node) + end + + example 'include the name of the element' do + @tag.name.should == 'p' + end + + example 'include the child element' do + @tag.children[0].is_a?(Oga::XML::Element).should == true + end + + example 'include the name of the child element' do + @tag.children[0].name.should == 'span' + end + end + end + + context '#on_text' do + before do + node = s(:text, 'Hello') + @tag = @builder.process(node) + end + + example 'return a Text node' do + @tag.is_a?(Oga::XML::Text).should == true + end + + example 'include the text of the node' do + @tag.text.should == 'Hello' + end + end + + context '#on_cdata' do + before do + node = s(:cdata, 'Hello') + @tag = @builder.process(node) + end + + example 'return a Cdata node' do + @tag.is_a?(Oga::XML::Cdata).should == true + end + + example 'include the text of the node' do + @tag.text.should == 'Hello' + end + end + + context '#on_attributes' do + before do + @node = s( + :attributes, + s(:attribute, 'foo', 'bar'), + s(:attribute, 'baz', 'wat') + ) + end + + example 'return the attributes as a Hash' do + @builder.process(@node).should == {'foo' => 'bar', 'baz' => 'wat'} + end + + example 'return an empty Hash by default' do + @builder.process(s(:attributes)).should == {} + end + end +end