punchblock 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +7 -0
- data/lib/punchblock.rb +1 -1
- data/lib/punchblock/component.rb +2 -0
- data/lib/punchblock/component/input.rb +9 -1
- data/lib/punchblock/component/output.rb +1 -1
- data/lib/punchblock/component/receive_fax.rb +24 -0
- data/lib/punchblock/component/send_fax.rb +62 -0
- data/lib/punchblock/connection/asterisk.rb +1 -1
- data/lib/punchblock/event/complete.rb +15 -1
- data/lib/punchblock/translator/asterisk.rb +30 -15
- data/lib/punchblock/translator/asterisk/call.rb +13 -27
- data/lib/punchblock/translator/asterisk/component.rb +4 -7
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +1 -5
- data/lib/punchblock/translator/asterisk/component/composed_prompt.rb +8 -9
- data/lib/punchblock/translator/asterisk/component/input.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/mrcp_prompt.rb +9 -9
- data/lib/punchblock/translator/asterisk/component/output.rb +134 -39
- data/lib/punchblock/translator/asterisk/component/record.rb +2 -3
- data/lib/punchblock/translator/asterisk/component/stop_by_redirect.rb +2 -3
- data/lib/punchblock/translator/dtmf_recognizer.rb +2 -4
- data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +6 -1
- data/lib/punchblock/translator/freeswitch/component/flite_output.rb +1 -1
- data/lib/punchblock/translator/freeswitch/component/output.rb +12 -10
- data/lib/punchblock/translator/freeswitch/component/tts_output.rb +1 -1
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/component/input_spec.rb +91 -0
- data/spec/punchblock/component/output_spec.rb +1 -2
- data/spec/punchblock/component/receive_fax_spec.rb +111 -0
- data/spec/punchblock/component/send_fax_spec.rb +110 -0
- data/spec/punchblock/connection/asterisk_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +53 -79
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +0 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component/composed_prompt_spec.rb +2 -2
- data/spec/punchblock/translator/asterisk/component/input_spec.rb +6 -6
- data/spec/punchblock/translator/asterisk/component/mrcp_native_prompt_spec.rb +3 -3
- data/spec/punchblock/translator/asterisk/component/mrcp_prompt_spec.rb +9 -11
- data/spec/punchblock/translator/asterisk/component/output_spec.rb +902 -28
- data/spec/punchblock/translator/asterisk/component/stop_by_redirect_spec.rb +1 -1
- data/spec/punchblock/translator/asterisk/component_spec.rb +2 -9
- data/spec/punchblock/translator/asterisk_spec.rb +42 -94
- data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +5 -5
- data/spec/punchblock/translator/freeswitch/component/output_spec.rb +7 -3
- data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +17 -5
- metadata +67 -61
|
@@ -15,20 +15,16 @@ module Punchblock
|
|
|
15
15
|
send_ref
|
|
16
16
|
rescue RubyAMI::Error
|
|
17
17
|
set_node_response false
|
|
18
|
-
terminate
|
|
19
18
|
rescue ChannelGoneError
|
|
20
19
|
set_node_response ProtocolError.new.setup(:item_not_found, "Could not find a call with ID #{call_id}", call_id)
|
|
21
|
-
terminate
|
|
22
20
|
end
|
|
23
|
-
exclusive :execute
|
|
24
21
|
|
|
25
22
|
def handle_ami_event(event)
|
|
26
23
|
if event.name == 'AsyncAGI' && event['SubEvent'] == 'Exec'
|
|
27
|
-
send_complete_event success_reason(event)
|
|
24
|
+
send_complete_event success_reason(event)
|
|
28
25
|
if @component_node.name == 'ASYNCAGI BREAK' && @call.channel_var('PUNCHBLOCK_END_ON_ASYNCAGI_BREAK')
|
|
29
26
|
@call.handle_hangup_event
|
|
30
27
|
end
|
|
31
|
-
terminate
|
|
32
28
|
end
|
|
33
29
|
end
|
|
34
30
|
|
|
@@ -15,15 +15,15 @@ module Punchblock
|
|
|
15
15
|
|
|
16
16
|
@output_incomplete = true
|
|
17
17
|
|
|
18
|
-
output_component = Output.
|
|
19
|
-
call.register_component output_component
|
|
20
|
-
fut = output_component.
|
|
18
|
+
@output_component = Output.new(output_command, @call)
|
|
19
|
+
call.register_component @output_component
|
|
20
|
+
fut = Celluloid::Future.new { @output_component.execute }
|
|
21
21
|
|
|
22
|
-
case output_command.response
|
|
22
|
+
case @output_command.response
|
|
23
23
|
when Ref
|
|
24
24
|
send_ref
|
|
25
25
|
else
|
|
26
|
-
set_node_response output_command.response
|
|
26
|
+
set_node_response @output_command.response
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
if @component_node.barge_in
|
|
@@ -41,7 +41,7 @@ module Punchblock
|
|
|
41
41
|
|
|
42
42
|
def process_dtmf(digit)
|
|
43
43
|
if @component_node.barge_in && @output_incomplete
|
|
44
|
-
|
|
44
|
+
@output_component.stop_by_redirect Punchblock::Event::Complete::Stop.new
|
|
45
45
|
@barged = true
|
|
46
46
|
end
|
|
47
47
|
super
|
|
@@ -58,14 +58,13 @@ module Punchblock
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def register_dtmf_event_handler
|
|
61
|
-
component = current_actor
|
|
62
61
|
@dtmf_handler_id = call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
63
|
-
|
|
62
|
+
process_dtmf event['Digit']
|
|
64
63
|
end
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
def unregister_dtmf_event_handler
|
|
68
|
-
call.
|
|
67
|
+
call.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
|
|
69
68
|
end
|
|
70
69
|
end
|
|
71
70
|
end
|
|
@@ -17,14 +17,13 @@ module Punchblock
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def register_dtmf_event_handler
|
|
20
|
-
component = current_actor
|
|
21
20
|
call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
22
|
-
|
|
21
|
+
process_dtmf event['Digit']
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
def unregister_dtmf_event_handler
|
|
27
|
-
call.
|
|
26
|
+
call.unregister_handler :ami, @dtmf_handler_id if instance_variable_defined?(:@dtmf_handler_id)
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
29
|
end
|
|
@@ -14,6 +14,7 @@ module Punchblock
|
|
|
14
14
|
raise OptionError, "The renderer #{renderer} is unsupported." unless renderer == 'unimrcp'
|
|
15
15
|
raise OptionError, "The recognizer #{recognizer} is unsupported." unless recognizer == 'unimrcp'
|
|
16
16
|
raise OptionError, 'An SSML document is required.' unless output_node.render_documents.count > 0
|
|
17
|
+
raise OptionError, 'Only one document is allowed.' if output_node.render_documents.count > 1
|
|
17
18
|
raise OptionError, 'A grammar is required.' unless input_node.grammars.count > 0
|
|
18
19
|
|
|
19
20
|
super
|
|
@@ -28,17 +29,16 @@ module Punchblock
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def execute_unimrcp_app
|
|
31
|
-
execute_app 'SynthAndRecog',
|
|
32
|
+
execute_app 'SynthAndRecog', render_doc, grammars
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
-
output_node.render_documents.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end.join ','
|
|
35
|
+
def render_doc
|
|
36
|
+
d = output_node.render_documents.first
|
|
37
|
+
if d.content_type
|
|
38
|
+
d.value.to_doc.to_s
|
|
39
|
+
else
|
|
40
|
+
d.url
|
|
41
|
+
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def unimrcp_app_options
|
|
@@ -16,50 +16,58 @@ module Punchblock
|
|
|
16
16
|
|
|
17
17
|
def execute
|
|
18
18
|
raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
|
|
19
|
-
raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
|
|
20
19
|
raise OptionError, 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :voice
|
|
21
20
|
|
|
22
|
-
[:start_offset, :start_paused, :repeat_interval, :
|
|
21
|
+
[:start_offset, :start_paused, :repeat_interval, :max_time].each do |opt|
|
|
23
22
|
raise OptionError, "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
early = !@call.answered?
|
|
25
|
+
@early = !@call.answered?
|
|
27
26
|
|
|
28
27
|
rendering_engine = @component_node.renderer || :asterisk
|
|
29
28
|
|
|
29
|
+
repeat_times = @component_node.repeat_times || 1
|
|
30
|
+
repeat_times = 1000 if repeat_times.zero?
|
|
31
|
+
|
|
30
32
|
case rendering_engine.to_sym
|
|
31
33
|
when :asterisk
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
validate_audio_only
|
|
35
|
+
setup_for_native
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
repeat_times.times do
|
|
38
|
+
render_docs.each do |doc|
|
|
39
|
+
playback(filenames(doc).values) || raise(PlaybackError)
|
|
40
|
+
end
|
|
38
41
|
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
when :native_or_unimrcp
|
|
43
|
+
setup_for_native
|
|
44
|
+
|
|
45
|
+
repeat_times.times do
|
|
46
|
+
render_docs.each do |doc|
|
|
47
|
+
doc.value.children.each do |node|
|
|
48
|
+
case node
|
|
49
|
+
when RubySpeech::SSML::Audio
|
|
50
|
+
playback([path_for_audio_node(node)]) || render_with_unimrcp(fallback_doc(doc, node))
|
|
51
|
+
when String
|
|
52
|
+
if node.include?(' ')
|
|
53
|
+
render_with_unimrcp(copied_doc(doc, node))
|
|
54
|
+
else
|
|
55
|
+
playback([node]) || render_with_unimrcp(copied_doc(doc, node))
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
render_with_unimrcp(copied_doc(doc, node.node))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
48
61
|
end
|
|
49
62
|
end
|
|
50
|
-
|
|
51
|
-
send_ref
|
|
52
|
-
|
|
53
|
-
opts = early ? "#{path},noanswer" : path
|
|
54
|
-
@call.execute_agi_command 'EXEC Playback', opts
|
|
55
|
-
raise PlaybackError if @call.channel_var('PLAYBACKSTATUS') == 'FAILED'
|
|
56
63
|
when :unimrcp
|
|
57
|
-
|
|
64
|
+
send_progress_if_necessary
|
|
58
65
|
send_ref
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
repeat_times.times do
|
|
67
|
+
render_with_unimrcp(*render_docs)
|
|
68
|
+
end
|
|
61
69
|
when :swift
|
|
62
|
-
|
|
70
|
+
send_progress_if_necessary
|
|
63
71
|
send_ref
|
|
64
72
|
@call.execute_agi_command 'EXEC Swift', swift_doc
|
|
65
73
|
else
|
|
@@ -80,26 +88,113 @@ module Punchblock
|
|
|
80
88
|
with_error 'option error', e.message
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
def stop_by_redirect(*args)
|
|
92
|
+
@stopped = true
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
|
|
83
96
|
private
|
|
84
97
|
|
|
85
|
-
def
|
|
86
|
-
|
|
98
|
+
def setup_for_native
|
|
99
|
+
raise OptionError, "A voice value is unsupported on Asterisk." if @component_node.voice
|
|
100
|
+
raise OptionError, 'Interrupt digits are not allowed with early media.' if @early && @component_node.interrupt_on
|
|
101
|
+
|
|
102
|
+
case @component_node.interrupt_on
|
|
103
|
+
when :any, :dtmf
|
|
104
|
+
interrupt = true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
send_progress_if_necessary
|
|
108
|
+
|
|
109
|
+
if interrupt
|
|
110
|
+
call.register_handler :ami, :name => 'DTMF', [:[], 'End'] => 'Yes' do |event|
|
|
111
|
+
stop_by_redirect finish_reason
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
send_ref
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def send_progress_if_necessary
|
|
119
|
+
@call.send_progress if @early
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_audio_only
|
|
123
|
+
render_docs.each do |doc|
|
|
124
|
+
filenames doc, -> { raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.' }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def path_for_audio_node(node)
|
|
129
|
+
node.src.sub('file://', '').gsub(/\.[^\.]*$/, '')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def filenames(doc, check_audio_only_policy = -> {})
|
|
133
|
+
names = {}
|
|
134
|
+
doc.value.children.each do |node|
|
|
87
135
|
case node
|
|
88
136
|
when RubySpeech::SSML::Audio
|
|
89
|
-
node
|
|
137
|
+
names[node] = path_for_audio_node node
|
|
90
138
|
when String
|
|
91
|
-
|
|
92
|
-
node
|
|
139
|
+
check_audio_only_policy.call if node.include?(' ')
|
|
140
|
+
names[nil] = node
|
|
141
|
+
else
|
|
142
|
+
check_audio_only_policy.call
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
names
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def playback_options(paths)
|
|
149
|
+
opts = paths.join '&'
|
|
150
|
+
opts << ",noanswer" if @early
|
|
151
|
+
opts
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def playback(paths)
|
|
155
|
+
return true if @stopped
|
|
156
|
+
@call.execute_agi_command 'EXEC Playback', playback_options(paths)
|
|
157
|
+
@call.channel_var('PLAYBACKSTATUS') != 'FAILED'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def fallback_doc(original, failed_audio_node)
|
|
161
|
+
children = failed_audio_node.nokogiri_children
|
|
162
|
+
copied_doc original, children
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def copied_doc(original, elements)
|
|
166
|
+
doc = RubySpeech::SSML.draw do
|
|
167
|
+
if Nokogiri.jruby?
|
|
168
|
+
self.write_attr 'version', original.value['version']
|
|
169
|
+
self.write_attr 'xml:lang', original.value['xml:lang']
|
|
93
170
|
else
|
|
94
|
-
|
|
171
|
+
original.value.attributes.each do |name, value|
|
|
172
|
+
attr_name = value.namespace && value.namespace.prefix ? [value.namespace.prefix, name].join(':') : name
|
|
173
|
+
self.write_attr attr_name, value
|
|
174
|
+
end
|
|
95
175
|
end
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
176
|
+
|
|
177
|
+
add_child Nokogiri.jruby? ? elements : elements.to_xml
|
|
178
|
+
end
|
|
179
|
+
Punchblock::Component::Output::Document.new(value: doc)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def render_with_unimrcp(*docs)
|
|
183
|
+
docs.each do |doc|
|
|
184
|
+
return if @stopped
|
|
185
|
+
UniMRCPApp.new('MRCPSynth', doc.value.to_s, mrcpsynth_options).execute @call
|
|
186
|
+
raise UniMRCPError if @call.channel_var('SYNTHSTATUS') == 'ERROR'
|
|
187
|
+
end
|
|
99
188
|
end
|
|
100
189
|
|
|
101
|
-
def
|
|
102
|
-
@component_node.render_documents
|
|
190
|
+
def render_docs
|
|
191
|
+
@component_node.render_documents
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def concatenated_render_doc
|
|
195
|
+
render_docs.inject RubySpeech::SSML.draw do |doc, argument|
|
|
196
|
+
doc + argument.value
|
|
197
|
+
end
|
|
103
198
|
end
|
|
104
199
|
|
|
105
200
|
def mrcpsynth_options
|
|
@@ -110,7 +205,7 @@ module Punchblock
|
|
|
110
205
|
end
|
|
111
206
|
|
|
112
207
|
def swift_doc
|
|
113
|
-
doc =
|
|
208
|
+
doc = concatenated_render_doc.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
|
|
114
209
|
doc << "|1|1" if [:any, :dtmf].include? @component_node.interrupt_on
|
|
115
210
|
doc.insert 0, "#{@component_node.voice}^" if @component_node.voice
|
|
116
211
|
doc
|
|
@@ -22,9 +22,8 @@ module Punchblock
|
|
|
22
22
|
|
|
23
23
|
@format = @component_node.format || 'wav'
|
|
24
24
|
|
|
25
|
-
component = current_actor
|
|
26
25
|
call.register_tmp_handler :ami, :name => 'MonitorStop' do |event|
|
|
27
|
-
|
|
26
|
+
finished
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
if @component_node.start_beep
|
|
@@ -33,7 +32,7 @@ module Punchblock
|
|
|
33
32
|
|
|
34
33
|
ami_client.send_action 'Monitor', 'Channel' => call.channel, 'File' => filename, 'Format' => @format, 'Mix' => true
|
|
35
34
|
unless max_duration == -1
|
|
36
|
-
after max_duration/1000 do
|
|
35
|
+
call.after max_duration/1000 do
|
|
37
36
|
ami_client.send_action 'StopMonitor', 'Channel' => call.channel
|
|
38
37
|
end
|
|
39
38
|
end
|
|
@@ -18,11 +18,10 @@ module Punchblock
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def stop_by_redirect(complete_reason)
|
|
21
|
-
component_actor = current_actor
|
|
22
21
|
call.register_handler :ami, lambda { |e| e['SubEvent'] == 'Start' }, :name => 'AsyncAGI' do |event|
|
|
23
|
-
|
|
22
|
+
send_complete_event complete_reason
|
|
24
23
|
end
|
|
25
|
-
call.
|
|
24
|
+
call.redirect_back
|
|
26
25
|
end
|
|
27
26
|
end
|
|
28
27
|
end
|
|
@@ -23,6 +23,8 @@ module Punchblock
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
include Celluloid
|
|
27
|
+
|
|
26
28
|
def initialize(responder, grammar, initial_timeout = nil, inter_digit_timeout = nil, terminator = nil)
|
|
27
29
|
@responder = responder
|
|
28
30
|
self.initial_timeout = initial_timeout || -1
|
|
@@ -68,10 +70,6 @@ module Punchblock
|
|
|
68
70
|
@matcher.match @buffer.dup
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
def after(*args, &block)
|
|
72
|
-
@responder.after *args, &block
|
|
73
|
-
end
|
|
74
|
-
|
|
75
73
|
def initial_timeout=(other)
|
|
76
74
|
raise OptionError, 'An initial timeout value that is negative (and not -1) is invalid.' if other < -1
|
|
77
75
|
@initial_timeout = other
|
|
@@ -36,7 +36,6 @@ module Punchblock
|
|
|
36
36
|
|
|
37
37
|
def validate
|
|
38
38
|
raise OptionError, 'An SSML document is required.' unless @component_node.render_documents.first.value
|
|
39
|
-
raise OptionError, 'Only a single document is supported.' unless @component_node.render_documents.size == 1
|
|
40
39
|
|
|
41
40
|
[:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
|
|
42
41
|
raise OptionError, "A #{opt} value is unsupported." if @component_node.send opt
|
|
@@ -48,6 +47,12 @@ module Punchblock
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
50
|
+
def concatenated_render_doc
|
|
51
|
+
@component_node.render_documents.inject RubySpeech::SSML.draw do |doc, argument|
|
|
52
|
+
doc + argument.value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
51
56
|
def finish_reason
|
|
52
57
|
Punchblock::Component::Output::Complete::Finish.new
|
|
53
58
|
end
|
|
@@ -17,17 +17,19 @@ module Punchblock
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def filenames
|
|
20
|
-
@filenames ||= @component_node.render_documents.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
@filenames ||= @component_node.render_documents.map do |doc|
|
|
21
|
+
doc.value.children.map do |node|
|
|
22
|
+
case node
|
|
23
|
+
when RubySpeech::SSML::Audio
|
|
24
|
+
node.src
|
|
25
|
+
when String
|
|
26
|
+
raise if node.include?(' ')
|
|
27
|
+
node
|
|
28
|
+
else
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
29
31
|
end
|
|
30
|
-
end.compact
|
|
32
|
+
end.compact.flatten
|
|
31
33
|
rescue
|
|
32
34
|
raise UnrenderableDocError, 'The provided document could not be rendered. See http://adhearsion.com/docs/common_problems#unrenderable-document-error for details.'
|
|
33
35
|
end
|