mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
Merge branch '334-name-pronunciation' into 'main'
Name pronunciation See merge request PronounsPage/PronounsPage!387
This commit is contained in:
commit
44d28fc78b
@ -7,21 +7,13 @@
|
||||
<small v-if="link">
|
||||
(<nuxt-link :to="'/' + pronoun.canonicalName"><Spelling escape :text="pronoun.canonicalName"/></nuxt-link>)
|
||||
</small>
|
||||
<template v-if="config.pronunciation.enabled && pronunciation && pronoun.pronounceable && example.pronounce(pronoun)">
|
||||
<a v-for="(pLink, name) in pronunciationLinks"
|
||||
class="mr-2"
|
||||
dir="ltr"
|
||||
:href="pLink"
|
||||
@click.prevent="pronounce(pLink)">
|
||||
<Icon v="volume"/><sub v-if="name">{{name}}</sub>
|
||||
</a>
|
||||
</template>
|
||||
<Pronunciation v-if="pronunciation && pronoun.pronounceable && example.toPronunciationString(pronoun)"
|
||||
:pronunciation="example.toPronunciationString(pronoun)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pronouns } from '../src/data';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
example: { required: true },
|
||||
@ -30,42 +22,5 @@
|
||||
link: { type: Boolean },
|
||||
pronunciation: { type: Boolean },
|
||||
},
|
||||
methods: {
|
||||
pronounce(link) {
|
||||
const sound = new Audio(link);
|
||||
sound.play();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pronounBase() {
|
||||
const name = this.pronoun.name();
|
||||
for (let key in pronouns) {
|
||||
if (!pronouns.hasOwnProperty(key)) { continue; }
|
||||
if (key === name) {
|
||||
return key;
|
||||
}
|
||||
for (let alias of pronouns[key].aliases) {
|
||||
if (alias === name) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
pronounToString() {
|
||||
return this.pronounBase && pronouns[this.pronounBase].equals(this.pronoun) ? this.pronounBase : this.pronoun.toString();
|
||||
},
|
||||
pronunciationLinks() {
|
||||
const justOne = Object.keys(this.config.pronunciation.voices).length === 1;
|
||||
|
||||
const links = {};
|
||||
for (let country in this.config.pronunciation.voices) {
|
||||
if (!this.config.pronunciation.voices.hasOwnProperty(country)) { continue; }
|
||||
links[justOne ? '' : country] = `/api/pronounce/${country}/${this.pronounToString}?example=${encodeURIComponent(this.example.toString())}`;
|
||||
}
|
||||
return links;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<span v-if="pronoun.getMorpheme(morpheme, counter)">
|
||||
<Morpheme :pronoun="pronoun" :morpheme="morpheme" :counter="counter" :prepend="prepend" :append="append"/>
|
||||
<span v-if="config.pronunciation.enabled && pronoun.pronounceable && pronoun.getPronunciation(morpheme, counter) && !pronoun.getPronunciation(morpheme, counter).startsWith('=')" class="text-muted">
|
||||
/{{prependPr}}{{pronoun.getPronunciation(morpheme, counter)}}{{appendPr}}/
|
||||
</span>
|
||||
<Pronunciation v-if="pronoun.pronounceable && pronoun.getPronunciation(morpheme, counter) && !pronoun.getPronunciation(morpheme, counter).startsWith('=')"
|
||||
:pronunciation="`/${prependPr}${pronoun.getPronunciation(morpheme, counter)}${appendPr}/`" text
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
</Tooltip>
|
||||
<nuxt-link v-if="link" :to="link" :class="`colour-${op.colour || 'default'}`"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
||||
<span v-else><Spelling :escape="escape" :markdown="markdown" :text="word"/></span>
|
||||
<Pronunciation v-if="pronunciation" :pronunciation="pronunciation" text/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@ -14,6 +15,7 @@
|
||||
export default {
|
||||
props: {
|
||||
word: { required: true },
|
||||
pronunciation: { default: null, type: String },
|
||||
opinion: { required: true },
|
||||
link: {},
|
||||
escape: { type: Boolean, 'default': () => true },
|
||||
|
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<ListInput v-model="v" :prototype="{value: '', opinion: 'meh'}" :group="group" :maxitems="maxitems">
|
||||
<ListInput v-model="v" :prototype="prototype" :group="group" :maxitems="maxitems">
|
||||
<template v-slot="s">
|
||||
<button type="button" :class="['btn', 'btn-outline-secondary', showOpinionSelector === s.i ? 'btn-secondary text-white border' : (validate(s.val) ? 'btn-outline-danger' : '')]"
|
||||
@click="showOpinionSelector = showOpinionSelector === s.i ? false : s.i">
|
||||
<Icon :v="getIcon(s.val.opinion)"/>
|
||||
</button>
|
||||
<input v-model="s.val.value" :class="['form-control', 'mw-input', validate(s.val) ? 'border-danger' : '']" @keyup="s.update(s.val)" required :maxlength="maxlength"/>
|
||||
<slot name="additional" :val="s.val"/>
|
||||
|
||||
<div v-if="showOpinionSelector === s.i" class="bg-light border rounded hanging shadow shadow-lg">
|
||||
<ul class="list-unstyled icons-list p-1 text-center mb-0">
|
||||
@ -41,6 +42,7 @@
|
||||
export default {
|
||||
props: {
|
||||
value: {},
|
||||
prototype: { 'default': () => { return { value: '', opinion: 'meh' } } },
|
||||
group: {},
|
||||
validation: {},
|
||||
customOpinions: { 'default': () => { return [] }},
|
||||
|
@ -76,6 +76,7 @@
|
||||
<ExpandableList :values="profile.names" :limit="16" class="list-unstyled" :isStatic="isStatic" :expand="expandLinks">
|
||||
<template v-slot="s">
|
||||
<Opinion :word="convertName(s.el.value)" :opinion="s.el.opinion" :escape="false" :markdown="profile.markdown"
|
||||
:pronunciation="s.el.pronunciation"
|
||||
:link="config.locale === 'tok' && config.pronouns.enabled ? `${config.pronouns.prefix}/${s.el.value}` : null"
|
||||
:customOpinions="profile.opinions"/>
|
||||
</template>
|
||||
|
41
components/Pronunciation.vue
Normal file
41
components/Pronunciation.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<span class="pronunciation">
|
||||
<span v-if="text" class="text-pronunciation">
|
||||
{{ pronunciation }}
|
||||
</span>
|
||||
<PronunciationSpeaker v-for="voice in voices" :key="voice"
|
||||
:pronunciation="pronunciation" :voice="voice"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
pronunciation: { required: true, type: String },
|
||||
text: { default: false, type: Boolean },
|
||||
},
|
||||
computed: {
|
||||
voices() {
|
||||
if (this.config.pronunciation.enabled) {
|
||||
return Object.keys(this.config.pronunciation.voices);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/variables";
|
||||
|
||||
.pronunciation {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-pronunciation {
|
||||
font-weight: normal;
|
||||
color: var(--#{$prefix}secondary-color);
|
||||
}
|
||||
</style>
|
49
components/PronunciationInput.vue
Normal file
49
components/PronunciationInput.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="input-group input-group-sm w-auto">
|
||||
<span class="input-group-text">/</span>
|
||||
<input class="form-control mw-input" v-model="rawPronunciation" :placeholder="$t('profile.pronunciation.ipa')" maxlength="255"/>
|
||||
<span class="input-group-text">/</span>
|
||||
<PronunciationSpeaker v-for="voice in voices" :key="voice"
|
||||
class="btn btn-outline-secondary"
|
||||
:pronunciation="value" :voice="voice"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { escapePronunciationString, unescapePronunciationString } from '../src/helpers.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: { default: null, type: String },
|
||||
},
|
||||
computed: {
|
||||
rawPronunciation: {
|
||||
get() {
|
||||
if (this.value) {
|
||||
const phonemes = this.value.substring(1, this.value.length - 1);
|
||||
return unescapePronunciationString(phonemes);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
set(rawPronunciation) {
|
||||
let pronunciation;
|
||||
if (rawPronunciation) {
|
||||
pronunciation = `/${escapePronunciationString(rawPronunciation)}/`;
|
||||
} else {
|
||||
pronunciation = null;
|
||||
}
|
||||
|
||||
this.$emit('input', pronunciation);
|
||||
},
|
||||
},
|
||||
voices() {
|
||||
if (this.config.pronunciation.enabled) {
|
||||
return Object.keys(this.config.pronunciation.voices);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
43
components/PronunciationSpeaker.vue
Normal file
43
components/PronunciationSpeaker.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<a :class="['mr-2', !pronunciation ? 'disabled' : '']" dir="ltr"
|
||||
:href="pronunciationLink" @click.prevent="pronounce()"
|
||||
>
|
||||
<Icon v="volume"/><sub v-if="name">{{ name }}</sub>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
pronunciation: { default: null, type: String },
|
||||
voice: { required: true, type: String },
|
||||
},
|
||||
computed: {
|
||||
pronunciationLink() {
|
||||
return `/api/pronounce/${this.voice}/${encodeURIComponent(this.pronunciation)}`;
|
||||
},
|
||||
name() {
|
||||
let voices;
|
||||
if (this.config.pronunciation.enabled) {
|
||||
voices = Object.keys(this.config.pronunciation.voices);
|
||||
} else {
|
||||
voices = null;
|
||||
}
|
||||
if (this.config.pronunciation.enabled && voices.length === 1 &&
|
||||
this.voice === voices[0]) {
|
||||
// don’t show voice name if it is considered the main voice for this locale
|
||||
return null;
|
||||
} else {
|
||||
return this.voice;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
pronounce() {
|
||||
const sound = new Audio(this.pronunciationLink);
|
||||
sound.play();
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -637,6 +637,8 @@ user:
|
||||
profile:
|
||||
description: 'Description'
|
||||
names: 'Names'
|
||||
pronunciation:
|
||||
ipa: 'Pronunciation using IPA'
|
||||
pronouns: 'Pronouns'
|
||||
pronounsInfo: >
|
||||
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
||||
|
@ -577,6 +577,8 @@ user:
|
||||
profile:
|
||||
description: 'Beschreibung'
|
||||
names: 'Namen'
|
||||
pronunciation:
|
||||
ipa: 'Aussprache in IPA'
|
||||
pronouns: 'Pronomen'
|
||||
pronounsInfo: >
|
||||
Du kannst entweder ein <strong>Pronomen</strong> (z.B. „sier“ oder „sie/ihr“) oder einen <strong>Link</strong> (z.B. „https://de.pronouns.page/dey“)
|
||||
|
@ -804,6 +804,8 @@ user:
|
||||
profile:
|
||||
description: 'Description'
|
||||
names: 'Names'
|
||||
pronunciation:
|
||||
ipa: 'Pronunciation using IPA'
|
||||
pronouns: 'Pronouns'
|
||||
pronounsInfo: >
|
||||
You can enter a <strong>pronoun</strong> (eg. “they” or “she/her”)
|
||||
|
@ -161,4 +161,5 @@ export default [
|
||||
'profile.calendar.customEvents.validation.missingDate',
|
||||
'profile.calendar.customEvents.validation.invalidDate',
|
||||
'profile.calendar.publicEvents.header',
|
||||
'profile.pronunciation.ipa',
|
||||
];
|
||||
|
@ -239,12 +239,12 @@ export default {
|
||||
use: 'yaml-loader',
|
||||
});
|
||||
config.module.rules.push({
|
||||
test: /.js/,
|
||||
test: /\.js$/,
|
||||
loader: 'string-replace-loader',
|
||||
options: {
|
||||
// To load .json files inside of .js files of type module in a node environment,
|
||||
// one has to either load from the filesystem or via a created require().
|
||||
// While a load vie filesystem is very unfriendly to webpack,
|
||||
// While a load via filesystem is very unfriendly to webpack,
|
||||
// the explicit creation of a require() function can be removed.
|
||||
// This probably gets replaced in the future by a `import from with { type: 'json' }`
|
||||
// statement, which is currently (2023-12) experimental in node and not well supported in webpack.
|
||||
@ -261,12 +261,8 @@ export default {
|
||||
],
|
||||
},
|
||||
});
|
||||
config.module.rules.push({
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto',
|
||||
});
|
||||
},
|
||||
transpile: ['markdown-it'],
|
||||
},
|
||||
env: {
|
||||
ENV: process.env.ENV,
|
||||
|
@ -110,7 +110,12 @@
|
||||
<p v-if="$te('profile.namesInfo')" class="small text-muted">
|
||||
<T>profile.namesInfo</T>
|
||||
</p>
|
||||
<OpinionListInput v-model="names" :customOpinions="opinions" :maxitems="128" :maxlength="config.profile.longNames ? 255 : 32"/>
|
||||
<OpinionListInput v-model="names" :prototype="{ value: '', opinion: 'meh', pronunciation: null }"
|
||||
:customOpinions="opinions" :maxitems="128" :maxlength="config.profile.longNames ? 255 : 32">
|
||||
<template v-slot:additional="s">
|
||||
<PronunciationInput v-model="s.val.pronunciation"/>
|
||||
</template>
|
||||
</OpinionListInput>
|
||||
<InlineMarkdownInstructions v-model="markdown"/>
|
||||
<PropagateCheckbox field="names" :before="beforeChanges.names" :after="names" v-if="otherProfiles > 0" @change="propagateChanged"/>
|
||||
</template>
|
||||
|
@ -255,7 +255,9 @@ const fetchProfiles = async (db, username, self, opts = undefined) => {
|
||||
|
||||
const profile_obj = {
|
||||
opinions: propv("opinions", () => JSON.parse(profile.opinions)),
|
||||
names: propv("names", () => JSON.parse(profile.names)),
|
||||
names: propv("names", () => {
|
||||
return JSON.parse(profile.names).map((name) => { return { pronunciation: null, ...name }});
|
||||
}),
|
||||
pronouns: propv("pronouns", () => JSON.parse(profile.pronouns)),
|
||||
description: propv("description", () => profile.description),
|
||||
age: propv("age", () => calcAge(profile.birthday)),
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { loadTsv } from '../loader.js';
|
||||
import { buildPronoun, parsePronouns } from '../../src/buildPronoun.js';
|
||||
import { Example } from '../../src/classes.js';
|
||||
import sha1 from 'sha1';
|
||||
import { handleErrorAsync } from '../../src/helpers.js';
|
||||
import { convertPronunciationStringToSsml, handleErrorAsync } from '../../src/helpers.js';
|
||||
|
||||
import awsConfig from '../aws.js';
|
||||
import Polly from 'aws-sdk/clients/polly.js';
|
||||
@ -11,28 +8,10 @@ import S3 from 'aws-sdk/clients/s3.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/pronounce/:voice/:pronoun*', handleErrorAsync(async (req, res) => {
|
||||
const pronounString = req.params.pronoun + req.params[0];
|
||||
const pronoun = buildPronoun(
|
||||
parsePronouns(loadTsv('pronouns/pronouns')),
|
||||
pronounString,
|
||||
);
|
||||
router.get('/pronounce/:voice/*', handleErrorAsync(async (req, res) => {
|
||||
const text = req.params[0];
|
||||
|
||||
if (!pronoun || !req.query.example) {
|
||||
return res.status(404).json({error: 'Not found'});
|
||||
}
|
||||
|
||||
let [singular, plural, isHonorific] = req.query.example.split('|');
|
||||
const example = new Example(
|
||||
Example.parse(singular),
|
||||
Example.parse(plural || singular),
|
||||
!!parseInt(isHonorific || '0'),
|
||||
)
|
||||
|
||||
const text = example.pronounce(pronoun);
|
||||
|
||||
// quick length check to avoid abuse. remove SSML tags but keep both tag value and attributes
|
||||
if (!text || text.replace(/<[^ ]+/g, '').replace('>', '').length > 256) {
|
||||
if (!text || text.length > 256) {
|
||||
return res.status(404).json({error: 'Not found'});
|
||||
}
|
||||
|
||||
@ -44,7 +23,8 @@ router.get('/pronounce/:voice/:pronoun*', handleErrorAsync(async (req, res) => {
|
||||
const s3 = new S3(awsConfig);
|
||||
const polly = new Polly(awsConfig);
|
||||
|
||||
const key = `pronunciation/${global.config.locale}-${req.params.voice}/${pronounString}/${sha1(text)}.mp3`;
|
||||
const ssml = convertPronunciationStringToSsml(text);
|
||||
const key = `pronunciation/${global.config.locale}-${req.params.voice}/${sha1(ssml)}.mp3`;
|
||||
|
||||
try {
|
||||
const s3getResponse = await s3.getObject({Key: key}).promise();
|
||||
@ -52,7 +32,7 @@ router.get('/pronounce/:voice/:pronoun*', handleErrorAsync(async (req, res) => {
|
||||
} catch {
|
||||
const pollyResponse = await polly.synthesizeSpeech({
|
||||
TextType: 'ssml',
|
||||
Text: text,
|
||||
Text: ssml,
|
||||
OutputFormat: 'mp3',
|
||||
LanguageCode: voice.language,
|
||||
VoiceId: voice.voice,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { buildDict, buildList, capitalise } from './helpers.js';
|
||||
import { buildDict, buildList, capitalise, escapePronunciationString } from './helpers.js';
|
||||
import MORPHEMES from '../data/pronouns/morphemes.js';
|
||||
|
||||
const config = process.env.CONFIG || global.config;
|
||||
@ -55,7 +55,7 @@ export class Example {
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
pronounce(pronoun) {
|
||||
toPronunciationString(pronoun) {
|
||||
let interchangable = false;
|
||||
|
||||
const buildPronunciation = m => {
|
||||
@ -68,29 +68,29 @@ export class Example {
|
||||
return pronunciation
|
||||
? (pronunciation.startsWith('=')
|
||||
? pronunciation.substring(1)
|
||||
: `<phoneme alphabet="ipa" ph="${pronunciation}">${morpheme}</phoneme>`
|
||||
: `/${pronunciation}/`
|
||||
)
|
||||
: ( config.pronunciation.ipa && morpheme
|
||||
? morpheme.split('').map(
|
||||
c => [' ', ',', '.', ':', ';', '–', '-'].includes(c)
|
||||
? c
|
||||
: `<phoneme alphabet="ipa" ph="${c}">${c}</phoneme>`
|
||||
: `/${c}/`
|
||||
).join('')
|
||||
: morpheme
|
||||
);
|
||||
}
|
||||
|
||||
const ssml = '<speak>' + this.parts(pronoun).map(part => {
|
||||
const pronunciationString = this.parts(pronoun).map(part => {
|
||||
return part.variable
|
||||
? buildPronunciation(part.str)
|
||||
: part.str;
|
||||
}).join('') + '</speak>';
|
||||
: escapePronunciationString(part.str);
|
||||
}).join('');
|
||||
|
||||
if (interchangable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ssml;
|
||||
return pronunciationString;
|
||||
}
|
||||
|
||||
toString() {
|
||||
|
@ -250,6 +250,54 @@ const escapeChars = {
|
||||
|
||||
export const escapeHtml = (text) => text.replace(/[&<>"]/g, tag => escapeChars[tag] || tag);
|
||||
|
||||
export const escapePronunciationString = (text) => {
|
||||
return text.replaceAll('\\', '\\\\')
|
||||
.replaceAll('/', '\\/');
|
||||
};
|
||||
|
||||
export const unescapePronunciationString = (pronunciationString) => {
|
||||
return pronunciationString.replaceAll('\\/', '/')
|
||||
.replaceAll('\\\\', '\\');
|
||||
};
|
||||
|
||||
export const convertPronunciationStringToSsml = (pronunciationString) => {
|
||||
const escapedString = escapeHtml(pronunciationString);
|
||||
let ssml = '';
|
||||
let escape = false;
|
||||
let currentPhonemes = null;
|
||||
for (const character of escapedString) {
|
||||
if (escape) {
|
||||
if (currentPhonemes === null) {
|
||||
ssml += character;
|
||||
} else {
|
||||
currentPhonemes += character;
|
||||
}
|
||||
escape = false;
|
||||
} else {
|
||||
if (character === '\\') {
|
||||
escape = true;
|
||||
} else if (character == '/') {
|
||||
if (currentPhonemes === null) {
|
||||
currentPhonemes = '';
|
||||
} else {
|
||||
ssml += `<phoneme alphabet="ipa" ph="${currentPhonemes}"></phoneme>`;
|
||||
currentPhonemes = null;
|
||||
}
|
||||
} else {
|
||||
if (currentPhonemes === null) {
|
||||
ssml += character;
|
||||
} else {
|
||||
currentPhonemes += character;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentPhonemes !== null) {
|
||||
ssml += `/${currentPhonemes}`;
|
||||
}
|
||||
return `<speak>${ssml}</speak>`;
|
||||
};
|
||||
|
||||
export class ImmutableArray extends Array {
|
||||
sorted(a, b) {
|
||||
return new ImmutableArray(...[...this].sort(a, b));
|
||||
|
57
test/helpers.test.js
Normal file
57
test/helpers.test.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from '@jest/globals';
|
||||
|
||||
import { convertPronunciationStringToSsml, escapePronunciationString } from '../src/helpers.js';
|
||||
|
||||
describe('when escaping pronunciation', () => {
|
||||
test.each([
|
||||
{
|
||||
description: 'slashes get escaped',
|
||||
text: 'w/o n/A',
|
||||
pronunciationString: String.raw`w\/o n\/A`,
|
||||
},
|
||||
{
|
||||
description: 'backslashes get escaped',
|
||||
text: String.raw`\n is the symbol for a newline, \t for a tab`,
|
||||
pronunciationString: String.raw`\\n is the symbol for a newline, \\t for a tab`,
|
||||
}
|
||||
])('$description', ({ text, pronunciationString }) => {
|
||||
expect(escapePronunciationString(text)).toBe(pronunciationString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when converting pronunciation', () => {
|
||||
test.each([
|
||||
{
|
||||
description: 'simple text is passed as-is',
|
||||
pronunciationString: 'text',
|
||||
ssml: '<speak>text</speak>',
|
||||
},
|
||||
{
|
||||
description: 'slashes describe IPA phonemes',
|
||||
pronunciationString: '/ðeɪ/',
|
||||
ssml: '<speak><phoneme alphabet="ipa" ph="ðeɪ"></phoneme></speak>',
|
||||
},
|
||||
{
|
||||
description: 'simple text and slashes can be combined',
|
||||
pronunciationString: '/ðeɪ/ are',
|
||||
ssml: '<speak><phoneme alphabet="ipa" ph="ðeɪ"></phoneme> are</speak>',
|
||||
},
|
||||
{
|
||||
description: 'slashes can be escaped at front',
|
||||
pronunciationString: String.raw`w\/o, n/A`,
|
||||
ssml: '<speak>w/o, n/A</speak>',
|
||||
},
|
||||
{
|
||||
description: 'slashes can be escaped at back',
|
||||
pronunciationString: String.raw`w/o, n\/A`,
|
||||
ssml: '<speak>w/o, n/A</speak>',
|
||||
},
|
||||
{
|
||||
description: 'provided HTML is escaped',
|
||||
pronunciationString: '<break time="1s"/>',
|
||||
ssml: '<speak><break time="1s"/></speak>',
|
||||
},
|
||||
])('$description', ({ pronunciationString, ssml }) => {
|
||||
expect(convertPronunciationStringToSsml(pronunciationString)).toBe(ssml);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user