Wplace-BlueMarble/build/cssMangler.js
2025-07-29 05:58:12 -04:00

141 lines
No EOL
5.9 KiB
JavaScript

/** Mangles all matching CSS selectors provided:
* - The CSS selector starts with the correct prefix
* - The prefix case matches (case-sensitive)
* - There is 1 (bundled) CSS file
* - There is 1 (bundled) JS file
* The default mangling is base64, as small as possible
* @since 0.56.1
* @example
* // (Assume 'bm-' is the input prefix, and 'b-' is the output prefix)
* // Input:
* // JS
* const element = docuement.createElement('p');
* element.id = 'bm-paragraph-id';
* element.className = 'bm-paragraph-class';
* // CSS
* #bm-paragraph-id {color:red;}
* .bm-paragraph-class {background-color:blue;}
*
* // Output:
* // JS
* const element = docuement.createElement('p');
* element.id = 'b-1'; // The longer the selector, the smaller it gets...
* element.className = 'b-0'; // ...therefore, the class selector is "0"
* // CSS
* #b-1 {color:red;}
* .b-0 {background-color:blue;}
* // Optional returned map Object:
* console.log(JSON.stringify(mangleSelectors('bm-', 'b-', 'bundled.js', 'bundled.css', true), null, 2));
* {
* "bm-paragraph-class": "b-0",
* "bm-paragraph-id": "b-1",
* }
*/
import fs from 'fs';
/** Mangles the CSS selectors in a JS and CSS file.
* Both the JS and CSS file are needed to ensure the names are synced.
* A prefix is needed on all selectors to ensure the proper matching.
* The prefix is case-sensitive.
* The default mangling is all valid single byte characters for CSS selectors (which is, ironically, 64 characters).
* You can optionally return the key-value mapping of all selector names.
* @param {string} inputPrefix - The prefix to search for.
* @param {string} outputPrefix - The prefix to replace with.
* @param {string} pathJS - The path to the JS file.
* @param {string} pathCSS - The path to the CSS file.
* @param {boolean} [returnMap=false] - Should this function return the key-value map Object?
* @param {string} [encoding=''] - The characters you want the mangled selectors to consist of.
* @returns {Object<string, string>|undefined} A mapping of the mangled CSS selectors as an Object, or `undefined` if `returnMap` is not `true`.
* @since 0.56.1
* @example
* // (Assume 'bm-' is the input prefix, and 'b-' is the output prefix)
* // Input:
* // JS
* const element = docuement.createElement('p');
* element.id = 'bm-paragraph-id';
* element.className = 'bm-paragraph-class';
* // CSS
* #bm-paragraph-id {color:red;}
* .bm-paragraph-class {background-color:blue;}
*
* // Output:
* // JS
* const element = docuement.createElement('p');
* element.id = 'b-1'; // The longer the selector, the smaller it gets...
* element.className = 'b-0'; // ...therefore, the class selector is "0"
* // CSS
* #b-1 {color:red;}
* .b-0 {background-color:blue;}
* // Optional returned map Object:
* console.log(JSON.stringify(mangleSelectors('bm-', 'b-', 'bundled.js', 'bundled.css', true), null, 2));
* {
* "bm-paragraph-class": "b-0",
* "bm-paragraph-id": "b-1",
* }
*/
export default function mangleSelectors(inputPrefix, outputPrefix, pathJS, pathCSS, returnMap=false, encoding='') {
encoding = encoding || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'; // Default encoding
const fileInputJS = fs.readFileSync(pathJS, 'utf8'); // The JS file
const fileInputCSS = fs.readFileSync(pathCSS, 'utf8'); // The CSS file
// One of each of all matching selectors
// File -> RegEx -> Array (Duplicates) -> Set (Unique) -> Array (Unique)
let matchedSelectors = [...new Set([...fileInputJS.matchAll(new RegExp(`\\b${escapeRegex(inputPrefix)}[a-zA-Z0-9_-]+`, 'g'))].map(match => match[0]))];
// Sort keys in selector from longest to shortest
// This will avoid partial matches, which could cause bugs
// E.g. `foo-foobar` will match before `foo-foo` matches
matchedSelectors.sort((a, b) => b.length - a.length);
// Converts the string[] to an Object (key-value)
matchedSelectors = Object.fromEntries(matchedSelectors.map((key, value) => [key, outputPrefix + numberToEncoded(matchedSelectors.indexOf(key), encoding)]));
// Compile the RegEx from the selector map
const regex = new RegExp(Object.keys(matchedSelectors).map(selector => escapeRegex(selector)).join('|'), 'g');
// Replaces the CSS selectors in both files with encoded versions
fs.writeFileSync(pathJS, fileInputJS.replace(regex, match => matchedSelectors[match]), 'utf8');
fs.writeFileSync(pathCSS, fileInputCSS.replace(regex, match => matchedSelectors[match]), 'utf8');
if (!!returnMap) {return matchedSelectors;} // Return the map Object optionally
}
/** Escapes characters in a string that are about to be inserted into a Regular Expression
* @param {string} string - The string to pass in
* @returns {string} String that has been escaped for RegEx
* @since 0.56.2
*/
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** Encodes a number into a custom encoded string.
* @param {number} number - The number to encode
* @param {string} encoding - The characters to use when encoding
* @since 0.56.12
* @returns {string} Encoded string
* @example
* const encode = '012abcABC'; // Base 9
* console.log(numberToEncoded(0, encode)); // 0
* console.log(numberToEncoded(5, encode)); // c
* console.log(numberToEncoded(15, encode)); // 1A
* console.log(numberToEncoded(12345, encode)); // 1BCaA
*/
function numberToEncoded(number, encoding) {
if (number === 0) return encoding[0]; // End quickly if number equals 0. No special calculation needed
let result = ''; // The encoded string
const base = encoding.length; // The number of characters used, which determines the base
// Base conversion algorithm
while (number > 0) {
result = encoding[number % base] + result; // Find's the character's encoded value determined by the modulo of the base
number = Math.floor(number / base); // Divides the number by the base so the next iteration can find the next modulo character
}
return result; // The final encoded string
}