diff --git a/models/bxml/Root.ts b/models/bxml/Root.ts index 1f35ce0..5611758 100644 --- a/models/bxml/Root.ts +++ b/models/bxml/Root.ts @@ -3,6 +3,14 @@ import { XMLBuilder, XMLWriterOptions } from 'xmlbuilder2/lib/interfaces'; import { Verb } from './Verb'; export const SSML_REGEX = /<([a-zA-Z\/\/].*?)>/g; +const SPEAK_SENTENCE_REGEX = /(]*(?)([\s\S]*?)(<\/SpeakSentence>)/g; + +/** + * Restore SSML tags inside SpeakSentence text content only. + * Attribute values and all other elements keep their XML escaping. + */ +export const unescapeSsml = (bxml: string): string => + bxml.replace(SPEAK_SENTENCE_REGEX, (_m, open, text, close) => open + text.replace(SSML_REGEX, '<$1>') + close); /** * @export @@ -54,6 +62,6 @@ export class Root { * @param options XML Serialization options */ toBxml(options?: XMLWriterOptions): string { - return this.generateXml().end(options).replace(SSML_REGEX, '<$1>'); + return unescapeSsml(this.generateXml().end(options)); } } diff --git a/models/bxml/verbs/Gather.ts b/models/bxml/verbs/Gather.ts index b3e202e..7d07c52 100644 --- a/models/bxml/verbs/Gather.ts +++ b/models/bxml/verbs/Gather.ts @@ -1,7 +1,7 @@ import { NestableVerb } from '../NestableVerb'; import { PlayAudio } from './PlayAudio'; import { SpeakSentence } from './SpeakSentence'; -import { SSML_REGEX } from '../Root'; +import { unescapeSsml } from '../Root'; import { XMLWriterOptions } from 'xmlbuilder2/lib/interfaces'; type AudioVerbs = Array; @@ -54,7 +54,7 @@ export class Gather extends NestableVerb { * @param options XML Serialization options */ toBxml(options?: XMLWriterOptions): string { - return this.generateXml().toString(options).replace(SSML_REGEX, '<$1>'); + return unescapeSsml(this.generateXml().toString(options)); } /** diff --git a/models/bxml/verbs/SpeakSentence.ts b/models/bxml/verbs/SpeakSentence.ts index a72442f..990ee1b 100644 --- a/models/bxml/verbs/SpeakSentence.ts +++ b/models/bxml/verbs/SpeakSentence.ts @@ -1,6 +1,6 @@ import { Verb } from '../Verb'; import { XMLWriterOptions } from 'xmlbuilder2/lib/interfaces'; -import { SSML_REGEX } from '../Root'; +import { unescapeSsml } from '../Root'; export interface SpeakSentenceAttributes { voice?: string; @@ -32,6 +32,6 @@ export class SpeakSentence extends Verb { * @param options XML Serialization options */ toBxml(options?: XMLWriterOptions): string { - return this.generateXml().toString(options).replace(SSML_REGEX, '<$1>'); + return unescapeSsml(this.generateXml().toString(options)); } } diff --git a/tests/unit/models/bxml/Response.test.ts b/tests/unit/models/bxml/Response.test.ts index e70de26..968216e 100644 --- a/tests/unit/models/bxml/Response.test.ts +++ b/tests/unit/models/bxml/Response.test.ts @@ -1,6 +1,8 @@ import { Response } from '../../../../models/bxml/Response'; import { PauseRecording } from '../../../../models/bxml/verbs/PauseRecording'; import { Root } from '../../../../models/bxml/Root'; +import { Redirect } from '../../../../models/bxml/verbs/Redirect'; +import { SpeakSentence } from '../../../../models/bxml/verbs/SpeakSentence'; describe('Response', () => { const pauseRecording = new PauseRecording(); @@ -43,4 +45,22 @@ describe('Response', () => { expect(response.toBxml()).toBe(expected); }); + describe('SSML un-escaping', () => { + test('should keep attribute values escaped in a Response', () => { + const redirect = new Redirect({ redirectUrl: 'https://example.com/?info=' }); + const bxml = new Response(redirect).toBxml(); + + expect(bxml).toContain('info=<https://cert.example.com/cert.pem>'); + expect(bxml).not.toContain('info= { + const redirect = new Redirect({ redirectUrl: 'https://example.com/?info=' }); + const speak = new SpeakSentence('hi there'); + const bxml = new Response([redirect, speak]).toBxml(); + + expect(bxml).toContain('info=<https://cert.example.com/cert.pem>'); + expect(bxml).toContain('hi there'); + }); + }); }); diff --git a/tests/unit/models/bxml/verbs/Gather.test.ts b/tests/unit/models/bxml/verbs/Gather.test.ts index 32b8dd2..039f754 100644 --- a/tests/unit/models/bxml/verbs/Gather.test.ts +++ b/tests/unit/models/bxml/verbs/Gather.test.ts @@ -65,4 +65,11 @@ describe('Gather', () => { gather.addAudioVerbs(playAudio); expect(gather.toBxml()).toBe(expected); }); + + test('should un-escape SSML in a Gather SpeakSentence but not in Gather attributes', () => { + const gather = new Gather({ gatherUrl: 'https://example.com/?a=' }, [new SpeakSentence('say this')]); + const expected = 'say this'; + + expect(gather.toBxml()).toBe(expected); + }); });