Skip to content

Commit e82c8be

Browse files
committed
Icons library: Add WP_Icons_Registry, core icon set and REST endpoint
Introduces a registry class to mediate access to a library of SVG icons. These icons can be queried by directly interfacing with the singleton class `WP_Icons_Registry`, or via the REST API using the following GET endpoints: - /wp/v2/icons - /wp/v2/icons/$name, e.g. /wp/v2/icons/core/audio Modifies the Gutenberg-to-Core copy process to copy from `@wordpress/icons`: - the icons (SVG files) to `wp-includes/icons/library/*.svg` - the manifest file to `wp-includes/icons/manifest.php` For 7.0, the registry remains closed to third-party icons, serving only core icons per the manifest file. Together, these APIs power the new Icon block. Developed in WordPress/gutenberg#72215 Developed in WordPress/gutenberg#74943 Developed in WordPress/wordpress-develop#10909 Props mcsf, wildworks, fabiankaegy, joen, jorgefilipecosta, ntsekouras. Fixes #64651. Built from https://develop.svn.wordpress.org/trunk@61674 git-svn-id: http://core.svn.wordpress.org/trunk@60982 1a063a9b-81f0-0310-95a4-ce76da25c4cd
1 parent 0058ea3 commit e82c8be

File tree

327 files changed

+3076
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

327 files changed

