Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions lib/compat/wordpress-7.0/class-wp-icons-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ private function __construct() {
return;
}

if ( ! ( $icon_data['public'] ?? false ) ) {
continue;
}

$this->register(
'core/' . $icon_name,
array(
Expand Down
16 changes: 16 additions & 0 deletions packages/icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ import { Icon, check } from '@wordpress/icons';

You can browse the icons docs and examples at [https://wordpress.github.io/gutenberg/?path=/docs/icons-icon--default](https://wordpress.github.io/gutenberg/?path=/docs/icons-icon--default)

## Adding new icons

To add a new icon to the library, follow these steps:

1. **Add the SVG file**: Place your SVG file in the `src/library/` directory. The filename should be in kebab-case (e.g., `my-new-icon.svg`).
2. **TypeScript files are auto-generated**: The TypeScript component files (`.tsx`) are automatically generated by the build script from the SVG files. You do not need to manually create or edit these files.

3. **Add the icon to `manifest.json`**: Add an entry for your new icon in `src/manifest.json`. The entry should include:
- `slug`: The icon identifier (should match the SVG filename without the `.svg` extension)
- `label`: The human-readable label for the icon. Use Title Case (for example, `My New Icon`).
- `filePath`: The relative path to the SVG file (e.g., `library/my-new-icon.svg`)
- `public` (optional): Set to `true` if you want to expose this icon as a core icon through the SVG Icons API. **Important**: Once an icon is made public, removing it is difficult, so carefully consider whether to make it public before setting this field to `true`.
4. **Do not edit `manifest.php`**: The `manifest.php` file is automatically generated from `manifest.json` by the build script. Do not edit it manually, as your changes will be overwritten when the build runs.

After adding your icon, run `npm run build` to generate the TypeScript files and update `manifest.php`.

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.
Expand Down
82 changes: 82 additions & 0 deletions packages/icons/lib/generate-manifest-php.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* External dependencies
*/
const path = require( 'path' );
const { readFile, writeFile } = require( 'fs' ).promises;

const MANIFEST_JSON_PATH = path.join( __dirname, '..', 'src', 'manifest.json' );
const MANIFEST_PHP_PATH = path.join( __dirname, '..', 'src', 'manifest.php' );

/**
* Escapes single quotes in PHP strings.
*
* @param {string} str String to escape.
* @return {string} Escaped string.
*/
function escapePHPString( str ) {
return str.replace( /'/g, "\\'" );
}

/**
* Formats a PHP array key with proper indentation and alignment.
*
* @param {string} key Array key.
* @param {number} maxKeyLength Maximum key length for alignment.
* @return {string} Formatted key.
*/
function formatPHPKey( key, maxKeyLength ) {
const padding = ' '.repeat( maxKeyLength - key.length );
return `\t'${ escapePHPString( key ) }'${ padding }`;
}

/**
* Generates PHP array from JSON manifest.
*
* @param {Array} manifest JSON manifest array.
* @return {string} PHP code.
*/
function generatePHPArray( manifest ) {
// Find the maximum key length for alignment
const maxKeyLength = Math.max(
...manifest.map( ( item ) => item.slug.length )
);

const phpEntries = manifest.map( ( item ) => {
const key = formatPHPKey( item.slug, maxKeyLength );
const label = escapePHPString( item.label );
const filePath = escapePHPString( item.filePath );

return `${ key } => array(
'label' => _x( '${ label }', 'icon label', 'gutenberg' ),
'filePath' => '${ filePath }',
),`;
} );

return `return array(
${ phpEntries.join( '\n' ) }
);`;
}

/**
* Generates manifest.php from manifest.json.
* Only includes icons with public: true.
*/
async function generateManifestPHP() {
const manifestJson = await readFile( MANIFEST_JSON_PATH, 'utf8' );
const manifest = JSON.parse( manifestJson );
const publicIcons = manifest.filter( ( item ) => item.public === true );
const phpHeader = `<?php
// This file is automatically generated. Do not edit directly.
if ( ! defined( 'ABSPATH' ) ) {
die( 'Silence is golden.' );
}

`;
const phpArray = generatePHPArray( publicIcons );
const phpContent = phpHeader + phpArray + '\n';
await writeFile( MANIFEST_PHP_PATH, phpContent, 'utf8' );
}

if ( module === require.main ) {
generateManifestPHP();
}
91 changes: 30 additions & 61 deletions packages/icons/lib/validate-collection.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
* External dependencies
*/
const path = require( 'path' );
const { readdir, stat } = require( 'fs/promises' );
const { createReadStream } = require( 'fs' );
const { createInterface } = require( 'readline/promises' );
const { readdir, stat, readFile } = require( 'fs/promises' );

const ICON_LIBRARY_DIR = path.join( __dirname, '..', 'src', 'library' );

Expand All @@ -14,7 +12,7 @@ const ICON_LIBRARY_DIR = path.join( __dirname, '..', 'src', 'library' );
* vice versa.
*/
async function validateCollection() {
const manifestPath = path.join( ICON_LIBRARY_DIR, '..', 'manifest.php' );
const manifestPath = path.join( ICON_LIBRARY_DIR, '..', 'manifest.json' );

try {
await stat( manifestPath );
Expand All @@ -24,81 +22,52 @@ async function validateCollection() {
);
}

const manifestContent = await readFile( manifestPath, 'utf8' );
const manifest = JSON.parse( manifestContent );

/*
* Collect policy violations as strings.
*/
const problems = [];

/*
* As a cheap substitute for actually parsing the PHP file, prepare to scan
* it line by line, looking for specific patterns to find the
* aforementioned violations.
*/
const rl = createInterface( {
input: createReadStream( manifestPath, {
encoding: 'utf8',
} ),
} );

/* Scan manifest.php for the keys (slugs) and `filePath` property (paths)
* Scan manifest.json for the slugs and `filePath` property (paths)
* of every icon, ensuring that for each icon the path matches the slug.
*
* Later we will reuse manifestSlugs to compare these with the SVG files
* Later we will reuse manifestPaths to compare these with the SVG files
* found in the file system.
*/
const manifestSlugs = [];
const manifestPaths = [];
for await ( const line of rl ) {
let match;
for ( const icon of manifest ) {
const expected = `library/${ icon.slug }.svg`;

/*
* Spot the opening of an icon definition, e.g.
*
* 'wordpress' => array(
* This is an unexpected failure and should thus throw an error
* immediately, not be added to `problems`.
*/
if ( ( match = line.match( /^\t'([^']+)'\s+=> array\($/ )?.[ 1 ] ) ) {
manifestSlugs.push( match );
continue;
if ( icon.filePath !== expected ) {
throw new Error(
`Invalid icon definition for icon '${ icon.slug }': expected 'filePath' to be '${ expected }', saw '${ icon.filePath }'`
);
}

manifestPaths.push( icon.filePath );

/*
* Spot the 'filePath' property inside an icon definition, e.g.
*
* 'filePath' => 'wordpress.svg',
* Verify that the corresponding SVG file is found.
*/
if ( ( match = line.match( /^\t\t'filePath' => '(.*)',$/ )?.[ 1 ] ) ) {
const expected = `library/${ manifestSlugs.at( -1 ) }.svg`;

/*
* This is an unexpected failure and should thus throw an error
* immediately, not be added to `problems`.
*/
if ( match !== expected ) {
throw new Error(
`Invalid icon definition for icon '${ manifestSlugs.at(
-1
) }': expected 'filePath' to be '${ expected }', saw '${ match }'`
);
}

manifestPaths.push( match );

/*
* Verify that the corresponding SVG file is found.
*/
if (
! ( await stat(
path.join( ICON_LIBRARY_DIR, '..', expected )
).catch( () => false ) )
) {
problems.push(
`- Icon file ${ path.join(
ICON_LIBRARY_DIR,
'..',
expected
) } not found`
);
}
if (
! ( await stat(
path.join( ICON_LIBRARY_DIR, '..', expected )
).catch( () => false ) )
) {
problems.push(
`- Icon file ${ path.join(
ICON_LIBRARY_DIR,
'..',
expected
) } not found`
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@
"scripts": {
"prelint:js": "npm run build",
"prepare": "npm run build",
"build": "node lib/generate-library.cjs"
"build": "node lib/generate-library.cjs && node lib/generate-manifest-php.cjs"
}
}
Loading
Loading