From dceaa853098a465b2eaa1b90900b5c1832131f26 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 14:16:19 -0500 Subject: Rename audio.js to audio-system.js --- ext/bg/background.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 7fd1c477..f2f70d4d 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -39,7 +39,7 @@ - + -- cgit v1.2.3 From 69cce49b0d5d9f11f4ffb529ae3d060536297c07 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 15:14:05 -0500 Subject: Move Anki note generation functionality into a new class --- ext/bg/background.html | 1 + ext/bg/js/anki-note-builder.js | 110 +++++++++++++++++++++++++++++++++++ ext/bg/js/backend.js | 9 +-- ext/bg/js/dictionary.js | 88 ---------------------------- ext/bg/js/settings/anki-templates.js | 8 ++- ext/bg/settings.html | 1 + 6 files changed, 122 insertions(+), 95 deletions(-) create mode 100644 ext/bg/js/anki-note-builder.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index f2f70d4d..8db017f1 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -22,6 +22,7 @@ + diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js new file mode 100644 index 00000000..f7555280 --- /dev/null +++ b/ext/bg/js/anki-note-builder.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/*global apiTemplateRender*/ + +class AnkiNoteBuilder { + constructor() { + this._markers = new Set([ + 'audio', + 'character', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'expression', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'kunyomi', + 'onyomi', + 'reading', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]); + } + + async createNote(definition, mode, options, templates) { + const isKanji = (mode === 'kanji'); + const tags = options.anki.tags; + const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; + const modeOptionsFieldEntries = Object.entries(modeOptions.fields); + + const note = { + fields: {}, + tags, + deckName: modeOptions.deck, + modelName: modeOptions.model + }; + + for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { + note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, options, templates, null); + } + + if (!isKanji && definition.audio) { + const audioFields = []; + + for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { + if (fieldValue.includes('{audio}')) { + audioFields.push(fieldName); + } + } + + if (audioFields.length > 0) { + note.audio = { + url: definition.audio.url, + filename: definition.audio.filename, + skipHash: '7e2c2f954ef6051373ba916f000168dc', // hash of audio data that should be skipped + fields: audioFields + }; + } + } + + return note; + } + + async formatField(field, definition, mode, options, templates, errors=null) { + const data = { + marker: null, + definition, + group: options.general.resultOutputMode === 'group', + merge: options.general.resultOutputMode === 'merge', + modeTermKanji: mode === 'term-kanji', + modeTermKana: mode === 'term-kana', + modeKanji: mode === 'kanji', + compactGlossaries: options.general.compactGlossaries + }; + const markers = this._markers; + const pattern = /\{([\w-]+)\}/g; + return await stringReplaceAsync(field, pattern, async (g0, marker) => { + if (!markers.has(marker)) { + return g0; + } + data.marker = marker; + try { + return await apiTemplateRender(templates, data); + } catch (e) { + if (errors) { errors.push(e); } + return `{${marker}-render-error}`; + } + }); + } +} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 60a87916..929281da 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -20,10 +20,10 @@ conditionsTestValue, profileConditionsDescriptor handlebarsRenderDynamic requestText, requestJson, optionsLoad -dictConfigured, dictTermsSort, dictEnabledSet, dictNoteFormat +dictConfigured, dictTermsSort, dictEnabledSet audioGetUrl, audioInject jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana -AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ +AnkiNoteBuilder, AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ class Backend { constructor() { @@ -31,6 +31,7 @@ class Backend { this.anki = new AnkiNull(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); + this.ankiNoteBuilder = new AnkiNoteBuilder(); this.options = null; this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; @@ -450,7 +451,7 @@ class Backend { ); } - const note = await dictNoteFormat(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); return this.anki.addNote(note); } @@ -463,7 +464,7 @@ class Backend { const notes = []; for (const definition of definitions) { for (const mode of modes) { - const note = await dictNoteFormat(definition, mode, options, templates); + const note = await this.ankiNoteBuilder.createNote(definition, mode, options, templates); notes.push(note); } } diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index ffeac80a..3dd1d0c1 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -/*global apiTemplateRender*/ - function dictEnabledSet(options) { const enabledDictionaryMap = new Map(); for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) { @@ -333,89 +331,3 @@ function dictTagsSort(tags) { function dictFieldSplit(field) { return field.length === 0 ? [] : field.split(' '); } - -async function dictFieldFormat(field, definition, mode, options, templates, exceptions) { - const data = { - marker: null, - definition, - group: options.general.resultOutputMode === 'group', - merge: options.general.resultOutputMode === 'merge', - modeTermKanji: mode === 'term-kanji', - modeTermKana: mode === 'term-kana', - modeKanji: mode === 'kanji', - compactGlossaries: options.general.compactGlossaries - }; - const markers = dictFieldFormat.markers; - const pattern = /\{([\w-]+)\}/g; - return await stringReplaceAsync(field, pattern, async (g0, marker) => { - if (!markers.has(marker)) { - return g0; - } - data.marker = marker; - try { - return await apiTemplateRender(templates, data); - } catch (e) { - if (exceptions) { exceptions.push(e); } - return `{${marker}-render-error}`; - } - }); -} -dictFieldFormat.markers = new Set([ - 'audio', - 'character', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'kunyomi', - 'onyomi', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' -]); - -async function dictNoteFormat(definition, mode, options, templates) { - const isKanji = (mode === 'kanji'); - const tags = options.anki.tags; - const modeOptions = isKanji ? options.anki.kanji : options.anki.terms; - const modeOptionsFieldEntries = Object.entries(modeOptions.fields); - - const note = { - fields: {}, - tags, - deckName: modeOptions.deck, - modelName: modeOptions.model - }; - - for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { - note.fields[fieldName] = await dictFieldFormat(fieldValue, definition, mode, options, templates); - } - - if (!isKanji && definition.audio) { - const audioFields = []; - - for (const [fieldName, fieldValue] of modeOptionsFieldEntries) { - if (fieldValue.includes('{audio}')) { - audioFields.push(fieldName); - } - } - - if (audioFields.length > 0) { - note.audio = { - url: definition.audio.url, - filename: definition.audio.filename, - skipHash: '7e2c2f954ef6051373ba916f000168dc', - fields: audioFields - }; - } - } - - return note; -} diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 244ec42e..32a990f9 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -17,8 +17,9 @@ */ /*global getOptionsContext, getOptionsMutable, settingsSaveOptions -ankiGetFieldMarkers, ankiGetFieldMarkersHtml, dictFieldFormat -apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates*/ +ankiGetFieldMarkers, ankiGetFieldMarkersHtml +apiOptionsGet, apiTermsFind, apiGetDefaultAnkiFieldTemplates, +AnkiNoteBuilder*/ function onAnkiFieldTemplatesReset(e) { e.preventDefault(); @@ -92,7 +93,8 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const options = await apiOptionsGet(optionsContext); let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = await apiGetDefaultAnkiFieldTemplates(); } - result = await dictFieldFormat(field, definition, mode, options, templates, exceptions); + const ankiNoteBuilder = new AnkiNoteBuilder(); + result = await ankiNoteBuilder.formatField(field, definition, mode, options, templates, exceptions); } } catch (e) { exceptions.push(e); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index e9fc6be5..0db76d71 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1090,6 +1090,7 @@ + -- cgit v1.2.3 From 79eb4bdc167409cda6cebf3c0939189ab4cbbaa0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 21:49:04 -0500 Subject: Remove bg/js/api.js --- ext/bg/background.html | 1 - ext/bg/js/api.js | 43 ------------------------------------------- 2 files changed, 44 deletions(-) delete mode 100644 ext/bg/js/api.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 8db017f1..f6e00bf5 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -23,7 +23,6 @@ - diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js deleted file mode 100644 index 9a05023e..00000000 --- a/ext/bg/js/api.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019-2020 Alex Yatskov - * Author: Alex Yatskov - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -function _apiInvoke(action, params={}) { - const data = {action, params}; - return new Promise((resolve, reject) => { - try { - const callback = (response) => { - if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(jsonToError(response.error)); - } else { - resolve(response.result); - } - } else { - const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; - reject(new Error(`${message} (${JSON.stringify(data)})`)); - } - }; - const backend = window.yomichanBackend; - backend.onMessage({action, params}, null, callback); - } catch (e) { - reject(e); - yomichan.triggerOrphaned(e); - } - }); -} -- cgit v1.2.3 From 9cd4a52b9e701f47c7ca7b44c52cbcd66b7bcb05 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 7 Mar 2020 14:39:25 -0500 Subject: Rename audio.js to audio-uri-builder.js --- ext/bg/background.html | 2 +- ext/bg/js/audio-uri-builder.js | 156 +++++++++++++++++++++++++++++++++++++++++ ext/bg/js/audio.js | 156 ----------------------------------------- 3 files changed, 157 insertions(+), 157 deletions(-) create mode 100644 ext/bg/js/audio-uri-builder.js delete mode 100644 ext/bg/js/audio.js (limited to 'ext/bg/background.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index f6e00bf5..44abe8fd 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -24,7 +24,7 @@ - + diff --git a/ext/bg/js/audio-uri-builder.js b/ext/bg/js/audio-uri-builder.js new file mode 100644 index 00000000..80e9cb9a --- /dev/null +++ b/ext/bg/js/audio-uri-builder.js @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017-2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/*global jpIsStringEntirelyKana*/ + +class AudioUriBuilder { + constructor() { + this._getUrlHandlers = new Map([ + ['jpod101', this._getUriJpod101.bind(this)], + ['jpod101-alternate', this._getUriJpod101Alternate.bind(this)], + ['jisho', this._getUriJisho.bind(this)], + ['text-to-speech', this._getUriTextToSpeech.bind(this)], + ['text-to-speech-reading', this._getUriTextToSpeechReading.bind(this)], + ['custom', this._getUriCustom.bind(this)] + ]); + } + + normalizeUrl(url, baseUrl, basePath) { + if (url) { + if (url[0] === '/') { + if (url.length >= 2 && url[1] === '/') { + // Begins with "//" + url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url; + } else { + // Begins with "/" + url = baseUrl + url; + } + } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) { + // No URI scheme => relative path + url = baseUrl + basePath + url; + } + } + return url; + } + + async getUri(mode, definition, options) { + const handler = this._getUrlHandlers.get(mode); + if (typeof handler === 'function') { + try { + return await handler(definition, options); + } catch (e) { + // NOP + } + } + return null; + } + + async _getUriJpod101(definition) { + let kana = definition.reading; + let kanji = definition.expression; + + if (!kana && jpIsStringEntirelyKana(kanji)) { + kana = kanji; + kanji = null; + } + + const params = []; + if (kanji) { + params.push(`kanji=${encodeURIComponent(kanji)}`); + } + if (kana) { + params.push(`kana=${encodeURIComponent(kana)}`); + } + + return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; + } + + async _getUriJpod101Alternate(definition) { + const response = await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); + xhr.addEventListener('load', () => resolve(xhr.responseText)); + xhr.send(`post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`); + }); + + const dom = new DOMParser().parseFromString(response, 'text/html'); + for (const row of dom.getElementsByClassName('dc-result-row')) { + try { + const url = row.querySelector('audio>source[src]').getAttribute('src'); + const reading = row.getElementsByClassName('dc-vocab_kana').item(0).textContent; + if (url && reading && (!definition.reading || definition.reading === reading)) { + return this.normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); + } + } catch (e) { + // NOP + } + } + + throw new Error('Failed to find audio URL'); + } + + async _getUriJisho(definition) { + const response = await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', `https://jisho.org/search/${definition.expression}`); + xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); + xhr.addEventListener('load', () => resolve(xhr.responseText)); + xhr.send(); + }); + + const dom = new DOMParser().parseFromString(response, 'text/html'); + try { + const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); + if (audio !== null) { + const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); + if (url) { + return this.normalizeUrl(url, 'https://jisho.org', '/search/'); + } + } + } catch (e) { + // NOP + } + + throw new Error('Failed to find audio URL'); + } + + async _getUriTextToSpeech(definition, options) { + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + } + + async _getUriTextToSpeechReading(definition, options) { + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + } + + async _getUriCustom(definition, options) { + const customSourceUrl = options.audio.customSourceUrl; + return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); + } +} diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js deleted file mode 100644 index 80e9cb9a..00000000 --- a/ext/bg/js/audio.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2017-2020 Alex Yatskov - * Author: Alex Yatskov - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/*global jpIsStringEntirelyKana*/ - -class AudioUriBuilder { - constructor() { - this._getUrlHandlers = new Map([ - ['jpod101', this._getUriJpod101.bind(this)], - ['jpod101-alternate', this._getUriJpod101Alternate.bind(this)], - ['jisho', this._getUriJisho.bind(this)], - ['text-to-speech', this._getUriTextToSpeech.bind(this)], - ['text-to-speech-reading', this._getUriTextToSpeechReading.bind(this)], - ['custom', this._getUriCustom.bind(this)] - ]); - } - - normalizeUrl(url, baseUrl, basePath) { - if (url) { - if (url[0] === '/') { - if (url.length >= 2 && url[1] === '/') { - // Begins with "//" - url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url; - } else { - // Begins with "/" - url = baseUrl + url; - } - } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) { - // No URI scheme => relative path - url = baseUrl + basePath + url; - } - } - return url; - } - - async getUri(mode, definition, options) { - const handler = this._getUrlHandlers.get(mode); - if (typeof handler === 'function') { - try { - return await handler(definition, options); - } catch (e) { - // NOP - } - } - return null; - } - - async _getUriJpod101(definition) { - let kana = definition.reading; - let kanji = definition.expression; - - if (!kana && jpIsStringEntirelyKana(kanji)) { - kana = kanji; - kanji = null; - } - - const params = []; - if (kanji) { - params.push(`kanji=${encodeURIComponent(kanji)}`); - } - if (kana) { - params.push(`kana=${encodeURIComponent(kana)}`); - } - - return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; - } - - async _getUriJpod101Alternate(definition) { - const response = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.send(`post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`); - }); - - const dom = new DOMParser().parseFromString(response, 'text/html'); - for (const row of dom.getElementsByClassName('dc-result-row')) { - try { - const url = row.querySelector('audio>source[src]').getAttribute('src'); - const reading = row.getElementsByClassName('dc-vocab_kana').item(0).textContent; - if (url && reading && (!definition.reading || definition.reading === reading)) { - return this.normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); - } - } catch (e) { - // NOP - } - } - - throw new Error('Failed to find audio URL'); - } - - async _getUriJisho(definition) { - const response = await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', `https://jisho.org/search/${definition.expression}`); - xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data'))); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - xhr.send(); - }); - - const dom = new DOMParser().parseFromString(response, 'text/html'); - try { - const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); - if (audio !== null) { - const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); - if (url) { - return this.normalizeUrl(url, 'https://jisho.org', '/search/'); - } - } - } catch (e) { - // NOP - } - - throw new Error('Failed to find audio URL'); - } - - async _getUriTextToSpeech(definition, options) { - const voiceURI = options.audio.textToSpeechVoice; - if (!voiceURI) { - throw new Error('No voice'); - } - - return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; - } - - async _getUriTextToSpeechReading(definition, options) { - const voiceURI = options.audio.textToSpeechVoice; - if (!voiceURI) { - throw new Error('No voice'); - } - - return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; - } - - async _getUriCustom(definition, options) { - const customSourceUrl = options.audio.customSourceUrl; - return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); - } -} -- cgit v1.2.3