+3076
-1
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
<?php
2+
3+
/**
4+
* Icons API: WP_Icons_Registry class
5+
*
6+
* @package WordPress
7+
* @since 7.0.0
8+
*/
9+
10+
/**
11+
* Core class used for interacting with registered icons.
12+
*
13+
* @since 7.0.0
14+
*/
15+
class WP_Icons_Registry {
16+
/**
17+
* Registered icons array.
18+
*
19+
* @var array[]
20+
*/
21+
private $registered_icons = array();
22+
23+
24+
/**
25+
* Container for the main instance of the class.
26+
*
27+
* @var WP_Icons_Registry|null
28+
*/
29+
private static $instance = null;
30+
31+
/**
32+
* Constructor.
33+
*
34+
* WP_Icons_Registry is a singleton class, so keep this private.
35+
*
36+
* For 7.0, the Icons Registry is closed for third-party icon registry,
37+
* serving only a subset of core icons.
38+
*
39+
* These icons are defined in @wordpress/packages (Gutenberg repository) as
40+
* SVG files and as entries in a single manifest file. On init, the
41+
* registry is loaded with those icons in the manifest which are marked
42+
* `public`.
43+
*/
44+
private function __construct() {
45+
$icons_directory = __DIR__ . '/icons/';
46+
$icons_directory = trailingslashit( $icons_directory );
47+
$manifest_path = $icons_directory . 'manifest.php';
48+
49+
if ( ! is_readable( $manifest_path ) ) {
50+
wp_trigger_error(
51+
__METHOD__,
52+
__( 'Core icon collection manifest is missing or unreadable.' )
53+
);
54+
return;
55+
}
56+
57+
$collection = include $manifest_path;
58+
59+
if ( empty( $collection ) ) {
60+
wp_trigger_error(
61+
__METHOD__,
62+
__( 'Core icon collection manifest is empty or invalid.' )
63+
);
64+
return;
65+
}
66+
67+
foreach ( $collection as $icon_name => $icon_data ) {
68+
if (
69+
empty( $icon_data['filePath'] )
70+
|| ! is_string( $icon_data['filePath'] )
71+
) {
72+
_doing_it_wrong(
73+
__METHOD__,
74+
__( 'Core icon collection manifest must provide valid a "filePath" for each icon.' ),
75+
'7.0.0'
76+
);
77+
return;
78+
}
79+
80+
if ( ! ( $icon_data['public'] ?? false ) ) {
81+
continue;
82+
}
83+
84+
$this->register(
85+
'core/' . $icon_name,
86+
array(
87+
'label' => $icon_data['label'],
88+
'filePath' => $icons_directory . $icon_data['filePath'],
89+
)
90+
);
91+
}
92+
}
93+
94+
/**
95+
* Registers an icon.
96+
*
97+
* @param string $icon_name Icon name including namespace.
98+
* @param array $icon_properties {
99+
* List of properties for the icon.
100+
*
101+
* @type string $label Required. A human-readable label for the icon.
102+
* @type string $content Optional. SVG markup for the icon.
103+
* If not provided, the content will be retrieved from the `filePath` if set.
104+
* If both `content` and `filePath` are not set, the icon will not be registered.
105+
* @type string $filePath Optional. The full path to the file containing the icon content.
106+
* }
107+
* @return bool True if the icon was registered with success and false otherwise.
108+
*/
109+
private function register( $icon_name, $icon_properties ) {
110+
if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) {
111+
_doing_it_wrong(
112+
__METHOD__,
113+
__( 'Icon name must be a string.' ),
114+
'7.0.0'
115+
);
116+
return false;
117+
}
118+
119+
$allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 );
120+
foreach ( array_keys( $icon_properties ) as $key ) {
121+
if ( ! array_key_exists( $key, $allowed_keys ) ) {
122+
_doing_it_wrong(
123+
__METHOD__,
124+
sprintf(
125+
// translators: %s is the name of any user-provided key
126+
__( 'Invalid icon property: "%s".' ),
127+
$key
128+
),
129+
'7.0.0'
130+
);
131+
return false;
132+
}
133+
}
134+
135+
if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) {
136+
_doing_it_wrong(
137+
__METHOD__,
138+
__( 'Icon label must be a string.' ),
139+
'7.0.0'
140+
);
141+
return false;
142+
}
143+
144+
if (
145+
( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) ||
146+
( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) )
147+
) {
148+
_doing_it_wrong(
149+
__METHOD__,
150+
__( 'Icons must provide either `content` or `filePath`.' ),
151+
'7.0.0'
152+
);
153+
return false;
154+
}
155+
156+
if ( isset( $icon_properties['content'] ) ) {
157+
if ( ! is_string( $icon_properties['content'] ) ) {
158+
_doing_it_wrong(
159+
__METHOD__,
160+
__( 'Icon content must be a string.' ),
161+
'7.0.0'
162+
);
163+
return false;
164+
}
165+
166+
$sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] );
167+
if ( empty( $sanitized_icon_content ) ) {
168+
_doing_it_wrong(
169+
__METHOD__,
170+
__( 'Icon content does not contain valid SVG markup.' ),
171+
'7.0.0'
172+
);
173+
return false;
174+
}
175+
}
176+
177+
$icon = array_merge(
178+
$icon_properties,
179+
array( 'name' => $icon_name )
180+
);
181+
182+
$this->registered_icons[ $icon_name ] = $icon;
183+
184+
return true;
185+
}
186+
187+
/**
188+
* Sanitizes the icon SVG content.
189+
*
190+
* Logic borrowed from twentytwenty.
191+
* @see twentytwenty_get_theme_svg
192+
*
193+
* @param string $icon_content The icon SVG content to sanitize.
194+
* @return string The sanitized icon SVG content.
195+
*/
196+
private function sanitize_icon_content( $icon_content ) {
197+
$allowed_tags = array(
198+
'svg' => array(
199+
'class' => true,
200+
'xmlns' => true,
201+
'width' => true,
202+
'height' => true,
203+
'viewbox' => true,
204+
'aria-hidden' => true,
205+
'role' => true,
206+
'focusable' => true,
207+
),
208+
'path' => array(
209+
'fill' => true,
210+
'fill-rule' => true,
211+
'd' => true,
212+
'transform' => true,
213+
),
214+
'polygon' => array(
215+
'fill' => true,
216+
'fill-rule' => true,
217+
'points' => true,
218+
'transform' => true,
219+
'focusable' => true,
220+
),
221+
);
222+
return wp_kses( $icon_content, $allowed_tags );
223+
}
224+
225+
/**
226+
* Retrieves the content of a registered icon.
227+
*
228+
* @param string $icon_name Icon name including namespace.
229+
* @return string|null The content of the icon, if found.
230+
*/
231+
private function get_content( $icon_name ) {
232+
if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) {
233+
$content = file_get_contents(
234+
$this->registered_icons[ $icon_name ]['filePath']
235+
);
236+
$content = $this->sanitize_icon_content( $content );
237+
238+
if ( empty( $content ) ) {
239+
wp_trigger_error(
240+
__METHOD__,
241+
__( 'Icon content does not contain valid SVG markup.' )
242+
);
243+
return null;
244+
}
245+
246+
$this->registered_icons[ $icon_name ]['content'] = $content;
247+
}
248+
return $this->registered_icons[ $icon_name ]['content'];
249+
}
250+
251+
/**
252+
* Retrieves an array containing the properties of a registered icon.
253+
*
254+
*
255+
* @param string $icon_name Icon name including namespace.
256+
* @return array|null Registered icon properties or `null` if the icon is not registered.
257+
*/
258+
public function get_registered_icon( $icon_name ) {
259+
if ( ! $this->is_registered( $icon_name ) ) {
260+
return null;
261+
}
262+
263+
$icon = $this->registered_icons[ $icon_name ];
264+
$icon['content'] = $icon['content'] ?? $this->get_content( $icon_name );
265+
266+
return $icon;
267+
}
268+
269+
/**
270+
* Retrieves all registered icons.
271+
*
272+
* @param string $search Optional. Search term by which to filter the icons.
273+
* @return array[] Array of arrays containing the registered icon properties.
274+
*/
275+
public function get_registered_icons( $search = '' ) {
276+
$icons = array();
277+
278+
foreach ( $this->registered_icons as $icon ) {
279+
if ( ! empty( $search ) && false === stripos( $icon['name'], $search ) ) {
280+
continue;
281+
}
282+
283+
$icon['content'] = $icon['content'] ?? $this->get_content( $icon['name'] );
284+
$icons[] = $icon;
285+
}
286+
287+
return $icons;
288+
}
289+
290+
/**
291+
* Checks if an icon is registered.
292+
*
293+
*
294+
* @param string $icon_name Icon name including namespace.
295+
* @return bool True if the icon is registered, false otherwise.
296+
*/
297+
public function is_registered( $icon_name ) {
298+
return isset( $this->registered_icons[ $icon_name ] );
299+
}
300+
301+
/**
302+
* Utility method to retrieve the main instance of the class.
303+
*
304+
* The instance will be created if it does not exist yet.
305+
*
306+
*
307+
* @return WP_Icons_Registry The main instance.
308+
*/
309+
public static function get_instance() {
310+
if ( null === self::$instance ) {
311+
self::$instance = new self();
312+
}
313+
314+
return self::$instance;
315+
}
316+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)