PolishWording plugin rework (#208)

* PolishWording plugin but on dev branch

* Some Changes

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
WKoA 2025-04-01 23:03:22 -04:00 committed by GitHub
parent 8ec570ebc4
commit 5a9c4c15c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 257 additions and 45 deletions

View file

@ -13,7 +13,8 @@ import {
definePluginSettings, definePluginSettings,
Settings, Settings,
} from "@api/Settings"; } from "@api/Settings";
import { Devs } from "@utils/constants"; import { makeRange } from "@components/PluginSettings/components";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const presendObject: MessageSendListener = (channelId, msg) => { const presendObject: MessageSendListener = (channelId, msg) => {
@ -21,17 +22,52 @@ const presendObject: MessageSendListener = (channelId, msg) => {
}; };
const settings = definePluginSettings({ const settings = definePluginSettings({
quickDisable: {
type: OptionType.BOOLEAN,
description: "Quick disable. Turns off message modifying without requiring a client reload.",
default: false,
},
blockedWords: { blockedWords: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Words that will not be capitalised", description: "Words that will not be capitalized (comma separated).",
default: "", default: "",
}, },
// fixApostrophes is the only one that defaults to enabled because in the version before this one,
// the other features did not exist / had a bug making them not work.
fixApostrophes: {
type: OptionType.BOOLEAN,
description: "Ensure contractions contain apostrophes.",
default: true,
},
expandContractions: {
type: OptionType.BOOLEAN,
description: "Expand contractions.",
default: false,
},
fixCapitalization: {
type: OptionType.BOOLEAN,
description: "Capitalize sentences.",
default: false,
},
fixPunctuation: {
type: OptionType.BOOLEAN,
description: "Punctate sentences.",
default: false,
},
fixPunctuationFrequency: {
type: OptionType.SLIDER,
description: "Percent period frequency (this majorly annoys some people).",
markers: makeRange(0, 100, 10),
stickToMarkers: false,
default: 100,
}
}); });
export default definePlugin({ export default definePlugin({
name: "PolishWording", name: "PolishWording",
description: "Tweaks your messages to make them look nicer and have better grammar", description: "Tweaks your messages to make them look nicer and have better grammar. See settings",
authors: [Devs.Samwich], authors: [Devs.Samwich, EquicordDevs.WKoA],
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
start: () => addMessagePreSendListener(presendObject), start: () => addMessagePreSendListener(presendObject),
stop: () => removeMessagePreSendListener(presendObject), stop: () => removeMessagePreSendListener(presendObject),
@ -39,41 +75,91 @@ export default definePlugin({
}); });
function textProcessing(input: string) { function textProcessing(input: string) {
// Quick disable, without having to reload the client
if (settings.store.quickDisable) return input;
let text = input; let text = input;
text = cap(text);
text = apostrophe(text); // Preserve code blocks
const codeBlockRegex = /```[\s\S]*?```|`[\s\S]*?`/g;
const codeBlocks: string[] = [];
text = text.replace(codeBlockRegex, match => {
codeBlocks.push(match);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
});
// Run message through formatters.
if (settings.store.fixApostrophes || settings.store.expandContractions) text = ensureApostrophe(text); // Note: if expanding contractions, fix them first.
if (settings.store.fixCapitalization) text = capitalize(text);
if (settings.store.fixPunctuation && (Math.random() * 100 < settings.store.fixPunctuationFrequency)) text = addPeriods(text);
if (settings.store.expandContractions) text = expandContractions(text);
text = text.replace(/__CODE_BLOCK_(\d+)__/g, (_, index) => codeBlocks[parseInt(index)]);
return text; return text;
} }
function apostrophe(textInput: string): string { // Injecting apostrophe as well as contraction expansion rely on this mapping
const corrected = const contractionsMap: { [key: string]: string; } = {
"wasn't, can't, don't, won't, isn't, aren't, haven't, hasn't, hadn't, doesn't, didn't, shouldn't, wouldn't, couldn't, i'm, you're, he's, she's, it's, they're, that's, who's, what's, there's, here's, how's, where's, when's, why's, let's, you'll, I'll, they'll, it'll, I've, you've, we've, they've, you'd, he'd, she'd, it'd, we'd, they'd, y'all".toLowerCase(); "wasn't": "was not",
const words: string[] = corrected.split(", "); "can't": "cannot",
const wordsInputted = textInput.split(" "); "don't": "do not",
"won't": "will not",
"isn't": "is not",
"aren't": "are not",
"haven't": "have not",
"hasn't": "has not",
"hadn't": "had not",
"doesn't": "does not",
"didn't": "did not",
"shouldn't": "should not",
"wouldn't": "would not",
"couldn't": "could not",
"that's": "that is",
"what's": "what is",
"there's": "there is",
"how's": "how is",
"where's": "where is",
"when's": "when is",
"who's": "who is",
"why's": "why is",
"you'll": "you will",
"i'll": "I will",
"they'll": "they will",
"it'll": "it will",
"i'm": "I am",
"you're": "you are",
"they're": "they are",
"he's": "he is",
"she's": "she is",
"i've": "I have",
"you've": "you have",
"we've": "we have",
"they've": "they have",
"you'd": "you would",
"he'd": "he would",
"she'd": "she would",
"it'd": "it would",
"we'd": "we would",
"they'd": "they would",
"y'all": "you all",
"here's": "here is",
};
wordsInputted.forEach(element => { const missingApostropheMap: { [key: string]: string; } = {};
words.forEach(wordelement => { for (const contraction in contractionsMap) {
if (removeApostrophes(wordelement) === element.toLowerCase()) { const withoutApostrophe = removeApostrophes(contraction.toLowerCase());
wordsInputted[wordsInputted.indexOf(element)] = restoreCap( missingApostropheMap[withoutApostrophe] = contraction;
wordelement,
getCapData(element),
);
}
});
});
return wordsInputted.join(" ");
} }
function getCapData(str: string) { function getCapData(str: string) {
const booleanArray: boolean[] = []; const booleanArray: boolean[] = [];
for (const char of str) { for (const char of str) {
if (char.match(/[a-zA-Z]/)) { // Only record capitalization for letters
booleanArray.push(char === char.toUpperCase()); booleanArray.push(char === char.toUpperCase());
} }
return booleanArray;
} }
return booleanArray;
function removeApostrophes(str: string): string {
return str.replace(/'/g, "");
} }
function restoreCap(str: string, data: boolean[]): string { function restoreCap(str: string, data: boolean[]): string {
@ -87,30 +173,152 @@ function restoreCap(str: string, data: boolean[]): string {
continue; continue;
} }
const isUppercase = data[dataIndex++]; const isUppercase = data[dataIndex];
resultString += isUppercase ? char.toUpperCase() : char.toLowerCase(); resultString += isUppercase ? char.toUpperCase() : char.toLowerCase();
// Increment index unless the data in shorter than the string, in which case we use the most recent for the rest
if (dataIndex < data.length - 1) dataIndex++;
} }
return resultString; return resultString;
} }
function cap(textInput: string): string { function ensureApostrophe(textInput: string): string {
const sentences = textInput.split(/(?<=\w\.)\s/); // This function makes sure all contractions have apostrophes
const blockedWordsArray: string[] = const potentialContractions = Object.keys(missingApostropheMap);
Settings.plugins.PolishWording.blockedWords.split(", "); if (potentialContractions.length === 0) {
return textInput; // Nothing to check if the map is empty
}
const findMissingRegex = new RegExp(
`\\b(${potentialContractions.join("|")})\\b`, // Match any of the keys as whole words
"gi" // Global (all occurrences), Case-insensitive
);
return textInput.replace(findMissingRegex, match => {
const lowerCaseMatch = match.toLowerCase();
if (Object.prototype.hasOwnProperty.call(missingApostropheMap, lowerCaseMatch)) {
const correctContraction = missingApostropheMap[lowerCaseMatch];
return restoreCap(correctContraction, getCapData(match));
}
return match;
});
}
function expandContractions(textInput: string) {
const contractionRegex = new RegExp(
`\\b(${Object.keys(contractionsMap).join("|")})\\b`,
"gi"
);
return textInput.replace(contractionRegex, match => {
const lowerCaseMatch = match.toLowerCase();
if (Object.prototype.hasOwnProperty.call(contractionsMap, lowerCaseMatch)) {
return restoreCap(contractionsMap[lowerCaseMatch], getCapData(match));
}
return match;
});
}
function removeApostrophes(str: string): string {
return str.replace(/'/g, "");
}
function capitalize(textInput: string): string {
// This one split ellipsis
// const sentenceSplitRegex = /((?<!\w\.\w.)(?<!\b[A-Z][a-z]\.)(?<![A-Z]\.)(?<=[.?!])\s+|\n+)/;
// Regex modified from several stack overflows, if you change make sure it's safe against https://devina.io/redos-checker
const sentenceSplitRegex = /((?<!\w\.\w.)(?<!\b[A-Z][a-z]\.)(?<![A-Z]\.)(?<!\.)(?<=[.?!])\s+|\n+)/;
const parts = textInput.split(sentenceSplitRegex);
const filteredParts = parts.filter(part => part !== undefined && part !== null);
const blockedWordsArray: string[] = (Settings.plugins.PolishWording.blockedWords || "")
.split(/,\s?/)
.filter(bw => bw)
.map(bw => bw.toLowerCase());
// Process alternating content and delimiters
let result = "";
for (let i = 0; i < filteredParts.length; i++) {
const element = filteredParts[i];
const isSentence = !sentenceSplitRegex.test(element); // if it matches the delimiter regex, it's a delimiter
if (isSentence) {
// Check if this is just whitespace
if (!element) continue;
else if (element.trim() === "") {
result += element;
continue;
}
// Find the first actual word character for capitalization check
const firstWordMatch = element.match(/^\s*([\w'-]+)/);
const firstWord = firstWordMatch ? firstWordMatch[1].toLowerCase() : "";
const isBlocked = firstWord ? blockedWordsArray.includes(firstWord) : false;
return sentences
.map(element => {
if ( if (
!blockedWordsArray.some(word => !isBlocked &&
element.toLowerCase().startsWith(word.toLowerCase()), !element.startsWith("http") // Don't break links
)
) { ) {
return element.charAt(0).toUpperCase() + element.slice(1); // Capitalize the first non-whitespace character (sentence splits can include newlines etc)
result += element.replace(/^(\s*)(\S)/, (match, leadingSpace, firstChar) => {
return leadingSpace + firstChar.toUpperCase();
});
} else { } else {
return element; result += element;
} }
}) } else {
.join(" "); // This a delimiter (whitespace/newline regex), so we'll add it to the string to properly reconstruct without being lossy
if (element) {
result += element;
}
}
}
// We'll fix capitalization of I's
result = result.replace(/\bi[\b']/g, "I");
return result;
}
function addPeriods(textInput: string) {
if (!textInput) {
return "";
}
const lines = textInput.split("\n");
const processedLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const strippedLine = line.trimEnd();
const urlRegex = /https?:\/\/\S+$|www\.\S+$/;
if (!strippedLine) {
if (i < lines.length - 1) {
processedLines.push("");
}
} else {
const lastChar = strippedLine.slice(-1);
if (
/[A-Za-z0-9]/.test(lastChar) && // If it doesn't already end with punctuation
!urlRegex.test(strippedLine) // If it doesn't end with a link
) {
processedLines.push(strippedLine + ".");
continue;
}
processedLines.push(strippedLine);
}
}
return processedLines.join("\n");
} }

View file

@ -1028,6 +1028,10 @@ export const EquicordDevs = Object.freeze({
name: "omaw", name: "omaw",
id: 1155026301791514655n id: 1155026301791514655n
}, },
WKoA: {
name: "WKoA",
id: 724416180097384498n
}
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly