#!/usr/bin/env node
/**
* Extract all SCSS variable values and derived colors for mastodon (vanilla) flavour.
* Reuses the same derived color patterns as glitch, plus mastodon-specific vars.
*/
const sass = require("sass");
const path = require("path");
const stylesDir = path.resolve(__dirname, "../app/javascript/styles/mastodon");
// All SCSS variables we need to extract (same as glitch + mastodon-specific)
const baseVars = [
"black", "white",
"red-600", "red-500",
"blurple-600", "blurple-500", "blurple-400", "blurple-300",
"grey-600", "grey-100",
"success-green", "error-red", "warning-red", "gold-star", "red-bookmark",
"classic-base-color", "classic-primary-color", "classic-secondary-color", "classic-highlight-color",
"base-shadow-color", "base-overlay-background", "base-border-color",
"simple-background-color", "valid-value-color", "error-value-color",
"ui-base-color", "ui-base-lighter-color", "ui-primary-color", "ui-secondary-color",
"ui-highlight-color", "ui-button-color",
"ui-button-background-color", "ui-button-focus-background-color",
"ui-button-focus-outline-color",
"ui-button-secondary-color", "ui-button-secondary-border-color",
"ui-button-secondary-focus-background-color", "ui-button-secondary-focus-color",
"ui-button-tertiary-color", "ui-button-tertiary-border-color",
"ui-button-tertiary-focus-background-color", "ui-button-tertiary-focus-color",
"ui-button-destructive-background-color", "ui-button-destructive-focus-background-color",
"primary-text-color", "darker-text-color", "dark-text-color",
"secondary-text-color", "highlight-text-color",
"action-button-color", "action-button-focus-color",
"passive-text-color", "active-passive-text-color",
"inverted-text-color", "lighter-text-color", "light-text-color",
];
// Derived colors: [variable, function, amount]
const derivedColors = [
["action-button-color", "lighten", 7],
["action-button-color", "lighten", 13],
["darker-text-color", "lighten", 4],
["darker-text-color", "lighten", 7],
["darker-text-color", "lighten", 8],
["darker-text-color", "lighten", 10],
["dark-text-color", "lighten", 4],
["dark-text-color", "lighten", 7],
["error-red", "lighten", 4],
["error-red", "lighten", 12],
["error-value-color", "lighten", 12],
["gold-star", "lighten", 6],
["gold-star", "lighten", 16],
["highlight-text-color", "lighten", 4],
["highlight-text-color", "lighten", 6],
["highlight-text-color", "lighten", 8],
["highlight-text-color", "lighten", 13],
["inverted-text-color", "lighten", 4],
["inverted-text-color", "lighten", 10],
["inverted-text-color", "lighten", 15],
["inverted-text-color", "lighten", 16],
["lighter-text-color", "lighten", 7],
["lighter-text-color", "lighten", 20],
["secondary-text-color", "lighten", 4],
["secondary-text-color", "lighten", 7],
["secondary-text-color", "lighten", 8],
["ui-base-color", "lighten", 2],
["ui-base-color", "lighten", 3],
["ui-base-color", "lighten", 4],
["ui-base-color", "lighten", 5],
["ui-base-color", "lighten", 6],
["ui-base-color", "lighten", 8],
["ui-base-color", "lighten", 11],
["ui-base-color", "lighten", 12],
["ui-base-color", "lighten", 13],
["ui-base-color", "lighten", 14],
["ui-base-color", "lighten", 16],
["ui-base-color", "lighten", 18],
["ui-base-color", "lighten", 20],
["ui-base-color", "lighten", 26],
["ui-base-color", "lighten", 27],
["ui-base-color", "lighten", 29],
["ui-base-color", "lighten", 30],
["ui-base-color", "lighten", 33],
["ui-base-color", "lighten", 34],
["ui-base-color", "lighten", 50],
["ui-base-lighter-color", "lighten", 4],
["ui-base-lighter-color", "lighten", 7],
["ui-highlight-color", "lighten", 4],
["ui-highlight-color", "lighten", 8],
["ui-highlight-color", "lighten", 10],
["ui-highlight-color", "lighten", 12],
["ui-primary-color", "lighten", 8],
["ui-primary-color", "lighten", 12],
["ui-primary-color", "lighten", 20],
["ui-secondary-color", "lighten", 6],
["ui-secondary-color", "lighten", 8],
["valid-value-color", "lighten", 8],
["valid-value-color", "lighten", 15],
["warning-red", "lighten", 12],
["white", "lighten", 4],
["white", "lighten", 7],
// darken calls
["action-button-color", "darken", 13],
["darker-text-color", "darken", 13],
["highlight-text-color", "darken", 4],
["lighter-text-color", "darken", 4],
["lighter-text-color", "darken", 7],
["simple-background-color", "darken", 2],
["simple-background-color", "darken", 8],
["simple-background-color", "darken", 14],
["simple-background-color", "darken", 24],
["ui-base-color", "darken", 2],
["ui-base-color", "darken", 4],
["ui-base-color", "darken", 5],
["ui-base-color", "darken", 6],
["ui-base-color", "darken", 7],
["ui-base-color", "darken", 8],
["ui-base-color", "darken", 10],
["ui-base-color", "darken", 12],
["ui-base-color", "darken", 13],
["ui-base-color", "darken", 14],
["ui-base-color", "darken", 20],
["ui-highlight-color", "darken", 2],
["ui-highlight-color", "darken", 3],
["ui-highlight-color", "darken", 5],
["ui-highlight-color", "darken", 8],
["ui-primary-color", "darken", 5],
["ui-primary-color", "darken", 14],
["ui-primary-color", "darken", 40],
["ui-secondary-color", "darken", 8],
["ui-secondary-color", "darken", 10],
["ui-secondary-color", "darken", 16],
["ui-secondary-color", "darken", 18],
["ui-secondary-color", "darken", 24],
// rgba calls
["action-button-color", "rgba", 0.15],
["action-button-color", "rgba", 0.3],
["base-overlay-background", "rgba", 0],
["base-overlay-background", "rgba", 0.1],
["base-overlay-background", "rgba", 0.3],
["base-overlay-background", "rgba", 0.5],
["base-overlay-background", "rgba", 0.6],
["base-overlay-background", "rgba", 0.7],
["base-overlay-background", "rgba", 0.8],
["base-overlay-background", "rgba", 0.9],
["base-shadow-color", "rgba", 0.1],
["base-shadow-color", "rgba", 0.2],
["base-shadow-color", "rgba", 0.25],
["base-shadow-color", "rgba", 0.3],
["base-shadow-color", "rgba", 0.35],
["base-shadow-color", "rgba", 0.4],
["base-shadow-color", "rgba", 0.45],
["base-shadow-color", "rgba", 0.6],
["base-shadow-color", "rgba", 0.65],
["base-shadow-color", "rgba", 0.75],
["base-shadow-color", "rgba", 0.8],
["base-shadow-color", "rgba", 0.85],
["black", "rgba", 0.45],
["black", "rgba", 0.65],
["black", "rgba", 0.85],
["black", "rgba", 0.9],
["darker-text-color", "rgba", 0.15],
["darker-text-color", "rgba", 0.3],
["dark-text-color", "rgba", 0.1],
["error-red", "rgba", 0.5],
["error-value-color", "rgba", 0.1],
["error-value-color", "rgba", 0.5],
["gold-star", "rgba", 0.15],
["gold-star", "rgba", 0.25],
["gold-star", "rgba", 0.3],
["gold-star", "rgba", 0.5],
["highlight-text-color", "rgba", 0.15],
["highlight-text-color", "rgba", 0.25],
["highlight-text-color", "rgba", 0.3],
["highlight-text-color", "rgba", 0.4],
["lighter-text-color", "rgba", 0.15],
["lighter-text-color", "rgba", 0.3],
["primary-text-color", "rgba", 0.7],
["primary-text-color", "rgba", 0.8],
["success-green", "rgba", 0.1],
["success-green", "rgba", 0.5],
["ui-base-color", "rgba", 0],
["ui-base-color", "rgba", 0.15],
["ui-base-color", "rgba", 0.25],
["ui-base-color", "rgba", 0.3],
["ui-base-color", "rgba", 0.85],
["ui-base-color", "rgba", 1],
["ui-base-lighter-color", "rgba", 0.6],
["ui-highlight-color", "rgba", 0],
["ui-highlight-color", "rgba", 0.1],
["ui-highlight-color", "rgba", 0.15],
["ui-highlight-color", "rgba", 0.23],
["ui-highlight-color", "rgba", 0.4],
["ui-highlight-color", "rgba", 0.5],
["ui-secondary-color", "rgba", 0.1],
["ui-secondary-color", "rgba", 0.3],
["ui-secondary-color", "rgba", 0.4],
["ui-secondary-color", "rgba", 0.5],
["ui-secondary-color", "rgba", 0.7],
["valid-value-color", "rgba", 0.25],
["valid-value-color", "rgba", 0.5],
["warning-red", "rgba", 0.15],
["white", "rgba", 0.15],
["white", "rgba", 0.2],
["white", "rgba", 0.3],
["white", "rgba", 0.35],
["white", "rgba", 0.7],
["white", "rgba", 0.75],
["white", "rgba", 0.8],
];
// Mix calls
const mixColors = [
["mix-ui-base-highlight-95", "mix($ui-base-color, $ui-highlight-color, 95%)"],
["mix-white-highlight-80", "mix($white, $ui-highlight-color, 80%)"],
["mix-ui-base-lighten4-highlight-95", "mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%)"],
["mix-ui-base-lighten8-highlight-95", "mix(lighten($ui-base-color, 8%), $ui-highlight-color, 95%)"],
["mix-ui-base-lighten12-highlight-80", "mix(lighten($ui-base-color, 12%), $ui-highlight-color, 80%)"],
];
function generateScss(themeImport) {
let scss = "";
if (themeImport) {
scss += `@import '${themeImport}';\n`;
}
scss += `@import 'variables';\n\n`;
scss += `:root {\n`;
for (const v of baseVars) {
scss += ` --extract-${v}: #{$${v}};\n`;
}
for (const [varName, fn, amount] of derivedColors) {
if (fn === "lighten") {
scss += ` --extract-${varName}-lighten-${amount}: #{lighten($${varName}, ${amount}%)};\n`;
} else if (fn === "darken") {
scss += ` --extract-${varName}-darken-${amount}: #{darken($${varName}, ${amount}%)};\n`;
} else if (fn === "rgba") {
scss += ` --extract-${varName}-a${Math.round(amount * 100)}: #{rgba($${varName}, ${amount})};\n`;
}
}
for (const [name, expr] of mixColors) {
scss += ` --extract-${name}: #{${expr}};\n`;
}
scss += `}\n`;
return scss;
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(c => {
const hex = Math.round(c).toString(16).padStart(2, '0');
return hex;
}).join('');
}
function rgbaToHex(r, g, b, a) {
const alpha = Math.round(a * 255).toString(16).padStart(2, '0');
return rgbToHex(r, g, b) + alpha;
}
function normalizeColor(value) {
// Convert rgb(r, g, b) to hex
const rgbMatch = value.match(/^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/);
if (rgbMatch) {
return rgbToHex(parseFloat(rgbMatch[1]), parseFloat(rgbMatch[2]), parseFloat(rgbMatch[3]));
}
// Convert rgba(r, g, b, a) to hex with alpha
const rgbaMatch = value.match(/^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/);
if (rgbaMatch) {
return rgbaToHex(parseFloat(rgbaMatch[1]), parseFloat(rgbaMatch[2]), parseFloat(rgbaMatch[3]), parseFloat(rgbaMatch[4]));
}
return value;
}
function extractValues(css) {
const values = {};
const regex = /--extract-([\w-]+):\s*([^;]+);/g;
let match;
while ((match = regex.exec(css)) !== null) {
values[match[1]] = normalizeColor(match[2].trim());
}
return values;
}
function toCssProperties(values) {
let css = ":root {\n";
for (const [name, value] of Object.entries(values)) {
css += ` --${name}: ${value};\n`;
}
css += "}\n";
return css;
}
const parentDir = path.resolve(stylesDir, "..");
const themes = {
dark: null,
light: "../mastodon-light/variables",
contrast: "../contrast/variables",
};
const results = {};
for (const [name, themeImport] of Object.entries(themes)) {
const scss = generateScss(themeImport);
try {
const result = sass.compileString(scss, {
loadPaths: [stylesDir, parentDir],
});
results[name] = extractValues(result.css);
} catch (err) {
console.error(`Error compiling ${name} theme:`, err.message);
results[name] = { error: err.message };
}
}
// Output as CSS properties files
const fs = require("fs");
const outDir = stylesDir;
for (const [name, values] of Object.entries(results)) {
if (values.error) {
console.error(`Skipping ${name}: ${values.error}`);
continue;
}
const suffix = name === "dark" ? "" : `-${name}`;
const filename = `properties${suffix}.css`;
fs.writeFileSync(path.join(outDir, filename), toCssProperties(values));
console.log(`Wrote ${filename} (${Object.keys(values).length} properties)`);
}