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
}
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});

View file

@ -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;
}

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){
//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(/(?<!␜)(?=\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
splitString.forEach((string) => {
@ -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");
}
}
}

View file

@ -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}`);
});
});
}

View file

@ -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{