Finished CSS selector obfuscator

This commit is contained in:
SwingTheVine 2025-07-29 05:58:12 -04:00
parent 3918bf839e
commit 97ba324457
10 changed files with 186 additions and 14 deletions

View file

@ -4,6 +4,7 @@
* 2. Bundle the JS files into one file (esbuild)
* 3. Bundle the CSS files into one file (esbuild)
* 4. Compress & obfuscate the bundled JS file (terner)
* 5. Runs the CSS selector mangler (cssMandler.js)
* @since 0.0.6
*/
@ -11,6 +12,7 @@
import esbuild from 'esbuild';
import fs from 'fs';
import { execSync } from 'child_process';
import mangleSelectors from './cssMangler.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
@ -57,7 +59,7 @@ const resultEsbuild = await esbuild.build({
write: false, // Should we write the outfile to the disk?
}).catch(() => process.exit(1));
// Retrieves the JS file and map file
// Retrieves the JS file
const resultEsbuildJS = resultEsbuild.outputFiles.find(file => file.path.endsWith('.js'));
// Obfuscates the JS file
@ -84,5 +86,15 @@ let resultTerser = await terser.minify(resultEsbuildJS.text, {
}
});
// Writes the obfuscated/mangled JS code to a file
fs.writeFileSync('dist/BlueMarble.user.js', resultTerser.code, 'utf8');
// Mangles the CSS selectors
mangleSelectors('bm-', 'bm-', 'dist/BlueMarble.user.js', 'dist/BlueMarble.user.css');
// Adds the banner
fs.writeFileSync('dist/BlueMarble.user.js', metaContent + resultTerser.code, 'utf8');
fs.writeFileSync(
'dist/BlueMarble.user.js',
metaContent + fs.readFileSync('dist/BlueMarble.user.js', 'utf8'),
'utf8'
);

141
build/cssMangler.js Normal file
View file

@ -0,0 +1,141 @@
/** 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
}

View file

@ -1 +1 @@
#bm-overlay{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000}div#bm-overlay{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-bar-drag{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-bar-drag.dragging{cursor:grabbing}#bm-contain-header{margin-bottom:.5em}#bm-overlay img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle}#bm-overlay h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-contain-automation input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-contain-automation label{margin-right:.5ch}.bm-help{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-button-coords{vertical-align:middle}#bm-button-coords svg{width:50%;margin:0 auto;fill:#111}#bm-contain-coords input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-contain-coords input[type=number]::-webkit-outer-spin-button,#bm-contain-coords input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-contain-buttons-template{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-input-file-template)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-output-status{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-contain-buttons-action{display:flex;justify-content:space-between}#bm-overlay small{font-size:x-small;color:#d3d3d3}#bm-contain-userinfo,#bm-contain-automation,#bm-contain-coords,#bm-contain-buttons-template,div:has(>#bm-input-file-template),#bm-output-status{margin-top:.5em}#bm-overlay button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-overlay button:hover,#bm-overlay button:focus-visible{background-color:#1061e5}#bm-overlay button:active,#bm-overlay button:disabled{background-color:#2e97ff}#bm-overlay button:disabled{text-decoration:line-through}
#bm-p{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000}div#bm-p{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-k{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-k.dragging{cursor:grabbing}#bm-b{margin-bottom:.5em}#bm-p img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle}#bm-p h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-q{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-h{vertical-align:middle}#bm-h svg{width:50%;margin:0 auto;fill:#111}div#bm-7{display:flex;gap:.5ch}#bm-8 svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-c input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-c input[type=number]::-webkit-outer-spin-button,#bm-c input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-e{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-p small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-c,#bm-0,div:has(>#bm-2),#bm-e{margin-top:.5em}#bm-p button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-p button:hover,#bm-p button:focus-visible{background-color:#1061e5}#bm-p button:active,#bm-p button:disabled{background-color:#2e97ff}#bm-p button:disabled{text-decoration:line-through}

File diff suppressed because one or more lines are too long

View file

@ -35,7 +35,7 @@
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/LICENSE.txt" target="_blank"><img alt="Software License: MPL-2.0" src="https://img.shields.io/badge/Software_License-MPL--2.0-brightgreen?style=flat"></a>
<a href="https://discord.gg/tpeBPy46hf" target="_blank"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="" target="_blank"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-35hrs_30mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-211-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-227-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Lines of Code" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=code"></a>
<a href="" target="_blank"><img alt="Total Comments" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=comments"></a>
<a href="" target="_blank"><img alt="Compression" src="https://img.shields.io/badge/Compression-68.88%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.55.11",
"version": "0.56.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.55.11",
"version": "0.56.16",
"devDependencies": {
"esbuild": "^0.25.0",
"terser": "^5.43.1"

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.56.0",
"version": "0.56.16",
"type": "module",
"scripts": {
"build": "node build/build.js",

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.56.0
// @version 0.56.16
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0

View file

@ -147,10 +147,11 @@ overlay.addDiv({'id': 'bm-overlay', 'style': 'top: 10px; right: 75px;'})
.addTextarea({'id': overlay.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${version}`, 'readOnly': true}).buildElement()
.addDiv({'id': 'bm-contain-buttons-action'})
.addDiv()
.addButton({'id': 'bm-button-teleport', 'className': 'bm-help', 'textContent': '🕴'}).buildElement()
.addButton({'id': 'bm-button-favorite', 'className': 'bm-help', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><polygon points="10,2 12,7.5 18,7.5 13.5,11.5 15.5,18 10,14 4.5,18 6.5,11.5 2,7.5 8,7.5" fill="gold" stroke="black" stroke-width="1.4"></polygon></svg>'}).buildElement()
.addButton({'id': 'bm-button-templates', 'className': 'bm-help', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect width="20" height="20" stroke-width="3" stroke="black" fill="none"></rect><circle cx="10" cy="8" r="4"></circle><circle cx="10" cy="18" r="7"></circle></svg>'}).buildElement()
.addSmall({'textContent': 'Made by SwingTheVine'}).buildElement()
.addButton({'id': 'bm-button-teleport', 'className': 'bm-help', 'textContent': '✈'}).buildElement()
.addButton({'id': 'bm-button-favorite', 'className': 'bm-help', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><polygon points="10,2 12,7.5 18,7.5 13.5,11.5 15.5,18 10,14 4.5,18 6.5,11.5 2,7.5 8,7.5" fill="white"></polygon></svg>'}).buildElement()
.addButton({'id': 'bm-button-templates', 'className': 'bm-help', 'innerHTML': '🖌'}).buildElement()
.buildElement()
.addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
.buildElement()
.buildElement()
.buildOverlay(document.body);

View file

@ -93,6 +93,24 @@ div#bm-overlay {
fill: #111;
}
/* Container for action buttons, that is inside the action button container */
div#bm-button-teleport {
display: flex;
gap: 0.5ch;
}
/* Favorite (Star) button image */
/* Templates (Person) button image */
#bm-button-favorite svg,
#bm-button-template svg {
height: 1em;
margin: 0 auto;
margin-top: 2px;
text-align: center;
line-height: 1em;
vertical-align: bottom;
}
/* Tile (x, y) & Pixel (x, y) input fields */
#bm-contain-coords input[type="number"] {
appearance: auto;