User:Glrx/Phoneme.js

// Use SSML to say Wikipedia IPAc-en phoneme string //  Only works in Edge browser because it is the only browser that obeys SSML

// Currently sets speaker as en-US. // The Wikipedia IPA templates create a span with class="IPA" to signal IPA strings //  the IPAc-he and IPAc-ka templates put the class on each phoneme! // The templates do not set a language attribute //  For example, the IPAc-it template does not set lang="it" for tha span. //    in a sense, it should not because the help information is in English. //  language may be inferred from child anchor element //    e.g., ... allows the inference lang="ar" //  Non-English strings may cause XML to be verbalized //    see examples on https://en.wikipedia.org/wiki/Template:IPA

// Copyright 2018 https://en.Wikipedia.org/wiki/User:Glrx. Permission CC-BY-SA 3.0.

/*jslint browser:true, for:true, white:true, single:true */ /*global console mw speechSynthesis SpeechSynthesisUtterance document */ /*property add, appendChild, color, createElement, find, firstChild, getAttribute, hook, length, log, nodeName, replace, setAttribute, speak, style, text, textContent, title

/** @type {Object.} */ var langtagFromTitle = { "Help:IPA" : "en-US", "Help:IPA/Arabic" : "ar", // works "Help:IPA/Cantonese" : "yue", "Help:IPA/English" : "en-US", "Help:IPA/French" : "fr-FR", // fr speaks XML "Help:IPA/Hungarian" : "hu", // works "Help:IPA/Irish" : "ga", // speaks XML "Help:IPA/Italian" : "it-IT", // it speaks XML "Help:IPA/Japanese" : "ja", // works "Help:IPA/Korean" : "ko", // works "Help:IPA/Mandarin" : "cmn", "Help:IPA/Māori" : "mi", // works "Help:IPA/Polish" : "pl", // works "Help:IPA/Portuguese" : "pt", // works "Help:IPA/Romanian" : "ro", // dies: bubbles up to anchor "Help:IPA/Spanish" : "es-ES", // es speaks XML "Help:IPA for Georgian" : "ka", "Help:IPA for Hebrew" : "he" };

/** Choose a local voice for the langtag * There may be a problem with the first call and Chrome: SpeechSynthesis.onvoiceschanged * @param {string} langtag * @returns {SpeechSynthesisVoice | null} - voice */ function voiceChoose(langtag) { "use strict"; /* sequence of SpeechSynthesisVoice */ var voices = speechSynthesis.getVoices; var voice = null; var i;	// look through the voices //  returns first match rather than best match for (i = 0; i < voices.length; i++) { var v = voices[i]; // console.log(v); if (v.lang == langtag && v.localService) { // console.log("..match"); voice = v;			return voice; }	}	return voice; }

/** * Takes an element with attribute data-ph, build a SpeechSynthesisUtterance * that uses the phoneme string, and speaks that utterance * @param {Element} el * @returns {null} function speakPhoneme(el) { "use strict"; /** @type {string} */ var str = el.getAttribute("data-ph"); /** @type {string} */ var langtag = el.getAttribute("data-langtag"); // use the Web Speech standard; some browswers will want webkit... prefix var u = new SpeechSynthesisUtterance;

//  // schemaLocation is recommended: //  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" //  xsi:schemaLocation="http://www.w3.org/2001/10/synthesis http://www.w3.org/TR/speech-synthesis11/synthesis.xsd" // if I set , then Edge speaks the markup /** @type {string} */ var strXML = '\r\n\r\n Phoneme speech not available. \r\n '; // set the language strXML = strXML.replace("en-US", langtag); // this needs to superquote double quotes, but hit all the main chars str = str.replace("&", "&amp;"); str = str.replace("'", "&apos;"); str = str.replace("\"", "&quot;");   str = str.replace("<", "&lt;");    str = str.replace(">", "&gt;");    strXML = strXML.replace("mama", str);    // language is set in the SSML, so do not set it here    // Some Web Speech implementions are finicky and will not accept "en"    // u.lang = "en-US";    // try choosing a local voice. Windows local voices may do SSML's phoneme    u.voice = voiceChoose(langtag);    // Specification says .text is a DOMSTRING    u.text = strXML;

// speak the SSML speechSynthesis.speak(u); }

mw.hook( "wikipage.content" ).add( function( $content ) {   "use strict";    // get a list of elements with class IPA    var clist = $content.find(".IPA");    // process each element    /** @type {number} */    var i;    /** @type {Element} */    var e;    /** @type {string} */    var langtag;    /** @type {string} */    var strContent;    /** @type {Element} */    var ch;    /** @type {Element} */    var spanSpeak;    for (i=0; i < clist.length; i+=1) {    	e = clist[i];        langtag = "en-US";        // textContent will extract text from internal spans...        strContent = e.textContent;

// remove the slashes (should anchor leading and trailing) // assuming the result is valid IPA string strContent = strContent.replace(/\//g, ""); // foreign languages use square brackets strContent = strContent.replace(/\[/g, ""); strContent = strContent.replace(/\]/g, ""); // OED strings have parens strContent = strContent.replace(/\(/g, "");       strContent = strContent.replace(/\)/g, ""); // comma was for alternatives strContent = strContent.replace(/,/g, ""); // hypen was for join strContent = strContent.replace(/\-/g, ""); // Edge complains about some phonemes; silently remove them strContent = strContent.replace(/˔/g, ""); // Hack -- look for language ch = e.firstChild; // if the first child is an anchor if (ch && ch.nodeName === "A") { // if the title is something like Help:IPA/Arabic, then we have a language if (! langtagFromTitle[ch.title]) { console.log("Missing title language: " + ch.title); } else { langtag = langtagFromTitle[ch.title]; }       }

// create a span for the phoneme speaker prompt spanSpeak = document.createElement("span"); // text for the prompt - ([speaker] speak) spanSpeak.textContent = "(\uD83D\uDD0A)"; // save the IPA string in the data-ph attribute spanSpeak.setAttribute("data-ph", strContent); spanSpeak.setAttribute("data-langtag", langtag); // show the IPA string as a tooltip spanSpeak.setAttribute("title", strContent + " using langtag " + langtag); // set the onclick action spanSpeak.setAttribute("onclick", "speakPhoneme(this);"); // color the span red spanSpeak.style.color = "red"; // add the span to class IPA span e.appendChild(spanSpeak); } });