diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index bbe760a..06e42f4 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -142,6 +142,39 @@ class commandProcessor{ return true } + strikethrough(preprocessor){ + //splice out our command + preprocessor.commandArray.splice(0,2); + + //Mark out the current message as a spoiler + preprocessor.chatType = 'strikethrough'; + + //Make sure to throw the send flag + return true + } + + bold(preprocessor){ + //splice out our command + preprocessor.commandArray.splice(0,2); + + //Mark out the current message as a spoiler + preprocessor.chatType = 'bold'; + + //Make sure to throw the send flag + return true + } + + italics(preprocessor){ + //splice out our command + preprocessor.commandArray.splice(0,2); + + //Mark out the current message as a spoiler + preprocessor.chatType = 'italics'; + + //Make sure to throw the send flag + return true + } + async announce(preprocessor){ //Get the current channel from the database const chanDB = await channelModel.findOne({name: preprocessor.socket.chan}); diff --git a/www/css/global.css b/www/css/global.css index f6b9b6a..267de1f 100644 --- a/www/css/global.css +++ b/www/css/global.css @@ -187,15 +187,41 @@ p.tooltip, h3.tooltip{ .spoiler:not(:hover){ color: black; background-color: black; + filter: brightness(0); - img{ + .interactive, a, img, video{ color: black; background-color: black; filter: brightness(0); } +} - .interactive, a{ - color: black; - background-color: black; - } +.strikethrough{ + text-decoration: line-through; +} + +.strikethrough img, .strikethrough video, img.strikethrough, video.strikethrough{ + /* Oh yeah? Well, I'll just make my own damn strikethrough! With blackjack, and hookers! */ + filter: url('/img/strikethrough.svg#strikethroughFilter'); +} + +.bold{ + font-weight: bold; +} + +.bold img, .bold video, img.bold, video.bold{ + max-height: 14em; +} + +.italics{ + font-style: italic; +} + +.italics img, .italics video, img.italics, video.italics{ + transform: skew(-6deg); + transform-origin: 50% 100%; +} + +.qoute{ + font-family: monospace; } \ No newline at end of file diff --git a/www/img/strikethrough.svg b/www/img/strikethrough.svg new file mode 100644 index 0000000..7c41d20 --- /dev/null +++ b/www/img/strikethrough.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 898ad9f..920e4e5 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -19,16 +19,20 @@ class chatPostprocessor{ } postprocess(chatEntry, rawData){ + //Create empty array to hold filter spans + this.filterSpans = []; + //Set raw message data this.rawData = rawData; //Set current chat nodes this.chatEntry = chatEntry; this.chatBody = this.chatEntry.querySelector(".chat-entry-body"); - this.filterSpans = []; - //Split the chat message into an array of objects representing each word + //Split the chat message into an array of objects representing each word/chunk this.splitMessage(); - //Inject links into un-processed placeholders + this.processQoute(); + + //Re-Hydrate and Inject links and embedded media into un-processed placeholders this.processLinks(); //Inject clickable command examples @@ -40,15 +44,24 @@ class chatPostprocessor{ //Inject clickable usernames this.processUsernames(); - //Inject whitespace into un-processed words + //Detect inline spoilers + this.processSpoilers(); + + //Detect inline strikethrough + this.processStrikethrough(); + + //Detect inline bold text + this.processBold(); + + //Detect inline italics + this.processItalics(); + + //Inject whitespace into long ass-words this.addWhitespace(); //Handle non-standard chat types this.handleChatType(); - - //Process spoilers - this.processSpoilers(); - + //Inject the pre-processed chat into the chatEntry node this.injectBody(); @@ -64,8 +77,9 @@ class chatPostprocessor{ //This also means all text should be added to element via textContent and *NOT* innerHTML //I'd rather not do this, but pre-processing everything while preserving codes is a fucking nightmare //Split string by word-boundries on words and non-word boundries around whitespace, with negative lookaheads to exclude file seperators so we don't split link placeholders + //Also split by any invisble whitespace as a crutch to handle mushed links/emotes //If we can one day figure out how to split non-repeating special chars instead of special chars with whitespace, that would be perf, unfortunately my brain hasn't rotted enough to understand regex like that just yet. - const splitString = utils.unescapeEntities(this.rawData.msg).split(/(? { @@ -90,6 +104,10 @@ class chatPostprocessor{ if(wordObj.type == 'word'){ //Create span node const span = document.createElement('span'); + + //Set span filter classes + span.classList.add(...wordObj.filterClasses); + //Set span text span.textContent = wordObj.string; @@ -98,7 +116,7 @@ class chatPostprocessor{ }else if(wordObj.type == 'link'){ //Create a link node from our link const link = document.createElement('a'); - link.classList.add('chat-link'); + link.classList.add('chat-link', ...wordObj.filterClasses); link.href = wordObj.link; //Use textContent to be safe since links can't be escaped serverside link.textContent = wordObj.link; @@ -108,7 +126,7 @@ class chatPostprocessor{ }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.classList.add('chat-dead-link', 'danger-link', ...wordObj.filterClasses); badLink.href = wordObj.link; //Use textContent to be safe since links can't be escaped serverside badLink.textContent = wordObj.link; @@ -118,7 +136,7 @@ class chatPostprocessor{ }else if(wordObj.type == 'malformedLink'){ //Create a text span node from our link const malformedLink = document.createElement('span'); - malformedLink.classList.add('chat-malformed-link'); + malformedLink.classList.add('chat-malformed-link', ...wordObj.filterClasses); //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; @@ -128,7 +146,7 @@ class chatPostprocessor{ }else if(wordObj.type == 'image'){ //Create an img node from our link const img = document.createElement('img'); - img.classList.add('chat-img'); + img.classList.add('chat-img', ...wordObj.filterClasses); img.src = wordObj.link; //Look for an emote by link since emotes are tx'd as bare links @@ -145,7 +163,7 @@ class chatPostprocessor{ }else if(wordObj.type == 'video'){ //Create a video node from our link const vid = document.createElement('video'); - vid.classList.add('chat-video'); + vid.classList.add('chat-video', ...wordObj.filterClasses); vid.src = wordObj.link; vid.controls = false; vid.autoplay = true; @@ -166,7 +184,7 @@ class chatPostprocessor{ //Create link node const link = document.createElement('a'); //Set class - link.classList.add('chat-link'); + link.classList.add('chat-link', ...wordObj.filterClasses); //Set href and inner text link.href = "javascript:"; link.textContent = wordObj.command; @@ -180,7 +198,7 @@ class chatPostprocessor{ //Create link node const link = document.createElement('a'); //set class - link.classList.add(wordObj.color); + link.classList.add(wordObj.color, ...wordObj.filterClasses); //Set href and inner text link.href = "javascript:"; link.textContent = wordObj.string; @@ -194,7 +212,7 @@ class chatPostprocessor{ //Create link node const link = document.createElement('a'); //set class - link.classList.add('chat-link'); + link.classList.add('chat-link', ...wordObj.filterClasses); //Set href and inner text link.href = `/c/${wordObj.chan}`; link.textContent = wordObj.string; @@ -266,6 +284,13 @@ class chatPostprocessor{ } } + processQoute(){ + //If the message starts off with '>' + if(this.messageArray[0].string[0] == '>'){ + this.chatBody.classList.add("qoute"); + } + } + processCommandExamples(){ //for each word object in the body this.messageArray.forEach((wordObj, wordIndex) => { @@ -374,39 +399,47 @@ class chatPostprocessor{ }); } - processSpoilers(){ + processFilter(delimiter, cb){ //Create empty array to hold spoilers (keep this seperate at first for internal function use) - const foundSpoilers = []; + const foundFilters = []; //Spoiler detection stage //For each word object in the message array - main: for(let wordIndex in this.messageArray){ + main: for(let wordIndex = 0; wordIndex < this.messageArray.length; wordIndex++){ //Get the current word object const wordObj = this.messageArray[wordIndex]; //If its a regular word and contains '##' - if(wordObj.type == 'word' && wordObj.string.match('##')){ + if(wordObj.type == 'word' && wordObj.string.match(utils.escapeRegex(delimiter))){ //Crawl through detected spoilers - for(let spoiler of foundSpoilers){ + for(let spoiler of foundFilters){ //If the current word object is part of a detected spoiler - if(wordIndex == spoiler.delimiters[0] || wordIndex == spoiler.delimiters[1]){ + if(wordIndex == spoiler[0] || wordIndex == spoiler[1]){ //ignore it and continue on to the next word object continue main; } } - //Crawl throw word objects after the current one, not sure why wordIndex is saved as a string.. Thanks JS - for(let endIndex = (Number(wordIndex) + 1); endIndex < this.messageArray.length; endIndex++){ + //Crawl throw word objects after the current one + for(let endIndex = (wordIndex + 1); endIndex < this.messageArray.length; endIndex++){ //Get the current end object const endObj = this.messageArray[endIndex]; //If its a regular word and contains '##' - if(endObj.type == 'word' && endObj.string.match('##')){ + if(endObj.type == 'word' && endObj.string.match(utils.escapeRegex(delimiter))){ + //Setup the found filter array + const foundFilter = [wordIndex, endIndex]; + //Scrape out delimiters - wordObj.string = wordObj.string.replaceAll("##",''); - endObj.string = endObj.string.replaceAll("##",''); - //Add it to the list of detected spoilers, skipping out the delimiters - foundSpoilers.push({class: "spoiler", index: [Number(wordIndex) + 1, endIndex - 1], delimiters: [Number(wordIndex), endIndex]}); + wordObj.string = wordObj.string.replaceAll(delimiter,''); + endObj.string = endObj.string.replaceAll(delimiter,''); + + //Add it to the list of detected filters + foundFilters.push(foundFilter); + + //Run the filter callback + cb(foundFilter) + //Break the nested end-detection loop break; } @@ -414,8 +447,42 @@ class chatPostprocessor{ } } - //Add found spoilers to filters list - this.filterSpans = this.filterSpans.concat(foundSpoilers); + return foundFilters; + } + + processSpoilers(){ + //Process spoilers using '##' delimiter + this.processFilter('##', (foundSpoiler)=>{ + //For each found spoiler add it to the list of found filter spans + this.filterSpans.push({class: "spoiler", index: [foundSpoiler[0] + 1, foundSpoiler[1] - 1], delimiters: [foundSpoiler[0], foundSpoiler[1]]}); + }); + } + + processStrikethrough(){ + //Process strikethrough's using '~~' delimiter + this.processFilter('~~', (foundStrikethrough)=>{ + for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){ + this.messageArray[wordIndex].filterClasses.push("strikethrough"); + } + }) + } + + processBold(){ + //Process strong text using '*' delimiter + this.processFilter('**', (foundStrikethrough)=>{ + for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){ + this.messageArray[wordIndex].filterClasses.push("bold"); + } + }) + } + + processItalics(){ + //Process italics using '__' delimiter + this.processFilter('*', (foundStrikethrough)=>{ + for(let wordIndex = foundStrikethrough[0]; wordIndex < foundStrikethrough[1]; wordIndex++){ + this.messageArray[wordIndex].filterClasses.push("italics"); + } + }) } processLinks(){ @@ -437,7 +504,8 @@ class chatPostprocessor{ //but we also don't want to clobber any surrounding whitespace string: wordObj.string.replace(`␜${linkIndex}`, '␜'), link: link.link, - type: link.type + type: link.type, + filterClasses: [] } } }) @@ -482,6 +550,15 @@ class chatPostprocessor{ }else if(this.rawData.type == "spoiler"){ //Set whole-body spoiler this.chatBody.classList.add("spoiler"); + }else if(this.rawData.type == "strikethrough"){ + //Set whole-body spoiler + this.chatBody.classList.add("strikethrough"); + }else if(this.rawData.type == "bold"){ + //Set whole-body spoiler + this.chatBody.classList.add("bold"); + }else if(this.rawData.type == "italics"){ + //Set whole-body spoiler + this.chatBody.classList.add("italics"); } } } \ No newline at end of file diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index a189760..49f9fc4 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -78,8 +78,8 @@ class commandPreprocessor{ Object.keys(this.emotes).forEach((key) => { //For each emote in the current list this.emotes[key].forEach((emote) => { - //Inject emote links into the message, add invisible whitespace to the end to keep next character from mushing into the link - this.message = this.message.replaceAll(`[${emote.name}]`, `${emote.link}ㅤ`); + //Inject emote links into the message, pad with invisible whitespace to keep link from getting mushed + this.message = this.message.replaceAll(`[${emote.name}]`, `ㅤ${emote.link}ㅤ`); }); }); } diff --git a/www/js/utils.js b/www/js/utils.js index 12661f1..64250f5 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -27,6 +27,14 @@ class canopyUtils{ //Grab text content and send that shit out return outNode.documentElement.textContent; } + + escapeRegex(string){ + /* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid + In my defense I only did it because browser-devs are taking fucking eons to implement RegExp.escape() + This should be replaced once that function becomes available in mainline versions of firefox/chromium: + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */ + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } } class canopyUXUtils{