ssml-check-core
Version:
Core library to check for valid SSML
879 lines (794 loc) • 31.5 kB
JavaScript
//
// Validates different tags
//
function createTagError(element, attribute, undefinedValue) {
const error = {type: 'tag', tag: element.name};
error.type = 'tag';
error.tag = element.name;
error.attribute = attribute;
error.value = (undefinedValue || !element.attributes) ? undefined : element.attributes[attribute];
if (element.position !== undefined) {
error.position = element.position;
}
return error;
}
function readDuration(text, platform, maximum) {
// It must be of the form #s or #ms
let time;
if (!maximum && (text === 'infinity')) {
time = Number.MAX_SAFE_INTEGER;
} else if (text.match('[0-9]+ms')) {
time = parseInt(text);
} else if (text.match(/^[0-9]+(\.[0-9]+)?s$/g)) {
time = 1000 * parseInt(text);
} else if ((platform === 'google') && text.match(/^[0-9]+(\.[0-9]+)?$/g)) {
time = 1000 * parseInt(text);
} else {
// No good
return undefined;
}
if (maximum) {
time = (time <= maximum) ? time : undefined;
}
return time;
}
function numberInRange(text, min, max, defaultValue) {
let value = parseFloat(text);
let inRange = true;
if (isNaN(value)) {
inRange = false;
value = defaultValue;
} else if (value < min) {
inRange = false;
value = min;
} else if (value > max) {
inRange = false;
value = max;
}
return {inRange: inRange, value: value};
}
//
// Set of functions that check individual tags
//
const check_alexa_name = (parent, index, errors, element, platform, locale) => {
// If a alexa:name tag is available it must have an attribute type with value 'first' and an attribute personId
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'type') {
if (['first'].indexOf(element.attributes.type) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.type = 'first';
}
} else if (attribute === 'personId') {
if ((element.attributes.personId).indexOf('amzn1.ask.person.') === -1) {
errors.push(createTagError(element, attribute));
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, type is required
if (attributes.indexOf('type') === -1) {
errors.push(createTagError(element, 'missing type'));
element.attributes = {type: 'first'};
}
// Also, personId is required
if (attributes.indexOf('personId') === -1) {
errors.push(createTagError(element, 'missing personId'));
element.attributes = {personId: 'amzn1.ask.person.ABCD'};
}
return false;
};
const check_amazon_auto_breaths = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Valid attributes are duration, frequency and volume
attributes.forEach((attribute) => {
if (attribute === 'duration') {
if (['default', 'x-short', 'short', 'medium', 'long', 'x-long']
.indexOf(element.attributes.duration) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.duration = 'medium';
}
} else if (attribute === 'volume') {
if (['default', 'x-soft', 'soft', 'medium', 'loud', 'x-loud']
.indexOf(element.attributes.volume) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.volume = 'medium';
}
} else if (attribute === 'frequency') {
if (['default', 'x-low', 'low', 'medium', 'high', 'x-high']
.indexOf(element.attributes.frequency) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.frequncy = 'medium';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_amazon_breath = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Valid attributes are duration and volume
attributes.forEach((attribute) => {
if (attribute === 'duration') {
if (['default', 'x-short', 'short', 'medium', 'long', 'x-long']
.indexOf(element.attributes.duration) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.duration = 'medium';
}
} else if (attribute === 'volume') {
if (['default', 'x-soft', 'soft', 'medium', 'loud', 'x-loud']
.indexOf(element.attributes.volume) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.volume = 'medium';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_amazon_domain = (parent, index, errors, element, platorm, locale) => {
// The locale is optional but if set
// This tag is only valid for en-US, de-DE, en-GB, en-CA, en-AU, and jp-JP
if (locale && (['en-US', 'en-GB', 'jp-JP', 'de-DE', 'en-CA', 'en-AU'].indexOf(locale) === -1)) {
// Keep the text, but remove the element
errors.push(createTagError(element, 'none'));
if (element.elements) {
parent.elements.splice(index, 1, ...element.elements);
} else {
parent.elements.splice(index, 1);
}
return true;
}
// name field is required and can be news or music
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'name') {
const allowedValues = {
conversational: ['en-US', 'jp-JP'],
'long-form': ['en-US'],
music: ['de-DE', 'en-US', 'en-CA', 'en-GB'],
news: ['en-US', 'en-AU'],
fun: ['jp-JP'],
};
if (!allowedValues[element.attributes.name] ||
((locale && (allowedValues[element.attributes.name].indexOf(locale) === -1)))) {
errors.push(createTagError(element, attribute));
element.attributes.name = 'news';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, name is required
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
element.attributes = {name: 'news'};
}
};
const check_amazon_effect = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'name') {
if (['drc', 'whispered'].indexOf(element.attributes.name) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.name = 'whispered';
}
} else if (attribute === 'phonation') {
if (['soft'].indexOf(element.attributes.phonation) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.phonation = 'soft';
}
} else if (attribute === 'vocal-tract-length') {
// Value can be between -50% and +100%, or an absolute value percentage
const match = element.attributes['vocal-tract-length'].match(/([\+|\-]?)(\d+)%/);
if (!match) {
errors.push(createTagError(element, attribute));
element.attributes['vocal-tract-length'] = '+100%';
} else if (match[1].length) {
const range = numberInRange(`${match[1]}${match[2]}`, -50, 100, undefined);
if (!range.inRange) {
errors.push(createTagError(element, attribute));
element.attributes['vocal-tract-length'] = `${range.value > 0 ? '+' : ''}${range.value}%`;
}
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, name is required
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
element.attributes = {name: 'whispered'};
}
return false;
};
const check_amazon_emotion = (parent, index, errors, element, platform, locale) => {
// This tag is only valid for en-US, en-GB, and jp-JP
if (['en-US', 'en-GB', 'jp-JP'].indexOf(locale) === -1) {
// Keep the text, but remove the element
errors.push(createTagError(element, 'none'));
if (element.elements) {
parent.elements.splice(index, 1, ...element.elements);
} else {
parent.elements.splice(index, 1);
}
return true;
}
// name and intensity are the supported attributes
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'name') {
if (['excited', 'disappointed'].indexOf(element.attributes.name) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.name = 'excited';
}
} else if (attribute === 'intensity') {
if (['low', 'medium', 'high'].indexOf(element.attributes.intensity) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.intensity = 'medium';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, name and intensity are both required
if (attributes.length !== 2) {
errors.push(createTagError(element, 'none'));
element.attributes.name = element.attributes.name || 'excited';
element.attributes.intensity = element.attributes.intensity || 'medium';
}
return false;
};
const check_audio = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
let removed;
attributes.forEach((attribute) => {
if ((platform === 'google') && (attribute === 'clipBegin')) {
if (readDuration(element.attributes.clipBegin, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.clipBegin = undefined;
}
} else if ((platform === 'google') && (attribute === 'clipEnd')) {
if (readDuration(element.attributes.clipEnd, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.clipEnd = undefined;
}
} else if ((platform === 'google') && (attribute === 'speed')) {
const speed = numberInRange(element.attributes.speed, 50, 200, 100);
if (!element.attributes.speed.match(/^(\+)?[0-9]+(\.[0-9]+)?%$/g) || !speed.inRange) {
errors.push(createTagError(element, attribute));
element.attributes.speed = speed.value + '%';
}
} else if ((platform === 'google') && (attribute === 'repeatCount')) {
if (!element.attributes.repeatCount.match(/^(\+)?[0-9]+(\.[0-9]+)?$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.repeatCount = '1';
}
} else if ((platform === 'google') && (attribute === 'repeatDur')) {
if (readDuration(element.attributes.repeatDur, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.repeatDur = undefined;
}
} else if ((platform === 'google') && (attribute === 'soundLevel')) {
// It's OK if it's of the form +xdB or - xdB; value doesn't matter
const soundLevel = numberInRange(element.attributes.soundLevel, -40, 40, 0);
if (!element.attributes.soundLevel.match(/^[+-][0-9]+(\.[0-9]+)?dB$/g) || !soundLevel.inRange) {
errors.push(createTagError(element, attribute));
element.attributes.soundLevel = (soundLevel.value < 0)
? (soundLevel.value + 'dB') : ('+' + soundLevel.value + 'dB');
}
} else if (attribute !== 'src') {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, src is required - if not present remove the whole element
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
parent.elements.splice(index, 1);
removed = true;
}
return removed;
};
const check_break = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Attribute must be time or strength
attributes.forEach((attribute) => {
if (attribute === 'strength') {
if (['none', 'x-weak', 'weak', 'medium', 'strong', 'x-strong']
.indexOf(element.attributes.strength) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.strength = 'medium';
}
} else if (attribute === 'time') {
// Must be valid duration
if (readDuration(element.attributes.time, platform, 10000) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.time = '10s';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// If there isn't a strength or time, add one
if (!element.attributes.strength && !element.attributes.time) {
element.attributes.strength = 'medium';
}
return false;
};
const check_desc = (parent, index, errors, element, platform, locale) => {
// Desc is valid as part of an audio tag on Google
let removed;
if (!parent || (parent.name !== 'audio')) {
// Invalid in this context
const error = {type: 'tag', tag: element.name};
if (element.position !== undefined) {
error.position = element.position;
}
errors.push(error);
parent.elements.splice(index, 1);
removed = true;
}
return removed;
};
const check_emphasis = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Must be level attribute
attributes.forEach((attribute) => {
if (attribute === 'level') {
if (['strong', 'moderate', 'reduced']
.indexOf(element.attributes.level) === -1) {
// None is also allowed on Google
if ((platform !== 'google') || (element.attributes.level !== 'none')) {
errors.push(createTagError(element, attribute));
element.attributes.level = 'moderate';
}
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, level is required
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
element.attributes = {level: 'moderate'};
}
return false;
};
const check_lang = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
const validLocales = [
'de-DE', 'en-AU', 'en-CA', 'en-GB', 'en-IN', 'en-US', 'es-ES', 'es-MX', 'es-US',
'fr-CA', 'fr-FR', 'hi-IN', 'it-IT', 'ja-JP', 'pt-BR'
];
// Must be xml:lang attribute
attributes.forEach((attribute) => {
if (attribute === 'xml:lang') {
if (!validLocales.includes(element.attributes['xml:lang'])) {
errors.push(createTagError(element, attribute));
element.attributes['xml:lang'] = 'en-US';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, xml:lang is required
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
element.attributes = {'xml:lang': 'en-US'};
}
return false;
};
const check_mark = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Only the name field is recognized and is required
attributes.forEach((attribute) => {
if (attribute !== 'name') {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// Also, name is required
if (attributes.length === 0) {
errors.push(createTagError(element, 'none'));
element.attributes = {name: 'mark'};
}
return false;
};
const check_media = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'xml:id') {
if (!element.attributes['xml:id'].match(/^([-_#]|[a-z]|[A-Z]|ß|ö|ä|ü|Ö|Ä|Ü|æ|é|[0-9])+$/g)) {
errors.push(createTagError(element, attribute));
element.attributes['xml:id'] = 'id_' + index;
}
} else if (attribute === 'begin') {
if (!element.attributes.begin.match(/^[+-]?[0-9]+(\.[0-9]+)?(h|min|s|ms)$/g)
&& !element.attributes.begin.match(/^([-_#]|[a-z]|[A-Z]|ß|ö|ä|ü|Ö|Ä|Ü|æ|é|[0-9])+\.(begin|end)[+-][0-9]+(\.[0-9]+)?(h|min|s|ms)$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.begin = '0';
}
} else if (attribute === 'end') {
if (!element.attributes.end.match(/^[+-]?[0-9]+(\.[0-9]+)?(h|min|s|ms)$/g)
&& !element.attributes.end.match(/^([-_#]|[a-z]|[A-Z]|ß|ö|ä|ü|Ö|Ä|Ü|æ|é|[0-9])+\.(begin|end)[+-][0-9]+(\.[0-9]+)?(h|min|s|ms)$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.end = undefined;
}
} else if (attribute === 'repeatCount') {
if (!element.attributes.repeatCount.match(/^(\+)?[0-9]+(\.[0-9]+)?$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.repeatCount = '1';
}
} else if (attribute === 'repeatDur') {
if (readDuration(element.attributes.repeatDur, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.repeatDur = undefined;
}
} else if (attribute === 'soundLevel') {
// It's OK if it's of the form +xdB or - xdB; value doesn't matter
if (!element.attributes.soundLevel.match(/^[+-]?[0-9]+(\.[0-9]+)?dB$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.soundLevel = '+0dB';
}
} else if (attribute === 'fadeInDur') {
if (readDuration(element.attributes.fadeInDur, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.fadeInDur = '0s';
}
} else if (attribute === 'fadeOutDur') {
if (readDuration(element.attributes.fadeOutDur, platform) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes.fadeOutDur = '0s';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
}
});
return false;
};
const check_p = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// No attributes allowed
attributes.forEach((attribute) => {
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
});
return false;
};
const check_s = check_p;
const check_par = (parent, index, errors, element, platform, locale) => {
// These elements house other par, seq, or media elements
if (element.elements) {
let i;
for (i = 0; i < element.elements.length; i++) {
const item = element.elements[i];
if (['par', 'seq', 'media'].indexOf(item.name) === -1) {
const error = {type: 'tag', tag: element.name};
error.value = item.name;
if (element.position !== undefined) {
error.position = element.position;
}
errors.push(error);
element.elements.splice(i, 1);
i--;
}
}
}
return false;
};
const check_seq = check_par;
const check_phoneme = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'alphabet') {
if (['ipa', 'x-sampa']
.indexOf(element.attributes.alphabet) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.alphabet = 'ipa';
}
// Note that we don't perform validation of the ph attribute
// This could be covered in a future version
} else if (attribute !== 'ph') {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_prosody = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'rate') {
if (['x-slow', 'slow', 'medium', 'fast', 'x-fast'].indexOf(element.attributes.rate) === -1) {
// Must be of the form #%
const rate = numberInRange(element.attributes.rate, 20, Number.MAX_SAFE_INTEGER, 100);
if (!element.attributes.rate.match(/^[0-9]+(\.[0-9]+)?%$/g) || !rate.inRange) {
errors.push(createTagError(element, attribute));
element.attributes.rate = rate.value + '%';
}
}
} else if (attribute === 'pitch') {
if (['x-low', 'low', 'medium', 'high', 'x-high'].indexOf(element.attributes.pitch) === -1) {
// It's OK, it has to be of the form +x% or -x%
const pitch = numberInRange(element.attributes.pitch, -33.3, 50, 0);
if (!element.attributes.pitch.match(/^[+-][0-9]+(\.[0-9]+)?%$/g) || !pitch.inRange) {
// On Google, it can be a semitone
if ((platform !== 'google') ||
!element.attributes.pitch.match(/^[+-]+[0-9]+(\.[0-9]+)?st$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.pitch = (pitch.value < 0)
? (pitch.value + '%') : ('+' + pitch.value + '%');
}
}
}
} else if (attribute === 'volume') {
if (['silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'].indexOf(element.attributes.volume) === -1) {
// It's OK if it's of the form +xdB or - xdB; value doesn't matter
if (!element.attributes.volume.match(/^[+-][0-9]+(\.[0-9]+)?dB$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.volume = '+0dB';
}
}
} else if ((attribute === 'amazon:max-duration') && (platform === 'amazon')) {
// Needs to be a valid duration
if (readDuration(element.attributes['amazon:max-duration'], platform, undefined) === undefined) {
errors.push(createTagError(element, attribute));
element.attributes['amazon:max-duration'] = '2s';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_say_as = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Attribute must be interpret-as or format
attributes.forEach((attribute) => {
if (attribute === 'interpret-as') {
if (['characters', 'spell-out', 'cardinal', 'ordinal',
'fraction', 'unit', 'date', 'time', 'telephone', 'expletive']
.indexOf(element.attributes['interpret-as']) === -1) {
// Some attributes are platform specific
let supported = false;
if ((platform === 'amazon') &&
['number', 'digits', 'address', 'interjection']
.indexOf(element.attributes['interpret-as'] !== -1)) {
supported = true;
} else if ((platform === 'google') &&
['bleep', 'verbatim'].indexOf(element.attributes['interpret-as'] !== -1)) {
supported = true;
}
if (!supported) {
errors.push(createTagError(element, attribute));
element.attributes['interpret-as'] = 'cardinal';
}
}
} else if (attribute === 'format') {
// Is this in support of a date or a time?
let isDate = (element.attributes['interpret-as'] === 'date');
if (isDate) {
if (['mdy', 'dmy', 'ymd', 'md', 'dm', 'ym',
'my', 'd', 'm', 'y'].indexOf(element.attributes.format) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.format = 'mdy';
}
} else if (platform === 'google') {
// We allow format for time variable
if (!element.attributes.format.match(/^[hmsZ^\s.!?:;(12|24)]*$/g)) {
errors.push(createTagError(element, attribute));
element.attributes.format = 'hms12';
}
} else {
// Format for Amazon is only supported on date
errors.push(createTagError(element, attribute));
element.attributes.format = undefined;
}
} else if ((platform === 'google') && (attribute === 'detail')) {
if (['1', '2'].indexOf(element.attributes.detail) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.detail = '1';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_sub = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// alias is optional
attributes.forEach((attribute) => {
if (attribute !== 'alias') {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_speak = (parent, index, errors, element, platform, locale) => {
return false;
};
const check_voice_amazon = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Attribute must be name
attributes.forEach((attribute) => {
if (attribute === 'name') {
if (['Ivy', 'Joanna', 'Joey', 'Justin', 'Kendra', 'Kimberly', 'Matthew', 'Salli',
'Nicole', 'Russell', 'Amy', 'Brian', 'Emma', 'Aditi', 'Raveena',
'Hans', 'Marlene', 'Vicki', 'Conchita', 'Enrique',
'Carla', 'Giorgio', 'Mizuki', 'Takumi', 'Celine', 'Lea', 'Mathieu']
.indexOf(element.attributes.name) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.name = 'Ivy';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
const check_voice_google = (parent, index, errors, element, platform, locale) => {
const is_valid_voice_name = (name) => {
// check that name is of the form <locale>-<type>-<name>
// where <locale> is a valid locale, <type> is one of Standard or Wavenet, and <name> is a single character
// This is looser than the Google documentation, but we don't want to make a live call to get the list of valid voices
const parts = name.split('-');
if (parts.length !== 4) {
return false;
}
if (['af', 'ar', 'bn', 'cmn', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi',
'fil', 'fr', 'gu', 'hi', 'hu', 'id', 'it', 'ja', 'kn', 'ko', 'ml', 'mr',
'nb', 'nl', 'pl', 'pt', 'ru', 'sk', 'sv', 'sw', 'ta', 'te', 'th', 'tr',
'uk', 'vi', 'yue', 'zh'].indexOf(parts[0]) === -1) {
return false;
}
if (['Standard', 'Wavenet'].indexOf(parts[2]) === -1) {
return false;
}
if (['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'].indexOf(parts[3]) === -1) {
return false;
}
if (parts[1].length !== 2) {
return false;
}
return true;
};
// Name attribute is optional
let hasName = false;
let hasOptionalAttribute = false;
const attributes = Object.keys(element.attributes || {});
attributes.forEach((attribute) => {
if (attribute === 'name') {
hasName = true;
if (!is_valid_voice_name(element.attributes.name)) {
errors.push(createTagError(element, attribute));
element.attributes.name = 'en-US-Standard-A';
}
} else if (attribute === 'gender') {
// Value must be 'male', 'female', or 'neutral'
hasOptionalAttribute = true;
if (['male', 'female', 'neutral'].indexOf(element.attributes.gender) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.gender = 'neutral';
}
} else if (attribute === 'variant') {
// Variant should be a name
hasOptionalAttribute = true;
if (!is_valid_voice_name(element.attributes.variant)) {
errors.push(createTagError(element, attribute));
element.attributes.variant = 'en-US-Standard-A';
}
} else if (attribute === 'language') {
hasOptionalAttribute = true;
// attribute must be a valid language code
if (!element.attributes.language.match(/^[a-z]{2}-[A-Z]{2}$/)) {
errors.push(createTagError(element, attribute));
element.attributes.language = 'en-US';
}
// required or ordering are also valid, but we don't check those as we
// are strict and require that any attribute that is set is valid
} else if ((attribute !== 'required') && (attribute !== 'ordering')) {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
// If they don't have a name, they must have one of the other attributes
if (!hasName && !hasOptionalAttribute) {
errors.push(createTagError(element, 'name', true));
if (!element.attributes) {
element.attributes = {};
}
element.attributes.name = 'en-US-Standard-A';
}
};
const check_voice = (parent, index, errors, element, platform, locale) => {
// Voice is implemented differently for each platform, therefore a platform value is required
if (platform === 'amazon') {
return check_voice_amazon(parent, index, errors, element, platform, locale);
} else if (platform === 'google') {
return check_voice_google(parent, index, errors, element, platform, locale);
} else {
errors.push(createTagError(element, 'voice', true));
element.attributes.voice = undefined;
}
return false;
};
const check_w = (parent, index, errors, element, platform, locale) => {
const attributes = Object.keys(element.attributes || {});
// Attribute must be role
attributes.forEach((attribute) => {
if (attribute === 'role') {
if (['amazon:VB', 'amazon:VBD', 'amazon:NN', 'amazon:DT', 'amazon:IN', 'amazon:JJ', 'amazon:SENSE_1']
.indexOf(element.attributes.role) === -1) {
errors.push(createTagError(element, attribute));
element.attributes.role = 'amazon:VB';
}
} else {
// Invalid attribute
errors.push(createTagError(element, attribute, true));
element.attributes[attribute] = undefined;
}
});
return false;
};
module.exports = {
check_alexa_name,
check_amazon_auto_breaths,
check_amazon_breath,
check_amazon_domain,
check_amazon_effect,
check_amazon_emotion,
check_audio,
check_break,
check_desc,
check_emphasis,
check_lang,
check_mark,
check_media,
check_p,
check_par,
check_phoneme,
check_prosody,
check_s,
check_say_as,
check_seq,
check_speak,
check_sub,
check_voice,
check_w,
};