UNPKG

node-profanity-filter

Version:

A Node.js profanity filter with support for exact matching, obfuscated word detection, and customizable replacement.

136 lines 5.16 kB
import { flaggedWords } from "./data/words-list.js"; import { Trie } from "./structures/trie.js"; export class Filter { words; wordBoundaries; parseObfuscated; replaceWith; static WORD_REGEX = /\b[\w@!$-]+\b/gi; static charMap = { '@': 'a', '$': 's', '1': 'i', "0": 'o' }; constructor({ wordBoundaries = false, parseObfuscated = true, replaceWith = '', disableDefaultList = false, excludeWords = [], includeWords = [] } = {}) { this.wordBoundaries = wordBoundaries; this.parseObfuscated = parseObfuscated; this.replaceWith = replaceWith; const baseWords = disableDefaultList ? [] : flaggedWords.filter(w => !excludeWords.includes(w)); const combinedWords = [...baseWords, ...includeWords]; this.words = new Trie(combinedWords); } /** * Helper method to normalize obfuscated text * @param text * @param maxRepeats the max num of repeats allowed for a char * @returns */ normalizeObfuscated(text, maxRepeats = 1) { let result = ''; let lastChar = ''; let repeatCount = 0; for (let i = 0; i < text.length; i++) { const c = text[i].toLowerCase(); const mapped = Filter.charMap[c] ?? c; if (mapped === lastChar) { if (++repeatCount <= maxRepeats) result += mapped; } else { lastChar = mapped; repeatCount = 1; result += mapped; } } return result; } /** * Whether the string contains profane words * @param string * @param wordBoundaries match whole words (defaults to config) * @returns */ isProfane(text, wordBoundaries = this.wordBoundaries) { if (wordBoundaries) { return (text.match(Filter.WORD_REGEX) || []).some(w => { const normalized = this.parseObfuscated ? this.normalizeObfuscated(w) : w.toLowerCase(); return this.words.contains(normalized); }); } const normalized = this.parseObfuscated ? this.normalizeObfuscated(text) : text.toLowerCase(); return this.words.containsIn(normalized.replace(/[^a-z0-9]/g, '')); } /** * Replace detected words * @param text * @param replaceWith word used for replacing ( defaults to config ) * @param wordBoundaries match whole words (defaults to config) * @returns */ sanitize(text, replaceWith = this.replaceWith, wordBoundaries = this.wordBoundaries) { if (wordBoundaries) { let result = ''; let lastIndex = 0; let match; while ((match = Filter.WORD_REGEX.exec(text)) !== null) { result += text.slice(lastIndex, match.index); const word = match[0]; const normalizedWord = this.parseObfuscated ? this.normalizeObfuscated(word) : word.toLowerCase(); result += this.words.contains(normalizedWord) ? replaceWith : word; lastIndex = Filter.WORD_REGEX.lastIndex; } result += text.slice(lastIndex); return result; } const normalized = this.parseObfuscated ? this.normalizeObfuscated(text) : text; const lower = normalized.toLowerCase(); let result = ''; let i = 0; while (i < text.length) { const matchLen = this.words.matchLengthAt(lower, i); if (matchLen > 0) { result += replaceWith.length === 1 ? replaceWith.repeat(matchLen) : replaceWith; i += matchLen; } else { result += text[i]; i++; } } return result; } /** * Get a list of all matched words and their position * @param text * @param wordBoundaries match whole words (defaults to config) * @returns */ getMatches(text, wordBoundaries = this.wordBoundaries) { const matches = []; if (wordBoundaries) { const words = text.match(/\b[\w@!$-]+\b/gi) || []; let offset = 0; for (const w of words) { const start = text.indexOf(w, offset); offset = start + w.length; const normalized = this.parseObfuscated ? this.normalizeObfuscated(w) : w.toLowerCase(); if (this.words.contains(normalized)) { matches.push({ word: w, start, end: start + w.length }); } } } else { const normalizedText = this.parseObfuscated ? this.normalizeObfuscated(text) : text.toLowerCase(); for (let i = 0; i < normalizedText.length; i++) { const len = this.words.matchLengthAt(normalizedText, i); if (len > 0) { matches.push({ word: text.slice(i, i + len), start: i, end: i + len }); i += len - 1; } } } return matches; } } //# sourceMappingURL=filter.js.map