diff --git a/CHANGELOG.md b/CHANGELOG.md
index 431111c..ce3768c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 0.2.0
+ Feature: SSML elements can now be imported from a Nokogiri Node or a string
+ Feature: SSML elements now respond to #children with an array of SSML elements, rather than a Nokogiri NodeSet
+ Bugfix/Feature: Comparing SSML elements now compares children
+
# 0.1.5
Feature: Now added support for SSML
diff --git a/README.md b/README.md
index c15b3fc..10ac622 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ speak = RubySpeech::SSML.draw do
end
end
end
-speak.to\_s
+speak.to_s
```
becomes:
diff --git a/lib/ruby_speech/ssml.rb b/lib/ruby_speech/ssml.rb
index 59b78eb..cc9fc1b 100644
--- a/lib/ruby_speech/ssml.rb
+++ b/lib/ruby_speech/ssml.rb
@@ -13,6 +13,8 @@ module SSML
InvalidChildError = Class.new StandardError
+ SSML_NAMESPACE = 'http://www.w3.org/2001/10/synthesis'
+
def self.draw(&block)
Speak.new.tap do |speak|
block_return = speak.instance_eval(&block) if block_given?
diff --git a/lib/ruby_speech/ssml/audio.rb b/lib/ruby_speech/ssml/audio.rb
index 84c0b37..a11c6da 100644
--- a/lib/ruby_speech/ssml/audio.rb
+++ b/lib/ruby_speech/ssml/audio.rb
@@ -14,7 +14,9 @@ module SSML
#
class Audio < Element
- VALID_CHILD_TYPES = [String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
+ register :audio
+
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text,String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
##
# Create a new SSML audio element
diff --git a/lib/ruby_speech/ssml/break.rb b/lib/ruby_speech/ssml/break.rb
index 6d26a8d..f080557 100644
--- a/lib/ruby_speech/ssml/break.rb
+++ b/lib/ruby_speech/ssml/break.rb
@@ -7,6 +7,8 @@ module SSML
#
class Break < Element
+ register :break
+
VALID_STRENGTHS = [:none, :'x-weak', :weak, :medium, :strong, :'x-strong'].freeze
##
diff --git a/lib/ruby_speech/ssml/element.rb b/lib/ruby_speech/ssml/element.rb
index e9272f7..0e51d80 100644
--- a/lib/ruby_speech/ssml/element.rb
+++ b/lib/ruby_speech/ssml/element.rb
@@ -1,6 +1,62 @@
+require 'active_support/core_ext/class/inheritable_attributes'
+
module RubySpeech
module SSML
class Element < Niceogiri::XML::Node
+ @@registrations = {}
+
+ class_inheritable_accessor :registered_ns, :registered_name
+
+ # 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 self.register(name)
+ self.registered_name = name.to_s
+ self.registered_ns = SSML_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 self.class_from_registration(name)
+ @@registrations[[name.to_s, SSML_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 self.import(node)
+ klass = class_from_registration(node.element_name)
+ event = if klass && klass != self
+ klass.import node
+ else
+ new.inherit node
+ end
+ end
+
+ # Inherit the attributes and children of an XML::Node
+ #
+ # @param [XML::Node] node the node to inherit
+ # @return [self]
+ def inherit(node)
+ inherit_attrs node.attributes
+ node.children.each do |c|
+ self << c.dup
+ end
+ self
+ end
+
def self.new(element_name, atts = {}, &block)
super(element_name) do |new_node|
atts.each_pair { |k, v| new_node.send :"#{k}=", v }
@@ -9,6 +65,10 @@ def self.new(element_name, atts = {}, &block)
end
end
+ def children
+ super.reject { |c| c.is_a?(Nokogiri::XML::Text) }.map { |c| Element.import c }
+ end
+
def method_missing(method_name, *args, &block)
const_name = method_name.to_s.sub('ssml', '').titleize.gsub(' ', '')
const = SSML.const_get const_name
@@ -24,7 +84,7 @@ def method_missing(method_name, *args, &block)
end
def eql?(o, *args)
- super o, :content, *args
+ super o, :content, :children, *args
end
end # Element
end # SSML
diff --git a/lib/ruby_speech/ssml/emphasis.rb b/lib/ruby_speech/ssml/emphasis.rb
index ac441fa..d1ca60e 100644
--- a/lib/ruby_speech/ssml/emphasis.rb
+++ b/lib/ruby_speech/ssml/emphasis.rb
@@ -7,8 +7,10 @@ module SSML
#
class Emphasis < Element
+ register :emphasis
+
VALID_LEVELS = [:strong, :moderate, :none, :reduced].freeze
- VALID_CHILD_TYPES = [String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text, String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
##
# Create a new SSML emphasis element
diff --git a/lib/ruby_speech/ssml/prosody.rb b/lib/ruby_speech/ssml/prosody.rb
index c399a35..a065e61 100644
--- a/lib/ruby_speech/ssml/prosody.rb
+++ b/lib/ruby_speech/ssml/prosody.rb
@@ -13,10 +13,12 @@ module SSML
#
class Prosody < Element
+ register :prosody
+
VALID_PITCHES = [:'x-low', :low, :medium, :high, :'x-high', :default].freeze
VALID_VOLUMES = [:silent, :'x-soft', :soft, :medium, :loud, :'x-loud', :default].freeze
VALID_RATES = [:'x-slow', :slow, :medium, :fast, :'x-fast', :default].freeze
- VALID_CHILD_TYPES = [String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text, String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
##
# Create a new SSML prosody element
@@ -150,6 +152,7 @@ def duration=(t)
#
def volume
value = read_attr :volume
+ return unless value
if VALID_VOLUMES.include?(value.to_sym)
value.to_sym
else
diff --git a/lib/ruby_speech/ssml/say_as.rb b/lib/ruby_speech/ssml/say_as.rb
index 3150a0f..43a3268 100644
--- a/lib/ruby_speech/ssml/say_as.rb
+++ b/lib/ruby_speech/ssml/say_as.rb
@@ -22,7 +22,9 @@ module SSML
#
class SayAs < Element
- VALID_CHILD_TYPES = [String].freeze
+ register :'say-as'
+
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text, String].freeze
##
# Create a new SSML say-as element
@@ -31,8 +33,8 @@ class SayAs < Element
#
# @return [Prosody] an element for use in an SSML document
#
- def self.new(interpret_as, atts = {}, &block)
- super 'say-as', atts.merge(:interpret_as => interpret_as), &block
+ def self.new(atts = {}, &block)
+ super 'say-as', atts, &block
end
##
diff --git a/lib/ruby_speech/ssml/speak.rb b/lib/ruby_speech/ssml/speak.rb
index 0d1f0b9..8fa1af7 100644
--- a/lib/ruby_speech/ssml/speak.rb
+++ b/lib/ruby_speech/ssml/speak.rb
@@ -8,7 +8,9 @@ module SSML
class Speak < Element
include XML::Language
- VALID_CHILD_TYPES = [String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
+ register :speak
+
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text, String, Audio, Break, Emphasis, Prosody, SayAs, Voice].freeze
##
# Create a new SSML speak root element
@@ -20,7 +22,7 @@ class Speak < Element
def self.new(atts = {}, &block)
new_node = super('speak', atts)
new_node[:version] = '1.0'
- new_node.namespace = 'http://www.w3.org/2001/10/synthesis'
+ new_node.namespace = SSML_NAMESPACE
new_node.language ||= "en-US"
new_node.instance_eval &block if block_given?
new_node
@@ -53,8 +55,9 @@ def to_doc
def +(other)
self.class.new(:base_uri => base_uri).tap do |new_speak|
- new_speak.children = self.children + other.children
- new_speak.children.each { |c| c.namespace = new_speak.namespace }
+ (self.children + other.children).each do |child|
+ new_speak << child
+ end
end
end
diff --git a/lib/ruby_speech/ssml/voice.rb b/lib/ruby_speech/ssml/voice.rb
index 61d04f6..7512575 100644
--- a/lib/ruby_speech/ssml/voice.rb
+++ b/lib/ruby_speech/ssml/voice.rb
@@ -8,8 +8,10 @@ module SSML
class Voice < Element
include XML::Language
+ register :voice
+
VALID_GENDERS = [:male, :female, :neutral].freeze
- VALID_CHILD_TYPES = [String, Audio,Break, Emphasis, Prosody, SayAs, Voice].freeze
+ VALID_CHILD_TYPES = [Nokogiri::XML::Element, Nokogiri::XML::Text, String, Audio,Break, Emphasis, Prosody, SayAs, Voice].freeze
##
# Create a new SSML voice element
diff --git a/lib/ruby_speech/version.rb b/lib/ruby_speech/version.rb
index 2eb3377..86c8781 100644
--- a/lib/ruby_speech/version.rb
+++ b/lib/ruby_speech/version.rb
@@ -1,3 +1,3 @@
module RubySpeech
- VERSION = "0.1.5"
+ VERSION = "0.2.0"
end
diff --git a/spec/ruby_speech/ssml/audio_spec.rb b/spec/ruby_speech/ssml/audio_spec.rb
index c546852..e0ed363 100644
--- a/spec/ruby_speech/ssml/audio_spec.rb
+++ b/spec/ruby_speech/ssml/audio_spec.rb
@@ -12,6 +12,21 @@ module SSML
its(:content) { should == 'Hello' }
end
+ it 'registers itself' do
+ Element.class_from_registration(:audio).should == Audio
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Audio }
+
+ its(:src) { should == 'http://whatever.you-say-boss.com' }
+ its(:content) { should == 'Hello' }
+ end
+
describe "#src" do
before { subject.src = 'http://whatever.you-say-boss.com' }
diff --git a/spec/ruby_speech/ssml/break_spec.rb b/spec/ruby_speech/ssml/break_spec.rb
index a39c01c..edff73f 100644
--- a/spec/ruby_speech/ssml/break_spec.rb
+++ b/spec/ruby_speech/ssml/break_spec.rb
@@ -12,6 +12,21 @@ module SSML
its(:time) { should == 3.seconds }
end
+ it 'registers itself' do
+ Element.class_from_registration(:break).should == Break
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Break }
+
+ its(:strength) { should == :strong }
+ its(:time) { should == 3.seconds }
+ end
+
describe "#strength" do
before { subject.strength = :strong }
diff --git a/spec/ruby_speech/ssml/emphasis_spec.rb b/spec/ruby_speech/ssml/emphasis_spec.rb
index 5e796a7..bb2b488 100644
--- a/spec/ruby_speech/ssml/emphasis_spec.rb
+++ b/spec/ruby_speech/ssml/emphasis_spec.rb
@@ -11,6 +11,20 @@ module SSML
its(:level) { should == :strong }
end
+ it 'registers itself' do
+ Element.class_from_registration(:emphasis).should == Emphasis
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Emphasis }
+
+ its(:level) { should == :strong }
+ end
+
describe "#level" do
before { subject.level = :strong }
diff --git a/spec/ruby_speech/ssml/prosody_spec.rb b/spec/ruby_speech/ssml/prosody_spec.rb
index eb5f0d7..44ddb08 100644
--- a/spec/ruby_speech/ssml/prosody_spec.rb
+++ b/spec/ruby_speech/ssml/prosody_spec.rb
@@ -16,6 +16,25 @@ module SSML
its(:volume) { should == :loud }
end
+ it 'registers itself' do
+ Element.class_from_registration(:prosody).should == Prosody
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Prosody }
+
+ its(:pitch) { should == :medium }
+ its(:contour) { should == 'something' }
+ its(:range) { should == '20Hz' }
+ its(:rate) { should == 2 }
+ its(:duration) { should == 10.seconds }
+ its(:volume) { should == :loud }
+ end
+
describe "#pitch" do
context "with a pre-defined value" do
before { subject.pitch = :medium }
diff --git a/spec/ruby_speech/ssml/say_as_spec.rb b/spec/ruby_speech/ssml/say_as_spec.rb
index 2e8dd82..e6e3638 100644
--- a/spec/ruby_speech/ssml/say_as_spec.rb
+++ b/spec/ruby_speech/ssml/say_as_spec.rb
@@ -3,7 +3,7 @@
module RubySpeech
module SSML
describe SayAs do
- subject { SayAs.new 'one', :format => 'two', :detail => 'three' }
+ subject { SayAs.new :interpret_as => 'one', :format => 'two', :detail => 'three' }
its(:name) { should == 'say-as' }
@@ -11,38 +11,48 @@ module SSML
its(:format) { should == 'two' }
its(:detail) { should == 'three' }
- describe "without interpret_as" do
- it "should raise an ArgumentError" do
- expect { SayAs.new }.should raise_error(ArgumentError)
- end
+ it 'registers itself' do
+ Element.class_from_registration(:'say-as').should == SayAs
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of SayAs }
+
+ its(:interpret_as) { should == 'one' }
+ its(:format) { should == 'two' }
+ its(:detail) { should == 'three' }
end
describe "comparing objects" do
it "should be equal if the content, interpret_as, format, age, variant, name are the same" do
- SayAs.new('jp', :format => 'foo', :detail => 'bar', :content => "hello").should == SayAs.new('jp', :format => 'foo', :detail => 'bar', :content => "hello")
+ SayAs.new(:interpret_as => 'jp', :format => 'foo', :detail => 'bar', :content => "hello").should == SayAs.new(:interpret_as => 'jp', :format => 'foo', :detail => 'bar', :content => "hello")
end
describe "when the content is different" do
it "should not be equal" do
- SayAs.new('jp', :content => "Hello").should_not == SayAs.new('jp', :content => "Hello there")
+ SayAs.new(:interpret_as => 'jp', :content => "Hello").should_not == SayAs.new(:interpret_as => 'jp', :content => "Hello there")
end
end
describe "when the interpret_as is different" do
it "should not be equal" do
- SayAs.new("Hello").should_not == SayAs.new("Hello there")
+ SayAs.new(:interpret_as => "Hello").should_not == SayAs.new(:interpret_as => "Hello there")
end
end
describe "when the format is different" do
it "should not be equal" do
- SayAs.new('jp', :format => 'foo').should_not == SayAs.new('jp', :format => 'bar')
+ SayAs.new(:interpret_as => 'jp', :format => 'foo').should_not == SayAs.new(:interpret_as => 'jp', :format => 'bar')
end
end
describe "when the detail is different" do
it "should not be equal" do
- SayAs.new('jp', :detail => 'foo').should_not == SayAs.new('jp', :detail => 'bar')
+ SayAs.new(:interpret_as => 'jp', :detail => 'foo').should_not == SayAs.new(:interpret_as => 'jp', :detail => 'bar')
end
end
end
diff --git a/spec/ruby_speech/ssml/speak_spec.rb b/spec/ruby_speech/ssml/speak_spec.rb
index 37b574f..7c6caea 100644
--- a/spec/ruby_speech/ssml/speak_spec.rb
+++ b/spec/ruby_speech/ssml/speak_spec.rb
@@ -15,6 +15,21 @@ module SSML
its(:base_uri) { should == 'blah' }
end
+ it 'registers itself' do
+ Element.class_from_registration(:speak).should == Speak
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Speak }
+
+ its(:language) { pending; should == 'jp' }
+ its(:base_uri) { should == 'blah' }
+ end
+
describe "#language" do
before { subject.language = 'jp' }
@@ -49,6 +64,17 @@ module SSML
Speak.new(:base_uri => 'foo').should_not == Speak.new(:base_uri => 'bar')
end
end
+
+ describe "when the children are different" do
+ it "should not be equal" do
+ s1 = Speak.new
+ s1 << SayAs.new(:interpret_as => 'date')
+ s2 = Speak.new
+ s2 << SayAs.new(:interpret_as => 'time')
+
+ s1.should_not == s2
+ end
+ end
end
it "should allow creating child SSML elements" do
diff --git a/spec/ruby_speech/ssml/voice_spec.rb b/spec/ruby_speech/ssml/voice_spec.rb
index 4491300..d5e26d9 100644
--- a/spec/ruby_speech/ssml/voice_spec.rb
+++ b/spec/ruby_speech/ssml/voice_spec.rb
@@ -16,6 +16,24 @@ module SSML
its(:name) { should == 'paul' }
end
+ it 'registers itself' do
+ Element.class_from_registration(:voice).should == Voice
+ end
+
+ describe "from a document" do
+ let(:document) { '' }
+
+ subject { Element.import parse_xml(document).root }
+
+ it { should be_instance_of Voice }
+
+ its(:language) { should == 'jp' }
+ its(:gender) { should == :male }
+ its(:age) { should == 25 }
+ its(:variant) { should == 2 }
+ its(:name) { should == 'paul' }
+ end
+
describe "#language" do
before { subject.language = 'jp' }
diff --git a/spec/ruby_speech/ssml_spec.rb b/spec/ruby_speech/ssml_spec.rb
index 1638d53..2463715 100644
--- a/spec/ruby_speech/ssml_spec.rb
+++ b/spec/ruby_speech/ssml_spec.rb
@@ -44,13 +44,13 @@ module RubySpeech
doc = RubySpeech::SSML.draw do
voice :gender => :male, :name => 'fred' do
string "Hi, I'm Fred. The time is currently "
- say_as 'date', :format => 'dmy' do
+ say_as :interpret_as => 'date', :format => 'dmy' do
"01/02/1960"
end
end
end
voice = SSML::Voice.new(:gender => :male, :name => 'fred', :content => "Hi, I'm Fred. The time is currently ")
- voice << SSML::SayAs.new('date', :format => 'dmy', :content => "01/02/1960")
+ voice << SSML::SayAs.new(:interpret_as => 'date', :format => 'dmy', :content => "01/02/1960")
expected_doc = SSML::Speak.new
expected_doc << voice
doc.should == expected_doc
@@ -78,7 +78,7 @@ module RubySpeech
audio :src => "hello"
emphasis
prosody
- say_as 'date'
+ say_as :interpret_as => 'date'
voice
end
emphasis do
@@ -87,7 +87,7 @@ module RubySpeech
audio :src => "hello"
emphasis
prosody
- say_as 'date'
+ say_as :interpret_as => 'date'
voice
end
prosody :rate => :slow do
@@ -96,15 +96,15 @@ module RubySpeech
audio :src => "hello"
emphasis
prosody
- say_as 'date'
+ say_as :interpret_as => 'date'
voice
end
- say_as 'date', :format => 'dmy' do
+ say_as :interpret_as => 'date', :format => 'dmy' do
"01/02/1960"
end
voice :gender => :male, :name => 'fred' do
string "Hi, I'm Fred. The time is currently "
- say_as 'date', :format => 'dmy' do
+ say_as :interpret_as => 'date', :format => 'dmy' do
"01/02/1960"
end
ssml_break
@@ -127,7 +127,7 @@ module RubySpeech
audio << SSML::Audio.new(:src => "hello")
audio << SSML::Emphasis.new
audio << SSML::Prosody.new
- audio << SSML::SayAs.new('date')
+ audio << SSML::SayAs.new(:interpret_as => 'date')
audio << SSML::Voice.new
expected_doc << audio
emphasis = SSML::Emphasis.new(:content => "HELLO?")
@@ -135,7 +135,7 @@ module RubySpeech
emphasis << SSML::Audio.new(:src => "hello")
emphasis << SSML::Emphasis.new
emphasis << SSML::Prosody.new
- emphasis << SSML::SayAs.new('date')
+ emphasis << SSML::SayAs.new(:interpret_as => 'date')
emphasis << SSML::Voice.new
expected_doc << emphasis
prosody = SSML::Prosody.new(:rate => :slow, :content => "H...E...L...L...O?")
@@ -143,12 +143,12 @@ module RubySpeech
prosody << SSML::Audio.new(:src => "hello")
prosody << SSML::Emphasis.new
prosody << SSML::Prosody.new
- prosody << SSML::SayAs.new('date')
+ prosody << SSML::SayAs.new(:interpret_as => 'date')
prosody << SSML::Voice.new
expected_doc << prosody
- expected_doc << SSML::SayAs.new('date', :format => 'dmy', :content => "01/02/1960")
+ expected_doc << SSML::SayAs.new(:interpret_as => 'date', :format => 'dmy', :content => "01/02/1960")
voice = SSML::Voice.new(:gender => :male, :name => 'fred', :content => "Hi, I'm Fred. The time is currently ")
- voice << SSML::SayAs.new('date', :format => 'dmy', :content => "01/02/1960")
+ voice << SSML::SayAs.new(:interpret_as => 'date', :format => 'dmy', :content => "01/02/1960")
voice << SSML::Break.new
voice << SSML::Audio.new(:src => "hello")
voice << SSML::Emphasis.new(:content => "I'm so old")
@@ -158,5 +158,27 @@ module RubySpeech
doc.should == expected_doc
end
end
+
+ describe "importing nested tags" do
+ let :voice do
+ SSML::Voice.new(:gender => :male, :name => 'fred', :content => "Hi, I'm Fred. The time is currently ").tap do |voice|
+ voice << SSML::SayAs.new(:interpret_as => 'date', :format => 'dmy', :content => "01/02/1960")
+ end
+ end
+
+ let :document do
+ SSML::Speak.new.tap { |doc| doc << voice }.to_s
+ end
+
+ subject { SSML::Element.import parse_xml(document).root }
+
+ it "should work" do
+ lambda { subject }.should_not raise_error
+ end
+
+ it { should be_a SSML::Speak }
+
+ its(:children) { should == [voice] }
+ end
end
end