mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-18 19:07:08 -04:00
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:
parent
8ec570ebc4
commit
5a9c4c15c2
2 changed files with 257 additions and 45 deletions
|
@ -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,43 +75,93 @@ 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) {
|
||||||
booleanArray.push(char === char.toUpperCase());
|
if (char.match(/[a-zA-Z]/)) { // Only record capitalization for letters
|
||||||
|
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 {
|
||||||
let resultString = "";
|
let resultString = "";
|
||||||
let dataIndex = 0;
|
let dataIndex = 0;
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
return sentences
|
const findMissingRegex = new RegExp(
|
||||||
.map(element => {
|
`\\b(${potentialContractions.join("|")})\\b`, // Match any of the keys as whole words
|
||||||
if (
|
"gi" // Global (all occurrences), Case-insensitive
|
||||||
!blockedWordsArray.some(word =>
|
);
|
||||||
element.toLowerCase().startsWith(word.toLowerCase()),
|
|
||||||
)
|
return textInput.replace(findMissingRegex, match => {
|
||||||
) {
|
const lowerCaseMatch = match.toLowerCase();
|
||||||
return element.charAt(0).toUpperCase() + element.slice(1);
|
|
||||||
} else {
|
if (Object.prototype.hasOwnProperty.call(missingApostropheMap, lowerCaseMatch)) {
|
||||||
return element;
|
const correctContraction = missingApostropheMap[lowerCaseMatch];
|
||||||
}
|
return restoreCap(correctContraction, getCapData(match));
|
||||||
})
|
}
|
||||||
.join(" ");
|
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;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isBlocked &&
|
||||||
|
!element.startsWith("http") // Don't break links
|
||||||
|
) {
|
||||||
|
// 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 {
|
||||||
|
result += element;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue