class chatPostprocessor{ constructor(client){ this.client = client; } preprocess(chatEntry, rawData){ this.rawData = rawData; //Set current chat nodes this.chatEntry = chatEntry; this.chatBody = this.chatEntry.querySelector(".chat-entry-body"); //Split the chat message into an array of objects representing each word this.splitMessage(); //Inject links into un-processed placeholders this.processLinks(); //Inject whitespace into un-processed words this.addWhitespace(); //Handle non-standard chat types this.handleChatType(); //Inject the pre-processed chat into the chatEntry node this.injectBody(); //Return the pre-processed node return this.chatEntry; } splitMessage(){ //Create an empty array to hold the body this.messageArray = []; //Split string by word-boundries, with negative lookaheads to exclude file seperators so we don't split link placeholders const splitString = this.rawData.msg.split(/(? { //create a word object const wordObj = { string: string, type: "word" } //Add it to our body array this.messageArray.push(wordObj); }); } injectBody(){ //Create an empty array to hold the objects to inject const injectionArray = [""]; const _this = this; //For each word object this.messageArray.forEach((wordObj) => { if(wordObj.type == 'word'){ //Inject current wordObj string into the chat body injectString(wordObj.string); }else if(wordObj.type == 'link'){ //Create a link node from our link const link = document.createElement('a'); link.classList.add('chat-link'); link.href = wordObj.link; //Use textContent to be safe since links can't be escaped serverside link.textContent = wordObj.link; //Append node to chatBody injectNode(wordObj, link); }else if(wordObj.type == 'deadLink'){ //Create a text span node from our link const badLink = document.createElement('a'); badLink.classList.add('chat-dead-link', 'danger-link'); badLink.href = wordObj.link; //Use textContent to be safe since links can't be escaped serverside badLink.textContent = wordObj.link; //Append node to chatBody injectNode(wordObj, badLink); }else if(wordObj.type == 'malformedLink'){ //Create a text span node from our link const malformedLink = document.createElement('span'); malformedLink.classList.add('chat-malformed-link'); //Use textContent to be safe since links can't be escaped (this is why we don't just add it using injectString) //arguably we could sanatize malformed links serverside since they're never actually used as links malformedLink.textContent = wordObj.link; //Append node to chatBody injectNode(wordObj, malformedLink); }else if(wordObj.type == 'image'){ //Create an img node from our link const img = document.createElement('img'); img.classList.add('chat-img'); img.src = wordObj.link; //Look for an emote by link since emotes are tx'd as bare links const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link); //If this is a known emote if(emote != null){ //Set the hover text to the emote's name img.title = `[${emote.name}]`; } //Append node to chatBody injectNode(wordObj, img); }else if(wordObj.type == 'video'){ //Create a video node from our link const vid = document.createElement('video'); vid.classList.add('chat-video'); vid.src = wordObj.link; vid.controls = false; vid.autoplay = true; vid.loop = true; vid.muted = true; //Look for an emote by link since emotes are tx'd as bare links const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link); //If this is a known emote if(emote != null){ //Set the hover text to the emote's name vid.title = `[${emote.name}]`; } injectNode(wordObj, vid); } }); //For each item found in the injection array injectionArray.forEach((item) => { //if it's a string if(typeof item == "string"){ //Add it to the chat's innerHTML (it should already be escaped by the server) this.chatBody.innerHTML += item; //Otherwise it should be a DOM node }else{ //Append the node to our chat body this.chatBody.appendChild(item); } }) //Like string.replace except it actually injects the node so we can keep things like event handlers function injectNode(wordObj, node, placeholder = '␜'){ //Split string by the placeholder so we can keep surrounding whitespace const splitWord = wordObj.string.split(placeholder, 2); //Append the first half of the string injectString(splitWord[0]); //Append the node injectionArray.push(node); //Append the second half of the string injectString(splitWord[1]); } function injectString(string){ //If the last item was a string if(typeof injectionArray[injectionArray.length - 1] == "string"){ //add the word string on to the end of the string injectionArray[injectionArray.length - 1] += string; }else{ //Pop the string at the end of the array injectionArray.push(string); } } } addWhitespace(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { //if the word object hasn't been pre-processed elsewhere if(wordObj.type == "word"){ //Create an empty array to hold our word var wordArray = []; //For each character in the string of the current word object this.messageArray[wordIndex].string.split("").forEach((char, charIndex) => { //push the current character to the wordArray wordArray.push(char); //After eight characters if(charIndex > 8){ //Push an invisible line-break character between every character wordArray.push("ㅤ"); } }); //Join the wordArray into a single string, and use it to set the current wordObject's string this.messageArray[wordIndex].string = wordArray.join(""); } }); } processLinks(){ //If we don't have links if(this.rawData.links == null){ //Don't bother return; } //For every link received in this message this.rawData.links.forEach((link, linkIndex) => { //For every word obj in the message array this.messageArray.forEach((wordObj, wordIndex) => { //Check current wordobj for link (placeholder may contain whitespace with it) if(wordObj.string.match(`␜${linkIndex}`)){ //Set current word object in the body array to the new link object this.messageArray[wordIndex] = { //Don't want to use a numbered placeholder to make this easier during body injection //but we also don't want to clobber any surrounding whitespace string: wordObj.string.replace(`␜${linkIndex}`, '␜'), link: link.link, type: link.type } } }) }); } handleChatType(){ if(this.rawData.type == "whisper"){ //add whisper class this.chatBody.classList.add('whisper'); }else if(this.rawData.type == "announcement"){ //Squash the high-level this.chatEntry.querySelector('.high-level').remove(); //Get the username and make it into an announcement title (little hacky but this *IS* postprocessing) const userNode = this.chatEntry.querySelector('.chat-entry-username'); userNode.innerHTML = `${userNode.innerHTML.slice(0,-2)} Announcement`; //Add/remove relevant classes userNode.classList.remove('chat-entry-username'); userNode.classList.add('announcement-title'); this.chatBody.classList.add('announcement-body'); this.chatEntry.classList.add('announcement'); }else if(this.rawData.type == "toke"){ //Squash the high-level this.chatEntry.querySelector('.high-level').remove(); //remove the username this.chatEntry.querySelector('.chat-entry-username').remove(); //Add toke/tokewhisper class this.chatBody.classList.add("toke"); }else if(this.rawData.type == "tokewhisper"){ //Squash the high-level this.chatEntry.querySelector('.high-level').remove(); //remove the username this.chatEntry.querySelector('.chat-entry-username').remove(); //Add toke/tokewhisper class this.chatBody.classList.add("tokewhisper","serverwhisper"); } } }