lib/ruby_speech/generic_element.rb
require 'active_support/core_ext/class/attribute'
module RubySpeech
module GenericElement
def self.included(klass)
klass.class_attribute :registered_ns, :registered_name, :defaults
klass.extend ClassMethods
end
module ClassMethods
@@registrations = {}
# Register a new stanza class to a name and/or namespace
#
# This registers a namespace that is used when looking
# up the class name of the object to instantiate when a new
# stanza is received
#
# @param [#to_s] name the name of the node
#
def register(name)
self.registered_name = name.to_s
self.registered_ns = namespace
@@registrations[[self.registered_name, self.registered_ns]] = self
end
# Find the class to use given the name and namespace of a stanza
#
# @param [#to_s] name the name to lookup
#
# @return [Class, nil] the class appropriate for the name
def class_from_registration(name)
@@registrations[[name.to_s, namespace]]
end
# Import an XML::Node to the appropriate class
#
# Looks up the class the node should be then creates it based on the
# elements of the XML::Node
# @param [XML::Node] node the node to import
# @return the appropriate object based on the node name and namespace
def import(node)
node = Nokogiri::XML.parse(node, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root unless node.is_a?(Nokogiri::XML::Node) || node.is_a?(GenericElement)
return node.content if node.is_a?(Nokogiri::XML::Text)
klass = class_from_registration node.node_name
if klass && klass != self
klass.import node
else
new(node.document).inherit node
end
end
end
def initialize(doc, atts = nil, &block)
@doc = doc
build atts, &block if atts || block_given?
end
attr_writer :parent
def node
@node || create_node
end
def parent
@parent || super
end
def inherit(node)
self.parent = node.parent
@node = node
self
end
def build(atts, &block)
mass_assign atts
block_return = eval_dsl_block &block
string block_return if block_return.is_a?(String) && !block_return.length.zero?
end
def version
read_attr :version
end
def version=(other)
self[:version] = other
end
##
# @return [String] the base URI to which relative URLs are resolved
#
def base_uri
read_attr 'xml:base'
end
##
# @param [String] uri the base URI to which relative URLs are resolved
#
def base_uri=(uri)
self['xml:base'] = uri
end
def +(other)
new_doc = Nokogiri::XML::Document.new
self.class.new(new_doc).tap do |new_element|
new_doc.root = new_element.node
string_types = [String, Nokogiri::XML::Text]
include_spacing = string_types.include?(self.nokogiri_children.last.class) && string_types.include?(other.nokogiri_children.first.class)
if Nokogiri.jruby?
new_element.add_child self.clone.nokogiri_children
new_element << " " if include_spacing
new_element.add_child other.clone.nokogiri_children
else
# TODO: This is yucky because it requires serialization
new_element.add_child self.nokogiri_children.to_xml
new_element << " " if include_spacing
new_element.add_child other.nokogiri_children.to_xml
end
end
end
def eval_dsl_block(&block)
return unless block_given?
@block_binding = eval "self", block.binding
instance_eval &block
end
def children(type = nil, attributes = nil)
if type
expression = namespace_href ? 'ns:' : ''
expression << type.to_s
expression << '[' << attributes.inject([]) do |h, (key, value)|
h << "@#{key}='#{value}'"
end.join(',') << ']' if attributes
xpath expression, :ns => self.class.namespace
else
super()
end.map { |c| self.class.import c }
end
def nokogiri_children
node.children
end
def <<(other)
case other
when GenericElement
super other.node
when String
string other
else
super other
end
end
def embed(other)
case other
when String
string other
when self.class.root_element
other.children.each do |child|
self << child
end
when self.class.module::Element
self << other
else
raise ArgumentError, "Can only embed a String or a #{self.class.module} element, not a #{other.class}"
end
end
def string(other)
self << Nokogiri::XML::Text.new(other.to_s, document)
end
def clone
self.class.import to_xml
end
def traverse(&block)
nokogiri_children.each { |j| j.traverse &block }
block.call self
end
# Helper method to read an attribute
#
# @param [#to_sym] attr_name the name of the attribute
# @param [String, Symbol, nil] to_call the name of the method to call on
# the returned value
# @return nil or the value
def read_attr(attr_name, to_call = nil)
val = self[attr_name]
val && to_call ? val.__send__(to_call) : val
end
# Helper method to write a value to an attribute
#
# @param [#to_sym] attr_name the name of the attribute
# @param [#to_s] value the value to set the attribute to
def write_attr(attr_name, value, to_call = nil)
self[attr_name] = value && to_call ? value.__send__(to_call) : value
end
# Attach a namespace to the node
#
# @overload namespace=(ns)
# Attach an already created XML::Namespace
# @param [XML::Namespace] ns the namespace object
# @overload namespace=(ns)
# Create a new namespace and attach it
# @param [String] ns the namespace uri
def namespace=(namespaces)
case namespaces
when Nokogiri::XML::Namespace
super namespaces
when String
ns = self.add_namespace nil, namespaces
super ns
end
end
# Helper method to get the node's namespace
#
# @return [XML::Namespace, nil] The node's namespace object if it exists
def namespace_href
namespace.href if namespace
end
# The node as XML
#
# @return [String] XML representation of the node
def inspect
self.to_xml
end
def to_s
to_xml
end
# Check that a set of fields are equal between nodes
#
# @param [Node] other the other node to compare against
# @param [*#to_s] fields the set of fields to compare
# @return [Fixnum<-1,0,1>]
def eql?(o, *fields)
o.is_a?(self.class) && ([:content, :children] + fields).all? { |f| self.__send__(f) == o.__send__(f) }
end
# @private
def ==(o)
eql?(o)
end
def create_node
@node = Nokogiri::XML::Node.new self.class.registered_name, @doc
mass_assign self.class.defaults
@node
end
def mass_assign(attrs)
attrs.each_pair { |k, v| send :"#{k}=", v } if attrs
end
def method_missing(method_name, *args, &block)
if node.respond_to?(method_name)
return node.send method_name, *args, &block
end
const_name = method_name.to_s.sub('ssml_', '').gsub('_', '-')
if const = self.class.class_from_registration(const_name)
embed const.new(self.document, *args, &block)
elsif @block_binding && @block_binding.respond_to?(method_name)
@block_binding.send method_name, *args, &block
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
node.respond_to?(method_name, include_private) || super
end
end # Element
end # RubySpeech