/* 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();