@coders4achange/blmjs
Version:
Replace insensitive words in a repository
397 lines (340 loc) • 11.2 kB
JavaScript
const fs = require("fs").promises;
const fsSync = require("fs");
const path = require("path");
const readline = require("readline");
const prompt = require("prompt");
const _ = require("lodash");
const { promisify } = require("util");
const getPromptAsync = promisify(prompt.get);
/**
* This class enables to replace words which have an inherrent bias.
*/
class BLM {
/**
* Initialise BLM constructor
*/
constructor() {
this.filePaths = [];
this.wordsFilePath = "./lib/words.json";
this.path_ = null;
this.didReplace = false;
this.wordsObj = {};
}
/**
* Recursively traverse a directory and return file paths.
*
* @param {*} dir Directory to traverse
*/
async _traverse(dir) {
const files = await fs.readdir(dir);
let filePaths = [];
for (const file of files) {
const fPath = path.resolve(dir, file);
const fsStat = await fs.stat(fPath);
if (fsStat.isDirectory()) {
const paths = await this._traverse(fPath);
filePaths = filePaths.concat(paths);
} else {
filePaths.push(fPath);
}
}
return filePaths;
}
/**
* Read file line by line and return all lines
*
* @param {*} fPath File path.
* @param {*} filter Filter lines with words present.
*/
async _getLinesFromFile(fPath, filter = false) {
const fileStream = fsSync.createReadStream(fPath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
const lines = [];
for await (const line of rl) {
if (!filter) {
lines.push(line);
} else {
if (this.replaceWords.some((w) => line.includes(w))) {
lines.push(line);
}
}
}
return lines;
}
/**
* Replace old word in line with new word
*
* Maintain capitalization of word
*
* @param {*} line The line to replace
* @param {*} oldWord Old word present in line
* @param {*} newWord New word to replace with
* @return {*} line with replaced word
*/
_replaceWordInLine(line, oldWord, newWord) {
this.didReplace = true;
const reg = new RegExp("(" + oldWord + ")", "ig");
return line.replace(reg, (match) => {
if (match[0].match(/[A-Z]/)) {
newWord = _.toUpper(newWord);
}
return newWord;
});
}
/**
* Interactive prompt to replace words in a line.
* @param {*} line
* @param {*} wordsInLine
*/
async _replaceWordsWithPrompt(line, wordsInLine) {
for (const w of wordsInLine) {
console.log(`\nWord [${w}] is present in line: [${line}]`);
console.log(
`Following replacement options are available: [${_.join(
this.wordsObj[w],
)}]`,
);
const property = {
name: "option",
type: "integer",
message: `Which word do you want to replace with? [0-${
this.wordsObj[w].length - 1
}]`,
validator: /[0-9]*/,
warning: "Respond with a number",
default: -1,
};
const result = await getPromptAsync(property);
if (result.option >= 0 && result.option < this.wordsObj[w].length) {
line = this._replaceWordInLine(
line,
w,
this.wordsObj[w][result.option],
);
} else {
console.log("Invalid option, ignoring replacement...");
}
}
return line;
}
/**
* Replace words in a line without an interactive prompt
*
* @param {*} line
* @param {*} wordsInLine
* @return {*} line with replaced word
*/
_replaceWordsWithoutPrompt(line, wordsInLine) {
_.forEach(wordsInLine, (w) => {
line = this._replaceWordInLine(line, w, this.wordsObj[w][0]);
});
return line;
}
/**
* Replace words in a line based on words file
*
* @param {*} line The line in the file
* @param {*} ignorePrompt Whether to take user input for every replacement.
*/
async _replaceWords(line, ignorePrompt = true) {
const wordsInLine = _.filter(this.replaceWords, (w) =>
line.includes(w),
);
if (!this.replaceWords.some((w) => line.includes(w))) {
return line;
}
if (ignorePrompt === true) {
return this._replaceWordsWithoutPrompt(line, wordsInLine);
}
return this._replaceWordsWithPrompt(line, wordsInLine);
}
/**
* Setup words file inside class instance.
*/
async _setupWordsFile() {
const wordsFile = await fs.readFile(this.wordsFilePath, "utf-8");
this.wordsObj = JSON.parse(wordsFile);
this.replaceWords = _.keys(this.wordsObj);
}
/**
* Setup file paths from dir/file received from user.
*/
async _setupFilePaths() {
const stat = fsSync.statSync(this.path_);
this.filePaths = [];
if (stat.isDirectory()) {
this.filePaths = this.filePaths.concat(
await this._traverse(this.path_),
);
} else {
this.filePaths = [this.path_];
}
}
/**
* Replace words in give file path, either interactively or not.
*
* @param {*} fPath
* @param {*} ignorePrompt
*/
async _replaceWordsInFPath(fPath, ignorePrompt = true) {
let lines = await this._getLinesFromFile(fPath);
lines = lines.map((line) => this._replaceWords(line, ignorePrompt));
lines = await Promise.all(lines);
const newFileContent = _.join(lines, "\n");
if (this.didReplace === true) {
await fs.writeFile(fPath, _.join(lines, "\n"));
} else {
console.log(`Words not found in file. ${fPath}`);
}
return newFileContent;
}
/**
* Use a specific words file.
*
* @param {*} wordsFile The file to use which contains the replacement word.
* @return {*} class instance
*/
use(wordsFile) {
this.wordsFilePath = wordsFile;
return this;
}
/**
* Path on which tool will run.
*
* @param {*} path
* @return {*} class instance
*/
with(path) {
if (path === null) {
throw new Error("Path cannot be null");
}
this.path_ = path;
return this;
}
/**
* Replace words in a specific file path
*
* - Load file and get lines from file path
* - Replace words in file
* - If any replacement is made, write new file in same location
*
* @param {*} fPath Path of the file
* @param {*} ignorePrompt Whether prompt is shown while replacement is done
* @return {*} replaced file contents
*/
async replace() {
await this._setupWordsFile();
await this._setupFilePaths();
const newFileContents = [];
for (const fPath of this.filePaths) {
newFileContents.push(await this._replaceWordsInFPath(fPath, true));
}
return newFileContents;
}
/**
* Replace words in file interactively
*/
async replaceInteractive() {
await this._setupWordsFile();
await this._setupFilePaths();
const property = {
name: "yesno",
message: "Do you want replace words in file?",
validator: /y[es]*|n[o]?/,
warning: "Must respond yes or no",
default: "no",
};
prompt.start({ noHandleSIGINT: true });
console.log(this.filePaths);
for (const fPath of this.filePaths) {
console.log(`\nProcessing file ${fPath}`);
let result = "no";
try {
result = await getPromptAsync(property);
} catch (err) {
console.log(`\nTerminating... ${err}`);
return;
}
if (/y[es]*/.test(result.yesno)) {
await this._replaceWordsInFPath(fPath, false);
} else {
console.log("\nIgnoring...");
}
}
}
/**
* Print summary of inappropriate words in given file paths.
*
* Dumps summary.json.
*/
async printSummary() {
await this._setupWordsFile();
await this._setupFilePaths();
const dumpObj = {};
for (const fPath of this.filePaths) {
const lines = await this._getLinesFromFile(fPath);
const wordCount = {};
_.forEach(this.replaceWords, (w) => {
wordCount[w] = 0;
});
wordCount["totalCount"] = 0;
for (const w of this.replaceWords) {
for (const line of lines) {
if (_.lowerCase(line).includes(w)) {
if (!_.keys(dumpObj).includes(fPath)) {
dumpObj[fPath] = [];
}
wordCount[w]++;
}
}
}
for (const w in wordCount) {
if (wordCount[w] == 0) {
delete wordCount[w];
}
}
if (_.some(_.values(wordCount), (cnt) => cnt > 0)) {
wordCount["totalCount"] = _.sum(_.values(wordCount));
dumpObj[fPath] = wordCount;
}
}
await fs.writeFile("summary.json", JSON.stringify(dumpObj), "utf8");
console.log(dumpObj);
return dumpObj;
}
/**
* Dump results.json with detailed view of inappriate words on every line.
*
* @param {*} verbose Whether to show the actual line in the dumped file.
*/
async dumpJSON(verbose = false) {
await this._setupWordsFile();
await this._setupFilePaths();
const dumpObj = {};
for (const fPath of this.filePaths) {
const lines = await this._getLinesFromFile(fPath);
for (const w of this.replaceWords) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (_.lowerCase(line).includes(w)) {
if (!_.keys(dumpObj).includes(fPath)) {
dumpObj[fPath] = [];
}
dumpObj[fPath].push({
lineno: i,
word: w,
replacements: this.wordsObj[w],
line: verbose ? line : null,
});
}
}
}
}
await fs.writeFile("results.json", JSON.stringify(dumpObj), "utf8");
return dumpObj;
}
}
module.exports = { BLM: new BLM() };