Initial commit
This commit is contained in:
171
scripts/XenForoCSSGenerator.js
Normal file
171
scripts/XenForoCSSGenerator.js
Normal file
@@ -0,0 +1,171 @@
|
||||
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(
|
||||
/(?<![#\w])\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g,
|
||||
(match, className) => `.${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}.`);
|
||||
Reference in New Issue
Block a user