diff --git a/scripts/auto-renamer b/scripts/auto-renamer new file mode 100755 index 0000000..a86644c --- /dev/null +++ b/scripts/auto-renamer @@ -0,0 +1,5 @@ +#!/bin/sh + +SCRIPT_DIR=$(dirname "$0") + +node "$SCRIPT_DIR/util/auto-renamer/index.js" $@ diff --git a/scripts/util/auto-renamer/index.js b/scripts/util/auto-renamer/index.js new file mode 100644 index 0000000..e51f6f0 --- /dev/null +++ b/scripts/util/auto-renamer/index.js @@ -0,0 +1,237 @@ +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 ) ); + } +}