From bf5964718d1e1187f17a4b18380513c893f719ed Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 20:35:56 +0200 Subject: [PATCH 1/8] feat: text censor (types file) --- src/pages/tools/string/censor/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/pages/tools/string/censor/types.ts diff --git a/src/pages/tools/string/censor/types.ts b/src/pages/tools/string/censor/types.ts new file mode 100644 index 0000000..aaca65b --- /dev/null +++ b/src/pages/tools/string/censor/types.ts @@ -0,0 +1,7 @@ +export type InitialValuesType = { + wordsToCensor: string; + censoredBySymbol: boolean; + censorSymbol: string; + eachLetter: boolean; + censorWord: string; +}; From 3cebda10917a0f60a41be476f6a5c70ed942c6cb Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 20:36:52 +0200 Subject: [PATCH 2/8] feat: text censor (service file) --- src/pages/tools/string/censor/service.ts | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/pages/tools/string/censor/service.ts diff --git a/src/pages/tools/string/censor/service.ts b/src/pages/tools/string/censor/service.ts new file mode 100644 index 0000000..660cdcc --- /dev/null +++ b/src/pages/tools/string/censor/service.ts @@ -0,0 +1,49 @@ +import { InitialValuesType } from './types'; + +export function censorText(input: string, options: InitialValuesType): string { + if (!input) return ''; + if (!options.wordsToCensor) return input; + + if (options.censoredBySymbol && !isSymbol(options.censorSymbol[0])) { + throw new Error('Enter a valid censor symbol (non-alphanumeric or emoji)'); + } + + // Split text into words and punctuation using Unicode-aware regex + const regex = /([\s\p{P}])/gu; + const textWords = input.split(regex); + + // Normalize censor words (trim and lowercase) + const wordsToCensor = options.wordsToCensor + .split('\n') + .map((word) => word.trim().toLowerCase()) + .filter((word) => word.length > 0); + + const censoredText = textWords + .map((word) => { + const lowerWord = word.toLowerCase(); + if (wordsToCensor.includes(lowerWord)) { + if (options.censoredBySymbol) { + return options.eachLetter + ? options.censorSymbol.repeat(word.length) + : options.censorSymbol; + } else { + return options.censorWord; + } + } + return word; + }) + .join(''); + + return censoredText; +} + +/** + * Determines if a character is a symbol or emoji. + * Accepts single characters that are non-letter and non-number, + * or extended pictographic characters (like emojis). + */ +function isSymbol(input: string): boolean { + return ( + /^[^\p{L}\p{N}]+$/u.test(input) || /\p{Extended_Pictographic}/u.test(input) + ); +} From ec33f2f7f4c9e300a024c084510d08f2b9d406c3 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 20:37:13 +0200 Subject: [PATCH 3/8] feat: text censor (index file) --- src/pages/tools/string/censor/index.tsx | 158 ++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/pages/tools/string/censor/index.tsx diff --git a/src/pages/tools/string/censor/index.tsx b/src/pages/tools/string/censor/index.tsx new file mode 100644 index 0000000..6167477 --- /dev/null +++ b/src/pages/tools/string/censor/index.tsx @@ -0,0 +1,158 @@ +import { Box } from '@mui/material'; +import { useState } from 'react'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { censorText } from './service'; +import ToolTextInput from '@components/input/ToolTextInput'; +import { InitialValuesType } from './types'; +import ToolContent from '@components/ToolContent'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { ToolComponentProps } from '@tools/defineTool'; +import TextFieldWithDesc from '@components/options/TextFieldWithDesc'; +import SelectWithDesc from '@components/options/SelectWithDesc'; +import CheckboxWithDesc from '@components/options/CheckboxWithDesc'; + +const initialValues: InitialValuesType = { + wordsToCensor: '', + censoredBySymbol: true, + censorSymbol: '█', + eachLetter: true, + censorWord: 'CENSORED' +}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Censor a Word in a Quote', + description: `In this example, we hide the unpleasant word "idiot" from Jim Rohn's quote. We specify this word in the words-to-censor option and mask it with a neat smiling face character "☺".`, + sampleText: + 'Motivation alone is not enough. If you have an idiot and you motivate him, now you have a motivated idiot. Jim Rohn', + sampleResult: + 'Motivation alone is not enough. If you have an ☺ and you motivate him, now you have a motivated ☺. Jim Rohn', + sampleOptions: { + ...initialValues, + wordsToCensor: 'idiot', + censorSymbol: '☺', + eachLetter: false + } + }, + { + title: 'Censor an Excerpt', + description: `In this example, we censor multiple words from an excerpt from the novel "The Guns of Avalon" by Roger Zelazny. To do this, we write out all unnecessary words in the multi-line text option and select the "Use a Symbol to Censor" censoring mode. We activate the "Mask Each Letter" option so that in place of each word exactly as many block characters "█" appeared as there are letters in that word.`, + sampleText: + '“In the mirrors of the many judgments, my hands are the color of blood. I sometimes fancy myself an evil which exists to oppose other evils; and on that great Day of which the prophets speak but in which they do not truly believe, on the day the world is utterly cleansed of evil, then I too will go down into darkness, swallowing curses. Until then, I will not wash my hands nor let them hang useless.” ― Roger Zelazny, The Guns of Avalon', + sampleResult: + '“In the mirrors of the many judgments, my hands are the color of █████. I sometimes fancy myself an ████ which exists to oppose other █████; and on that great Day of which the prophets speak but in which they do not truly believe, on the day the world is utterly cleansed of ████, then I too will go down into ████████, swallowing ██████. Until then, I will not wash my hands nor let them hang useless.” ― Roger Zelazny, The Guns of Avalon', + sampleOptions: { + ...initialValues, + wordsToCensor: 'blood\nevil\ndarkness\ncurses', + eachLetter: true + } + }, + { + title: "Censor Agent's Name", + description: `In this example, we hide the name of an undercover FBI agent. We replace two words at once (first name and last name) with the code name "Agent 007"`, + sampleText: + 'My name is John and I am an undercover FBI agent. I usually write my name in lowercase as "john" because I find uppercase letters scary. Unfortunately, in documents, my name is properly capitalized as John and it makes me upset.', + sampleResult: + 'My name is Agent 007 and I am an undercover FBI agent. I usually write my name in lowercase as "Agent 007" because I find uppercase letters scary. Unfortunately, in documents, my name is properly capitalized as Agent 007 and it makes me upset.', + sampleOptions: { + ...initialValues, + censoredBySymbol: false, + wordsToCensor: 'john', + censorWord: 'Agent 007' + } + } +]; + +export default function CensorText({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + function compute(initialValues: InitialValuesType, input: string) { + setResult(censorText(input, initialValues)); + } + + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + title: 'Words to Censor', + component: ( + + updateField('wordsToCensor', val)} + description={`Specify all unwanted words that + you want to hide in text`} + /> + + ) + }, + { + title: 'Censor Mode', + component: ( + + updateField('censoredBySymbol', value)} + description={'Select the censoring mode.'} + /> + + {values.censoredBySymbol && ( + updateField('censorSymbol', val)} + description={`A symbol, character, or pattern to use for censoring.`} + /> + )} + + {values.censoredBySymbol && ( + updateField('eachLetter', value)} + title="Mask each letter" + description="Put a masking symbol in place of each letter of the censored word." + /> + )} + + {!values.censoredBySymbol && ( + updateField('censorWord', val)} + description={`Replace all censored words with this word.`} + /> + )} + + ) + } + ]; + + return ( + + } + resultComponent={ + + } + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + exampleCards={exampleCards} + /> + ); +} From 21741be5af024681644fe53cfe79e29a6be8c357 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 20:44:41 +0200 Subject: [PATCH 4/8] feat: text censor (meta file) --- src/pages/tools/string/censor/meta.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/pages/tools/string/censor/meta.ts diff --git a/src/pages/tools/string/censor/meta.ts b/src/pages/tools/string/censor/meta.ts new file mode 100644 index 0000000..2678d19 --- /dev/null +++ b/src/pages/tools/string/censor/meta.ts @@ -0,0 +1,16 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('string', { + name: 'Text Censor', + path: 'censor', + shortDescription: + 'Quickly mask bad words or replace them with alternative words.', + icon: 'mingcute:hashtag-fill', + description: + "utility for censoring words in text. Load your text in the input form on the left, specify all the bad words in the options, and you'll instantly get censored text in the output area.", + longDescription: + 'With this online tool, you can censor certain words in any text. You can specify a list of unwanted words (such as swear words or secret words) and the program will replace them with alternative words and create a safe-to-read text. The words can be specified in a multi-line text field in the options by entering one word per line.', + keywords: ['text', 'censor', 'words', 'characters'], + component: lazy(() => import('./index')) +}); From 2509878d4276f15d8a329db5c9153220a662cf92 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 20:45:21 +0200 Subject: [PATCH 5/8] feat: text censor (added to string tool) --- src/pages/tools/string/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/string/index.ts b/src/pages/tools/string/index.ts index e7cecd4..e4979fc 100644 --- a/src/pages/tools/string/index.ts +++ b/src/pages/tools/string/index.ts @@ -16,6 +16,7 @@ import { tool as stringRepeat } from './repeat/meta'; import { tool as stringTruncate } from './truncate/meta'; import { tool as stringBase64 } from './base64/meta'; import { tool as stringStatistic } from './statistic/meta'; +import { tool as stringCensor } from './censor/meta'; export const stringTools = [ stringSplit, @@ -35,5 +36,6 @@ export const stringTools = [ stringRotate, stringRot13, stringBase64, - stringStatistic + stringStatistic, + stringCensor ]; From f7028f868397004d5e4bbd9e5393c229274026b7 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 21:01:38 +0200 Subject: [PATCH 6/8] feat: text censor (icon changed) --- src/pages/tools/string/censor/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/string/censor/meta.ts b/src/pages/tools/string/censor/meta.ts index 2678d19..0a7e6d0 100644 --- a/src/pages/tools/string/censor/meta.ts +++ b/src/pages/tools/string/censor/meta.ts @@ -6,7 +6,7 @@ export const tool = defineTool('string', { path: 'censor', shortDescription: 'Quickly mask bad words or replace them with alternative words.', - icon: 'mingcute:hashtag-fill', + icon: 'hugeicons:text-footnote', description: "utility for censoring words in text. Load your text in the input form on the left, specify all the bad words in the options, and you'll instantly get censored text in the output area.", longDescription: From eff16d69a8fca45e36e41c56049a2290460cb37c Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 21:02:55 +0200 Subject: [PATCH 7/8] feat: text censor (service updated to match censor patter instead of spliting input) --- src/pages/tools/string/censor/service.ts | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/pages/tools/string/censor/service.ts b/src/pages/tools/string/censor/service.ts index 660cdcc..7aa4b95 100644 --- a/src/pages/tools/string/censor/service.ts +++ b/src/pages/tools/string/censor/service.ts @@ -4,46 +4,46 @@ export function censorText(input: string, options: InitialValuesType): string { if (!input) return ''; if (!options.wordsToCensor) return input; - if (options.censoredBySymbol && !isSymbol(options.censorSymbol[0])) { + if (options.censoredBySymbol && !isSymbol(options.censorSymbol)) { throw new Error('Enter a valid censor symbol (non-alphanumeric or emoji)'); } - // Split text into words and punctuation using Unicode-aware regex - const regex = /([\s\p{P}])/gu; - const textWords = input.split(regex); - - // Normalize censor words (trim and lowercase) const wordsToCensor = options.wordsToCensor .split('\n') - .map((word) => word.trim().toLowerCase()) + .map((word) => word.trim()) .filter((word) => word.length > 0); - const censoredText = textWords - .map((word) => { - const lowerWord = word.toLowerCase(); - if (wordsToCensor.includes(lowerWord)) { - if (options.censoredBySymbol) { - return options.eachLetter - ? options.censorSymbol.repeat(word.length) - : options.censorSymbol; - } else { - return options.censorWord; - } - } - return word; - }) - .join(''); + let censoredText = input; + + for (const word of wordsToCensor) { + const escapedWord = escapeRegex(word); + const pattern = new RegExp(`\\b${escapedWord}\\b`, 'giu'); + + const replacement = options.censoredBySymbol + ? options.eachLetter + ? options.censorSymbol.repeat(word.length) + : options.censorSymbol + : options.censorWord; + + censoredText = censoredText.replace(pattern, replacement); + } return censoredText; } /** - * Determines if a character is a symbol or emoji. - * Accepts single characters that are non-letter and non-number, - * or extended pictographic characters (like emojis). + * Escapes RegExp special characters in a string + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Determines if a string is a valid symbol or emoji (multi-codepoint supported). */ function isSymbol(input: string): boolean { return ( - /^[^\p{L}\p{N}]+$/u.test(input) || /\p{Extended_Pictographic}/u.test(input) + /^[^\p{L}\p{N}]+$/u.test(input) || // Not a letter or number + /\p{Extended_Pictographic}/u.test(input) // Emoji or pictographic symbol ); } From d0e7bbbfd5ebf61bbae48ef4d4e62824bc6c4cab Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Tue, 8 Jul 2025 21:03:52 +0200 Subject: [PATCH 8/8] feat: text censor (tip added to "wordsToCensor" input ) --- src/pages/tools/string/censor/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/string/censor/index.tsx b/src/pages/tools/string/censor/index.tsx index 6167477..3f6bb60 100644 --- a/src/pages/tools/string/censor/index.tsx +++ b/src/pages/tools/string/censor/index.tsx @@ -89,7 +89,7 @@ export default function CensorText({ value={values.wordsToCensor} onOwnChange={(val) => updateField('wordsToCensor', val)} description={`Specify all unwanted words that - you want to hide in text`} + you want to hide in text (separated by a new line)`} /> )