-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add Icon APIs: registry and /wp/v2/icons endpoint #10909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mcsf
wants to merge
7
commits into
WordPress:trunk
Choose a base branch
from
mcsf:add/icons-apis
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,034
−4
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
615e544
Build: Copy Icons manifest file from Gutenberg
mcsf f7d5a91
Copy icon SVGs to wp-includes/icons/library
mcsf 3bc226a
Always assert glob not empty
mcsf 2a04dd6
Prefer `glob`; `fs.globSync` requires node >= 22
mcsf a0ab11e
Add Icon APIs: registry and /wp/v2/icons endpoint
mcsf 5cc4fdc
Update tests/qunit/fixtures/wp-api-generated.js
mcsf 8446d5e
Add Tests_REST_WpRestIconsController
mcsf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Icons API: WP_Icons_Registry class | ||
| * | ||
| * @package WordPress | ||
| * @since 7.0.0 | ||
| */ | ||
|
|
||
| /** | ||
| * FIXME | ||
| * | ||
| * @since 7.0.0 | ||
| */ | ||
| class WP_Icons_Registry { | ||
| /** | ||
| * Registered icons array. | ||
| * | ||
| * @var array[] | ||
| */ | ||
| private $registered_icons = array(); | ||
|
|
||
|
|
||
| /** | ||
| * Container for the main instance of the class. | ||
| * | ||
| * @var WP_Icons_Registry|null | ||
| */ | ||
| private static $instance = null; | ||
|
|
||
| /** | ||
| * Constructor. | ||
| * | ||
| * WP_Icons_Registry is a singleton class, so keep this private. | ||
| */ | ||
| private function __construct() { | ||
| $icons_directory = __DIR__ . '/icons/'; | ||
| $icons_directory = trailingslashit( $icons_directory ); | ||
| $manifest_path = $icons_directory . 'manifest.php'; | ||
|
|
||
| if ( ! is_readable( $manifest_path ) ) { | ||
| wp_trigger_error( | ||
| __METHOD__, | ||
| __( 'Core icon collection manifest is missing or unreadable.', 'gutenberg' ) | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| $collection = include $manifest_path; | ||
|
|
||
| if ( empty( $collection ) ) { | ||
| wp_trigger_error( | ||
| __METHOD__, | ||
| __( 'Core icon collection manifest is empty or invalid.', 'gutenberg' ) | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| foreach ( $collection as $icon_name => $icon_data ) { | ||
| if ( | ||
| empty( $icon_data['filePath'] ) | ||
| || ! is_string( $icon_data['filePath'] ) | ||
| ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| __( 'Core icon collection manifest must provide valid a "filePath" for each icon.', 'gutenberg' ), | ||
| '7.0.0' | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| $this->register( | ||
| 'core/' . $icon_name, | ||
| array( | ||
| 'label' => $icon_data['label'], | ||
| 'filePath' => $icons_directory . $icon_data['filePath'], | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers an icon. | ||
| * | ||
| * @param string $icon_name Icon name including namespace. | ||
| * @param array $icon_properties { | ||
| * List of properties for the icon. | ||
| * | ||
| * @type string $label Required. A human-readable label 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 ) { | ||
| if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| __( 'Icon name must be a string.', 'gutenberg' ), | ||
| '7.0.0' | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| $allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 ); | ||
| foreach ( array_keys( $icon_properties ) as $key ) { | ||
| if ( ! array_key_exists( $key, $allowed_keys ) ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| sprintf( | ||
| // translators: %s is the name of any user-provided key | ||
| __( 'Invalid icon property: "%s".', 'gutenberg' ), | ||
| $key | ||
| ), | ||
| '7.0.0' | ||
| ); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| __( 'Icon label must be a string.', 'gutenberg' ), | ||
| '7.0.0' | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| if ( | ||
| ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) || | ||
| ( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) ) | ||
| ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| __( 'Icons must provide either `content` or `filePath`.', 'gutenberg' ), | ||
| '7.0.0' | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| if ( isset( $icon_properties['content'] ) ) { | ||
| if ( ! is_string( $icon_properties['content'] ) ) { | ||
| _doing_it_wrong( | ||
| __METHOD__, | ||
| __( 'Icon content must be a string.', 'gutenberg' ), | ||
| '7.0.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' ), | ||
| '7.0.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. | ||
| * | ||
| * Logic borrowed from twentytwenty. | ||
| * @see twentytwenty_get_theme_svg | ||
| * | ||
| * @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( | ||
| '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|null The content of the icon, if found. | ||
| */ | ||
| private function get_content( $icon_name ) { | ||
| if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { | ||
| $content = file_get_contents( | ||
| $this->registered_icons[ $icon_name ]['filePath'] | ||
| ); | ||
| $content = $this->sanitize_icon_content( $content ); | ||
|
|
||
| if ( empty( $content ) ) { | ||
| wp_trigger_error( | ||
| __METHOD__, | ||
| __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ) | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| $this->registered_icons[ $icon_name ]['content'] = $content; | ||
| } | ||
| 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( $icon_name ) { | ||
| if ( ! $this->is_registered( $icon_name ) ) { | ||
| return null; | ||
| } | ||
|
|
||
| $icon = $this->registered_icons[ $icon_name ]; | ||
| $icon['content'] = $icon['content'] ?? $this->get_content( $icon_name ); | ||
|
|
||
| return $icon; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves all registered icons. | ||
| * | ||
| * @param string $search Optional. Search term by which to filter the icons. | ||
| * @return array[] Array of arrays containing the registered icon properties. | ||
| */ | ||
| public function get_registered_icons( $search = '' ) { | ||
| $icons = array(); | ||
|
|
||
| foreach ( $this->registered_icons as $icon ) { | ||
| if ( ! empty( $search ) && false === stripos( $icon['name'], $search ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| $icon['content'] = $icon['content'] ?? $this->get_content( $icon['name'] ); | ||
| $icons[] = $icon; | ||
| } | ||
|
|
||
| return $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 ] ); | ||
| } | ||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think we should merge in this API without a public method for registering icons.
The icon library we are shipping with Core is the gutenberg icons. Those icons were never meant to be frontend design icons. They are editor interface icons.
From my POV there is very little utility to the icon block in core without the ability to register custom icons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand where you're coming from, but I'd argue that the Icon block is already useful in its initial form, having access to 321 core icons.
Though
@wordpress/iconswas developed to support the needs of the editor, IMO it's fair to say that the icon set can accommodate more uses. Folks like @jasmussen, @jameskoster and others have been iterating on those icons and are involved in the making of the Icon block, which I take as a sign that, though imperfect, the icon set has evolved beyond its original purpose.Keeping the Icons registry closed was a way to conserve some more room for adjustments, as the icon block itself gets adopted. It should be understood as an intermediate step, so that we can confidently expand the scope of the registry in 7.1 (third-party registration, but possibly also categories, etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the ping. As happens more often than I like, I find myself agreeing with both ends of the spectrum.
On the one hand, let's be honest that the utility of this first iteration is limited: there are many inline contexts where I would like to use icons, but can't. There's just that one set, and although there are many nice icons there, the majority of them were designed for blocks, or WordPress specific features.
On the other hand, while this effort is also about adding a block that lets you intiutively create decorative bullets and expanded patterns, it's also a mammoth infrastructure effort, to bring SVG support to WordPress, to wrap it in a UI that we can grow, expand, and iterate. Most of this work may not be quite as exciting and immediately useful, but wait til you see what we can do with it. We're building a mountain, moving dirt to one place, and eventually what we build will be visible on the horison.
In saying that, I'm validating every aspect of Fabian's point. I think this is especially important if we were to land in 7.0 and market this new feature: we should be very honest that this is a small first step, not the end-state. There are shortcomings: SVG support is the big deal. But with that, I'm also validating Miguel's instinct: this ground work is critical, and there's no better test crucible than to ship this in a major release, so we can work out the inevitable issues.
None of this is strongly felt. There's always another release. But it's a soft appeal: I'd rather take 4 deliberate, but small steps, than make one big jump and risk landing in the wrong place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After talking with Joen, I also opened WordPress/gutenberg#75526 to distinguish "front-end-ready" icons from internal ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also agree with this approach. To get attention and move this PR forward, we might want to submit a core ticket first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> https://core.trac.wordpress.org/ticket/64651
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the incremental approach @mcsf is suggesting is a good call. Shipping the registry and REST endpoint now gives an initial foundation to build on, and keeping register() private for this first iteration is a good way to leave room for refinement before committing to a public API surface.
I think merging this PR (and the icon block) would allow some icon block testing before committing to how third parties can add additional icons. We would get a better sense of how the block is used, and the icons package already offers some icons that can be useful.