const fs = require('fs'); const path = require('path'); const { argv } = require('process'); const config = { // replace character of key with value, any character, apart from ['.', '_', '-', ' '] 'replace': { 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', ',': '-', '&': 'And', }, 'rules': { 'underscore-before-and-after-number': true, // Will not do trailing or leading for filename 'enforce-leading-zero': true, // adds a leading zero to any number below 10 'camel-case-rules': { 'enforce-snake-case-for-filetypes': ['py'], // using underscores 'enforce-kebab-case-for-filetypes': ['css', 'html', 'scss', 'tex'], // using hyphens }, 'file-start-letter': 'lower', // lower, upper, unchanged 'directory-start-letter': 'upper', // lower, upper, unchanged 'replace-dots-with': '', // will not replace as leading character to not break dotfiles } } /** * Recursively find all files with extension in a directory * @param {string} dir The directory to search. Either absolute or relative path * @param {string} extension The file extension to look for * @param {string[]} ignoreList A list of filenames or directories to ignore * @returns {{ files: string, directories: string }} returns a list of html files with their full path */ const treeWalker = (dir, extension, ignoreList) => { const ls = fs.readdirSync(dir); const fileList = []; const dirList = []; for (let file in ls) { if (fs.statSync(path.join(dir, ls[file])).isDirectory()) { // Filter ignored directories if (ignoreList === undefined || !ignoreList.includes(ls[file])) { const newData = treeWalker(path.join(dir, ls[file]), extension, ignoreList); const newFiles = newData.files; dirList.push( path.join( dir, ls[ file ] ) ); for (let dir = 0; dir < newData.directories.length; dir++) { dirList.push( newData.directories[dir] ); } for (let file = 0; file < newFiles.length; file++) { fileList.push(newFiles[file]); } } } else if (extension == '*' || ls[file].includes(extension)) { if (ignoreList === undefined || !ignoreList.includes(ls[file])) { fileList.push(path.join(dir, ls[file])); } } } return { 'files': fileList, 'directories': dirList }; } /** * @param {string} filename The filename to fix according to the rules * @returns {string} the fixed filename */ const fixName = ( fn, ft ) => { let out = ''; const enforceSnake = config.rules['camel-case-rules']['enforce-snake-case-for-filetypes'].includes( ft ); const enforceKebab = config.rules['camel-case-rules']['enforce-kebab-case-for-filetypes'].includes( ft ); const isDir = ft === 'directory'; const startLetter = isDir ? config.rules['directory-start-letter'] : config.rules['file-start-letter']; let nextUpperCase = false; for ( let i = 0; i < fn.length; i++ ) { const c = fn[i]; if ( c == '.' ) { // Rule: Removed after number, allowed elsewhere if ( i > 0 && /[0-9]/.test( fn[ i - 1 ] ) ) { out += config.rules[ 'replace-dots-with' ]; } else { out += '.'; } } else if ( /[A-Z]/.test( c ) ) { // If we reach a capital letter and enforce either kebab-case or snake_case, we can assume that this is the start of a CamelCase word if ( enforceKebab ) { out += '-' + c.toLowerCase(); } else if ( enforceSnake ) { out += '_' + c.toLowerCase(); } else { nextUpperCase = false; if ( i == 0 && startLetter === 'lower' ) { out += c.toLowerCase(); } else { out += c; } } } else if ( c == ' ' ) { // We always replace spaces, the question is just to what if ( enforceKebab ) { out += '-'; } else if ( enforceSnake ) { out += '_'; } else { nextUpperCase = true; } } else if ( c == '_' ) { // If we are not enforcing snake_case, then replace it if ( !enforceSnake ) { if ( needsUnderscore( i, fn ) ) { out += '_'; } else if ( enforceKebab ) { out += '-'; } else { nextUpperCase = true; } } else { out += '_' } } else if ( c == '-' ) { // If we are not enforcing kebab-case if ( !enforceKebab ) { if ( enforceSnake ) { out += '_'; } else { nextUpperCase = true; } } else { out += '-' } } else { let curr = config.replace[ c ] === undefined ? c : config.replace[ c ]; if ( config.rules[ 'underscore-before-and-after-number' ] || config.rules['enforce-leading-zero'] ) { if ( /[0-9]/.test( c ) ) { if ( i < fn.length - 1 ) { if ( config.rules['enforce-leading-zero'] ) { const prevIsNumber = i > 0 && /[0-9]/.test( fn[i - 1] ); const nextIsNumber = /[0-9]/.test( fn[i + 1] ); if ( !nextIsNumber && ( i == 0|| !prevIsNumber ) ) { curr = '0' + curr; } } if ( config.rules['underscore-before-and-after-number'] ) { if ( !( /[0-9]/.test( fn[ i + 1 ] ) ) && fn[ i + 1 ] != '_' ) { curr += '_'; } } } else { if ( config.rules['enforce-leading-zero'] && ( i > 0 && !( /[0-9]/.test( fn[i - 1] ) ) ) ) { curr = '0' + curr; } } } else { if ( config.rules['underscore-before-and-after-number'] && /[0-9]/.test( fn[ i + 1 ] ) ) { curr += '_'; } } } if ( nextUpperCase || ( i == 0 && startLetter === 'upper' ) ) { nextUpperCase = false; out += curr.toUpperCase(); } else { if ( i == 0 && startLetter === 'upper' ) { out += curr.toUpperCase(); } else { out += curr.toLowerCase(); } } } } return out; } const needsUnderscore = ( i, fn ) => { return ( i > 0 && /[0-9]/.test( fn[ i - 1 ] ) ) || ( i < fn - 1 && /[0-9]/.test( fn[ i + 1 ] ) ) } const separateDirAndFileAndFiletype = ( filename ) => { const loc = filename.lastIndexOf( '/' ) + 1; let ftl = filename.lastIndexOf( '.' ) + 1; let fn = filename.substring( loc, ftl - 1 ); let ft = filename.substring( ftl ); if ( fs.statSync( filename ).isDirectory() ) { ftl = filename.length; fn = filename.substring( loc ); ft = 'directory'; } const dir = filename.slice( 0, loc - 1 ); return { 'filename': fn, 'dir': dir, 'filetype': ft }; } const fixDirName = ( directory, top ) => { if ( directory === top ) { return top; } const f = separateDirAndFileAndFiletype( directory ); return fixDirName( f.dir, top ) + '/' + fixName( f.filename, f.filetype ); } if (argv[2] == '-h') { console.log('auto-renamer [directory]\n\n=> Recursively rename files in directory'); } else if (argv[2] == '-v') { console.log('auto-renamer version 1.0.0, developed by Janis Hutz (development@janishutz.com)'); } else { // Recursively add all files in the directory const fp = path.resolve( argv[2] ); const list = treeWalker(fp, '*', ['.git', '@girs']); const files = list.files; const directories = list.directories; for (let i = 0; i < files.length; i++) { const file = files[i]; const f = separateDirAndFileAndFiletype( file ); let fixedFile = fixName( f.filename, f.filetype ); // Rename const fixedPath = f.dir + '/' + fixedFile + '.' + f.filetype; console.log( file + ' -> ' + fixedPath ); fs.renameSync( file, fixedPath ); } // Fix directory names after file names. Sort array by decending length directories.sort( ( a, b ) => { return b.length - a.length; } ); // separate directories up until we reach the path of dir started in for (let i = 0; i < directories.length; i++) { const dir = directories[i]; const fixed = fixDirName( dir, fp ); console.log( dir + ' -> ' + fixed ); fs.renameSync( dir, fixDirName( dir, fp ) ); } }