Skip to content

Instantly share code, notes, and snippets.

@rodrigocfd
Last active December 15, 2024 23:25
Show Gist options
  • Select an option

  • Save rodrigocfd/59d6e09ae50f7f005a6eb43b1a63a0d6 to your computer and use it in GitHub Desktop.

Select an option

Save rodrigocfd/59d6e09ae50f7f005a6eb43b1a63a0d6 to your computer and use it in GitHub Desktop.
Convert spaces to tabs
/**
* Converts spaces to tab on all files within a folder, recursively.
* Rodrigo Dias <rcesar@gmail.com>
* Friday, December 13, 2024.
*/
const fs = require('fs');
const path = require('path');
const skipDir = ['.git', 'node_modules'];
const NC = '\x1b[0m';
const RED = '\x1b[31m';
const GRE = '\x1b[32m';
const BLU = '\x1b[34m';
const target = process.argv[2];
if (!target) {
console.log('Usage: node tabify.js FOLDER [--crlf]\n');
console.log('Options:');
console.log(' --crlf Uses CR/LF for linebreaks; default is LF.');
process.exit(1);
} else if (!fs.lstatSync(target).isDirectory()) {
console.log(`${RED}Not a folder: ${target}${NC}`);
process.exit(1);
}
const useCrLf = process.argv[3] === '--crlf';
let numSubFolders = 0;
let numFiles = 0;
let numFilesDone = 0;
recursivePass(target);
console.log(`${BLU}Summary:${NC}`)
console.log(`${GRE} Found ${numFiles} file(s) and ${numSubFolders} subfolder(s).${NC}`);
console.log(`${GRE} Tabs replaced in ${numFilesDone} file(s).${NC}`);
/**
* @param {string} target
*/
function recursivePass(target) {
if (target.endsWith('/') || target.endsWith('\\'))
target = target.substring(0, target.length - 1); // remove trailing slash
fs.readdirSync(target, {}).forEach(name => {
const thisPath = target + path.sep + name;
if (fs.lstatSync(thisPath).isDirectory()) {
if (skipDir.indexOf(name) === -1) {
++numSubFolders;
recursivePass(thisPath);
}
} else {
++numFiles;
if (processFile(thisPath)) ++numFilesDone;
}
});
}
/**
* @param {string} filePath
* @returns {boolean}
*/
function processFile(filePath) {
const lines = fs.readFileSync(filePath, {encoding: 'utf8'}).split(/\r?\n/g);
let wasTreated = false;
if (!fileHasTabs(lines)) {
const indents = getIndents(lines);
if (indents.length) {
const lowestIndent = findLowestIndent(indents);
swapSpacesWithTabs(lines, indents, lowestIndent);
wasTreated = true;
}
}
const newText = lines.join(useCrLf ? '\r\n' : '\n');
fs.writeFileSync(filePath, newText);
return wasTreated;
}
/**
* @param {string[]} lines
* @returns {boolean}
*/
function fileHasTabs(lines) {
for (const line of lines) {
if (line.startsWith('\t'))
return true;
}
return false;
}
/**
* @param {string[]} lines
* @returns {{line: number; spaces: number}[]}
*/
function getIndents(lines) {
const indents = [];
for (let i = 0; i < lines.length; ++i) {
if (lines[i].startsWith(' ')) {
indents.push({
line: i,
spaces: (line => {
let c = 0;
while (line.charAt(c) === ' ') ++c;
return c;
})(lines[i]),
});
}
}
return indents;
}
/**
* @param {{line: number; spaces: number}[]} indents
* @returns {number}
*/
function findLowestIndent(indents) {
let lowest = Number.MAX_VALUE;
for (const indent of indents) {
if (indent.spaces < lowest)
lowest = indent.spaces;
}
return lowest;
}
/**
* @param {string[]} lines
* @param {{line: number; spaces: number}[]} indents
* @param {number} numSpaces
*/
function swapSpacesWithTabs(lines, indents, numSpaces) {
for (const indent of indents) {
const numTabs = (indent.spaces - (indent.spaces % numSpaces)) / numSpaces;
lines[indent.line] = '\t'.repeat(numTabs)
+ lines[indent.line].substring(numTabs * numSpaces); // in-place modification
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment