security-words-picker
Version:
A robust and versatile package for selecting, filtering, and managing word lists with advanced security features.
332 lines (291 loc) • 11.7 kB
JavaScript
const fs = require('fs');
const path = require('path');
/**
* Retrieves a specified number of words based on optional constraints.
*
* @param {Object} options - An object containing optional parameters.
* @param {number} amountOfWords - Number of words to retrieve.
* @param {Array<string>} [customWordsArray] - (Optional) Custom array of words to use instead of words.txt or words.js.
* @param {function} [customErrorHandler] - (Optional) Custom function to handle errors.
* @returns {Array|string} - An array or string of words matching the specified criteria.
*/
function getWords(options = {}, amountOfWords, customWordsArray = null, customErrorHandler = null) {
// Validate 'amountOfWords' parameter
if (typeof amountOfWords !== 'number' || amountOfWords <= 0) {
throw new Error("'amountOfWords' must be a positive number.");
}
// Load words from words.txt if available, else use words.js, else use customWordsArray or throw error
let defaultWords = [];
const wordsTxtPath = path.join(__dirname, 'words', 'words.txt');
const wordsJsPath = path.join(__dirname, 'words', 'words.js');
try {
if (customWordsArray && Array.isArray(customWordsArray)) {
defaultWords = customWordsArray;
console.log(`Using customWordsArray with ${defaultWords.length} words.`);
} else if (fs.existsSync(wordsTxtPath)) {
const data = fs.readFileSync(wordsTxtPath, 'utf8');
// Split by newlines and remove any surrounding quotes and commas
defaultWords = data.split('\n').map(line => line.trim().replace(/^["']|["']$/g, '')).filter(line => line.length > 0);
console.log(`Loaded ${defaultWords.length} words from words.txt`);
} else if (fs.existsSync(wordsJsPath)) {
defaultWords = require(wordsJsPath);
if (!Array.isArray(defaultWords)) {
throw new Error('words.js must export an array of words.');
}
console.log(`Loaded ${defaultWords.length} words from words.js`);
} else {
throw new Error('No words.txt or words.js found in the /words directory, and no customWordsArray provided.');
}
} catch (err) {
if (customErrorHandler && typeof customErrorHandler === 'function') {
customErrorHandler(err);
} else {
console.error(`Error loading words: ${err.message}`);
process.exit(1);
}
}
// Destructure options with default values
const {
lengthMin,
lengthMax,
fixLength,
reverse = false,
asString = false,
sort,
caseOption,
filterStartsWith,
filterEndsWith,
excludeSubstrings,
blacklist,
whitelist,
excludeAmbiguous = false,
pattern,
phoneticDistinct = true,
includeMetadata = false,
history = new Set(),
allowNumbers = false,
allowSpecialChars = false,
uniqueCharacters = false,
maxRepeatLetters,
excludeWordsWithRepeatingLetters = false,
minConsonants,
minVowels,
customFilter
} = options;
// Early return if amountOfWords is zero
if (amountOfWords === 0) return asString ? "" : [];
// Initialize Sets for blacklist and whitelist for O(1) lookups
const blacklistSet = blacklist ? new Set(blacklist.map(word => word.toLowerCase())) : null;
const whitelistSet = whitelist ? new Set(whitelist.map(word => word.toLowerCase())) : null;
// Characters considered ambiguous
const ambiguousChars = /[l1I0O]/i;
// Function to calculate entropy (simple estimation based on word length and uniqueness)
const calculateEntropy = (word) => {
if (customEntropyCalculator && typeof customEntropyCalculator === 'function') {
return customEntropyCalculator(word);
}
// Simple entropy calculation: number of unique characters
const uniqueChars = new Set(word.toLowerCase()).size;
return uniqueChars * Math.log2(26);
};
// Function to check phonetic distinctness (simple implementation using Soundex)
const soundex = (word) => {
const a = word.toLowerCase().split('');
const f = a.shift();
const codes = {
a: '', e: '', i: '', o: '', u: '',
b: '1', f: '1', p: '1', v: '1',
c: '2', g: '2', j: '2', k: '2', q: '2', s: '2', x: '2', z: '2',
d: '3', t: '3',
l: '4',
m: '5', n: '5',
r: '6'
};
const soundexArr = [f.toUpperCase()];
let prev = '';
a.forEach(char => {
const code = codes[char];
if (code && code !== prev) {
soundexArr.push(code);
prev = code;
}
});
// Pad with zeros or truncate to ensure length 4
while (soundexArr.length < 4) soundexArr.push('0');
return soundexArr.slice(0, 4).join('');
};
let phoneticMap = {};
// Start filtering
let filteredWords = defaultWords.filter(word => {
const lowerWord = word.toLowerCase();
// Apply whitelist: if whitelist is present, only include those words
if (whitelistSet && !whitelistSet.has(lowerWord)) {
return false;
}
// Apply blacklist
if (blacklistSet && blacklistSet.has(lowerWord)) {
return false;
}
// Apply length filters
if (fixLength !== undefined && word.length !== fixLength) {
return false;
}
if (lengthMin !== undefined && word.length < lengthMin) {
return false;
}
if (lengthMax !== undefined && word.length > lengthMax) {
return false;
}
// Exclude words with ambiguous characters
if (excludeAmbiguous && ambiguousChars.test(word)) {
return false;
}
// Apply filterStartsWith
if (filterStartsWith && Array.isArray(filterStartsWith) && filterStartsWith.length > 0) {
const startsWithMatch = filterStartsWith.some(prefix => word.toLowerCase().startsWith(prefix.toLowerCase()));
if (!startsWithMatch) return false;
}
// Apply filterEndsWith
if (filterEndsWith && Array.isArray(filterEndsWith) && filterEndsWith.length > 0) {
const endsWithMatch = filterEndsWith.some(suffix => word.toLowerCase().endsWith(suffix.toLowerCase()));
if (!endsWithMatch) return false;
}
// Apply excludeSubstrings
if (excludeSubstrings && Array.isArray(excludeSubstrings) && excludeSubstrings.length > 0) {
const hasExcludedSub = excludeSubstrings.some(sub => word.toLowerCase().includes(sub.toLowerCase()));
if (hasExcludedSub) return false;
}
// Apply pattern
if (pattern instanceof RegExp && !pattern.test(word)) {
return false;
}
// Exclude words already in history to ensure uniqueness across sessions
if (history.has(lowerWord)) {
return false;
}
// Apply uniqueCharacters
if (uniqueCharacters) {
const uniqueChars = new Set(word.toLowerCase()).size;
if (uniqueChars !== word.length) {
return false;
}
}
// Apply maxRepeatLetters
if (maxRepeatLetters !== undefined && maxRepeatLetters > 0) {
const letterCounts = {};
for (let char of word.toLowerCase()) {
letterCounts[char] = (letterCounts[char] || 0) + 1;
if (letterCounts[char] > maxRepeatLetters) {
return false;
}
}
}
// Apply allowNumbers and allowSpecialChars
if (!allowNumbers && /\d/.test(word)) {
return false;
}
if (!allowSpecialChars && /[^a-zA-Z]/.test(word)) {
return false;
}
// Apply customFilter
if (customFilter && typeof customFilter === 'function') {
if (!customFilter(word)) {
return false;
}
}
// All filters passed
return true;
});
// Apply phonetic distinctness
if (phoneticDistinct) {
filteredWords = filteredWords.filter(word => {
const s = soundex(word);
if (phoneticMap[s]) {
return false;
}
phoneticMap[s] = true;
return true;
});
}
// Apply weighted selection
if (options.weightedSelection && typeof options.weightedSelection === 'object') {
const weightedWords = [];
filteredWords.forEach(word => {
const weight = options.weightedSelection[word.toLowerCase()] || 1;
for (let i = 0; i < weight; i++) {
weightedWords.push(word);
}
});
filteredWords = weightedWords;
}
// Apply custom shuffle
const shuffle = (array) => {
if (options.customShuffle && typeof options.customShuffle === 'function') {
options.customShuffle(array);
return;
}
// Fisher-Yates Shuffle with simple randomness
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
shuffle(filteredWords);
// Apply sort if required
if (sort === 'asc') {
filteredWords.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
} else if (sort === 'desc') {
filteredWords.sort((a, b) => b.toLowerCase().localeCompare(a.toLowerCase()));
}
// Select the desired number of words
let selectedWords = filteredWords.slice(0, amountOfWords);
// Apply 'reverse' if needed
if (reverse) {
selectedWords = selectedWords.reverse();
}
// Apply 'caseOption' if needed
if (caseOption === 'upper') {
selectedWords = selectedWords.map(word => word.toUpperCase());
} else if (caseOption === 'lower') {
selectedWords = selectedWords.map(word => word.toLowerCase());
} else if (caseOption === 'capitalize') {
selectedWords = selectedWords.map(word => word.charAt(0).toUpperCase() + word.slice(1));
}
// Update history to include selected words
selectedWords.forEach(word => history.add(word.toLowerCase()));
// Return as string if required
if (asString) {
return selectedWords.join(', ');
}
// Return as array, possibly with metadata
if (includeMetadata) {
return selectedWords.map(word => ({
word: word,
length: word.length,
entropy: calculateEntropy(word),
// Add more metadata as needed
}));
}
return selectedWords;
}
// Helper function to calculate Scrabble score
function calculateScrabbleScore(word) {
const scores = {
a: 1, b: 3, c: 3, d: 2, e: 1, f: 4, g: 2, h: 4, i: 1,
j: 8, k: 5, l: 1, m: 3, n: 1, o: 1, p: 3, q: 10,
r: 1, s: 1, t: 1, u: 1, v: 4, w: 4, x: 8, y: 4,
z: 10
};
return word.toLowerCase().split('').reduce((acc, char) => acc + (scores[char] || 0), 0);
}
// Export the functions for use in other files
module.exports = {
getWords,
pickWords: (options = {}) => {
const { count, ...restOptions } = options;
if (!count || typeof count !== 'number' || count <= 0) {
throw new Error("'count' must be a positive number.");
}
return getWords(restOptions, count);
}
};