Finished up with inline and full-body chat-filters.

This commit is contained in:
rainbow napkin 2025-01-11 13:00:34 -05:00
parent 77bc549653
commit 4c31dbde1e
6 changed files with 198 additions and 40 deletions

View file

@ -142,6 +142,39 @@ class commandProcessor{
return true 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){ async announce(preprocessor){
//Get the current channel from the database //Get the current channel from the database
const chanDB = await channelModel.findOne({name: preprocessor.socket.chan}); const chanDB = await channelModel.findOne({name: preprocessor.socket.chan});

View file

@ -187,15 +187,41 @@ p.tooltip, h3.tooltip{
.spoiler:not(:hover){ .spoiler:not(:hover){
color: black; color: black;
background-color: black; background-color: black;
filter: brightness(0);
img{ .interactive, a, img, video{
color: black; color: black;
background-color: black; background-color: black;
filter: brightness(0); filter: brightness(0);
} }
}
.interactive, a{ .strikethrough{
color: black; text-decoration: line-through;
background-color: black;
} }
.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;
} }

14
www/img/strikethrough.svg Normal file
View file

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="strikethroughFilter">
<feFlood
result="floodFill"
x="0"
y="50%"
width="100%"
height="1"
flood-color="black"
flood-opacity="1"
/>
<feBlend in="floodFill" in2="SourceGraphic" mode="normal" />
</filter>
</svg>

After

Width:  |  Height:  |  Size: 376 B

View file

@ -19,16 +19,20 @@ class chatPostprocessor{
} }
postprocess(chatEntry, rawData){ postprocess(chatEntry, rawData){
//Create empty array to hold filter spans
this.filterSpans = [];
//Set raw message data
this.rawData = rawData; this.rawData = rawData;
//Set current chat nodes //Set current chat nodes
this.chatEntry = chatEntry; this.chatEntry = chatEntry;
this.chatBody = this.chatEntry.querySelector(".chat-entry-body"); 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(); this.splitMessage();
//Inject links into un-processed placeholders this.processQoute();
//Re-Hydrate and Inject links and embedded media into un-processed placeholders
this.processLinks(); this.processLinks();
//Inject clickable command examples //Inject clickable command examples
@ -40,15 +44,24 @@ class chatPostprocessor{
//Inject clickable usernames //Inject clickable usernames
this.processUsernames(); 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(); this.addWhitespace();
//Handle non-standard chat types //Handle non-standard chat types
this.handleChatType(); this.handleChatType();
//Process spoilers
this.processSpoilers();
//Inject the pre-processed chat into the chatEntry node //Inject the pre-processed chat into the chatEntry node
this.injectBody(); this.injectBody();
@ -64,8 +77,9 @@ class chatPostprocessor{
//This also means all text should be added to element via textContent and *NOT* innerHTML //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 //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 //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. //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(/(?<!␜)(?=\w)\b|(?<=\w)\b|(?=\s)\B|(?<=\s)\B/g); const splitString = utils.unescapeEntities(this.rawData.msg).split(/(?<!␜)(?=\w)\b|(?<=\w)\b|(?=\s)\B|(?<=\s)\B|/g);
//for each word in the splitstring //for each word in the splitstring
splitString.forEach((string) => { splitString.forEach((string) => {
@ -90,6 +104,10 @@ class chatPostprocessor{
if(wordObj.type == 'word'){ if(wordObj.type == 'word'){
//Create span node //Create span node
const span = document.createElement('span'); const span = document.createElement('span');
//Set span filter classes
span.classList.add(...wordObj.filterClasses);
//Set span text //Set span text
span.textContent = wordObj.string; span.textContent = wordObj.string;
@ -98,7 +116,7 @@ class chatPostprocessor{
}else if(wordObj.type == 'link'){ }else if(wordObj.type == 'link'){
//Create a link node from our link //Create a link node from our link
const link = document.createElement('a'); const link = document.createElement('a');
link.classList.add('chat-link'); link.classList.add('chat-link', ...wordObj.filterClasses);
link.href = wordObj.link; link.href = wordObj.link;
//Use textContent to be safe since links can't be escaped serverside //Use textContent to be safe since links can't be escaped serverside
link.textContent = wordObj.link; link.textContent = wordObj.link;
@ -108,7 +126,7 @@ class chatPostprocessor{
}else if(wordObj.type == 'deadLink'){ }else if(wordObj.type == 'deadLink'){
//Create a text span node from our link //Create a text span node from our link
const badLink = document.createElement('a'); 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; badLink.href = wordObj.link;
//Use textContent to be safe since links can't be escaped serverside //Use textContent to be safe since links can't be escaped serverside
badLink.textContent = wordObj.link; badLink.textContent = wordObj.link;
@ -118,7 +136,7 @@ class chatPostprocessor{
}else if(wordObj.type == 'malformedLink'){ }else if(wordObj.type == 'malformedLink'){
//Create a text span node from our link //Create a text span node from our link
const malformedLink = document.createElement('span'); 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) //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 //arguably we could sanatize malformed links serverside since they're never actually used as links
malformedLink.textContent = wordObj.link; malformedLink.textContent = wordObj.link;
@ -128,7 +146,7 @@ class chatPostprocessor{
}else if(wordObj.type == 'image'){ }else if(wordObj.type == 'image'){
//Create an img node from our link //Create an img node from our link
const img = document.createElement('img'); const img = document.createElement('img');
img.classList.add('chat-img'); img.classList.add('chat-img', ...wordObj.filterClasses);
img.src = wordObj.link; img.src = wordObj.link;
//Look for an emote by link since emotes are tx'd as bare links //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'){ }else if(wordObj.type == 'video'){
//Create a video node from our link //Create a video node from our link
const vid = document.createElement('video'); const vid = document.createElement('video');
vid.classList.add('chat-video'); vid.classList.add('chat-video', ...wordObj.filterClasses);
vid.src = wordObj.link; vid.src = wordObj.link;
vid.controls = false; vid.controls = false;
vid.autoplay = true; vid.autoplay = true;
@ -166,7 +184,7 @@ class chatPostprocessor{
//Create link node //Create link node
const link = document.createElement('a'); const link = document.createElement('a');
//Set class //Set class
link.classList.add('chat-link'); link.classList.add('chat-link', ...wordObj.filterClasses);
//Set href and inner text //Set href and inner text
link.href = "javascript:"; link.href = "javascript:";
link.textContent = wordObj.command; link.textContent = wordObj.command;
@ -180,7 +198,7 @@ class chatPostprocessor{
//Create link node //Create link node
const link = document.createElement('a'); const link = document.createElement('a');
//set class //set class
link.classList.add(wordObj.color); link.classList.add(wordObj.color, ...wordObj.filterClasses);
//Set href and inner text //Set href and inner text
link.href = "javascript:"; link.href = "javascript:";
link.textContent = wordObj.string; link.textContent = wordObj.string;
@ -194,7 +212,7 @@ class chatPostprocessor{
//Create link node //Create link node
const link = document.createElement('a'); const link = document.createElement('a');
//set class //set class
link.classList.add('chat-link'); link.classList.add('chat-link', ...wordObj.filterClasses);
//Set href and inner text //Set href and inner text
link.href = `/c/${wordObj.chan}`; link.href = `/c/${wordObj.chan}`;
link.textContent = wordObj.string; 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(){ processCommandExamples(){
//for each word object in the body //for each word object in the body
this.messageArray.forEach((wordObj, wordIndex) => { 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) //Create empty array to hold spoilers (keep this seperate at first for internal function use)
const foundSpoilers = []; const foundFilters = [];
//Spoiler detection stage //Spoiler detection stage
//For each word object in the message array //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 //Get the current word object
const wordObj = this.messageArray[wordIndex]; const wordObj = this.messageArray[wordIndex];
//If its a regular word and contains '##' //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 //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 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 //ignore it and continue on to the next word object
continue main; continue main;
} }
} }
//Crawl throw word objects after the current one, not sure why wordIndex is saved as a string.. Thanks JS //Crawl throw word objects after the current one
for(let endIndex = (Number(wordIndex) + 1); endIndex < this.messageArray.length; endIndex++){ for(let endIndex = (wordIndex + 1); endIndex < this.messageArray.length; endIndex++){
//Get the current end object //Get the current end object
const endObj = this.messageArray[endIndex]; const endObj = this.messageArray[endIndex];
//If its a regular word and contains '##' //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 //Scrape out delimiters
wordObj.string = wordObj.string.replaceAll("##",''); wordObj.string = wordObj.string.replaceAll(delimiter,'');
endObj.string = endObj.string.replaceAll("##",''); endObj.string = endObj.string.replaceAll(delimiter,'');
//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]}); //Add it to the list of detected filters
foundFilters.push(foundFilter);
//Run the filter callback
cb(foundFilter)
//Break the nested end-detection loop //Break the nested end-detection loop
break; break;
} }
@ -414,8 +447,42 @@ class chatPostprocessor{
} }
} }
//Add found spoilers to filters list return foundFilters;
this.filterSpans = this.filterSpans.concat(foundSpoilers); }
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(){ processLinks(){
@ -437,7 +504,8 @@ class chatPostprocessor{
//but we also don't want to clobber any surrounding whitespace //but we also don't want to clobber any surrounding whitespace
string: wordObj.string.replace(`${linkIndex}`, '␜'), string: wordObj.string.replace(`${linkIndex}`, '␜'),
link: link.link, link: link.link,
type: link.type type: link.type,
filterClasses: []
} }
} }
}) })
@ -482,6 +550,15 @@ class chatPostprocessor{
}else if(this.rawData.type == "spoiler"){ }else if(this.rawData.type == "spoiler"){
//Set whole-body spoiler //Set whole-body spoiler
this.chatBody.classList.add("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");
} }
} }
} }

View file

@ -78,8 +78,8 @@ class commandPreprocessor{
Object.keys(this.emotes).forEach((key) => { Object.keys(this.emotes).forEach((key) => {
//For each emote in the current list //For each emote in the current list
this.emotes[key].forEach((emote) => { 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 //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}`); this.message = this.message.replaceAll(`[${emote.name}]`, `${emote.link}`);
}); });
}); });
} }

View file

@ -27,6 +27,14 @@ class canopyUtils{
//Grab text content and send that shit out //Grab text content and send that shit out
return outNode.documentElement.textContent; 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{ class canopyUXUtils{