import fs from 'fs'; import path from 'path'; /** * Contains args of the program. * @type {Array} */ const ARGS = process.argv.slice(2); /** * Get a specific arg by his name. * @param {string} name * @param {*} fallback * @return {*} */ function getArg(name, fallback) { const index = ARGS.indexOf(`--${name}`); return index !== -1 ? ARGS[index + 1] : fallback; } const PREFIX = getArg('prefix', '\\$' ); const INPUT_DIRS = getArg('input', './resources/css').split(','); const OUTPUT_FILE = getArg('output', './extra.less'); const EXCLUDE = getArg('exclude', '*.min.css,app.css').split(','); /** * Recursively collect CSS files in a folder. * @param {string} dir * @returns {string[]} */ function getCssFiles(dir){ if(!fs.existsSync(dir)) return []; const RESULTS = []; for( const ENTRY of fs.readdirSync(dir, { withFileTypes: true }) ){ const PATH = path.join(dir, ENTRY.name); if(ENTRY.isDirectory()){ RESULTS.push(...getCssFiles(PATH)); } else if( ENTRY.isFile() && ENTRY.name.endsWith('.css')){ const ENT_EXCLUDE = EXCLUDE.some(pattern => { const re = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); return re.test(ENTRY.name); }); if (!ENT_EXCLUDE) RESULTS.push(PATH); } } return RESULTS; } /** * @param {string} part * @param {string} prefix * @return {string} */ function prefixSelectorPart(part, prefix){ return part.replace( /(? `.${prefix}${className}` ); } /** * Prefix all classes. * @param {string} selector * @param {string} prefix * @returns {string} */ function prefixClasses(selector, prefix){ return selector.split(',').map(part => prefixSelectorPart(part.trim(), prefix)).join(', '); } function css( css, prefix ){ const LINES = css.split('\n'); const OUT = []; let inKeyFrames = false; let braceDepth = 0; let pendingSelector = ''; for(let i = 0; i < LINES.length; i++){ const LINE = LINES[i]; const L = LINE.trim(); if( L === '' || L.startsWith('//') || L.startsWith('*') ){ OUT.push(LINE); continue; } if( L.startsWith('@import') || L.startsWith('@source') || L.startsWith('@theme') ) // Ignore imports. continue; if( L.match(/^@keyframes\s/) ){ inKeyFrames = true; OUT.push(LINE); continue; } if( inKeyFrames ){ const OPEN_COUNT = (LINE.match(/\{/g) || []).length; const CLOSE_COUNT = (LINE.match(/\}/g) || []).length; OUT.push(LINE); braceDepth += OPEN_COUNT - CLOSE_COUNT; if( braceDepth <= 0 ){ inKeyFrames = false; braceDepth = 0; } } if(L.startsWith('@media') || L.startsWith('@supports') || L.startsWith('@layer')){ OUT.push(LINE); continue; } if( L.endsWith('{') ){ const SELECTOR = L.slice(0,-1).trim(); if( SELECTOR.startsWith('@')){ OUT.push(SELECTOR); continue; } const PREFIXED = prefixClasses(SELECTOR,prefix); OUT.push(LINE.replace(SELECTOR,PREFIXED)); continue; } OUT.push(LINE); } return OUT.join('\n'); } console.log(`CSS → XenForo extra.less`); console.log(`Prefix : .${PREFIX}`); console.log(`Sources : ${INPUT_DIRS.join(', ')}`); console.log(`Output : ${OUTPUT_FILE}\n`); const FILES = INPUT_DIRS.flatMap(dir => getCssFiles(dir.trim())); if (FILES.length === 0) { console.error( "No files found in this directory."); process.exit(1); } console.log( `${FILES.length} files found: ` ); FILES.forEach(f => console.log(` - ${f}`)); const PARTS = []; for( const F of FILES){ const REL_PATH = path.relative(process.cwd(), F); const CONTENT = fs.readFileSync(F, 'utf-8'); const NEW = css(CONTENT, PREFIX); PARTS.push(`/* File: ${REL_PATH} */`); PARTS.push(NEW); PARTS.push(''); } const FINAL = PARTS.join('\n'); fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); fs.writeFileSync(OUTPUT_FILE, FINAL, 'utf-8'); console.log( `File generated: ${OUTPUT_FILE}.`);