1/* Articulate.js (1.1.0). (C) 2017 Adam Coti. MIT @license: en.wikipedia.org/wiki/MIT_License 2 3 See Github page at: https://github.com/acoti/articulate.js 4 See Web site at: http://articulate.purefreedom.com 5 6*/ 7 8 9(function($) { 10 "use strict"; 11 12 var ignoreTagsUser = new Array(); 13 var recognizeTagsUser = new Array(); 14 var replacements = new Array(); 15 var customTags = new Array(); 16 17 var rateDefault = 1.10; 18 var pitchDefault = 1; 19 var volumeDefault = 1; 20 21 var rateUserDefault; 22 var pitchUserDefault; 23 var volumeUserDefault; 24 var voiceUserDefault; 25 26 var rate = rateDefault; 27 var pitch = pitchDefault; 28 var volume = volumeDefault; 29 var voices = new Array(); 30 31 function voiceTag(prepend,append) { 32 this.prepend = prepend; 33 this.append = append; 34 } 35 36 function voiceObj(name,language) { 37 this.name = name; 38 this.language = language; 39 } 40 41 42 43 44 45 // This populates the "voices" array with objects that represent the available voices in the 46 // current browser. Each object has two properties: name and language. It is loaded 47 // asynchronously in deference to Chrome. 48 49 function populateVoiceList() { 50 var systemVoices = speechSynthesis.getVoices(); 51 for(var i = 0; i < systemVoices.length ; i++) { 52 voices.push(new voiceObj(systemVoices[i].name,systemVoices[i].lang)); 53 } 54 } 55 56 populateVoiceList(); 57 58 if (typeof speechSynthesis !== 'undefined' && speechSynthesis.onvoiceschanged !== undefined) { 59 speechSynthesis.onvoiceschanged = populateVoiceList; 60 } 61 62 63 // After checking for compatability, define the utterance object and then cancel the speech 64 // immediately even though nothing is yet spoken. This is to fix a quirk in Windows Chrome. 65 66 if ("speechSynthesis" in window) { 67 var speech = new SpeechSynthesisUtterance(); 68 window.speechSynthesis.cancel(); 69 } 70 71 72 // Hated to do a browser detect, but Windows Chrome is a bit buggy and inconsistent with the default 73 // voice that it uses unless that default voice ('native') is specified directly -- see line 165. 74 // Every browser is fine with 'native' specified directly except Microsoft Edge, which is why 75 // this browser detect ened up necessary for the time being. I think this will all resolve itself 76 // in future browser versions, but for now, I felt this was the safest solution. But I feel dirty. 77 78 var chrome = /chrome/i.test( navigator.userAgent ); 79 var edge = /edge/i.test( navigator.userAgent ); 80 var isChrome = ((chrome) && (!edge)); 81 82 83 var methods = { 84 speak : function(options) { 85 var opts = $.extend( {}, $.fn.articulate.defaults, options ); 86 87 var toSpeak = ""; 88 var obj, processed, finished; 89 var voiceTags = new Array(); 90 91 92 // Default values. 93 94 voiceTags["q"] = new voiceTag("quote,",", unquote,"); 95 voiceTags["ol"] = new voiceTag("Start of list.","End of list."); 96 voiceTags["ul"] = new voiceTag("Start of list.","End of list."); 97 voiceTags["blockquote"] = new voiceTag("Blockquote start.","Blockquote end."); 98 voiceTags["img"] = new voiceTag("There's an embedded image with the description,",""); 99 voiceTags["table"] = new voiceTag("There's an embedded table with the caption,",""); 100 voiceTags["figure"] = new voiceTag("There's an embedded figure with the caption,",""); 101 102 var ignoreTags = ["audio","button","canvas","code","del","dialog","dl","embed","form","head","iframe","meter","nav","noscript","object","s","script","select","style","textarea","video"]; 103 104 105 // Check to see if the browser supports the functionality. 106 107 if (!("speechSynthesis" in window)) { 108 alert("Sorry, this browser does not support the Web Speech API."); 109 return 110 }; 111 112 113 // If something is currently being spoken, ignore new voice request. Otherwise it would be queued, 114 // which is doable if someone wanted that, but not what I wanted. 115 116 if (window.speechSynthesis.speaking) { 117 return 118 }; 119 120 121 // Cycle through all the elements in the original jQuery selector, process and clean 122 // them one at a time, and continually append it to the variable "toSpeak". 123 124 this.each(function() { 125 obj = $(this).clone();
125 // clone the DOM node and its descendants of what the user wants spoken 126 processed = processDOMelements(obj); // process and manipulate DOM tree of this clone 127 processed = jQuery(processed).html(); // convert the result of all that to a string 128 finished = cleanDOMelements(processed); // do some text manipulation 129 toSpeak = toSpeak + " " + finished; // add it to what will ultimately be spoken after cycling through selectors 130 }); 131 132 133 // Check if users have set their own rate/pitch/volume defaults, otherwise use defaults. 134 135 if (rateUserDefault !== undefined) { 136 rate = rateUserDefault; 137 } else { 138 rate = rateDefault; 139 } 140 141 if (pitchUserDefault !== undefined) { 142 pitch = pitchUserDefault; 143 } else { 144 pitch = pitchDefault; 145 } 146 147 if (volumeUserDefault !== undefined) { 148 volume = volumeUserDefault; 149 } else { 150 volume = volumeDefault; 151 } 152 153 154 // To debug, un-comment the following to see exactly what's about to be spoken. 155 // console.log(toSpeak); 156 157 // This is where the magic happens. Well, not magic, but at least we can finally hear something. 158 // After the line that fixes the Windows Chrome quir
158k, the custom voice is set if one has been chosen. 159 160 speech = new SpeechSynthesisUtterance(); 161 speech.text = toSpeak; 162 speech.rate = rate; 163 speech.pitch = pitch; 164 speech.volume = volume; 165 if (isChrome) { speech.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == "native"; })[0]; }; 166 if (voiceUserDefault !== undefined) { speech.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == voiceUserDefault; })[0]; }; 167 window.speechSynthesis.speak(speech); 168 169 170 171 function processDOMelements(clone) { 172 173 var copy, prepend; 174 175 176 // Remove tags from the "ignoreTags" array because the user called "articulate('recognize')" 177 // and said he/she doesn't want some tags un-spoken. Double negative there, but it does make sense. 178 179 if (recognizeTagsUser.length > 0) { 180 for (var prop in recognizeTagsUser) { 181 var index = ignoreTags.indexOf(recognizeTagsUser[prop]); 182 if (index > -1) { 183 ignoreTags.splice(index, 1); 184 } 185 }; 186 }; 187 188 189 // Remove DOM objects from those listed in the "ignoreTags" array now that the user has specified 190 // which ones, if any, he/she wants to keep. 191 192 for (var prop in ignoreTags) { 193 jQuery(clone).find(ignoreTags[prop]).addBack(ignoreTags[prop]).not("[data-articulate-recognize]").each(function() { 194 jQuery(this).html(""); 195 }); 196 }; 197 198 199 // Remove DOM objects as specified in the "ignoreTagsUser" array that the user specified when 200 // calling "articulate('ignore')". 201 202 if (ignoreTagsUser.length > 0) { 203 for (var prop in ignoreTagsUser) { 204 jQuery(clone).find(ignoreTagsUser[prop]).addBack(ignoreTagsUser[prop]).not("[data-articulate-recognize]").each(function() { 205 jQuery(this).html(""); 206 }); 207 }; 208 }; 209 210 211 // Remove DOM objects as specified in the HTML with "data-articulate-ignore". 212 213 jQuery(clone).find("[data-articulate-ignore]").addBack("[data-articulate-ignore]").each(function() { 214 jQuery(this).html(""); 215 }); 216 217 218 // Search for prepend data as specified in the HTML with "data-articulate-prepend". 219 220 jQuery(clone).find("[data-articulate-prepend]").addBack("[data-articulate-prepend]").each(function() { 221 copy = jQuery(this).data("articulate-prepend"); 222 jQuery(this).prepend(copy + " "); 223 }); 224 225 226 // Search for append data as specified in the HTML with "data-articulate-append". 227 228 jQuery(clone).find("[data-articulate-append]").addBack("[data-articulate-append]").each(function() { 229 copy = jQuery(this).data("articulate-append"); 230 jQuery(this).append(" " + copy); 231 }); 232 233 234 // Search for tags to prepend and append as specified by the "voiceTags" array. 235 236 var count = 0; 237 238 for (var tag in voiceTags) { 239 count++ 240 if (count <= 4) { 241 jQuery(clone).find(tag).each(function() { 242 if (customTags[tag]) { 243 jQuery(this).prepend(customTags[tag].prepend + " "); 244 jQuery(this).append(" " + customTags[tag].append); 245 } else { 246 jQuery(this).prepend(voiceTags[tag].prepend + " "); 247 jQuery(this).append(" " + voiceTags[tag].append); 248 }; 249 }); 250 }; 251 }; 252 253 254 // Search for <h1> through <h6> and <li> and <br> to add a pause at the end of those tags. This is done 255 // because these tags require a pause, but often don't have a comma or period at the end of their text. 256 257 jQuery(clone).find("h1,h2,h3,h4,h5,h6,li,p").addBack("h1,h2,h3,h4,h5,h6,li,p").each(function() { 258 jQuery(this).append(". "); 259 }); 260 261 jQuery(clone).find("br").each(function() { 262 jQuery(this).after(", "); 263 }); 264 265 266 // Search for <figure>, check for <figcaption>, insert that text if it exists 267 // and then remove the whole DOM object 268 269 jQuery(clone).find("figure").addBack("figure").each(function() { 270 copy = jQuery(this).find("figcaption").html(); 271 if (customTags["figure"]) { 272 prepend = customTags["figure"].prepend 273 } 274 else { 275 prepend = voiceTags["figure"].prepend 276 } 277 if ((copy != undefined) && (copy !== "")) { 278 jQuery("<div>" + prepend + " " + copy + ".</div>").insertBefore(this); 279 } 280 jQuery(this).remove(); 281 }); 282 283 284 // Search for <image>, check for ALT attribute, insert that text if it exists and then 285 // remove the whole DOM object. Had to make adjustments for nesting in <picture> tags. 286 287 jQuery(clone).find("img").addBack("img").each(function() { 288 copy = jQuery(this).attr("alt"); 289 var parent = jQuery(this).parent(); 290 var parentName = parent.get(0).tagName; 291 292 if (customTags["img"]) { 293 prepend = customTags["img"].prepend 294 } 295 else { 296 prepend = voiceTags["img"].prepend 297 }; 298 299 if ((copy !== undefined) && (copy != "")) { 300 if (parentName == "PICTURE") { 301 var par 302 jQuery("<div>" + prepend + " " + copy + ".</div>").insertBefore(parent); 303 } else { 304 jQuery("<div>" + prepend + " " + copy + ".</div>").insertBefore(this); 305 } 306 }; 307 jQuery(this).remove(); 308 }); 309 310 311 // Search for <table>, check for <caption>, insert that text if it exists 312 // and then remove the whole DOM object. 313 314 jQuery(clone).find("table").addBack("table").each(function() { 315 copy = jQuery(this).find("caption").text(); 316 if (customTags["table"]) { 317 prepend = customTags["table"].prepend 318 } 319 else { 320 prepend = voiceTags["table"].prepend 321 } 322 if ((copy !== undefined) && (copy != "")) { 323 jQuery("<div>" + prepend + " " + copy + ".</div>").insertBefore(this); 324 } 325 jQuery(this).remove(); 326 }); 327 328 329 // Search for DOM object to be replaced as specified in the HTML with "data-articulate-swap". 330 331 jQuery(clone).find("[data-articulate-swap]").addBack("[data-articulate-swap]").each(function() { 332 copy = jQuery(this).data("articulate-swap"); 333 jQuery(this).text(copy); 334 }); 335 336 337 // Search for DOM object to spelled out as specified in the HTML with "data-articulate-spell". 338 // I find this function fun if, admittedly, not too practical. 339 340 jQuery(clone).find("[data-articulate-spell]").addBack("[data-articulate-spell]").each(function() { 341 copy = jQuery(this).text(); 342 copy = copy.split("").join(" "); 343 jQuery(this).text(copy); 344 }); 345 346 347 return clone; 348 } 349 350 351 352 function cleanDOMelements(final) { 353 354 var start,ended,speak,part1,part2,final; 355 356 357 // Search for <articulate> in comments, copy the text, place it outside the comment, 358 // and then splice together "final" string again, which omits the comment. 359 360 while (final.indexOf("<!-- <articulate>") != -1) { 361 start = final.indexOf("<!-- <articulate>"); 362 ended = final.indexOf("</articulate> -->",start); 363 364 if (ended == -1) { break; } 365 366 speak = final.substring(start + 17,ended); 367 part1 = final.substring(0,start); 368 part2 = final.substring(ended + 17); 369 final = part1 + " " + speak + " " + part2; 370 }; 371 372 373 // Strip out remaining comments. 374 375 final = final.replace(/<!--[\s\S]*?-->/g,""); 376 377 378 // Strip out remaining HTML tags. 379 380 final = final.replace(/(<([^>]+)>)/ig,""); 381 382 383 // Replace a string of characters with another as specified by "articulate('replace')". 384 385 var len = replacements.length; 386 var i = 0; 387 var old, rep; 388 389 while (i < len) { 390 old = replacements[i]; 391 old = old.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 392 rep = replacements[i + 1] + " "; 393 var regexp = new RegExp(old, "gi"); 394 var final = final.replace(regexp,rep); 395 i = i + 2; 396 }; 397 398 399 // Replace double smart quotes with proper text, same as what was done previously with <q>. 400 401 if (customTags["q"]) { 402 final = final.replace(/â/g,customTags["q"].prepend + " "); 403 final = final.replace(/â/g," " + customTags["q"].append); 404 } else { 405 final = final.replace(/â/g,voiceTags["q"].prepend + " "); 406 final = final.replace(/â/g," " + voiceTags["q"].append); 407 } 408 409 410 // Replace em-dashes and double-dashes with a pause since the browser doesn't do so when reading. 411 412 final = final.replace(/â/g, ", "); 413 final = final.replace(/--/g, ", "); 414 415 416 // When read from the DOM, a few special characters (& for example) display as their hex codes 417 // rather than resolving into their actual character -- this technique fixes that. 418 419 var txt = document.createElement("textarea"); 420 txt.innerHTML = final; 421 final = txt.value; 422 423 424 // Strip out new line characters and carriage returns, which cause unwanted pauses. 425 426 final = final.replace(/(\r\n|\n|\r)/gm,""); 427 428 429 // Strip out multiple spaces and periods and commas -- for neatness more than anything else since 430 // none of this will affect the speech. But it helps when checking progress in the console. 431 432 final = final.replace(/ +/g, ' '); 433 final = final.replace(/\.\./g, '.'); 434 final = final.replace(/,,/g, ','); 435 final = final.replace(/ ,/g, ','); 436 437 return final; 438 } 439 440 return this; 441 }, 442 443 444 // All the functions for Articulate.js. 445 446 pause : function() { 447 window.speechSynthesis.pause(); 448 return this; 449 }, 450 451 452 resume : function() { 453 window.speechSynthesis.resume(); 454 return this; 455 }, 456 457 458 stop : function() { 459 window.speechSynthesis.cancel(); 460 return this; 461 }, 462 463 464 enabled : function() { 465 return ("speechSynthesis" in window); 466 }, 467 468 469 isSpeaking : function() { 470 return (window.speechSynthesis.speaking); 471 }, 472 473 474 isPaused : function() { 475 return (window.speechSynthesis.paused); 476 }, 477 478 479 rate : function() { 480 var num = arguments[0]; 481 if ((num >= 0.1) && (num <= 10)) { 482 rateUserDefault = num; 483 } else if (num === undefined) { 484 rateUserDefault = void 0; 485 rate = rateDefault; 486 }; 487 return this; 488 }, 489 490 491 pitch : function() { 492 var num = arguments[0]; 493 if ((num >= 0.1) && (num <= 2)) { 494 pitchUserDefault = num; 495 } else if (num === undefined) { 496 pitchUserDefault = void 0; 497 pitch = pitchDefault; 498 }; 499 return this; 500 }, 501 502 503 volume : function() { 504 var num = arguments[0]; 505 if ((num >= 0) && (num <= 1)) { 506 volumeUserDefault = num; 507 } else if (num === undefined) { 508 volumeUserDefault = void 0; 509 volume = volumeDefault; 510 }; 511 return this; 512 }, 513 514 515 ignore : function() { 516 var len = arguments.length; 517 ignoreTagsUser.length = 0; 518 while (len > 0) { 519 len--; 520 ignoreTagsUser.push(arguments[len]); 521 }; 522 return this; 523 }, 524 525 526 recognize : function() { 527 var len = arguments.length; 528 recognizeTagsUser.length = 0; 529 while (len > 0) { 530 len--; 531 recognizeTagsUser.push(arguments[len]); 532 }; 533 return this; 534 }, 535 536 537 replace : function() { 538 var len = arguments.length; 539 replacements.length = 0; 540 var i = 0; 541 while (i < len) { 542 replacements.push(arguments[i], arguments[i + 1]); 543 i = i + 2; 544 if ((len - i) == 1) { break; }; 545 }; 546 return this; 547 }, 548 549 550 customize : function() { 551 var len = arguments.length; 552 if (len == 0) { 553 customTags = []; 554 }; 555 if (len == 2) { 556 if (["img","table","figure"].indexOf(arguments[0]) == -1) { console.log("Error: When customizing, tag indicated must be either 'img', 'table', or 'figure'."); return; } 557 customTags[arguments[0].toString()] = new voiceTag(arguments[1].toString()); 558 }; 559 if (len == 3) { 560 if (["q","ol","ul","blockquote"].indexOf(arguments[0]) == -1) { console.log("Error: When customizing, tag indicated must be either 'q', 'ol', 'ul' or 'blockquote'."); return; } 561 customTags[arguments[0].toString()] = new voiceTag(arguments[1].toString(), arguments[2].toString()); 562 }; 563 return this; 564 }, 565 566 567 getVoices : function() { 568 569 570 // If no arguments, then the user has requested the array of voices populated earlier. 571 572 if (arguments.length == 0) { 573 return voices; 574 }; 575 576 577 // If there's another argument, we'll assume it's a jQuery selector designating where to put the dropdown menu. 578 // And if there's a third argument, that will be custom text for the dropdown menu. 579 // Then we'll create a dropdown menu with the voice names and, in parenthesis, the language code. 580 581 var obj = jQuery(arguments[0]); 582 var customTxt = "Choose a Different Voice"; 583 584 if (arguments[1] !== undefined) { 585 customTxt = arguments[1]; 586 }; 587 588 ob
588j.append(jQuery("<select id='voiceSelect'><option value='none'>" + customTxt + "</option></select>")); 589 for(var i = 0; i < voices.length ; i++) { 590 var option = document.createElement('option'); 591 option.textContent = voices[i].name + ' (' + voices[i].language + ')'; 592 option.setAttribute('value', voices[i].name); 593 option.setAttribute('data-articulate-language', voices[i].language); 594 obj.find("select").append(option); 595 } 596 597 598 // Add an onchange event to the dropdown menu. 599 600 obj.on('change', function() { 601 jQuery(this).find("option:selected").each(function() { 602 if (jQuery(this).val() != "none") { 603 voiceUserDefault = jQuery(this).val(); 604 } 605 }); 606 }); 607 return this; 608 }, 609 610 611 setVoice : function() { 612 613 614 // The setVoice function has to have two attributes -- if not, exit the function. 615 616 if (arguments.length < 2) { 617 return this 618 } 619 620 var requestedVoice, requestedLanguage; 621 622 623 // User wants to change the voice directly. If that name indeed exists, update the "voiceUserDefault" variable. 624 625 if (arguments[0] == "name") { 626 requestedVoice = arguments[1]; 627 for (var i = 0; i < voices.length; i++) { 628 if (voices[i].name == requestedVoice) { 629 voiceUserDefault = requestedVoice; 630 }; 631 }; 632 }; 633 634 635 // User wants to change the voice by only specifying the first two characters of the language code. Case insensitive. 636 637 if (arguments[0] == "language") { 638 requestedLanguage = arguments[1].toUpperCase(); 639 if (requestedLanguage.length == 2) { 640 for (var i = 0; i < voices.length; i++) { 641 if (voices[i].language.substring(0,2).toUpperCase() == requestedLanguage) { 642 voiceUserDefault = voices[i].name; 643 break 644 }; 645 }; 646 } else { 647 648 649 // User wants to change the voice by specifying the complete language code. 650 651 for (var i = 0; i < voices.length; i++) { 652 if (voices[i].language == requestedLanguage) { 653 voiceUserDefault = voices[i].name; 654 break 655 }; 656 }; 657 } 658 }; 659 return this; 660 }, 661 662 }; 663 664 665 666 667 $.fn.articulate = function(method) { 668 669 if (methods[method]) { 670 return methods[method].apply( this, Array.prototype.slice.call(arguments, 1)); 671 } else if (typeof method === "object" || ! method) { 672 return methods.speak.apply(this, arguments); 673 } else { 674 jQuery.error("Method " + method + " does not exist on jQuery.articulate"); 675 } 676 677 }; 678 679 680}(jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.