Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
[WIP] add icons API
  • Loading branch information
t-hamano committed Nov 15, 2025
commit e7f8082eaaf1035984c48422776ef7f9aa74c65c
1 change: 1 addition & 0 deletions backport-changelog/6.9/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Submit a PR to add the WP_REST_Icon_Controller and WP_Icons_Registry classes to the core.
229 changes: 229 additions & 0 deletions lib/class-wp-icons-registry-gutenberg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php

class WP_Icons_Registry_Gutenberg {
/**
* Registered icons array.
*
* @var array[]
*/
private $registered_icons = array();


/**
* Container for the main instance of the class.
*
* @var WP_Icons_Registry_Gutenberg|null
*/
private static $instance = null;

public function __construct() {
$icons_directory = __DIR__ . '/../packages/icons/src/library/';
if ( ! is_dir( $icons_directory ) ) {
return;
}

$svg_files = glob( $icons_directory . '*.svg' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In WordPress core, the problem is how to handle this. For example, it might be necessary to synchronize the SVG icon of packages/icons/src/library with wp-includes/icons/.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First step would be #72299, after which we can open a Trac ticket extending the copy logic in tools/webpack/packages.js.

if ( empty( $svg_files ) ) {
return;
}

foreach ( $svg_files as $svg_file ) {

$icon_name = basename( $svg_file, '.svg' );
$svg_content = file_get_contents( $svg_file );

if ( false === $svg_content ) {
continue;
}

$this->register(
'core/' . $icon_name,
array(
'name' => $icon_name,
'content' => $svg_content,
Copy link
Contributor Author

@t-hamano t-hamano Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For actual use in the Icon Block, we may also need the label field.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One outstanding question for me is whether we'd like to properly annotate SVGs with metadata like title/label and, maybe, keywords (e.g. to match "edit" to the pencil icon). That would be better than programmatically adjusting the case and spacing.

And, independently of the above, all the labels (and possibly keywords) would need to exist statically in the code in order for i18n to be possible. In its simplest form, a build step in wpdev might, for instance, generate a PHP file with these strings. There is also JSON-based i18n (developed for block.json), in case we'd like to settle on defining metadata via JSON, although I'm less familiar with how that works.

)
);
}
}

/**
* Registers an icon.
*
* @param string $icon_name Icon name including namespace.
* @param array $icon_properties {
* List of properties for the icon.
*
* @type string $title Required. A human-readable title for the icon.
* @type string $content Optional. SVG markup for the icon.
* If not provided, the content will be retrieved from the `filePath` if set.
* If both `content` and `filePath` are not set, the icon will not be registered.
* @type string $filePath Optional. The full path to the file containing the icon content.
* }
* @return bool True if the icon was registered with success and false otherwise.
*/
private function register( $icon_name, $icon_properties ) {
Copy link
Contributor Author

@t-hamano t-hamano Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, we do not allow external icon registration, which means that even if a developer runs the following code, an error will occur:

WP_Icons_Registry_Gutenberg::get_instance()->register();

if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Icon name must be a string.', 'gutenberg' ),
'6.9.0'
);
return false;
}

if ( ! isset( $icon_properties['content'] ) || ! is_string( $icon_properties['content'] ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Icon content must be a string.', 'gutenberg' ),
'6.9.0'
);
return false;
}

$sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] );

if ( empty( $sanitized_icon_content ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Icon content does not contain valid SVG markup.', 'gutenberg' ),
'6.9.0'
);
return false;
}

$icon = array_merge(
$icon_properties,
array( 'name' => $icon_name )
);

$this->registered_icons[ $icon_name ] = $icon;

return true;
}

/**
* Sanitizes the icon SVG content.
*
* @param string $icon_content The icon SVG content to sanitize.
* @return string The sanitized icon SVG content.
*/
private function sanitize_icon_content( $icon_content ) {
$allowed_tags = array(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'svg' => array(
'class' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
'aria-hidden' => true,
'role' => true,
'focusable' => true,
),
'path' => array(
'fill' => true,
'fill-rule' => true,
'd' => true,
'transform' => true,
),
'polygon' => array(
'fill' => true,
'fill-rule' => true,
'points' => true,
'transform' => true,
'focusable' => true,
),
);
return wp_kses( $icon_content, $allowed_tags );
}

/**
* Retrieves the content of a registered icon.
*
* @param string $icon_name Icon name including namespace.
* @return string The content of the icon.
* @since 6.9.0
*/
private function get_content( $icon_name ) {
return $this->registered_icons[ $icon_name ]['content'];
}

/**
* Retrieves an array containing the properties of a registered icon.
*
*
* @param string $icon_name Icon name including namespace.
* @return array|null Registered icon properties or `null` if the icon is not registered.
*/
public function get_registered( $icon_name ) {
if ( ! $this->is_registered( $icon_name ) ) {
return null;
}

$icon = $this->registered_icons[ $icon_name ];
$content = $this->get_content( $icon_name );
$icon['content'] = $content;

return $icon;
}

/**
* Retrieves all registered icons.
*
* @return array[] Array of arrays containing the registered icon properties.
*/
public function get_all_registered() {
$icons = $this->registered_icons;

foreach ( $icons as $index => $icon ) {
$content = $this->get_content( $icon['name'] );
$icons[ $index ]['content'] = $content;
}

return array_values( $icons );
}

/**
* Checks if an icon is registered.
*
*
* @param string $icon_name Icon name including namespace.
* @return bool True if the icon is registered, false otherwise.
*/
public function is_registered( $icon_name ) {
return isset( $this->registered_icons[ $icon_name ] );
}

/**
* Magic method for object serialization.
*
*/
public function __wakeup() {
if ( ! $this->registered_icons ) {
return;
}
if ( ! is_array( $this->registered_icons ) ) {
throw new UnexpectedValueException();
}
foreach ( $this->registered_icons as $value ) {
if ( ! is_array( $value ) ) {
throw new UnexpectedValueException();
}
}
}

/**
* Utility method to retrieve the main instance of the class.
*
* The instance will be created if it does not exist yet.
*
*
* @return WP_Icons_Registry The main instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}

return self::$instance;
}
}
Loading