/*********************************************************************************************
 * Script for the playback of the Voice Reader Web player - Version 202402281604
 *
 * Including functions and classes for the following tasks:
 *   - Parse and analyze the HTML structure
 *   - Load the required MP3 files package-wise
 *   - Handle user input to start, pause and stop the playback
 *   - Highlight the currently selected text part in the HTML structure
 *   - Change settings for the playback (e.g. speed, pitch, highlighting)
 *
 * There are two general playback modes:
 *   - In the standard mode the website will be parsed when the player is started for the
 *     first time and depending on whitelists and blacklist defined by the customer the
 *     extracted snippets will be read consequently
 *   - In the single string mode the customer determines a single string to be read by the
 *     player, e.g. when the user clicks on some link or button
 *
 * Important methods:
 *   - startPausePlayer() starts the playback; initializes the player at first start
 *   - stopPlayback() stops the playback
 *   - initialize() gets called from startPausePlayer() to parse the customer's website
 *     and start loading audio files for text snippets
 *   - setSingleString() determines a single string to be read;
 *     the textParts array will be ignored
 *   - Loads the currently selected package of audio files (depending on the current position
 *     of the cursor);
 *     The script will proceed to load all other snippets on the website automatically
 *
 * Linguatec 2017
 *********************************************************************************************/

 // IE < 9 does not know Object.keys
 if (!Object.keys) {
	Object.keys = function(obj) {
		var keys = [];
		for (var i in obj) {
			if (obj.hasOwnProperty(i)) {
				keys.push(i);
			}
		}
		return keys;
	};
}
// trim() does not work in IE < 9
if(typeof String.prototype.trim !== 'function') {
  String.prototype.trim = function() {
	return this.replace(/^\s+|\s+$/g, ''); 
  }
}
// Create the global array of playback script objects, if not already existing
if (!VR20S_playback) {
	var VR20S_playback = [];
}
// Create the global variable that contains the ID of the player that was started last
if (!VR20S_playing) {
	var VR20S_playing = null;
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Push this playback script object on the array
VR20S_playback.push(
new function() {

function TextPart(tags,originalText,readText) {
	this.tagList = tags;
	this.originalText = originalText;
	this.readText = readText;
	this.mp3 = null;
	this.vvt = null;
	this.isLoading = false;
}

/*********************************************************************************************
 * Script version number 
 *********************************************************************************************/
var vrwebPlayerVersion = '202402281604';

/*********************************************************************************************
 * Paths and identifiers 
 *********************************************************************************************/

// determine the path to this script
var scripts = document.getElementsByTagName('script');
var myPath = scripts[scripts.length-1].src.split('?')[0]; // remove any ?query
var scriptPath = myPath.split('/').slice(0, -1).join('/')+'/'; // remove the filename from the path

// constant: URL to the web service that returns an mp3 for given input text
var URL_WEBSERVICE_MP3 = '//vrweb15.linguatec.org/VoiceReaderWeb22WebService/TtsWebService.asmx/StartSynthesize22Ex2';

var player_ID = 'jquery_jplayer_1'; // ID of the player HTML tag - default value will be overwritten
var playerDivId = "vrweb_div_0"; // ID of the player div HTML tag - default value will be overwritten
var pb_count = VR20S_playback.length; // number of the playback script

var self = this; // a reference to the object itself
var doc = document; // the document that is analyzed - in case of popup it is set to the opener
var customerKey = makeCustomerKey(); // the customer key (for statistics), created when the playback script is loaded

/*********************************************************************************************
 * Settings
 *********************************************************************************************/
var readerOptions = null; // contains all options defined by the user
var isMobileDevice = false; // is a mobile device used?
var isPopup = false; // is this a popup?
var markerMode = false; // is this a player for marked text?
var highlightClass = 'VR20S_highlight'; // name of the highlight class that can be configured by the customer
var highlightColorClass = null; // class of the highlight color selected in the settings window
var highlightingActivated = true; // is the highlighting activated?
var settingsHighlightClass = null; // if the highlighting class setting is null, the styles are only derived from the predefined highlighting class in the configuration
var subtitlesActivated = false; // activate the subtitles box?
var settingsSubtitleClass = null; // modifiable class for the color of the subtitles
var speed = 100; // spead of reading
var pitch = 100; // voice pitch
var makeVVT = 0; // specify 0 to do not generate; 2 to generate on the word base
var lastUsedLanguage = null; // last language used in single string mode (must be remembered for reloading the MP3 when the speed or the pitch was changed)
var singleStringMode = false; // mode in which the player plays a single given string rather than the text parts from the parsed website

/*********************************************************************************************
 * Highlighting
 *********************************************************************************************/
var highlightCount = 0; // count of the highlightings
var lastHighlight = null; // index of the last part that was highlighted in the text
var highlightColorArray = ['VR20S_Color_Cyan','VR20S_Color_LightBlue','VR20S_Color_Yellow ','VR20S_Color_LightGreen','VR20S_Color_Magenta','VR20S_Color_LightGrey','VR20S_Color_White','VR20S_Color_Black'];
var subtitleColorArray = ['VR20S_Color_Blue','VR20S_Color_LightBlue','VR20S_Color_LightYellow ','VR20S_Color_Green','VR20S_Color_Red','VR20S_Color_LightGrey','VR20S_Color_White','VR20S_Color_Black'];

/*********************************************************************************************
 * Player Status
 *********************************************************************************************/
var firstStartFlag = true; // A Flag to indicate weather playCurrent() has been executed before.
var firstStart = 1; // has the player been started before? 
var playerStatus = 0; // status of the player: 0 = ready, 1 = playing, 2 = paused, 3 = suspended
this.markersInserted = false; // have the markers for EOS etc. been inserted into the document structure by this player?
var currentSentenceID = 0; // manages the sentence IDs to assign while parsing the website
var selectionHtml = null; // contains the last text that was selected by the user (must be updated by function calls)

var textParts = new Array(); // the array of parts that have been loaded into the player
var singleStringPart = null; // text part for a single string given in a function call
var completePagePart = null; // all the parts united; to make the complete page available for download as an mp3
var currentPart = 0; // index of the currently played / highlighted part (no single string mode)
var pausedPart = null; // index of the part that was paused
var suspendingPart = null; // index of the part that has to be reloaded, thereby suspending the player

var readingBuffer = 0; // current string length of already generated audio files of following parts
var bufferThreshold = 300; // constant: minimum length of the string of already generated audio files to start reading (to avoid interrupts)
var bufferPointer = 0; // pointer to the text part that needs to be loaded next to fill the buffer
var waitingTime = 0; // time the user has already waited for the current part to start playing

/*********************************************************************************************
 * Audio files
 *********************************************************************************************/
var packageSize = 2; // size of the packages of mp3 files to load at once
var loadPointer = 0; // pointer to the position in the array of text parts where to load the next package of mp3 files
var totalLoaded = 0; // how many files have already been loaded in total? -> end the procedure when it is equal to the number of parts
var manualPointer = 0; // manual selection of the next position to load a package (e.g. if the user skips through the text):
                       // must be 0 at the beginning so that the first couple of files won't be skipped
var stoppedByError = false; // the loading process has been stopped by an error message
 
/*********************************************************************************************
 * Foobar
 *********************************************************************************************/
// Colors to mark the snippets in debug mode
var colors = new Array("blue","red","green","orange","purple"); // Available colors for debug mode
var currentColor = 0; // Next used color to mark a snippet in debug mode

/*********************************************************************************************
 * Voice names
 *********************************************************************************************/
var maleSpeakers = new Array();
var femaleSpeakers = new Array();

// Arabic
maleSpeakers['ar-sa'] = ['Tarik'];
// Basque
femaleSpeakers['eu-es'] = ['Miren'];
// Cantonese (Hong Kong)
femaleSpeakers['zh-hk'] = ['Sinji-Ml'];
// Catalan
femaleSpeakers['ca-es'] = ['Montserrat'];
maleSpeakers['ca-es'] = ['Jordi'];
// Czech
femaleSpeakers['cs-cz'] = ['Zuzana-Ml','Iveta'];
// Danish
femaleSpeakers['da-dk'] = ['Sara'];
maleSpeakers['da-dk'] = ['Magnus'];
// Dutch (Belgium)
femaleSpeakers['nl-be'] = ['Ellen'];
// Dutch (Netherlands)
femaleSpeakers['nl-nl'] = ['Claire-ML'];
maleSpeakers['nl-nl'] = ['Xander'];
// English (American)
femaleSpeakers['en-us'] = ['Ava-Ml','Samantha','Zoe-Ml'];
maleSpeakers['en-us'] = ['Tom'];
// English (Australian)
femaleSpeakers['en-au'] = ['Karen'];
maleSpeakers['en-au'] = ['Lee'];
// English (British)
femaleSpeakers['en-gb'] = ['Serena','Kate']; // !! there is also a speaker for Scottish English !!
maleSpeakers['en-gb'] = ['Daniel','Oliver'];
// English (Indian)
femaleSpeakers['eni'] = ['Veena']; // !! no culture code available !!
// English (Irish)
femaleSpeakers['en-ie'] = ['Moira'];
// English (Scottish)
femaleSpeakers['ens'] = ['Fiona']; // !! no culture code available !!
// English (South African)
femaleSpeakers['en-za'] = ['Tessa'];
// Finnish
femaleSpeakers['fi-fi'] = ['Satu'];
// French (Canadian)
femaleSpeakers['fr-ca'] = ['Amelie-Ml','Chantal'];
maleSpeakers['fr-ca'] = ['Nicolas'];
// French (France)
femaleSpeakers['fr-fr'] = ['Audrey-Ml','Aurelie'];
maleSpeakers['fr-fr'] = ['Thomas'];
// Galician
femaleSpeakers['gl-es'] = ['Carmela'];
// German
femaleSpeakers['de-de'] = ['Petra-Ml','Anna-Ml'];
maleSpeakers['de-de'] = ['Viktor','Markus'];
// Greek
femaleSpeakers['el-gr'] = ['Melina'];
maleSpeakers['el-gr'] = ['Nikos'];
// Hebrew
femaleSpeakers['he-il'] = ['Carmit'];
// Hindi
femaleSpeakers['hi-in'] = ['Lekha'];
// Hungarian
femaleSpeakers['hu-hu'] = ['Mariska'];
// Indonesian
femaleSpeakers['id-id'] = ['Damayanti'];
// Italian
femaleSpeakers['it-it'] = ['Alice-Ml','Federica-Ml','Paola'];
maleSpeakers['it-it'] = ['Luca'];
// Japanese
femaleSpeakers['ja-jp'] = ['Daisuke'];
maleSpeakers['ja-jp'] = ['Ichiro'];
// Korean
femaleSpeakers['ko-kr'] = ['Sora'];
// Mandarin (China)
femaleSpeakers['zh-cn'] = ['Tiantian-Ml'];
// Mandarin (Taiwan)
femaleSpeakers['zh-tw'] = ['Mei-Jia'];
// Norwegian
femaleSpeakers['nn-no'] = ['Nora'];
maleSpeakers['nn-no'] = ['Henrik'];
// Polish
femaleSpeakers['pl-pl'] = ['Ewa','Zosia'];
// Portuguese (Brazil)
femaleSpeakers['pt-br'] = ['Luciana'];
maleSpeakers['pt-br'] = ['Felipe'];
// Portuguese (Portugal)
femaleSpeakers['pt-pt'] = ['Joana','Catarina'];
// Romanian
femaleSpeakers['ro-ro'] = ['Ioana'];
// Russian
femaleSpeakers['ru-ru'] = ['Milena','Katya-Ml'];
maleSpeakers['ru-ru'] = ['Yuri'];
// Slovak
femaleSpeakers['sk-sk'] = ['Laura'];
// Spanish (Argentina)
maleSpeakers['es-ar'] = ['Diego'];
// Spanish (Colombian)
femaleSpeakers['es-co'] = ['Soledad'];
maleSpeakers['es-co'] = ['Carlos'];
// Spanish (Mexican)
femaleSpeakers['es-mx'] = ['Paulina-Ml','Angelica'];
maleSpeakers['es-mx'] = ['Juan'];
// Spanish (Spain)
femaleSpeakers['es-es'] = ['Monica-Ml'];
maleSpeakers['es-es'] = ['Jorge'];
// Swedish
femaleSpeakers['sv-se'] = ['Alva','Klara'];
maleSpeakers['sv-se'] = ['Oskar'];
// Thai
femaleSpeakers['th-th'] = ['Kanya'];
// Turkish
femaleSpeakers['tr-tr'] = ['Yelda'];
maleSpeakers['tr-tr'] = ['Cem-Ml'];
// Valencian
femaleSpeakers['vae'] = ['Empar']; // !! no culture code available !!

/*********************************************************************************************
 * Multilanguage mapping
 *********************************************************************************************/
var multiLanguages = new Array();
multiLanguages['de'] = 'ged';
multiLanguages['de-de'] = 'ged';
multiLanguages['en'] = 'eng';
multiLanguages['en-gb'] = 'eng';
multiLanguages['en-us'] = 'eng';
multiLanguages['fr']    = 'frf';
multiLanguages['fr-fr'] = 'frf';
multiLanguages['it'] = 'iti';
multiLanguages['it-it'] = 'iti';
multiLanguages['es'] = 'spe';
multiLanguages['es-es'] = 'spe';

/*********************************************************************************************
 * Error message localisation
 *********************************************************************************************/
var tapSubPlayerMessage = new Array();
tapSubPlayerMessage['en'] = 'Please click <div class="VR20S_h_button_play"></div> again!';
tapSubPlayerMessage['de'] = 'Bitte nochmals auf <div class="VR20S_h_button_play"></div> tippen!';
tapSubPlayerMessage['es'] = 'Por favor, haga clic <div class="VR20S_h_button_play"></div> otra vez!';
tapSubPlayerMessage['fr'] = 'Cliquez <div class="VR20S_h_button_play"></div> encore une fois!';
tapSubPlayerMessage['it'] = 'Per favore clicca <div class="VR20S_h_button_play"></div> ancora!';

// ******************************************************************************************************************

/* determine the player ID */
this.setPlayerID = function(newID) {
	player_ID = newID;
}

/* determine the player div ID */
this.setPlayerDivID = function(newID) {
	playerDivId = newID;
}

/* determine if a mobile device is used */
this.setMobileDevice = function(isMobile) {
	isMobileDevice = isMobile;
}

/* change the script path */
this.setScriptPath = function(path) {
	scriptPath = path;
};

/* determine the highlight count (usually player_id - 1) */
this.setHighlightCount = function(count) {
	highlightCount = count;
}

/* pass the user configuration variable from another script */
this.setReaderOptions = function(values) {
	readerOptions = values;
	if (getBooleanConfigValue("vrweb_debug")) {
		console.log("[VRWEB DEBUG] Reader options:");
		console.log(readerOptions);
	}
	if (getBooleanConfigValue("vrweb_shortcut_activate")){
		setShortcuts(___jQuery.grep(Object.keys(readerOptions), function(key, index){
			return key.match(/_shortcut_(pause|stop|forward|rewind|close)/);
		}).sort());
	}
	// update the HTML selection
	this.updateSelectionHtml();
	subtitlesActivated = getBooleanConfigValue("vrweb_subtitle");
	if (getConfigValue("vrweb_highlight") != null && getConfigValue("vrweb_highlight") != undefined && getConfigValue("vrweb_highlight") != '') {
		highlightingActivated = getBooleanConfigValue("vrweb_highlight");
	} else {
		highlightingActivated = true;
	}
}

/*    call this if this is a popup playback script */
this.setPopupOptions = function(flag) {
	isPopup = flag;
	doc = window.opener.document;
}

/*    returns the array of original text strings from the text parts */
this.getPartsOriginalText = function() {
	var a = [];
	for (var i=0; i<textParts.length; i++) {
		a.push(textParts[i].originalText.replace(/\"/g,"'"));
	}
	return a;
}

/*    returns the array of read text strings from the text parts */
this.getPartsReadText = function() {
	var a = [];
	for (var i=0; i<textParts.length; i++) {
		a.push(textParts[i].readText.replace(/\"/g,"'"));
	}
	return a;
}

/*    returns the array of the tag lists from the text parts */
this.getPartsTagList = function() {
	var a = [];
	for (var i=0; i<textParts.length; i++) {
		a.push(textParts[i].tagList);
	}
	return a;
}

  /* 	 passes the text parts to be read including the tag lists and loads the corresponding MP3 files */
this.passTextParts = function(originalTextArray,readTextArray,tagListArray) {
	var len = originalTextArray.length >= readTextArray.length ? originalTextArray.length : readTextArray.length;
	for (var i=0; i<len; i++) {
		addTextPartFromString(originalTextArray[i],readTextArray[i],tagListArray[i]);
	}
	loadPackage();
};

/*    updates the selectionHtml variable */
this.updateSelectionHtml = function() {
	selectionHtml = getSelectionHtml();
}

/*    starts the download of the MP3 file for the complete page */
this.downloadCompletePage = function() {
	// Single string mode: Download the existing MP3 for the single string part
	if (singleStringMode) {
		//window.open(singleStringPart.mp3,'Download MP3');
		downloadMp3(singleStringPart.mp3);
	} else {
		// The page must have been initialized before all the parts can be united
		var waitT = 0;
		if (firstStart == 1) {
			this.initialize(true);
			waitT = 500;
		}

		// Wait for the initialization to finish, if necessary
		setTimeout(function() {
			if (getBooleanConfigValue("vrweb_read_selections")){
				completePagePart = createCompletePagePart();
			} else if (!completePagePart) {
				completePagePart = createCompletePagePart();
			}
			if (!completePagePart.mp3) {
				// send the request and start the download automatically when it is finished
				sendRequest(0,completePagePart,null,false,true,true);
			} else { // Part object already exists - start download immediately
				downloadMp3(completePagePart.mp3);
			}
		},waitT);
	}
}

/* downloads an MP3 file */
function downloadMp3(url){
	var a = document.createElement('a');
	a.href = url;
	a.target = "_blank";
	a.download = "voicereader.mp3";
	a.style.display = 'none';
	doc.body.appendChild(a);
	a.click();
	delete a;
}

/*    opens the settings dialog */
this.openSettings = function(settingsNumber) {
	if (isPopup && (window.innerWidth < 400 || window.innerHeight < 550)) {
		___jQuery("#vrweb_settings_"+settingsNumber).parent().css('margin-top','5px');
		window.resizeTo(335,485);
	}
	___jQuery("#vrweb_settings_"+settingsNumber).css('display','block');
}

/*    closes the settings menu */
this.closeSettings = function(settingsNumber) {
	___jQuery("#vrweb_settings_"+settingsNumber).css('display','none');
}

/* collects the defined shortcuts and binds the correspondent functionality to them */
function setShortcuts(values){
	var shortcutMatrix = [];
	___jQuery.each(values, function(index, value) {
		var options = getConfigValue(value).split("");
		if(options.length > 0){
			var key = options.pop();
			shortcutMatrix.push([0,false,false,false]);
			shortcutMatrix[index][0] = key.toUpperCase().charCodeAt(0);
			if(___jQuery.inArray("A",options) != -1){
				shortcutMatrix[index][1] = true;
			}
			if(___jQuery.inArray("C",options) != -1){
				shortcutMatrix[index][2] = true;
			}
			if(___jQuery.inArray("S",options) != -1){
				shortcutMatrix[index][3] = true;
			}
		}
	});

	var correspondingKeysPressed = function(event, currentCase){
		if(event.keyCode === shortcutMatrix[currentCase][0] && event.altKey === shortcutMatrix[currentCase][1] &&
		 event.ctrlKey === shortcutMatrix[currentCase][2] && event.shiftKey === shortcutMatrix[currentCase][3]){
			return true;
		} else {
			return false;
		}
	}

	___jQuery(document).keydown(function(event){
		if(correspondingKeysPressed(event, 0)){ // close shortcut
			if(!isPopup){
				self.stopPlayback();
				doc.getElementById(playerDivId).style.display = 'none';
			} else {
				window.close();
			}
		}else if(correspondingKeysPressed(event, 1)){ // forward shortcut
			self.goToNext();
		}else if(correspondingKeysPressed(event, 2)){ // pause shortcut
			self.startPausePlayer();
		}else if(correspondingKeysPressed(event, 3)){ // rewind shortcut
			self.goToPrevious();
		}else if(correspondingKeysPressed(event, 4)){ // stop shortcut
			self.stopPlayback();
		}
	});
}

/* returns the value of a configuration variable */
function getConfigValue(type) {
	if (typeof(readerOptions[type]) == "undefined") {
		return false;
	} else {
		return readerOptions[type];
	}
}

/* returns the boolean value of a configuration variable that can be either 0 or 1 */
function getBooleanConfigValue(type) {
	if (!readerOptions || typeof(readerOptions[type]) == "undefined" || readerOptions[type] == '0' || readerOptions[type] == '' || (typeof(readerOptions[type]) == "string" && readerOptions[type].toLowerCase() == 'false')) {
		return false;
	} else {
		return true;
	}
}

/* checks if a given HTML element has a given class */
function hasClass( elem, klass ) {
	return (" " + elem.className + " " ).indexOf( " "+klass+" " ) > -1;
}

/*********************************************************************************************/
/* Functions to handle the text parts and the cursor pointing to the currently selected part */
/*********************************************************************************************/
  
/* PUBLIC Function
	determines a single string to be read and loads the corresponding MP3 file,
	ignoring the parsed website */
this.setSingleString = function(string,language,instantStart) {
	// first of all delete the last single text part to make sure it will not be read again
	// before the new one is loaded
	singleStringPart = null;

	// the player is in single string mode from now on!
	singleStringMode = true;

	self.stopPlayback(); // stop the playback if it is still running

	// save the used language
	lastUsedLanguage = language;
	
  // remove whitespaces at the beginning and at the end
  string = ___jQuery.trim(string);

  // text part must contain word characters or else it will be ignored
  if (string.length > 0) {
	var stringPronounce = string;

	var tagList = [];
	singleStringPart = new TextPart(tagList, string, stringPronounce);
  
	// load the mp3 file
	sendRequest(0, singleStringPart, language, false, false,true);
	
	// update the subtitles
	if (subtitlesActivated) {
		this.updateSubtitles(singleStringPart);
	}

	// make the player play the loaded text part as soon as it is available
	if (instantStart) {
		self.startPausePlayer();
	}
  }
};

/* adds a text part to the queue, defined by a list of tag IDs */
function addTextPartFromTags(tagList) {
	var string = "";
	// the string that is passed to the reader service may differ from the original string in the HTML structure
	var stringPronounce = "";
	// consider pronounce and spell tags
	for (var i = 0, max = tagList.length; i < max; i++) {
		var textSpan = document.getElementById(tagList[i]);
		//span with an image inside has no text but attribute data-vrstext
		if (textSpan.dataset.vrstext != null && textSpan.dataset.vrstext != '') {
			var textContent = textSpan.dataset.vrstext;
		} else { //SPAN => innerHTML
			var textContent = textSpan.innerHTML;
		}
		// preprocess the text from the span
		textContent = textContent.replace('',''); // questionmark in a box hex: EF8485
		textContent = textContent.replace('',''); // questionmark in a box hex: EF8484
		textContent = textContent.replace(/&nbsp;/g,' '); // " " must be preserved
		textContent = textContent.replace(/&amp;/g,'&'); // "&" must be preserved
		textContent = textContent.replace(/&shy;/g,''); // "soft hyphen" must be removed
		textContent = textContent.replace(/\u00AD/g,''); // "soft hyphen" must be removed
		textContent = textContent.replace(/&gt;/g,'>'); // ">" must be preserved
		textContent = textContent.replace(/&lt;/g,'<'); // "&" must be preserved
		textContent = textContent.replace(/München I /g,'München eins');
		textContent = textContent.replace(/München I./g,'München eins.');
		textContent = textContent.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, '');

		string += textContent;
		var parentElement = textSpan.parentNode;
		if (parentElement != null && parentElement.tagName == 'PRONOUNCE' && parentElement.hasAttribute('like')) {
			stringPronounce += parentElement.getAttribute('like');
		} else if (parentElement != null && parentElement.tagName == 'SPAN' && ___jQuery(parentElement).data('vrweb-pronounce') !== undefined && ___jQuery(parentElement).data('vrweb-pronounce') !== '') {
			stringPronounce += ___jQuery(parentElement).data('vrweb-pronounce');
		} else if (parentElement != null && parentElement.tagName == 'SPELL') {
			if (parentElement.hasAttribute('speed')) {
				stringPronounce += handleSpellTags(textSpan.innerHTML,parentElement.getAttribute('speed'));
			} else {
				stringPronounce += handleSpellTags(textSpan.innerHTML,"normal");
			}
		} else if (getConfigValue('vrweb_activate_multilingual') == 'on' && !textContent.match(/^[\.,;\!\?]$/)) {
			var currentLanguage = findAttribute("lang", parentElement);
			if (currentLanguage != "" && currentLanguage in multiLanguages) {
				stringPronounce += "<lang=" + multiLanguages[currentLanguage] + "/>" + textContent + "<lang=END_" + multiLanguages[currentLanguage] + "/>";
			} else {
			  stringPronounce += textContent;
			}
		} else {
			stringPronounce += textContent;
		}
		// insert a whitespace after each snippet to make sure the system
		// does not concatenate two distinct words
		if (textContent.match(/\S$/)) {
			string += ' ';
			stringPronounce += ' ';
		}
	}
	stringPronounce = stringPronounce.trim();
	//remove <lang=END_eng/><lang=eng/>
	if (getConfigValue('vrweb_activate_multilingual') == 'on' && stringPronounce.length > 20) {
		if (getBooleanConfigValue("vrweb_debug")) {
			console.log("[VRWEB DEBUG] string before replace: '" + stringPronounce + "'");
		}
		for (language in multiLanguages) {
			var str = new RegExp("<lang=END_" + multiLanguages[language] + "\/>\\s*<lang=" + multiLanguages[language] + "\/>","g");
			stringPronounce = stringPronounce.replace(str, ' ');
		}
	}
	// proceed with the created strings
	addTextPartFromString(string,stringPronounce,tagList);
}
/* finds an attribute for the given element up to the body */
function findAttribute(name, element) {
  var i = 0;
  while (i < 1000) {
	if (element === null || element === undefined || element.nodeName=="BODY")
		  return "";
    if (element != null && element.hasAttribute(name))
      return element.getAttribute(name).toLowerCase();
    else    
			element = element.parentNode;
    i++;
  }
  return "";
}
/* adds a text part to the queue, defined by a single string
   (passing a tagList is optional) */
function addTextPartFromString(string,stringPronounce,tagList) {
	if (typeof stringPronounce === "undefined" || stringPronounce == null) {
		stringPronounce = string;
	}
	if (typeof tagList === "undefined" || tagList == null) {
		tagList = [];
	}

	stringPronounce = stringPronounce.replace(/[\+\-\*\&\,]{3,}/g,'');
	stringPronounce = stringPronounce.replace(/[\.\,\?\!]{4,}/g,'');
	
	// remove whitespaces at the beginning and at the end
	string = string.trim();
	stringPronounce = stringPronounce.trim();
	// text part must contain word characters or else it will be ignored
	if (stringPronounce=="") {
		string = "";
	}
	if (string.match(/\S/)) {
		textParts.push(new TextPart(tagList,string,stringPronounce));
		// show the play button, if it is not yet visible
		if (document.getElementById(player_ID+"_button_play") != undefined) {
			document.getElementById(player_ID+"_button_play").style.visibility = 'visible';
		}
	}
}

/* handles the spell tags */
function handleSpellTags(text,speed){
	var replacement = "";
	if(speed == "slow") {
		text = ", "+text.split("").join(", ")+", ";
	} else {
		text = " "+text.split("").join(" ")+" ";
	}
	return text;
}

/* encodes HTML entities */
function encodeHtmlEntities(str) {
	var newStr = str.replace('<','&lt;');
	newStr = newStr.replace('>','&gt;');
	return newStr;
};

/* selects the next snippet */
function advanceCursor() {
	moveCursor(currentPart+1);
}

/* selects the previous snippet */
function decreaseCursor() {
	moveCursor(currentPart-1);
}

/* determines the current snippet to be read (currentPart) and adapts the highlighting */
function moveCursor(nextFile) {
	currentPart = nextFile;
	if (currentPart < 0) { // index of the part must be >=0
		currentPart = 0;
	}
	// Reset the buffer and set the buffer pointer
	readingBuffer = 0;
	bufferPointer = currentPart;
	checkBufferUpdate(currentPart); // The buffer starting at this position may already have been filled
	// reset the waiting time
	waitingTime = 0;
	// No highlighting in the player for marked text
	if (!markerMode) {
		// remove the last highlighting, if existing
		if (lastHighlight != null) {
			removeHighlighting(textParts[lastHighlight]);
		}
		// highlight the new snippet
		addHighlighting(textParts[currentPart]);
	}
	if (subtitlesActivated){
		self.updateSubtitles(textParts[currentPart]);
	}
}


/*********************************************************************************************/
/*                       Functions to handle the player and its status                       */
/*********************************************************************************************/

/* checks if this is the active player (i.e. the player that was started last) */
this.isPlayerActive = function() {
	return (VR20S_playing == player_ID);
}

/* variables and functions for words highlighting */

var lastCurTime = 320000;//last curPlayer.status.currentTime
var updCurTime = 0;//currenttime += 50 msec
var lastCurWordNr = -1;//remember last current word
var lastHighlightedWordId = '';//remember last highlighted word
var lastHighlightedSentenceId = '';//remember the sentence with last highlighted word
var timerIdHighlightWord = 0;

function removeWordHighlightOnClose(sentenceId) {
	//remove previous highlighting
	if ((document.getElementById(lastHighlightedWordId) != null) && (document.getElementById(lastHighlightedWordId) != undefined) && (document.getElementById(lastHighlightedWordId) != '')) {
		document.getElementById(lastHighlightedWordId).className = document.getElementById(lastHighlightedWordId).className.replace(/\bVR20S_highlight_word\b/g, "");
	}
	clearTimeout(timerIdHighlightWord);
	lastCurWordNr = -1;
	lastHighlightedWordId = '';
	lastHighlightedSentenceId = '';
}

/* function to get player's current time and highlight corresponding word */
function highlightWord(curPlayer, sentenceId, wordsTimeArr) {
	
	if (playerStatus == 2) {//if paused
		//remove highlighting
		if ((document.getElementById(lastHighlightedWordId) != null) && (document.getElementById(lastHighlightedWordId) != undefined) && (document.getElementById(lastHighlightedWordId) != '')) {
			document.getElementById(lastHighlightedWordId).className = document.getElementById(lastHighlightedWordId).className.replace(/\bVR20S_highlight_word\b/g, "");
		}
		timerIdHighlightWord = setTimeout(function() { highlightWord(curPlayer, sentenceId, wordsTimeArr); }, 50);
		return;
	}
	
	var curTime = curPlayer.status.currentTime;//get player's current time
	//compare times
	if (curTime == lastCurTime) {
		updCurTime += 0.050;
	} else {
		lastCurTime = curTime;
		updCurTime = curTime;
	}
	
	//compare current time with words periods, find current word number
	var curWordNr = lastCurWordNr > 0 ? lastCurWordNr : 0;
	while ((updCurTime > wordsTimeArr[curWordNr + 1]) && (curWordNr < wordsTimeArr.length)) {
		curWordNr++;
	}
	
	if (sentenceId != lastHighlightedSentenceId && lastHighlightedSentenceId != '') {
		clearTimeout(timerIdHighlightWord);
		curWordNr = 0;
		lastCurWordNr = -1;
	}
	lastHighlightedSentenceId = sentenceId;
	if (curWordNr > lastCurWordNr) {
		if (curWordNr >= wordsTimeArr.length) {
			// remove highlighting
			clearTimeout(timerIdHighlightWord);
			if ((document.getElementById(lastHighlightedWordId) != null) && (document.getElementById(lastHighlightedWordId) != undefined) && (document.getElementById(lastHighlightedWordId) != '')) {
				document.getElementById(lastHighlightedWordId).className = document.getElementById(lastHighlightedWordId).className.replace(/\bVR20S_highlight_word\b/g, "");
			}
			return;
		}
		
		if ((document.getElementById(sentenceId + '_' + curWordNr) != null) && (document.getElementById(sentenceId + '_' + curWordNr) != undefined) && (document.getElementById(lastHighlightedWordId) != '')) {
			//remove previous highlighting
			if ((document.getElementById(lastHighlightedWordId) != null) && (document.getElementById(lastHighlightedWordId) != undefined) && (document.getElementById(lastHighlightedWordId) != '')) {
				document.getElementById(lastHighlightedWordId).className = document.getElementById(lastHighlightedWordId).className.replace(/\bVR20S_highlight_word\b/g, "");
			}
			//add new highlighting
			classWordHighlighted = "VR20S_highlight_word";
			arrClasses = document.getElementById(sentenceId + '_' + curWordNr).className.split(" ");
			if (arrClasses.indexOf(classWordHighlighted) == -1) {
				document.getElementById(sentenceId + '_' + curWordNr).className += " " + classWordHighlighted;
			}
			lastCurWordNr = curWordNr;
			lastHighlightedWordId = sentenceId + '_' + curWordNr;
		}
	}
	if ((curWordNr < wordsTimeArr.length - 1) && (playerStatus == 1)) {//check if player plays and it's not the last word in the sentence
		timerIdHighlightWord = setTimeout(function() { highlightWord(curPlayer, sentenceId, wordsTimeArr); }, 50);
	}
}

function parseVVTData(VVTData) {
	var matchDigit = /^\d+$/;
	var matchWord = /[a-zA-Z]+/;
	var matchTime = /\d{2}:\d{2}:\d{2}.\d{3}/;
	var matchTimeStartEnd = /\d{2}:\d{2}:\d{2}.\d{3}\s-->\s\d{2}:\d{2}:\d{2}.\d{3}/;
	VVTData = VVTData.replace(/[\n\r]+/g, "\n");
	var allVVT = VVTData.split("\n");
	if (getBooleanConfigValue("vrweb_debug")) {
		console.log(allVVT);
	}
	
	var allWordsTime = [];
	var allWords = [];
	for (var j = 0; j < allVVT.length; j++) {
		if ((j != 0) && (j % 3 == 0)) {
			allVVT[j] = ___jQuery.trim(allVVT[j]);
			allWords.push(allVVT[j]);
		} else if (j % 3 == 2) {
			allVVT[j] = ___jQuery.trim(allVVT[j]);
			allWordsTime.push(allVVT[j]);
		}
	}
	
	for (var i = 0; i < allWordsTime.length; i++) {
		if (allWordsTime[i] == '' || allWordsTime[i].match(matchWord) || allWordsTime[i].match(matchDigit) || !(allWordsTime[i].match(matchTimeStartEnd))) {
			allWordsTime.splice(i, 1);
			i--;
		} else if (allWordsTime[i].match(matchTime)) {
			var tempArr = allWordsTime[i].split(" ");
			allWordsTime[i] = tempArr[0];
			var regex = /(\d{2}):(\d{2}):(\d{2}).(\d{3})/;
			var match = regex.exec(allWordsTime[i]);
			
			var hours = Number(match[1]);
			var minutes = Number(match[2]);
			var seconds = Number(match[3]);
			var miliseconds = match[4];
			
			var timeInSec = hours * 3600 + minutes * 60 + seconds;
			var finalTime = timeInSec + "." + miliseconds;
			allWordsTime[i] = Number(finalTime);
		}
	}
	
	if (getBooleanConfigValue("vrweb_debug")) {
		console.log(allWordsTime);
		console.log(allWords);
	}
	var outputFromVVT = [allWordsTime, allWords];
	return outputFromVVT;
}

function parseSentencesIntoWords(sentenceIds, outputFromVVT) {
	var wordsFromServer = outputFromVVT[1];
	var firstSentenceId = sentenceIds[0];
	var wordID = 0;
	var spacesCounter = 0;
	
	for (var i = 0; i < sentenceIds.length; i++) {
		sentenceId = sentenceIds[i];
		var arrClasses = document.getElementById(sentenceId).className.split(" ");
		if (arrClasses.indexOf("VR20S_sentence_parsed") == -1) {
			var words1 = document.getElementById(sentenceId).innerHTML.split(" ");
			var spannedSentence = '';
			var words = words1.filter(function (el) {
				return el != "";
			});
			
			if (words.length <= wordsFromServer.length) {
				for (var j = 0; j < words.length; j++) {
					var spannedWord = "<span id=" + firstSentenceId + "_" + wordID + " class='VR20S_W'>" + words[j] + "</span> ";
					spannedSentence += spannedWord;
					wordID++;
					//check if words[j] contains &nbsp;
					var str = "&nbsp;", startIndex = 0, index, indices = [];
					while ((index = words[j].indexOf(str, startIndex)) > -1) {
						indices.push(index);
						startIndex = index + startIndex + 1;
					}
					if (indices.length > 0) {
						wordID += indices.length;
					}
				}	
			} else {//words.length > wordsFromServer.length 
				for (var k = 0; k < wordsFromServer.length; k++) {
					var str1 = " ", startIndex1 = 0, index1, indices1 = [];
					while ((index1 = wordsFromServer[k].indexOf(str1, startIndex1)) > -1) {
						indices1.push(index1);
						startIndex1 = index1 + startIndex1 + 1;
					}
					
					var combinedWord = '';
					var m = k + spacesCounter + indices1.length;
					for (var n = k + spacesCounter; n <= m; n++) {
						combinedWord = combinedWord + words[n] + ' ';
					}
					combinedWord = combinedWord.trim();
					
					var spannedWord = "<span id=" + firstSentenceId + "_" + wordID + " class='VR20S_W'>" + combinedWord + "</span> ";
					spannedSentence += spannedWord;
					wordID++;
					spacesCounter += indices1.length;
				}
			}
			document.getElementById(sentenceId).innerHTML = spannedSentence;
			document.getElementById(sentenceId).className += " " + "VR20S_sentence_parsed";
			
			if (getBooleanConfigValue("vrweb_debug")) {
				console.log("[VRWEB DEBUG] sentence parsed into words = " + document.getElementById(sentenceId).innerHTML);
			}
		}
	}
}

function removeWordSegmentation(element) {
	var words = element.getElementsByClassName("VR20S_W");
	while (words != null && words.length > 0) {
		var parentElement = words[0].parentNode;
		if (parentElement != null) {
			parentElement.innerHTML = parentElement.innerHTML.replace(/\<span[^>]*\>/g, '');
			parentElement.innerHTML = parentElement.innerHTML.replace(/\<\/span\>/g, '');
			parentElement.className = parentElement.className.replace(/\bVR20S_sentence_parsed\b/g, '');
		} else {
			if (getBooleanConfigValue("vrweb_debug")) {
				console.log("Warning: no words found!");
				console.log(typeof(words));
				console.log(words.outerHTML);
			}
			return;
		}
		words = element.getElementsByClassName("VR20S_W");
	}
}

/* plays the current MP3 file in the queue */
function playCurrent() {
	// Don't play if the loading process has been stopped due to an error
	if (stoppedByError) { 
		if (getBooleanConfigValue("vrweb_debug")) {
			console.log("[VRWEB DEBUG] playCurrent() return by stoppedByError");
		}
	return;
	}

	var playerElement = ___jQuery("#" + player_ID);
	// Set message for mobile devices if player is not playing
	playerElement.html(tapSubPlayerMessage[getConfigValue("vrweb_guilang")]).addClass("vrweb_tapSubPlayerMessage").hide();
	if (singleStringMode) { // single string mode is activated
		if (singleStringPart == null) {
			console.log('[VRWEB WARNING] Es konnten keine Daten zum Vorlesen geladen werden. Bitte klicken Sie erneut auf "Play" oder laden Sie diese Seite neu, falls das Problem weiter besteht.');
		} else if (singleStringPart.mp3 == null) { // not available: wait
			setTimeout(playCurrent, 500);
			if (getBooleanConfigValue("vrweb_debug")) {
				console.log("[VRWEB DEBUG] playCurrent() setTimeOut to 500");
			}
		} else { // mp3 is available: play it
			playerElement.jPlayer('setMedia', {
				mp3: singleStringPart.mp3
			});
			playerElement.jPlayer("play");
			
			
			if (getBooleanConfigValue("vrweb_debug")) {
				console.log("[VRWEB DEBUG] playCurrent() call Send play for singleStringMode");
			}
		}
	} else if (textParts.length == 0) { // no text parts loaded
		console.log('[VRWEB WARNING] Es konnten keine Daten zum Vorlesen geladen werden. Bitte klicken Sie erneut auf "Play" oder laden Sie diese Seite neu, falls das Problem weiter besteht.');
		self.stopPlayback();
	} else {
		// check if there are parts left to be played
		if (currentPart > textParts.length - 1) {
			if (getBooleanConfigValue("vrweb_debug")){
				console.log("[VRWEB DEBUG] playCurrent() Keine weiteren Daten zum Vorlesen vorhanden.");
			}
			self.stopPlayback();
		} else {
			var playPauseButton = ___jQuery("#" + player_ID + "_button_play");
			// check if the mp3 file is already available and the buffer is big enough to start reading
			// (exception: if the user has already waited for 3 seconds the buffer does no longer matter)
			if (textParts[currentPart].mp3 == null || (!checkBuffer() && waitingTime < 3000)) { // not available: wait
        if (getConfigValue("vrweb_player_style") == 'img') {
  				playPauseButton.attr('src',scriptPath + '../buttons/' + getConfigValue("vrweb_player_color") +'/wait.png');
        } else {
  				playPauseButton.removeClass("VR20S_h_button_play").addClass("VR20S_h_button_waiting");
        }
		playPauseButton.attr('name',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_wait']);
		playPauseButton.attr('aria-label',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_wait']);
				// load this mp3 file next, if it is not already included in the currently loaded package
				if (loadPointer > currentPart || loadPointer+packageSize-1 < currentPart) {
					manualPointer = currentPart;
				}
				// has the loading process been stopped by an error the last time MP3 files should be loaded?
				/*if (stoppedByError) {
					loadPackage(); // start the loading process again
					stopepdByError = false;
				}*/
				// wait 0,5 seconds, then try again
				waitingTime += 500; // keep track of the waiting time
				setTimeout(playCurrent, 500);
				if (getBooleanConfigValue("vrweb_debug")) {
					console.log("[VRWEB DEBUG] playCurrent() wait more +500=" + waitingTime);
				}
			} else { // mp3 is available: play it
				if (makeVVT == 2 && !markerMode && !isPopup) {
					var sentenceIds = textParts[currentPart]['tagList'];
					var sentenceId = textParts[currentPart]['tagList'][0];
					___jQuery.ajax({
						url: textParts[currentPart]['vvt'],
						success: function (result) {
							var outputFromVVT = parseVVTData(result);
							parseSentencesIntoWords(sentenceIds, outputFromVVT);
							playerElement.jPlayer('setMedia', {
								mp3: textParts[currentPart].mp3
							}).jPlayer("play");
							highlightWord(playerElement.data('jPlayer'), sentenceId, outputFromVVT[0]);
						},
						error: function (result) {
							makeVVT = 0;
							playerElement.jPlayer('setMedia', {
								mp3: textParts[currentPart].mp3
							}).jPlayer("play");
							removeWordHighlightOnClose(textParts[currentPart]);
						},
						async: true
					});
				} else {
					playerElement.jPlayer('setMedia', {
						mp3: textParts[currentPart].mp3
					}).jPlayer("play");
				}
				
				if (getConfigValue("vrweb_player_style") == 'img') {
						playPauseButton.attr('src',scriptPath + '../buttons/' + getConfigValue("vrweb_player_color") +'/pause.png');
				} else {
						playPauseButton.removeClass("VR20S_h_button_play VR20S_h_button_waiting").addClass("VR20S_h_button_pause");
				}
				playPauseButton.attr('name',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_pause']);
				playPauseButton.attr('aria-label',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_pause']);

				// Check if player is really playing the mp3, otherwise show a message
				var playAgainDelay = isMobileDevice	? 1200 : 500;
				if (firstStartFlag === true) { // Could the player ever play something?
					var timer = setInterval(function() {
						
						if (isPlayerPlaying()) {
							// Is playing atm
							firstStartFlag = false;
							clearInterval(timer);
							playerElement.hide();
							setPlayerStatus(1);
						} else {
							// Not playing atm
							setPlayerStatus(2);
							playerElement.show().jPlayer("stop");
						}
					}, playAgainDelay);
				}

				if (getBooleanConfigValue("vrweb_debug")){
					console.log("[VRWEB DEBUG] playCurrent() Send play, currentPart=" + currentPart);
				}
				// reset the waiting time
				waitingTime = 0;
			}
		}
	}
}

var buttonsNamesAccessibility = {
	'en': {
		'player_icon': 'read button',
		'player_play': 'play',
		'player_pause': 'pause',
		'player_wait': 'wait',
		'player_previous': 'previous sentence',
		'player_next': 'next sentence',
		'player_settings': 'settings',
		'player_close': 'close player',
		'settings_close': 'close settings',
		'settings_download': 'download'
	},
	'de': {
		'player_icon': 'Vorlese-Knopf',
		'player_play': 'abspielen',
		'player_pause': 'anhalten',
		'player_wait': 'warten',
		'player_previous': 'zum vorherigen Satz',
		'player_next': 'zum nächsten Satz',
		'player_settings': 'einstellungen',
		'player_close': 'Player schließen',
		'settings_close': 'Einstellungen schließen',
		'settings_download': 'herunterladen'
	}
};

/* changes the player status and updates the styles of the play/pause button accordingly */
function setPlayerStatus(newStatus) {
	if (newStatus != null && newStatus >= 0 && newStatus <= 3) {
		playerStatus = newStatus;

		var playPauseButton = ___jQuery("#"+player_ID+"_button_play");
		if (playerStatus == 1) { // playing
      if (getConfigValue("vrweb_player_style") == 'img') {
  			playPauseButton.attr('src',scriptPath + '../buttons/' + getConfigValue("vrweb_player_color") +'/pause.png');
      } else {
  			playPauseButton.removeClass("VR20S_h_button_play VR20S_h_button_waiting").addClass("VR20S_h_button_pause");
      }
		playPauseButton.attr('name',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_pause']);
		playPauseButton.attr('aria-label',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_pause']);
		} else if (playerStatus == 3) { // suspended
      if (getConfigValue("vrweb_player_style") == 'img') {
  			playPauseButton.attr('src',scriptPath + '../buttons/' + getConfigValue("vrweb_player_color") +'/wait.png');
      } else {
  			playPauseButton.removeClass("VR20S_h_button_play VR20S_h_button_pause").addClass("VR20S_h_button_waiting");
      }
		playPauseButton.attr('name',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_wait']);
		playPauseButton.attr('aria-label',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_wait']);
		} else { // ready / paused
      if (getConfigValue("vrweb_player_style") == 'img') {
  			playPauseButton.attr('src',scriptPath + '../buttons/' + getConfigValue("vrweb_player_color") +'/play.png');
      } else {
  			playPauseButton.removeClass("VR20S_h_button_pause VR20S_h_button_waiting").addClass("VR20S_h_button_play");
      }
		playPauseButton.attr('name',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_play']);
		playPauseButton.attr('aria-label',buttonsNamesAccessibility[getConfigValue("vrweb_guilang")]['player_play']);
		}

		// reset the waiting time
		waitingTime = 0;
	}
}

/*    resets the counter and plays the first MP3 file in the queue */
this.startPausePlayer = function() {
	// pause all other currently playing players
	for (var i=0; i < VR20S_playback.length; i++) {
		if (i != pb_count) {
			VR20S_playback[i].pausePlayback();
		}
	}
	// register this player as playing
	VR20S_playing = player_ID;

	// Player started for the first time?
	if (firstStart == 1) {
		console.log("[VRWEB INFO] startPausePlayer() start version: " + vrwebPlayerVersion + "; player ID: " + player_ID);
		// initialize the settings
		this.setSpeed(getConfigValue("vrweb_sndspeed"));
		this.setPitch(getConfigValue("vrweb_sndpitch"));
		makeVVT = getConfigValue("vrweb_highlight_word") == 0 ? 0 : 2;
		// The stylesheet class can be defined by the customer, the default is defined in the provided CSS file
		this.setHighlightClass(getConfigValue("vrweb_highlight_class"));

		if (!singleStringMode && !isPopup) { // normal case; popup: do not analyze the document, the text snippets are given by the original document
			this.initialize(true);
		}
		firstStart = 0;

		// create the jPlayer object
		___jQuery("#" + player_ID).jPlayer({
			swfPath: scriptPath + "jquery.jplayer.swf",
			ready: function() {
				if (!stoppedByError) {
					// move the cursor to part 0 and play it
					if (!singleStringMode) {
						moveCursor(0);
					}
					// Update the player status
					setPlayerStatus(1);
					// Init the player with silence
					___jQuery(this).jPlayer("setMedia", {
						mp3: scriptPath + "mobileinit.mp3",
					});
					// play the current text part
					playCurrent();
				}
			},
			ended: function(event) {
				if (!stoppedByError) {
					self.goToNext();
				}
			},
			errorAlerts: true,
			supplied: "mp3",
			error: function(event) {
				if (!stoppedByError) {
					handlePlayerError();
				}
			}/*,
			timeupdate: function(event) { // 4Hz
				console.log(event.jPlayer.status.currentTime);
			}*/
		});
		
		//set default highlighting to show on the screen
		if (highlightingActivated) {
			this.setHighlighting(true, "default");
		}
		
	} else { // Player has been started before
		// has the player been stopped by an error the last time MP3 files should be loaded?
		/*if (stoppedByError) {
			loadPackage(); // start the loading process again
			stopepdByError = false;
		}*/

		// play the first MP3 file in the queue
		if (playerStatus == 0) {
			// move the cursor to part 0 and play it
			if (!singleStringMode) {
				moveCursor(0);
			}
			playCurrent();
			// update the player status
			setPlayerStatus(1);
		// player is currently playing: pause
		} else if (playerStatus == 1) {
			this.pausePlayback();
		} else if (playerStatus == 2) {
			// was the cursor moved after the playback was paused?
			if (pausedPart == currentPart) { // cursor not moved -> just continue the playback
				___jQuery("#"+player_ID).jPlayer("play");
			} else { // cursor moved -> a new mp3 must be loaded!
				playCurrent();
			}
			// update the player status
			setPlayerStatus(1);
		} else if (playerStatus == 3) { // player is suspended: switch to pause
			this.pausePlayback();
		}
	}

	// show / hide the subtitles box
	if (subtitlesActivated) {
		this.showSubtitles();
	} else {
		this.disableSubtitles();
	}
}

/* checks if the player is currently playing
   (ATTENTION: does NOT check the playerStatus variable!) */
function isPlayerPlaying() {
	return !___jQuery("#"+player_ID).data().jPlayer.status.paused;
}

/*    increases the counter and plays the next MP3 file in the queue
   in case the playback is already running */
this.goToNext = function() {
	var playPauseButton = ___jQuery("#"+player_ID+"_button_play");
	if (!singleStringMode && currentPart < textParts.length - 1) { // not available in single string mode
		if (playerStatus == 1) {
			___jQuery("#"+player_ID).jPlayer("stop");
			advanceCursor();
			playCurrent();
		} else if (playerStatus == 2) {
			advanceCursor();
		}
	} else {
		self.stopPlayback();
	}
}

/* decreases the counter and plays the previous MP3 file in the queue
   in case the playback is already running */
this.goToPrevious = function() {
	var playPauseButton = ___jQuery("#" + player_ID + "_button_play");
	if (!singleStringMode && currentPart > 0) { // not available in single string mode
		if (playerStatus == 1) {
			___jQuery("#"+player_ID).jPlayer("stop");
			decreaseCursor();
			playCurrent();
		} else if (playerStatus == 2) {
			decreaseCursor();
		}
	}
}

/* PUBLIC Function
   stops the playback and removes the highlighting */
this.stopPlayback = function() {
	if (typeof(___jQuery("#"+player_ID)) !== "undefined" && typeof(___jQuery("#"+player_ID).data().jPlayer) !== "undefined") {
		___jQuery("#"+player_ID).jPlayer("stop");
		if (getBooleanConfigValue("vrweb_debug")){
			console.log("[VRWEB DEBUG] stopPlayback() send stop");
		}
	}
	// remove the highlighting
	if (!markerMode && lastHighlight != null && currentPart < textParts.length) {
		removeHighlighting(textParts[currentPart]);
	}
	// hide the subtitle box if activated
	if (subtitlesActivated) {
		self.hideSubtitles();
	}
	// remove words highlighting if activated
	removeWordHighlightOnClose(textParts[currentPart]);
	
	// reset the player status and update the styles of the play/pause button
	setPlayerStatus(0);
}

/* PUBLIC Function
   pauses the playback if the player is currently playing */
this.pausePlayback = function() {
	if (playerStatus == 1 && currentPart != null) {
		// note down the part that was paused
		pausedPart = currentPart;
		// pause the playback
		___jQuery("#"+player_ID).jPlayer("pause");
		if (getBooleanConfigValue("vrweb_debug")){
			console.log("[VRWEB DEBUG] pausePlayback() send pause");
		}
		// update the player status
		setPlayerStatus(2);
	}
}

/* reload an MP3 file if it is missing (e.g. because it was removed from the server by timeout) */
function handlePlayerError() {
	//console.log("[VRWEB ERROR] handle player error");
	setPlayerStatus(3); // set the player status to "suspended"
	suspendingPart = currentPart;
	textParts[currentPart].mp3 = null;
	// Was the loading process finished and needs to be restarted at the end?
	/*var restartLoading = false;
	if (totalLoaded >= textParts.length) {
		restartLoading = true;
	}*/
	totalLoaded -= 1;
	manualPointer = currentPart;
	sendRequest(currentPart,textParts[currentPart],null,true,false,true);
	/*if (restartLoading) {
		selectNextPackage();
	}*/
}

/*********************************************************************************************/
/*         Functions to handle the highlighting of text parts in the HTML structure          */
/*********************************************************************************************/

/* (de-)activates / changes the class for the highlighting */
this.setHighlighting = function(activated,newClass) {
	// remove the current highlighting
	if (textParts[lastHighlight]) {
		removeHighlighting(textParts[lastHighlight]);
	}
	// change the highlight settings
	highlightingActivated = activated;
	if (newClass == 'default') {
		settingsHighlightClass = null;
	} else if (newClass != null) {
		settingsHighlightClass = newClass;
	}
	// highlight the currently selected part
	if (highlightingActivated) {
		addHighlighting(textParts[currentPart]);
	}
}

/* highlights the snippet that is being read by changing style attributes of the corresponding HTML tags */
function addHighlighting(textPart) {
	if (textPart == null) {
		lastHighlight = null;
	} else if (highlightingActivated) {
		var tagList = textPart.tagList;
		for (var i=0, max=tagList.length; i < max; i++) {
			___jQuery("#"+tagList[i], doc).addClass(highlightClass);
			// the highlighting class from the settings dialog has a higher priority, for this reason it must be added last
			if (settingsHighlightClass != null) {
				___jQuery("#"+tagList[i], doc).addClass(settingsHighlightClass);
			}

			___jQuery("#"+tagList[i], doc).addClass("VR20S_highlight_"+highlightCount); // register this player	
		}
		lastHighlight = currentPart;
	}
}

/* removes all highlighting span tags in a HTML tag*/
function removeHighlighting(textPart) {
	var tagList = textPart.tagList;
	if (tagList == null) {
		return;
	}

	var regexHighlight = /(\<span\s+[^>]*?style=\")([^"]*?)(\"[^>]*?\>)/;
	// check if there is another player that also has its highlight on the same span	
	var otherPlayer = null;
	if (tagList.length > 0) {
		otherPlayer = ___jQuery("#"+tagList[0], doc).attr("class").split(" ").filter(function(c){
		return c.match(/^VR20S_highlight_\d?/);
		}).length > 1;
	}

	for (var i=0, max=tagList.length; i < max; i++) {
		// unregister this player
		___jQuery("#"+tagList[i], doc).removeClass("VR20S_highlight_"+highlightCount);
		// only remove the highlighting if there is no other player that has its highlight on the same span
		if (!otherPlayer) {
			___jQuery("#" + tagList[i], doc).removeClass(highlightClass);
			}
		// the highlighting class of the settings dialog is independent of other players
		if (settingsHighlightClass != null) {
			___jQuery("#"+tagList[i], doc).removeClass(settingsHighlightClass);
		}
	}
}

/* creates the div container for the subtitles */
function createSubtitleContainer(subtitleZIndex) {
	if (___jQuery(".VR20S_subtitle", doc).length == 0) {
		___jQuery('body', doc).append(___jQuery('<div/>', doc).addClass('VR20S_subtitle' + ((getConfigValue("vrweb_subtitle_class")) ? " " + getConfigValue("vrweb_subtitle_class") : "")));
		___jQuery('.VR20S_subtitle', doc).css({"display":"none","zIndex":subtitleZIndex});
		___jQuery('.VR20S_subtitle', doc).append(___jQuery('<span/>', doc).addClass('VR20S_subtitleText'));
		___jQuery('.VR20S_subtitle', doc).append(___jQuery('<button>X</button>', doc).addClass('VR20S_closeSubtitle').click( function(){ self.disableSubtitles(true); } ));
	}
}

/* creates / shows the subtitle box */
this.showSubtitles = function(subtitleZIndex) {
	if (___jQuery(".VR20S_subtitle", doc).length == 0) {
		createSubtitleContainer(subtitleZIndex);
	}
	subtitlesActivated = true;
	// Only update the subtitle display if this is the active player
	if (this.isPlayerActive()) {
		___jQuery(".VR20S_subtitle", doc).css("display","block");
		// update the caption
		if (singleStringMode) {
			this.updateSubtitles(singleStringPart);
		} else {
			this.updateSubtitles(textParts[currentPart]);
		}
	}
}

/* updates the subtitle box (will be made visible if it is currently hidden) */
this.updateSubtitles = function(textPart) {
	___jQuery(".VR20S_subtitle", doc).css("display","block");
	___jQuery(".VR20S_subtitleText", doc).text(textPart.originalText);
	this.setSubtitleColor(settingsSubtitleClass);
}

/* hides the subtitle box */
this.hideSubtitles = function() {
	___jQuery(".VR20S_subtitle", doc).css("display","none");
}

/* disables the subtitle box */
this.disableSubtitles = function(byClick) {
	// Only update the subtitle display if this is the active player
	if (this.isPlayerActive()) {
		this.hideSubtitles();
	}
	subtitlesActivated = false;

  // only change the active color, when the close button is clicked
  if (byClick) {
    ___jQuery('#'+player_ID+'_vrweb_s_subtitle_colors > .active').removeClass('active'); // deactivate currently active color
    ___jQuery('#'+player_ID+'_vrweb_s_subtitle_colors > .vrweb_s_color_border:first').addClass('active'); // activate the no-color option
  }
}

/* changes the color of the subtitle box */
this.setSubtitleColor = function(newClass) {
	if (___jQuery(".VR20S_subtitle", doc).length == 0) {
		createSubtitleContainer();
	}
	// change the class
	settingsSubtitleClass = newClass;
	// Only update the subtitle display if this is the active player
	if (this.isPlayerActive()) {
		// remove all other color classes
		for (var i=0; i < subtitleColorArray.length; i++) {
			___jQuery(".VR20S_subtitle", doc).removeClass(subtitleColorArray[i]);
		}
		// add the class to the subtitle div
		___jQuery(".VR20S_subtitle", doc).addClass(newClass);
	}
}

/*********************************************************************************************/
/*              Functions and classes to parse and analyze the HTML structure                */
/*********************************************************************************************/

/* contains the positive and negative lists of tag names / classes / IDs to integrate or to leave out */
function ReaderConfiguration() {
	this.eosTags = new Array(); // list of tags which force an end of sentence tag at the end of the content (e.g. headlines)
	this.whitelisting = 0; // is whitelisting activated? (will be activated automatically when entries are added to a whitelist)
	this.blacklisting = 0; // is blacklisting activated? (will be activated automatically when entries are added to a blacklist)
	this.preference = 1; // standard choice when neither whitelisting nor blacklisting are activated or when both are activated
	this.readTags = new Array();
	this.readClasses = new Array();
	this.readIDs = new Array();
	this.skipTags = new Array();
	this.skipClasses = new Array();
	this.skipIDs = new Array();

	/* add an EOS tag (or a list of EOS tags) to the list */
	this.addEOSTags = function(newTags) {
		if (newTags instanceof Array) {
			for (var i=0; i < newTags.length; i++) {
				this.eosTags.push(newTags[i]);
			}
		} else {
			this.eosTags.push(newTags);
		}
	}

	/* checks if a given element is an EOS tag */
	this.isEOSTag = function(tag) {
		return (typeof(tag) === "undefined" || ___jQuery.inArray(tag.toLowerCase(),this.eosTags) === -1) ? false : true;
	}

	/* activate or deactivate whitelisting */
	this.setWhitelisting = function(value) {
		if (value == 0 || value == '') {
			this.whitelisting = 0;
		} else {
			this.whitelisting = 1;
		}
	}

	/* activate or deactivate blacklisting */
	this.setBlacklisting = function(value) {
		if (value == 0 || value == '') {
			this.blacklisting = 0;
		} else {
			this.blacklisting = 1;
		}
	}

	/*
	 * Functions to add elements to the whitelist/blacklist
	 */

	this.addTagToRead = function(name) { // add tag to whitelist
		this.readTags.push(name.toUpperCase()); // tag names must be in upper case!
		this.setWhitelisting(1);
	}

	this.addClassToRead = function(name) { // add class to whitelist
		this.readClasses.push(name);
		this.setWhitelisting(1);
	}

	this.addIDToRead = function(name) { // add ID to whitelist
		this.readIDs.push(name.toUpperCase());
		this.setWhitelisting(1);
	}

	this.addTagToSkip = function(name) { // add tag to blacklist
		this.skipTags.push(name.toUpperCase()); // tag names must be in upper case!
		this.setBlacklisting(1);
	}

	this.addClassToSkip = function(name) { // add class to blacklist
		this.skipClasses.push(name);
		this.setBlacklisting(1);
	}

	this.addIDToSkip = function(name) { // add ID to blacklist
		this.skipIDs.push(name.toUpperCase());
		this.setBlacklisting(1);
	}

	/* analyze a reader configuration variable to create the respective whitelist/blacklist */
	this.analyzeList = function(varName,func) {
		if (typeof readerOptions[varName] !== undefined) {
			// Split at semicolons
			var strArray = readerOptions[varName].split(/\s*,\s*/);
			if (strArray.length > 0) {
				for (var i=0; i < strArray.length; i++) {
					if (strArray[i] !== '') {
						if (varName === 'vrweb_readelementsclass') {
							this.addClassToRead(strArray[i]);
						} else if (varName === 'vrweb_readelementsid') {
							this.addIDToRead(strArray[i]);
						} else if (varName === 'vrweb_readelementsname') {
							this.addTagToRead(strArray[i]);
						} else if (varName === 'vrweb_exclelementsclass') {
							this.addClassToSkip(strArray[i]);
						} else if (varName === 'vrweb_exclelementsid') {
							this.addIDToSkip(strArray[i]);
						} else if (varName === 'vrweb_exclelementsname') {
							this.addTagToSkip(strArray[i]);
						}
					}
				}
			}
		}
	}

	/* automatically  analyze all reader configuration variables to create the whitelists and blacklists */
	this.analyzeAllLists = function() {
		this.analyzeList('vrweb_readelementsclass');
		this.analyzeList('vrweb_readelementsid');
		this.analyzeList('vrweb_readelementsname');
		this.analyzeList('vrweb_exclelementsclass');
		this.analyzeList('vrweb_exclelementsid');
		this.analyzeList('vrweb_exclelementsname');
	}

	/* check if an element may be read or not, depending on the whitelist and/or blacklist */
	this.checkElement = function(element) {
		if (element.tagName == undefined && element.className == undefined && element.id == undefined || element.style.display == "none") {
			return -1;
		}
		var is_whitelisted = 0;
		var readClassSelector = (___jQuery.isEmptyObject(this.readClasses) || this.readClasses.length===0)? "" : "." + this.readClasses.join(",.");
		if ((typeof element.tagName == "string") && ___jQuery.inArray(element.tagName.toUpperCase(), this.readTags) != -1 || 
			___jQuery(element).is(readClassSelector) ||
			(typeof element.id == "string") && ___jQuery.inArray(element.id.toUpperCase(),this.readIDs) != -1) {
			is_whitelisted = 1;
		}
		var is_blacklisted = 0;
		var skipClassSelector = (___jQuery.isEmptyObject(this.skipClasses) || this.skipClasses.length===0)? "" : "."+this.skipClasses.join(",.");
		if ((typeof element.tagName == "string") && ___jQuery.inArray(element.tagName.toUpperCase(), this.skipTags) != -1 ||
			___jQuery(element).is(skipClassSelector) ||
			(typeof element.id == "string") && ___jQuery.inArray(element.id.toUpperCase(), this.skipIDs) != -1) {
			is_blacklisted = 1;
		}

		if (getBooleanConfigValue("vrweb_debug")) { // debug mode activated?
			//console.log("[VRWEB DEBUG] check element tagName=" + element.tagName + " | className=" + element.className + " | id=" + element.id + " | Whitelisted: " + is_whitelisted + " | Blacklisted: " + is_blacklisted);
		}

		// whitelisting activated?
		if (this.whitelisting) {
			// blacklisting also activated?
			if (this.blacklisting) {
				// is the element on the whitelist?
				if (is_whitelisted) {
					// is the element on both lists?
					if (is_blacklisted) {
						return 0;
					} else { // only on the whitelist
						return 1;
					}
				} else if (is_blacklisted) { // only on the blacklist
					return -1;
				} else { // element is not listed at all
					return 0;
				}
			} else { // only whitelisting activated
				return is_whitelisted;
			}
		} else if (this.blacklisting) { // only blacklisting activated
			if (is_blacklisted) {
				return -1;
			} else {
				return 0;
			}
		} else { // neither whitelisting nor blacklisting activated
			return 0;
		}
	}
}

/* iterates over all child nodes of an HTML element
   to join or separate text snippets */
function iterateRecursively(config,element,level,snippetIDs,parentAccepted) {
	// iterate over all child nodes
	var includedElements = null;
	var max = 0;
	try {
		includedElements = element.childNodes;
	}
	catch (ex) { 
		if (getBooleanConfigValue("vrweb_debug")) {
			console.log("[VRWEB DEBUG] Exception in iterateRecursively() by element tagName=" + element.tagName + "; className=" + element.className + "; id=" + element.id + "; title=" + element.title);
			console.log("exception=" + ex);
		}
		return snippetIDs;
	}

	if (includedElements != null)
		max = includedElements.length;
	for (var i=0; i < max; i++) {
		var child = includedElements[i];
		var result = config.checkElement(child);
		if (result == 1 && getBooleanConfigValue("vrweb_debug")) {
			console.log("[VRWEB DEBUG] " + result + " | " + Array((level+1)*4).join(" ") + child.tagName + " / " + child.className + " / " + child.id);
		}
		// Skip garbage elements (otherwise they may break a sentence when they are not whitelisted)
		if (child.tagName == undefined) {
			continue;
		}
		// If the element is neither blacklisted nor whitelisted, the setting for the parent element determines if it will be read or not
		if (result == 0) {
			result = parentAccepted;
		}
		// no-read-tag: clear the collection of snippets
		if (result == -1) {
			if (getBooleanConfigValue("vrweb_debug") && snippetIDs.length > 0) {
				console.log("[VRWEB DEBUG] " + snippetIDs);
			}
			addTextPartFromTags(snippetIDs);
			snippetIDs = new Array();
		}
		// iterate over child nodes recursively
		if (child.dataset.vrstext == null) {
			snippetIDs = iterateRecursively(config,child,level+1,snippetIDs,result);
		}
		if (result == 1) {
			// found a snippet tag with readable content: push the snippet ID on the list
			var tContent = '';
			if (child.dataset.vrstext != null && child.dataset.vrstext != undefined) {
				tContent = child.dataset.vrstext;
			} else {
				tContent = child.textContent === undefined ? child.innerText : child.textContent; // textContent is not available in IE < 9
			}
			if (child.tagName == 'SPAN' && hasClass(child,'VR20S') && tContent != undefined && tContent.match(/\S/)) {
				snippetIDs.push(child.id);
				if (getBooleanConfigValue("vrweb_debug")) {
					console.log("[VRWEB DEBUG] child.id=" + child.id);
					console.log("[VRWEB DEBUG] child.innerHTML='" + child.innerHTML + "'");
				}
				
				// span tag contains an end of sentence class: clear the collection of snippets
				if (hasClass(child,'VR20S_EOS')) {
					if (getBooleanConfigValue("vrweb_debug") && snippetIDs.length > 0) {
						console.log("[VRWEB DEBUG] snippetIDs=" + snippetIDs);
					}
					addTextPartFromTags(snippetIDs);
					snippetIDs = new Array();
				}
			}
		}
	}
	return snippetIDs;
}

/* returns the next available color to mark text snippets (for debugging only) */
function getNextColor() {
	currentColor++;
	if (currentColor > colors.length-1) {
		currentColor = 0;
	}
	return colors[currentColor];
}

/* returns the next available sentence ID */
function getNextSentenceID() {
	currentSentenceID++;
	return currentSentenceID;
}

/* returns the text selected by the user
   Author: Tim Down */
function getSelectionHtml() {
	var html = "";
	if (typeof window.getSelection != "undefined") {
		var sel = window.getSelection();
		if (sel.rangeCount) {
			var container = document.createElement("div");
			for (var i = 0, len = sel.rangeCount; i < len; ++i) {
				container.appendChild(sel.getRangeAt(i).cloneContents());
			}
			html = container.innerHTML;
		}
	} else if (typeof document.selection != "undefined") {
		if (document.selection.type == "Text") {
			html = document.selection.createRange().htmlText;
		}
	}
	return html;
}

/* finds <br> tags and adds class "VR20S_EOS" to elements before <br> */
function analyzeBRs() {
	var allBRs = document.getElementsByTagName("br");
	
	for (var i = 0; i < allBRs.length; i++) {
		var curPrev = ___jQuery(allBRs[i]).prev();
		var childrenCurPrev = curPrev.find("span.VR20S");
		if (childrenCurPrev.length > 0) {
			___jQuery(childrenCurPrev[childrenCurPrev.length - 1]).addClass("VR20S_EOS");
		} else if (curPrev.hasClass("VR20S")) {
			curPrev.addClass("VR20S_EOS");
		}
	}
}

function analyzeEOS() {
	var EOSelements = ["h1","h2","h3","h4","h5","li","p", "section", "article", "aside", "header", "nav", "footer"];
	
	for (var i = 0; i < EOSelements.length; i++) {
		var curTagName = EOSelements[i];
		var allElems = document.getElementsByTagName(curTagName);
		for (var j = 0; j < allElems.length; j++) {
			var curElem = ___jQuery(allElems[j]);
			var elemChildren = curElem.find("span.VR20S");
			if (elemChildren.length > 0) {
				___jQuery(elemChildren[elemChildren.length - 1]).addClass("VR20S_EOS");
			} else {
				curElem.addClass("VR20S_EOS");
			}
		}
	}
}

/* calls the recursive method that analyzes the document to be read */
function analyzeDocument(config) {
	analyzeDocumentRec(config,___jQuery('body'),false,0);
	if (getBooleanConfigValue("vrweb_brhandling")) {
		analyzeBRs();
	}
	analyzeEOS();
}

/* analyzes the document to be read and inserts spans to mark snippets */
function analyzeDocumentRec(config,parent,parentEOS,layer) {
	// do not change script or style tags
	if (parent.prop("tagName") === 'SCRIPT' || parent.prop("tagName") === 'NOSCRIPT' || parent.prop("tagName") === 'STYLE' || parent.prop("tagName") === 'IFRAME' || parent.prop("tagName") === 'svg') {
		return;
	}

	// skip the player divs
	if (parent.prop("className") === 'VR20S_h_player_container' || parent.prop("className") === 'vrweb_settings') {
		return;
	}

	// check if the tag is an EOS tag (this attribute can be inherited from the parent element)
	var isEOS = parentEOS;
	if (config.isEOSTag(parent.prop("tagName"))) {
		isEOS = true;
	}

	// Keep track of the number of parts to have the information available if an element is the last child element
	var _content = null;
	try {
		_content = parent.contents();
	}
	catch (ex) { 
		if (getBooleanConfigValue("vrweb_debug")) {
			console.log("[VRWEB DEBUG] Exception in analyzeDocumentRec() by element tagName=" + parent.prop("tagName") + "; className=" + parent.prop("className") + "; id=" + parent.prop("id") + "; title=" + parent.prop("title"));
			console.log("exception=" + ex);
			console.log("[VRWEB DEBUG] Element skipped");
		}
		return;
	}
	var numberOfParts = _content.length;
	var count = 0;
	_content.each(function(index) {
		count++;
		if (this.nodeType === 3) { // nodeType 3: Text  (http://www.w3schools.com/jsref/prop_node_nodetype.asp)
			if (___jQuery(this).text().trim() != '') {
				var oldText = ___jQuery(this).text();
				// temporarilly replace ordinal numbers like dates by a given string to prevent them to be interpreted as eos marker
				oldText = oldText.replace(/(\d{1,2})\./g,"$1{ORDINAL_DOT}");
				oldText = oldText.replace(/(dr|fr|hr|med|soc|jur)\./gi,"$1{ORDINAL_DOT}");//exclude abbrev.
				// end of snippet markers: punctuation marks, newlines, braces, longer rows of whitespaces (attention: dot can also occur in abbreviations!)
				var parts = oldText.split(/(\n|\s{3,}|[!?:;\(\)]\s|\.(?:\s|$))/);
				// stick current snippet to the previous one, if snippet's length is less than 5 symbols and it is a word (contains letters)
				for (var i = 1; i < parts.length; i++) {
					if (parts[i].length < 5 && /[a-zA-Z]/.test(parts[i])) {
						parts[i - 1] += parts[i];
						parts.splice(i, 1);
						i--;
					}
				}
				var newText = '';
				var openSpan = false;
				for (var i=0; i < parts.length; i++) {
					parts[i] = parts[i].replace(/\{ORDINAL_DOT\}/g,".");
					parts[i] = encodeHtmlEntities(parts[i]);
					if (parts[i].match(/^([.!?:;\)])(\s?)$/)) { // part is an end of sentence marker ONLY, without any text: add it to prev sentence or ignore
						if (openSpan) { // close an opened span before this sequence
							// add current part to the end of opended sentence, without following whitespaces and close the sentence
							newText += parts[i].replace(/([.!?:;\(\)])/, "$1</span>");
							newText = newText.replace('{EOSCLASS_PLACEHOLDER}',' VR20S_EOS');
							openSpan = false;
						} else { 
							//BV: 151204 ignore such part at all, leave it outside of sentences
							//newText += parts[i];						
							// no opened tag: create a new tag just for the EOS marker (for the case where the last words of a sentence were enclosed e.g. by a link tag)
							//ES: 20210511 return this back
							newText += '<span id="VR20S_' + getNextSentenceID() + '" class="VR20S VR20S_EOS">' + parts[i].replace(/([.!?:;\(\)])/, "$1</span>");
						}
					} else if (parts[i].match(/^[\(]\s?$/)) { // part is a begin of sentence marker
						if (openSpan) { // close an opened span AFTER this sequence
							// insert the closing span tag BEFORE the begin of sentence marker
							newText += "</span>";
							newText = newText.replace('{EOSCLASS_PLACEHOLDER}',' VR20S_EOS');
						}
						// open a new span tag
						var debugStyle = '';
						if (getBooleanConfigValue("vrweb_debug")) { // debug mode: mark the snippets with different colors
							debugStyle = ' style="color:' + getNextColor() + '"';
						}
						newText += '<span id="VR20S_' + getNextSentenceID() + '" class="VR20S{EOSCLASS_PLACEHOLDER}"' + debugStyle + '>' + parts[i];
						openSpan = true;
					} else if (parts[i].match(/^(\n|\s+)$/)) { // part is a newline or a longer row of whitespaces
						if (openSpan) { // close an opened span before this sequence
							newText += '</span>' + parts[i];
							// Only insert an EOS tag if this is the last child element
							if (isEOS && count == numberOfParts) {
								newText = newText.replace('{EOSCLASS_PLACEHOLDER}',' VR20S_EOS');
							} else {
								newText = newText.replace('{EOSCLASS_PLACEHOLDER}','');
							}
							openSpan = false;
						} else {
							newText += parts[i];
						}
					} else if (parts[i].trim().match(/\S/) && !openSpan) { // part contains a non-space character: start a span
						var debugStyle = '';
						if (getBooleanConfigValue("vrweb_debug")) { // debug mode: mark the snippets with different colors
							debugStyle = ' style="color:' + getNextColor() + '"';
						}
						newText += '<span id="VR20S_' + getNextSentenceID() + '" class="VR20S{EOSCLASS_PLACEHOLDER}"' + debugStyle + '>' + parts[i];
						openSpan = true;
					} else {
						newText += parts[i];
					}
				}
				// if there is still a span that has not been closed, close it at the end of the string
				if (openSpan) {
					newText += '</span>';
					// Only insert an EOS tag if this is the last child element
					if (isEOS && count == numberOfParts) {
						newText = newText.replace('{EOSCLASS_PLACEHOLDER}',' VR20S_EOS');
					} else {
						newText = newText.replace('{EOSCLASS_PLACEHOLDER}','');
					}
					openSpan = false;
				}
				// replace the old text by the altered one
				___jQuery(this).replaceWith(newText);
			}
		} else if (this.nodeName === 'IMG' || this.nodeName === 'img' || this.nodeName === 'INPUT' || this.nodeName === 'input' || this.nodeName === 'AREA' || this.nodeName === 'area') {
			if ((getConfigValue('vrweb_read_arialabel') == '1') || (getConfigValue('vrweb_read_alt') == '1')) {
				var altText = '';
				altText = ___jQuery(this).attr('aria-label');
				if ((getConfigValue('vrweb_read_arialabel') == '1') && (altText != undefined) && altText && (altText != '')) {
					___jQuery(this).wrap( '<span id="VR20S_' + getNextSentenceID() + '" class="VR20S VR20S_EOS" data-vrstext="' + altText + '"></span>' );
					altText = '';
				}
				 else {
					altText = ___jQuery(this).attr('alt');
				}
					
				if ((getConfigValue('vrweb_read_alt') == '1') && (altText != undefined) && altText && (altText != '')) {
					___jQuery(this).wrap( '<span id="VR20S_' + getNextSentenceID() + '" class="VR20S VR20S_EOS" data-vrstext="' + altText + '"></span>' );
				}
			}
		} 
		else {
			// If this is the last child element, the EOS tag attribute will be inherited by child elements
			if (count == numberOfParts) {
				analyzeDocumentRec(config,___jQuery(this),isEOS,layer+1);
			} else {		
				analyzeDocumentRec(config,___jQuery(this),false,layer+1);
			}
		}
	});
}

/* parses the raw HTML file to determine sections that shall be read
   and to split the text to be read into sentences;
   Gets called from startPausePlayer() when the player is started
   for the very first time   */
this.initialize = function(startLoading) {
	if (getBooleanConfigValue("vrweb_read_selections")) {
		markerMode = true;
	}
	if (markerMode) {
		// reset the text parts array
		textParts = new Array();
		// reset the cursor
		currentPart = 0;
		// reset the variables for the package loading
		loadPointer = 0;
		totalLoaded = 0;
		bufferPointer = 0;
	
		var selectionPrepared = selectionHtml;

		// remove all span tags
		selectionPrepared = selectionPrepared.replace(/\<span[^>]+?\>/ig, '');
		selectionPrepared = selectionPrepared.replace(/\<\/span\>/ig, '');

		// apply pronounce-Tags
		selectionPrepared = selectionPrepared.replace(/\<pronounce\s+like=\"([^"]+?)\"\s*\>[^<]+?(\<\/pronounce\>|$)/ig, '$1');
		// apply spell-Tags
		selectionPrepared = selectionPrepared.replace(/\<spell(?:\s+speed=\"([^"]+)\")?\s*\>([^<]+)(\<\/spell\>|$)/ig, function(fullMatch, speed, text) {
				return handleSpellTags(text,speed);
			}
		);
		// replace BR with whitespaces / semicolons, depending on the configuration
		var replaceBR = ' ';
		if (getBooleanConfigValue("vrweb_brhandling")) {
			replaceBR = ';';
		}
		selectionPrepared = selectionPrepared.replace(/\<BR\s*\/?\>/gi, replaceBR);

		// remove all other HTML tags from the selection string
		selectionPrepared = selectionPrepared.replace(/\<[^>]+?\>/g, '');

		// remove HTML entities
		selectionPrepared = selectionPrepared.replace(/&[^;]+?;/g, '');
		
		selectionPrepared = selectionPrepared.replace(/[\+\-\*\&\,]{3,}/g,'');
		selectionPrepared = selectionPrepared.replace(/[\.\,\?\!]{4,}/g,'');
		
		// split the selected string into sentences:
		// insert EOS markers first, then split at these EOS markers
		// (that way the EOS markers themselves will still be contained in the snippets shown in the subtitles)
		selectionPrepared = selectionPrepared.replace(/([.!?:;\(\)])(\s*)/g, "$1{END_OF_SENTENCE}");
		var selectionParts = selectionPrepared.split("{END_OF_SENTENCE}");

		// remove semicolons and commas from the beginning of the sentences and extra spaces before semicolons
		//add the parts to the queue
		for (var i=0; i < selectionParts.length; i++) {
			selectionParts[i] = selectionParts[i].replace(/^;\s*/gm,'');
			selectionParts[i] = selectionParts[i].replace(/^\,\s*/gm,'');
			selectionParts[i] = selectionParts[i].replace(/\s{1,};/gm,';');
			if (selectionParts[i] != "") {
				addTextPartFromString(selectionParts[i],null,null);
			}
		}
		
	} else { // normal mode => parse the HTML file
		var config = new ReaderConfiguration();

		/* configuration	*/
		config.addEOSTags(new Array("h1","h2","h3","h4","h5","li","p")); // add end of sentence tags

		// Simple Parse
		if (getBooleanConfigValue("vrweb_simpleparse") != false) {
			config.addTagToRead("read");
		} else { // No Simple Parse - use whitelists and blacklists
			config.analyzeAllLists();
		}
		// the icon, the player container and the settings div must always be skipped
		config.addClassToSkip("vrweb_icon");
		config.addClassToSkip("vrweb_player");
		config.addClassToSkip("vrweb_settings_wrapper");
		// skip invalid elements
		//___jQuery.each(['SVG', 'IMAGE', 'SCRIPT', 'RECT'], function(index, tagName){ // TODO add svg, canvas etc to read the alternative text
		___jQuery.each(['SVG', 'SCRIPT', 'RECT'], function(index, tagName){
			config.addTagToSkip(tagName);
		});

		// Check if the markers have already been inserted by another player
		var markersExisting = false;
		for (var i=0; i < VR20S_playback.length; i++) {
			var player = VR20S_playback[i];
			if (player.markersInserted != "undefined" && player.markersInserted) {
				markersExisting = true;
				break;
			}
		}
		if (!markersExisting) { // Must not insert markers if another player has already done so!
			analyzeDocument(config);
			// Set flag to true
			self.markersInserted = true;
		}
		
		// If there is a whitelist, skip everything that's not on the whitelist;
		// else: include everything by standard
		var parentAccepted = 1;
		if (config.whitelisting) {
			parentAccepted = -1;
		}
		// iterate over all child nodes of an HTML element
		// to join or separate text snippets
		removeWordSegmentation(document.body);
		iterateRecursively(config,document.body,0,new Array(),parentAccepted);
		
		// open the debug window
//BV		if (getBooleanConfigValue("vrweb_debug")) {
//BV			createDebugWindow();
//BV		}
	}

	// load the mp3 files in packages
	if (startLoading) {
		loadPackage();
	}
}

/* creates the popup window and writes all of the sentences to read to it */
function createDebugWindow(){
	var debugWindow = window.open("", "Voice Reader Web 15 Debug Window", "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=780, height=200, top=100, left=200");
	if (debugWindow != null) { //in case of popup blocker
		var html = "<title>Voice Reader Web DEBUG</title>Die folgenden Sätze werden vorgelesen:<ol>";
		for(var i=0; i<textParts.length; i++){
			html += "<li>"+textParts[i].readText+"</li>";
		}
		html += "</ol><hr>Achtung: dieses Fenster darf nicht in Produktion gehen, löschen Sie dazu den Parameter vrweb_debug.";
		debugWindow.document.write(html);
	}
}

/* constructs a random customer key */
function makeCustomerKey(){
	var customerKey = "";
	var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

	for(var i = 0; i < 10; i++) {
		customerKey += possible.charAt(Math.floor(Math.random() * possible.length));
	}
	customerKey += vrwebPlayerVersion;
	if (getBooleanConfigValue("vrweb_debug")) {
		console.log("[VRWEB DEBUG] new id=" + customerKey);
	}
	return customerKey;
}

/*********************************************************************************************/
/*             Functions to load the MP3 files for the text parts package-wise               */
/*********************************************************************************************/

/* loads the next package of mp3 files corresponding to parts (starting at the pointer) */
function loadPackage() {
	var requestsSent = 0; // how many request have been sent for this package?
	for (var i=loadPointer; i < Math.min(loadPointer+packageSize,textParts.length); i++) {
		if (textParts[i] == null) { // Error: ignore this part
			//console.log("[VRWEB DEBUG] Skip #" + i + " - ERROR");
		} else if (textParts[i].isLoading == true) { // is the mp3 currently loading?
			//console.log("[VRWEB DEBUG] Skip #" + i + " - currently loading");
		} else if (textParts[i].mp3 != null) { // has the mp3 already been loaded? -> skip this part
			//console.log("[VRWEB DEBUG] Skip #" + i + " - already loaded");
		} else {
			requestsSent++;
			//console.log("[VRWEB DEBUG] Send a request for #" + i + "...");
			sendRequest(i,textParts[i],null,true,false,true);
		}
	}
	// Has at least one request been sent which should be waited for?
	// If not, the next package must be selected here
	if (requestsSent == 0) {
		selectNextPackage();
	}
}

/* checks if the buffer is big enough to start reading */
function checkBuffer() {
	// If the buffer pointer has reached the last text part, the threshold does no longer apply
	return (readingBuffer > bufferThreshold || bufferPointer >= textParts.length);
}

/* updates the buffer variable by registering as many loaded parts in the queue as possible */
function checkBufferUpdate(pos) {
	if (bufferPointer == null) { // Buffer is currently locked
		// try again shortly after
		setTimeout(checkBufferUpdate(number),100);
	} else {
		if (pos == bufferPointer) { // The text part makes the buffer grow
			// lock the buffer pointer because of possible parallelism
			bPointer = bufferPointer;
			bufferPointer = null;

			// Extend the buffer and increase the buffer pointer as much as possible
			for (var i=pos; i < textParts.length; i++ ) {
				if (textParts[i].mp3 != null) {
					readingBuffer += textParts[i].readText.length;
					bPointer++;
				} else {
					break;
				}
			}

			// reset the pointer, so when another text part has been loaded it can also be added to the buffer
			bufferPointer = bPointer;

//BV			if (getBooleanConfigValue("vrweb_debug")) { // debug mode activated?
//BV				console.log("[VRWEB DEBUG] Current buffer size: " + readingBuffer);
//BV			}
		}
	}
}

/* this method is called when a request has been finished */
function finalizeRequest(number) {
	if (getBooleanConfigValue("vrweb_debug")){
		console.log("[VRWEB DEBUG] Request for part #"+number+" was finished");
	}
	totalLoaded++;
	// Update the buffer
	checkBufferUpdate(number);
	// Has the current package been finished?
	for (var i=loadPointer; i < loadPointer+packageSize; i++ ) {
		if (i < textParts.length && textParts[i].mp3 == null) {
			return;
		}
	}
	// The return statement was not called - the package has been finished
	//console.log("[VRWEB DEBUG] The package was finished *")
	selectNextPackage();
	// If the player was suspended because it was waiting for this part
	// to finish loading, continue playing now
	if (playerStatus == 3 && suspendingPart == number) {
		suspendingPart = null;
		setPlayerStatus(1);
		playCurrent();
	}
}

/* selects the next package to load, then calls the method to load it */
function selectNextPackage() {
	//console.log("[VRWEB DEBUG] Select the next package")
	// have all files been loaded? -> finish the cycle
	if (totalLoaded >= textParts.length) {
		//console.log("[VRWEB DEBUG] All files have been loaded - quit")
		// reset all variables and quit the method
		loadPointer = 0;
		manualPointer = null;
		return;
	} else { // proceed
		// move the pointer to the next position:
		// is there a valid manual selection by the user?
		if (manualPointer != null && manualPointer != loadPointer && manualPointer < textParts.length-1) {
			loadPointer = manualPointer;
			manualPointer = null;
			//console.log("[VRWEB DEBUG] Manually selected pointer: " + loadPointer)
		} else { // standard case: proceed right after the last package
			loadPointer += packageSize;
			// has the end of the page been reached?
			if (loadPointer > textParts.length-1) {
				loadPointer = 0; // continue at the begin of the page
			}
			//console.log("[VRWEB DEBUG] Incremented pointer: " + loadPointer)
		}
		// load the package
		loadPackage();
	}
}

/* removes all existing parts to make them be loaded again */
function removeParts() {
	if (singleStringMode) {
		sendRequest(0, singleStringPart, lastUsedLanguage, false, false, true);
	} else {
		for (var i=0; i < textParts.length; i++) {
			textParts[i].mp3 = null;
			textParts[i].isLoading = false;
		}
		totalLoaded = 0;
		loadPointer = currentPart;
		loadPackage();
	}
}

/* creates an object containing all the parts on the parsed website (to make it available for download) */
function createCompletePagePart() {
	var completePage = null;
	var tagList = [];
	var originalText = '';
	var readText = '';
	for (var i=0; i < textParts.length; i++) {
		if (textParts[i] != null) { // Error: ignore this part
			for (t in textParts[i].tagList) {
				tagList.push(t);
			}
			if (textParts[i].originalText.match('[.!?:;]\s*$')) {
				originalText += textParts[i].originalText + ' ';
			} else {
				originalText += textParts[i].originalText + '. ';
			}
			if (textParts[i].readText.match('[.!?:;]\s*$')) {
				readText += textParts[i].readText + ' ';
			} else {
				readText += textParts[i].originalText + '. ';
			}
		}
	}
	completePage = new TextPart(tagList,originalText,readText);
	return completePage;
}

/* sends a request to the VoiceReader web service and adds the obtained MP3 file to the queue */
function sendRequest(number,textPart,otherLanguage,continueRequests,downloadMP3,doRepeat) {
	var voice = getConfigValue("vrweb_voicename");
	var language = otherLanguage;
	if (!language) language = getConfigValue("vrweb_lang").toLowerCase();
	
	// Create XML HTTP request
	var url = encodeURI(URL_WEBSERVICE_MP3);
	var params = "isLidEnabled=false&voice=" + voice + "&language=" + language + "&format=mp3" + "&coding=6" + "&speed=" + speed + "&pitch=" + pitch + "&timbre=100&volume=100" + "&customerId=" + getConfigValue("vrweb_customerid") + "&customerKey=" + customerKey + "&typeSubtitles=" + makeVVT +"&input=" + encodeURIComponent(textPart.readText);

	// lock the text part, so it will not be loaded two times
	textPart.isLoading = true;

	var request = new XMLHttpRequest();
	request.open("POST", url, true);
	// Header information
	request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	// Handle the finishing of the request
	request.onreadystatechange = function() {
		if (request.readyState == 4) {
			var link = ___jQuery(request.responseText).text().replace(/\n/g," ").replace(/^\s*/,"");
			var regexError = link.match(/^\s*ERROR:?\s*(.*?)$/);
			//var regexMP3 = link.match(/https?\:\/\/vr.*?\.mp3/);//check if domain name includes 'vr'
			var regexMP3 = link.match(/https?\:\/\/.*?\.mp3/);
			if (regexError) { // Error message returned
				if (doRepeat == true) {
					setTimeout(function() {sendRequest(number,textPart,otherLanguage,continueRequests,downloadMP3,false)}, 300);
				} else {
					console.log('[VRWEB ERROR] RETURNED ERROR: ' + regexError[0]);
					//alert('This service is currently not available. Please try again later.'); // WARNING
					stoppedByError = true;
					self.stopPlayback();
					textPart.isLoading = false; // unlock the text part
					return;
				}
			} else if (regexMP3) { // valid MP3 file returned
				textPart.mp3 = encodeURI(regexMP3);
				if (makeVVT == 2) {
					textPart.vvt = textPart.mp3.replace("mp3", "vvt");
				} else {
					textPart.vvt = null;
				}
				textPart.isLoading = false; // unlock the text part

				// mark the mp3 file as loaded, select the next package
				// if the current package was finished
				if (continueRequests) {
					finalizeRequest(number);
				}
				// MP3 file (e.g. for downloading the complete page): Start download immediately
				if (downloadMP3) {
					downloadMp3(textPart.mp3);
				}
			} else if (link == "DEMO") { // Request is refused because the customer is just using the demo of VR Web: Discard all further TextParts
				textParts = textParts.splice(number,textParts.length-number);
				// move the cursor if it highlights a sentence that can not be read
				if (cursor > number) {
					moveCursor(number);
				}
			} else { // invalid string returned
				console.log('[VRWEB ERROR] There was no valid MP3 file returned to the request. Returned value:' + link);
				stoppedByError = true;
				self.stopPlayback();
				textPart.isLoading = false; // unlock the text part
				return;
			}
		}
	}
	// Send the content
	request.send(params);
}

/* returns the voice selected by the customer or a default voice
   for the selected language and gender */
function chooseVoice(otherLanguage) {
	var voice = getConfigValue("vrweb_voicename");
	// Invalid choice => check other parameters
	if (voice === "undefined" || voice == false || otherLanguage != null) {
		var language = otherLanguage;
		// no language selected: choose the one selected by the configuration variable
		if (!language) language = getConfigValue("vrweb_lang").toLowerCase();
		var gender = getConfigValue("vrweb_sndgender").toLowerCase();

		// Default gender: female
		if (gender === "undefined" || gender == false) {
			gender = 'f';
		}
		// Default language: German
		if (language === "undefined" || language == false) {
			language = 'de-de';
		}
		var defaultVoice = 'Petra-Ml';

		if (gender == 'm') { // prefer the male voice
			if (language in maleSpeakers) {
				voice = maleSpeakers[language][0];
			} else if (language in femaleSpeakers) {
				voice = femaleSpeakers[language][0];
			} else { // no voice for this language => default speaker
				voice = defaultVoice;
			}
		} else { // prefer the female voice
			if (language in femaleSpeakers) {
				voice = femaleSpeakers[language][0];
			} else if (language in maleSpeakers) {
				voice = maleSpeakers[language][0];
			} else { // no voice for this language => default speaker
				voice = defaultVoice;
			}
		}
	}
	return voice;
}

/* checks if a value is a numeric value between two margins */
function isValidNumericValue(value,low,high) {
	if (value === "undefined" || value == false ||
	(___jQuery.isNumeric(value) && ((high && parseInt(value) > high) ||
	(low && parseInt(value) < low)))
		) {
		return false;
	} else {
		return true;
	}
}

/****** SETTINGS ******/

/* changes the setting for the speed */
this.setSpeed = function(newValue) {
	if (isValidNumericValue(newValue, 50, 150)) {
		speed = newValue;
		removeParts();
	}
}

/* changes the setting for the pitch */
this.setPitch = function(newValue) {
	if (isValidNumericValue(newValue, 50, 150)) {
		pitch = newValue;
		removeParts();
	}
}

/* changes the highlight class */
this.setHighlightClass = function(newClass) {
	if (newClass) highlightClass = newClass;
}

}
);
