/* eslint-disable no-console */
import fetch from 'cross-fetch';
import fs from 'node:fs/promises';
import path from 'path';
import yargs from 'yargs';
import { fileURLToPath } from 'url';
import { Queue, sleep, retry } from '@mui/internal-waterfall';
const currentDirectory = fileURLToPath(new URL('.', import.meta.url));
// Icons we don't publish.
// This is just a list of new icons.
// In the future we might change what icons we want to exclude (for example by popularity)
const ignoredIconNames = new Set([
// TODO v6: Whatsapp duplicates with WhatsApp
// We don't need it https://fonts.google.com/icons?icon.set=Material+Icons&icon.query=whatsapp
// 'whatsapp'
'123',
'6_ft_apart',
'add_chart', // Leads to inconsistent casing with `Addchart`
'exposure_neg_1', // Google product
'exposure_neg_2', // Google product
'exposure_plus_1', // Google product
'exposure_plus_2', // Google product
'exposure_zero', // Google product
'horizontal_distribute', // Advanced text editor
'motion_photos_on', // Google product
'motion_photos_pause', // Google product
'motion_photos_paused', // Google product
'polymer', // Legacy brand
'vertical_distribute', // Advanced text editor
]);
const legacyIconNames = new Set([
'battery_20',
'battery_30',
'battery_50',
'battery_60',
'battery_80',
'battery_90',
'battery_charge_20',
'battery_charging_20',
'battery_charge_30',
'battery_charging_30',
'battery_charge_50',
'battery_charging_50',
'battery_charge_60',
'battery_charging_60',
'battery_charge_80',
'battery_charging_80',
'battery_charge_90',
'battery_charging_90',
'signal_cellular_1_bar',
'signal_cellular_2_bar',
'signal_cellular_3_bar',
'signal_cellular_connected_no_internet_1_bar',
'signal_cellular_connected_no_internet_2_bar',
'signal_cellular_connected_no_internet_3_bar',
'signal_wifi_1_bar',
'signal_wifi_1_bar_lock',
'signal_wifi_2_bar',
'signal_wifi_2_bar_lock',
'signal_wifi_3_bar',
'signal_wifi_3_bar_lock',
]);
// list of icons that need to be overridden
const overrides = new Map([
[
// official icon is not rounded: https://fonts.google.com/icons?selected=Material+Icons+Round:apps:&icon.query=apps&icon.size=24&icon.color=%23e8eaed&icon.set=Material+Icons&icon.style=Rounded
// fixes https://github.com/mui/material-ui/issues/41064
'apps_rounded',
'',
],
[
// fixes https://github.com/mui/material-ui/issues/32016
'cases',
'',
],
[
// fixes https://github.com/mui/material-ui/issues/34863
'label_important_outlined',
'',
],
]);
const themeMap = {
baseline: '', // filled
outline: '_outlined',
round: '_round',
twotone: '_two_tone',
sharp: '_sharp',
};
const themeFileMap = {
baseline: '', // filled
outline: '_outlined',
round: '_rounded',
twotone: '_two_tone',
sharp: '_sharp',
};
const familyMap = {
baseline: 'Material Icons',
outline: 'Material Icons Outlined',
round: 'Material Icons Round',
sharp: 'Material Icons Sharp',
twotone: 'Material Icons Two Tone',
};
/**
* Downloads an icon in various themes and saves it as an SVG file.
*
* @param {Object} icon - The icon object.
* @param {string} icon.name - The name of the icon.
* @param {number} icon.version - The version of the icon.
* @param {number} icon.popularity - The popularity of the icon.
* @param {number} icon.codepoint - The codepoint of the icon.
* @param {string[]} icon.unsupported_families - The unsupported families of the icon.
* @param {string[]} icon.categories - The categories of the icon.
* @param {string[]} icon.tags - The tags associated with the icon.
* @param {number[]} icon.sizes_px - The available sizes of the icon in pixels.
* @returns {Promise} A promise that resolves when all icons are downloaded and saved.
*/
function downloadIcon(icon) {
console.log(`downloadIcon ${icon.index}: ${icon.name}`);
return Promise.all(
Object.keys(themeMap).map(async (theme) => {
const formattedTheme = themeMap[theme].split('_').join('');
const family = familyMap[theme];
if (icon.unsupported_families.includes(family)) {
return;
}
const response = await fetch(
`https://fonts.gstatic.com/s/i/materialicons${formattedTheme}/${icon.name}/v${icon.version}/24px.svg`,
);
if (response.status !== 200) {
throw new Error(`status ${response.status}`);
}
const SVG = await response.text();
await fs.writeFile(
path.join(
currentDirectory,
`../material-icons/${icon.name}${themeFileMap[theme]}_24px.svg`,
),
overrides.get(`${icon.name}${themeFileMap[theme]}`) || SVG,
);
}),
);
}
async function run() {
try {
const argv = yargs(process.argv.slice(2))
.usage('Download the SVG from material.io/resources/icons')
.describe('start-after', 'Resume at the following index').argv;
console.log('run', argv);
const iconDir = path.join(currentDirectory, '../material-icons');
await fs.rm(iconDir, { recursive: true, force: true });
await fs.mkdir(iconDir, { recursive: true });
const response = await fetch(
'https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true',
);
const text = await response.text();
const data = JSON.parse(text.replace(")]}'", ''));
let icons = data.icons;
icons = icons.filter((icon) => {
return !ignoredIconNames.has(icon.name) && !legacyIconNames.has(icon.name);
});
icons = icons.map((icon, index) => ({ index, ...icon }));
icons = icons.splice(argv.startAfter || 0);
console.log(`${icons.length} icons to download`);
const queue = new Queue(
async (icon) => {
await retry(async ({ tries }) => {
await sleep((tries - 1) * 100);
await downloadIcon(icon);
});
},
{ concurrency: 5 },
);
queue.push(icons);
await queue.wait({ empty: true });
} catch (err) {
console.log('err', err);
throw err;
}
}
run();