Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion models/bxml/Root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /(<SpeakSentence\b[^>]*(?<!\/)>)([\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
Expand Down Expand Up @@ -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));
}
}
4 changes: 2 additions & 2 deletions models/bxml/verbs/Gather.ts
Original file line number Diff line number Diff line change
@@ -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<PlayAudio | SpeakSentence>;
Expand Down Expand Up @@ -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));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions models/bxml/verbs/SpeakSentence.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
}
}
20 changes: 20 additions & 0 deletions tests/unit/models/bxml/Response.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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=<https://cert.example.com/cert.pem>' });
const bxml = new Response(redirect).toBxml();

expect(bxml).toContain('info=&lt;https://cert.example.com/cert.pem&gt;');
expect(bxml).not.toContain('info=<https');
});

test('should un-escape SSML in SpeakSentence text but not in sibling attributes', () => {
const redirect = new Redirect({ redirectUrl: 'https://example.com/?info=<https://cert.example.com/cert.pem>' });
const speak = new SpeakSentence('hi <break time="1s"/> there');
const bxml = new Response([redirect, speak]).toBxml();

expect(bxml).toContain('info=&lt;https://cert.example.com/cert.pem&gt;');
expect(bxml).toContain('hi <break time="1s"/> there');
});
});
});
7 changes: 7 additions & 0 deletions tests/unit/models/bxml/verbs/Gather.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=<b>' }, [new SpeakSentence('say <emphasis>this</emphasis>')]);
const expected = '<Gather gatherUrl="https://example.com/?a=&lt;b&gt;"><SpeakSentence>say <emphasis>this</emphasis></SpeakSentence></Gather>';

expect(gather.toBxml()).toBe(expected);
});
});