PageSourceSearch

https://maciejowice.pl/wp-content/themes/itlu/js/articulate.js

js maciejowice.pl collected 2026-10-02 18:52:05 UTC 25,734 bytes, 680 lines download raw bytes

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 (&amp; 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.