From aa66222073068237a6b6ae78d831d1c512564ae5 Mon Sep 17 00:00:00 2001 From: jeremyfelt Date: Wed, 5 Nov 2025 11:46:20 -0800 Subject: [PATCH] Add support for registered theme image sizes --- blocks/build/theme-image/index.asset.php | 2 +- blocks/build/theme-image/index.js | 2 +- blocks/src/theme-image/block.json | 12 +- blocks/src/theme-image/index.js | 191 ++++------------------- src/Block.php | 39 ++--- src/Init.php | 3 +- src/StyleRegistry.php | 160 +++++++++++++++++++ theme-image-block.php | 23 +++ 8 files changed, 240 insertions(+), 192 deletions(-) create mode 100644 src/StyleRegistry.php diff --git a/blocks/build/theme-image/index.asset.php b/blocks/build/theme-image/index.asset.php index a9f8ca2..1bdc2a3 100644 --- a/blocks/build/theme-image/index.asset.php +++ b/blocks/build/theme-image/index.asset.php @@ -1 +1 @@ - array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '7a2b70d857e40af2cf1b'); + array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'f97d82e990dc108628c4'); diff --git a/blocks/build/theme-image/index.js b/blocks/build/theme-image/index.js index d007112..884e7ee 100644 --- a/blocks/build/theme-image/index.js +++ b/blocks/build/theme-image/index.js @@ -1 +1 @@ -(()=>{"use strict";const e=window.wp.blockEditor,l=window.wp.blocks,t=window.wp.components,a=window.wp.i18n,n=window.wp.element,i=JSON.parse('{"UU":"happyprime/theme-image"}');(0,l.registerBlockType)(i.UU,{edit:function({attributes:l,setAttributes:i}){const{themeImage:o,imageSize:r,inlineSVG:p,linkUrl:c,linkTarget:m,linkRel:s,width:h,height:u}=l,[d,g]=(0,n.useState)(!1),[v,_]=(0,n.useState)(!1),[y,b]=(0,n.useState)(null),[k,w]=(0,n.useState)(!1),f=h&&(h.includes("clamp")||h.includes("calc")||h.includes("min")||h.includes("max")),R=u&&(u.includes("clamp")||u.includes("calc")||u.includes("min")||u.includes("max")),E=happyprimeData?.images||[],C=[{value:"",label:(0,a.__)("Select an image","happyprime")},...E.map(e=>({value:e.slug,label:e.label}))],S=E.find(e=>e.slug===o),x=[{value:"original",label:(0,a.__)("Original","happyprime")}];S&&S.variations&&Object.keys(S.variations).forEach(e=>{const l=e.charAt(0).toUpperCase()+e.slice(1);x.push({value:e,label:l})});let T=S?.value||"";S&&"original"!==r&&S.variations&&S.variations[r]&&(T=S.variations[r].path),(0,n.useEffect)(()=>{if(p&&T&&T.toLowerCase().endsWith(".svg")){const e=`${happyprimeData.themeUrl}/${T}`;fetch(e).then(e=>e.text()).then(e=>{const l=e.replace(/^<\?xml\s+.*?\?>\s*/s,"");b(l)}).catch(e=>{console.error("Failed to fetch SVG:",e),b(null)})}else b(null)},[p,T]);const U=(0,e.useBlockProps)({className:p?"has-inline-svg":""}),I=T&&happyprimeData?.themeUrl?`${happyprimeData.themeUrl}/${T}`:"",B=T&&T.toLowerCase().endsWith(".svg");let $,N=null;if(p&&y){const e=[];h&&!h.includes(";")?e.push(`width: ${h}`):h||e.push("width: 100%"),u&&!u.includes(";")&&e.push(`height: ${u}`);const l=e.join("; "),t=y.match(/]*)>/);if(t){const e=t[1].match(/style="([^"]*)"/);if(e){const t=e[1];N=y.replace(/]*)>/,e=>e.replace(/style="[^"]*"/,`style="${t}; ${l}"`))}else N=y.replace(/]*)>/,``)}}if(I)if(p&&N)$=c?React.createElement("a",{href:c,target:m,rel:s,dangerouslySetInnerHTML:{__html:N}}):null;else{const e={};h&&!h.includes(";")&&(e.width=h),u&&!u.includes(";")&&(e.height=u);const l=React.createElement("img",{src:I,alt:S?.alt||(0,a.__)("Theme image preview","happyprime"),style:Object.keys(e).length>0?e:void 0});$=c?React.createElement("a",{href:c,target:m,rel:s},l):l}else $=React.createElement("div",{className:"theme-image-placeholder"},(0,a.__)("Select a theme image from the sidebar","happyprime"));const O=p&&N&&!c?{...U,dangerouslySetInnerHTML:{__html:N}}:U;return React.createElement(React.Fragment,null,I&&React.createElement(e.BlockControls,{group:"block"},React.createElement(t.ToolbarButton,{icon:"admin-links",label:(0,a.__)("Link","happyprime"),onClick:()=>w(!0),isActive:!!c}),c&&React.createElement(t.ToolbarButton,{icon:"editor-unlink",label:(0,a.__)("Unlink","happyprime"),onClick:()=>{i({linkUrl:"",linkTarget:"",linkRel:""})}})),k&&React.createElement(t.Popover,{position:"bottom center",onClose:()=>w(!1),anchor:document.querySelector(".wp-block-happyprime-theme-image")},React.createElement(e.__experimentalLinkControl,{value:{url:c,opensInNewTab:"_blank"===m,nofollow:s?.includes("nofollow")},onChange:e=>{const l=[];e?.opensInNewTab&&l.push("noopener","noreferrer"),e?.nofollow&&l.push("nofollow"),i({linkUrl:e?.url||"",linkTarget:e?.opensInNewTab?"_blank":"",linkRel:l.join(" ")})},onRemove:()=>{i({linkUrl:"",linkTarget:"",linkRel:""}),w(!1)},settings:[{id:"opensInNewTab",title:(0,a.__)("Open in new tab","happyprime")},{id:"nofollow",title:(0,a.__)("Mark as nofollow","happyprime")}]})),React.createElement(e.InspectorControls,null,React.createElement(t.PanelBody,{title:(0,a.__)("Settings","happyprime"),initialOpen:!0},React.createElement(t.SelectControl,{label:(0,a.__)("Theme Image","happyprime"),value:o,options:C,onChange:e=>{i({themeImage:e,imageSize:"original"})},help:(0,a.__)("Select an image from the theme directory.","happyprime")}),S&&S.variations&&Object.keys(S.variations).length>0&&React.createElement(t.SelectControl,{label:(0,a.__)("Size","happyprime"),value:r,options:x,onChange:e=>i({imageSize:e}),help:(0,a.__)("Select the image size variation.","happyprime")}),d||f?React.createElement(React.Fragment,null,React.createElement(t.TextControl,{label:(0,a.__)("Width","happyprime"),value:h,onChange:e=>i({width:e}),help:h&&h.includes(";")?(0,a.__)("Invalid value: semicolons are not allowed","happyprime"):(0,a.__)("Enter any valid CSS value","happyprime"),placeholder:"clamp(10rem, 50vw, 30rem)"}),React.createElement(t.Button,{variant:"link",onClick:()=>{g(!1),i({width:""})},style:{display:"block",marginTop:"-8px",marginBottom:"16px"}},(0,a.__)("Use preset units","happyprime"))):React.createElement(React.Fragment,null,React.createElement(t.__experimentalUnitControl,{label:(0,a.__)("Width","happyprime"),labelPosition:"top",__unstableInputWidth:"80px",value:h,onChange:e=>i({width:e}),units:[{value:"px",label:"px",default:0},{value:"%",label:"%",default:100},{value:"em",label:"em",default:0},{value:"rem",label:"rem",default:0},{value:"vw",label:"vw",default:0},{value:"vh",label:"vh",default:0},{value:"lh",label:"lh",default:1}]}),React.createElement(t.Button,{variant:"link",onClick:()=>g(!0),style:{display:"block",marginTop:"-8px",marginBottom:"16px"}},(0,a.__)("Use custom CSS","happyprime"))),v||R?React.createElement(React.Fragment,null,React.createElement(t.TextControl,{label:(0,a.__)("Height","happyprime"),value:u,onChange:e=>i({height:e}),help:u&&u.includes(";")?(0,a.__)("Invalid value: semicolons are not allowed","happyprime"):(0,a.__)("Enter any valid CSS value","happyprime"),placeholder:"clamp(10rem, 50vh, 30rem)"}),React.createElement(t.Button,{variant:"link",onClick:()=>{_(!1),i({height:""})},style:{display:"block",marginTop:"-8px",marginBottom:"16px"}},(0,a.__)("Use preset units","happyprime"))):React.createElement(React.Fragment,null,React.createElement(t.__experimentalUnitControl,{label:(0,a.__)("Height","happyprime"),labelPosition:"top",__unstableInputWidth:"80px",value:u,onChange:e=>i({height:e}),units:[{value:"px",label:"px",default:0},{value:"%",label:"%",default:100},{value:"em",label:"em",default:0},{value:"rem",label:"rem",default:0},{value:"vw",label:"vw",default:0},{value:"vh",label:"vh",default:0},{value:"lh",label:"lh",default:1}]}),React.createElement(t.Button,{variant:"link",onClick:()=>_(!0),style:{display:"block",marginTop:"-8px",marginBottom:"16px"}},(0,a.__)("Use custom CSS","happyprime"))),B&&React.createElement(t.ToggleControl,{label:(0,a.__)("Inline SVG","happyprime"),checked:p,onChange:e=>i({inlineSVG:e}),help:(0,a.__)("Render SVG code inline for better styling control.","happyprime")}))),React.createElement("div",O,$))}})})(); \ No newline at end of file +(()=>{"use strict";const e=window.wp.blockEditor,t=window.wp.blocks,l=window.wp.components,a=window.wp.i18n,n=window.wp.element,i=JSON.parse('{"UU":"happyprime/theme-image"}');(0,t.registerBlockType)(i.UU,{edit:function({attributes:t,setAttributes:i}){const{themeImage:o,imageSize:r,imageStyle:p,inlineSVG:s,linkUrl:c,linkTarget:m,linkRel:h}=t,[g,u]=(0,n.useState)(null),[y,_]=(0,n.useState)(!1),d=happyprimeData?.images||[],b=happyprimeData?.styles||[],w=[{value:"",label:(0,a.__)("Select an image","happyprime")},...d.map(e=>({value:e.slug,label:e.label}))],k=d.find(e=>e.slug===o),v=[{value:"original",label:(0,a.__)("Original","happyprime")}];k&&k.variations&&Object.keys(k.variations).forEach(e=>{const t=e.charAt(0).toUpperCase()+e.slice(1);v.push({value:e,label:t})});let f=k?.value||"";k&&"original"!==r&&k.variations&&k.variations[r]&&(f=k.variations[r].path),(0,n.useEffect)(()=>{if(s&&f&&f.toLowerCase().endsWith(".svg")){const e=`${happyprimeData.themeUrl}/${f}`;fetch(e).then(e=>e.text()).then(e=>{const t=e.replace(/^<\?xml\s+.*?\?>\s*/s,"");u(t)}).catch(e=>{console.error("Failed to fetch SVG:",e),u(null)})}else u(null)},[s,f]);const S=(0,e.useBlockProps)({className:s?"has-inline-svg":""}),R=f&&happyprimeData?.themeUrl?`${happyprimeData.themeUrl}/${f}`:"",E=f&&f.toLowerCase().endsWith(".svg"),C=b.find(e=>e.slug===p),T=C?.width||"",U=C?.height||"";let I,$=null;if(s&&g){const e=[];T&&!T.includes(";")?e.push(`width: ${T}`):T||e.push("width: 100%"),U&&!U.includes(";")&&e.push(`height: ${U}`);const t=e.join("; "),l=g.match(/]*)>/);if(l){const e=l[1].match(/style="([^"]*)"/);if(e){const l=e[1];$=g.replace(/]*)>/,e=>e.replace(/style="[^"]*"/,`style="${l}; ${t}"`))}else $=g.replace(/]*)>/,``)}}if(R)if(s&&$)I=c?React.createElement("a",{href:c,target:m,rel:h,dangerouslySetInnerHTML:{__html:$}}):null;else{const e={};T&&!T.includes(";")&&(e.width=T),U&&!U.includes(";")&&(e.height=U);const t=React.createElement("img",{src:R,alt:k?.alt||(0,a.__)("Theme image preview","happyprime"),style:Object.keys(e).length>0?e:void 0});I=c?React.createElement("a",{href:c,target:m,rel:h},t):t}else I=React.createElement("div",{className:"theme-image-placeholder"},(0,a.__)("Select a theme image from the sidebar","happyprime"));const N=s&&$&&!c?{...S,dangerouslySetInnerHTML:{__html:$}}:S;return React.createElement(React.Fragment,null,R&&React.createElement(e.BlockControls,{group:"block"},React.createElement(l.ToolbarButton,{icon:"admin-links",label:(0,a.__)("Link","happyprime"),onClick:()=>_(!0),isActive:!!c}),c&&React.createElement(l.ToolbarButton,{icon:"editor-unlink",label:(0,a.__)("Unlink","happyprime"),onClick:()=>{i({linkUrl:"",linkTarget:"",linkRel:""})}})),y&&React.createElement(l.Popover,{position:"bottom center",onClose:()=>_(!1),anchor:document.querySelector(".wp-block-happyprime-theme-image")},React.createElement(e.__experimentalLinkControl,{value:{url:c,opensInNewTab:"_blank"===m,nofollow:h?.includes("nofollow")},onChange:e=>{const t=[];e?.opensInNewTab&&t.push("noopener","noreferrer"),e?.nofollow&&t.push("nofollow"),i({linkUrl:e?.url||"",linkTarget:e?.opensInNewTab?"_blank":"",linkRel:t.join(" ")})},onRemove:()=>{i({linkUrl:"",linkTarget:"",linkRel:""}),_(!1)},settings:[{id:"opensInNewTab",title:(0,a.__)("Open in new tab","happyprime")},{id:"nofollow",title:(0,a.__)("Mark as nofollow","happyprime")}]})),React.createElement(e.InspectorControls,null,React.createElement(l.PanelBody,{title:(0,a.__)("Settings","happyprime"),initialOpen:!0},React.createElement(l.SelectControl,{label:(0,a.__)("Theme Image","happyprime"),value:o,options:w,onChange:e=>{i({themeImage:e,imageSize:"original"})},help:(0,a.__)("Select an image from the theme directory.","happyprime")}),k&&k.variations&&Object.keys(k.variations).length>0&&React.createElement(l.SelectControl,{label:(0,a.__)("Size","happyprime"),value:r,options:v,onChange:e=>i({imageSize:e}),help:(0,a.__)("Select the image size variation.","happyprime")}),React.createElement(l.SelectControl,{label:(0,a.__)("Image Style","happyprime"),value:p,options:[{value:"",label:(0,a.__)("Default","happyprime")},...b.map(e=>({value:e.slug,label:e.name}))],onChange:e=>i({imageStyle:e}),help:(0,a.__)("Select a registered style to control image dimensions.","happyprime")}),E&&React.createElement(l.ToggleControl,{label:(0,a.__)("Inline SVG","happyprime"),checked:s,onChange:e=>i({inlineSVG:e}),help:(0,a.__)("Render SVG code inline for better styling control.","happyprime")}))),React.createElement("div",N,I))}})})(); \ No newline at end of file diff --git a/blocks/src/theme-image/block.json b/blocks/src/theme-image/block.json index 9d2bf23..5925298 100644 --- a/blocks/src/theme-image/block.json +++ b/blocks/src/theme-image/block.json @@ -26,6 +26,10 @@ "type": "string", "default": "original" }, + "imageStyle": { + "type": "string", + "default": "" + }, "inlineSVG": { "type": "boolean", "default": false @@ -41,14 +45,6 @@ "linkRel": { "type": "string", "default": "" - }, - "width": { - "type": "string", - "default": "" - }, - "height": { - "type": "string", - "default": "" } }, "textdomain": "happyprime", diff --git a/blocks/src/theme-image/index.js b/blocks/src/theme-image/index.js index 11f18f3..6400065 100644 --- a/blocks/src/theme-image/index.js +++ b/blocks/src/theme-image/index.js @@ -11,12 +11,9 @@ import { registerBlockType } from '@wordpress/blocks'; import { PanelBody, SelectControl, - TextControl, ToggleControl, - Button, ToolbarButton, Popover, - __experimentalUnitControl as UnitControl, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState, useEffect } from '@wordpress/element'; @@ -38,34 +35,18 @@ function Edit({ attributes, setAttributes }) { const { themeImage, imageSize, + imageStyle, inlineSVG, linkUrl, linkTarget, linkRel, - width, - height, } = attributes; - const [isCustomWidth, setIsCustomWidth] = useState(false); - const [isCustomHeight, setIsCustomHeight] = useState(false); const [svgContent, setSvgContent] = useState(null); const [isEditingLink, setIsEditingLink] = useState(false); - // Check if width/height contains custom CSS functions - const hasCustomWidthCSS = - width && - (width.includes('clamp') || - width.includes('calc') || - width.includes('min') || - width.includes('max')); - const hasCustomHeightCSS = - height && - (height.includes('clamp') || - height.includes('calc') || - height.includes('min') || - height.includes('max')); - - // Get registered theme images from localized data + // Get registered theme images and styles from localized data const registeredImages = happyprimeData?.images || []; + const registeredStyles = happyprimeData?.styles || []; // Build the options array for the SelectControl const themeImages = [ @@ -139,10 +120,17 @@ function Edit({ attributes, setAttributes }) { : ''; const isSVG = imagePath && imagePath.toLowerCase().endsWith('.svg'); + // Get the selected style's dimensions + const currentStyle = registeredStyles.find( + (style) => style.slug === imageStyle + ); + const width = currentStyle?.width || ''; + const height = currentStyle?.height || ''; + // Process inline SVG if needed let processedSvg = null; if (inlineSVG && svgContent) { - // Build styles array - use defaults if not specified for editor preview + // Build styles array for editor preview // Validate to prevent CSS injection by rejecting values with semicolons const styles = []; if (width && !width.includes(';')) { @@ -367,145 +355,24 @@ function Edit({ attributes, setAttributes }) { /> )} - {isCustomWidth || hasCustomWidthCSS ? ( - <> - - setAttributes({ width: value }) - } - help={ - width && width.includes(';') - ? __( - 'Invalid value: semicolons are not allowed', - 'happyprime' - ) - : __( - 'Enter any valid CSS value', - 'happyprime' - ) - } - placeholder="clamp(10rem, 50vw, 30rem)" - /> - - - ) : ( - <> - - setAttributes({ width: value }) - } - units={[ - { value: 'px', label: 'px', default: 0 }, - { value: '%', label: '%', default: 100 }, - { value: 'em', label: 'em', default: 0 }, - { value: 'rem', label: 'rem', default: 0 }, - { value: 'vw', label: 'vw', default: 0 }, - { value: 'vh', label: 'vh', default: 0 }, - { value: 'lh', label: 'lh', default: 1 }, - ]} - /> - - - )} - - {isCustomHeight || hasCustomHeightCSS ? ( - <> - - setAttributes({ height: value }) - } - help={ - height && height.includes(';') - ? __( - 'Invalid value: semicolons are not allowed', - 'happyprime' - ) - : __( - 'Enter any valid CSS value', - 'happyprime' - ) - } - placeholder="clamp(10rem, 50vh, 30rem)" - /> - - - ) : ( - <> - - setAttributes({ height: value }) - } - units={[ - { value: 'px', label: 'px', default: 0 }, - { value: '%', label: '%', default: 100 }, - { value: 'em', label: 'em', default: 0 }, - { value: 'rem', label: 'rem', default: 0 }, - { value: 'vw', label: 'vw', default: 0 }, - { value: 'vh', label: 'vh', default: 0 }, - { value: 'lh', label: 'lh', default: 1 }, - ]} - /> - - - )} + ({ + value: style.slug, + label: style.name, + })), + ]} + onChange={(value) => + setAttributes({ imageStyle: value }) + } + help={__( + 'Select a registered style to control image dimensions.', + 'happyprime' + )} + /> {isSVG && ( $attributes Block attributes. { - * @type string $themeImage The slug of the theme image to display. Required. - * @type string $imageSize The size variation to display. Default 'original'. - * @type bool $inlineSVG Whether to inline SVG content instead of using an img tag. Default false. - * @type string $linkUrl URL for wrapping the image in a link. Default empty string. - * @type string $linkTarget Target attribute for the link (e.g., '_blank'). Default empty string. - * @type string $linkRel Rel attribute for the link (e.g., 'nofollow'). Default empty string. - * @type string $width CSS width value for the image. Default empty string. - * @type string $height CSS height value for the image. Default empty string. + * @type string $themeImage The slug of the theme image to display. Required. + * @type string $imageSize The size variation to display. Default 'original'. + * @type string $imageStyle The slug of the registered style to apply. Default empty string. + * @type bool $inlineSVG Whether to inline SVG content instead of using an img tag. Default false. + * @type string $linkUrl URL for wrapping the image in a link. Default empty string. + * @type string $linkTarget Target attribute for the link (e.g., '_blank'). Default empty string. + * @type string $linkRel Rel attribute for the link (e.g., 'nofollow'). Default empty string. * } * @param string $content Block content. * @@ -49,19 +48,21 @@ public static function render( $attributes, $content ): string { $link_target = isset( $attributes['linkTarget'] ) ? esc_attr( $attributes['linkTarget'] ) : ''; $link_rel = isset( $attributes['linkRel'] ) ? esc_attr( $attributes['linkRel'] ) : ''; - // Validate and sanitize width/height. Reject values with semicolons to prevent CSS injection. + // Get width/height from registered style if imageStyle is set. $width = ''; $height = ''; - if ( isset( $attributes['width'] ) && ! empty( $attributes['width'] ) ) { - $width_value = $attributes['width']; - if ( false === strpos( $width_value, ';' ) ) { - $width = esc_attr( $width_value ); - } - } - if ( isset( $attributes['height'] ) && ! empty( $attributes['height'] ) ) { - $height_value = $attributes['height']; - if ( false === strpos( $height_value, ';' ) ) { - $height = esc_attr( $height_value ); + if ( isset( $attributes['imageStyle'] ) && ! empty( $attributes['imageStyle'] ) ) { + $style_slug = sanitize_key( $attributes['imageStyle'] ); + $style_data = StyleRegistry::get( $style_slug ); + + if ( $style_data ) { + // Validate to prevent CSS injection by rejecting values with semicolons. + if ( ! empty( $style_data['width'] ) && false === strpos( $style_data['width'], ';' ) ) { + $width = esc_attr( $style_data['width'] ); + } + if ( ! empty( $style_data['height'] ) && false === strpos( $style_data['height'], ';' ) ) { + $height = esc_attr( $style_data['height'] ); + } } } diff --git a/src/Init.php b/src/Init.php index 6a0f23f..2eafb71 100644 --- a/src/Init.php +++ b/src/Init.php @@ -38,13 +38,14 @@ public static function enqueue_editor_assets(): void { $script_handle = generate_block_asset_handle( 'happyprime/theme-image', 'editorScript' ); if ( wp_script_is( $script_handle, 'registered' ) ) { - // Pass theme URL and registered images to JavaScript. + // Pass theme URL, registered images, and styles to JavaScript. wp_localize_script( $script_handle, 'happyprimeData', array( 'themeUrl' => get_template_directory_uri(), 'images' => Registry::get_for_editor(), + 'styles' => StyleRegistry::get_for_editor(), ) ); } diff --git a/src/StyleRegistry.php b/src/StyleRegistry.php new file mode 100644 index 0000000..5c6b8f5 --- /dev/null +++ b/src/StyleRegistry.php @@ -0,0 +1,160 @@ + + */ + private static array $styles = array(); + + /** + * Register a theme image style. + * + * @param string $slug Unique identifier for the style. + * @param array $args { + * Style configuration arguments. + * + * @type string $name Display name for the style (required). + * @type string $width CSS width value (optional). + * @type string $height CSS height value (optional). + * } + * + * @return bool True if registered successfully, false otherwise. + */ + public static function register( string $slug, array $args ): bool { + if ( empty( $slug ) || empty( $args['name'] ) ) { + return false; + } + + // Sanitize the slug. + $slug = sanitize_key( $slug ); + + // This style is already registered. + if ( self::has( $slug ) ) { + return false; + } + + // Set defaults for optional fields. + $defaults = array( + 'name' => '', + 'width' => '', + 'height' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + // Store the registered style. + self::$styles[ $slug ] = array( + 'name' => sanitize_text_field( $args['name'] ), + 'width' => sanitize_text_field( $args['width'] ), + 'height' => sanitize_text_field( $args['height'] ), + ); + + return true; + } + + /** + * Get all registered styles. + * + * @return array Registered styles, keyed by style slug. + */ + public static function get_all(): array { + return self::$styles; + } + + /** + * Get a specific registered style by slug. + * + * @param string $slug Style slug. + * + * @return array{ + * name: string, + * width: string, + * height: string + * }|null Style data or null if not found. + */ + public static function get( string $slug ): ?array { + $slug = sanitize_key( $slug ); + return self::$styles[ $slug ] ?? null; + } + + /** + * Check if a style is registered. + * + * @param string $slug Style slug. + * + * @return bool True if registered, false otherwise. + */ + public static function has( string $slug ): bool { + $slug = sanitize_key( $slug ); + return isset( self::$styles[ $slug ] ); + } + + /** + * Unregister a theme image style. + * + * @param string $slug Style slug. + * + * @return bool True if unregistered, false if not found. + */ + public static function unregister( string $slug ): bool { + $slug = sanitize_key( $slug ); + + if ( ! isset( self::$styles[ $slug ] ) ) { + return false; + } + + unset( self::$styles[ $slug ] ); + return true; + } + + /** + * Prepare styles for JavaScript consumption. + * + * Transforms the registered styles into a format suitable for the block editor. + * + * @return array> Styles formatted for JavaScript. + */ + public static function get_for_editor(): array { + $styles = array(); + + foreach ( self::$styles as $slug => $data ) { + $styles[] = array( + 'slug' => $slug, + 'name' => $data['name'], + 'width' => $data['width'], + 'height' => $data['height'], + ); + } + + return $styles; + } + + /** + * Clear all registered styles. + * + * Primarily useful for testing. + */ + public static function clear(): void { + self::$styles = array(); + } +} diff --git a/theme-image-block.php b/theme-image-block.php index c604b88..a089fb8 100644 --- a/theme-image-block.php +++ b/theme-image-block.php @@ -82,4 +82,27 @@ function register_theme_image( string $slug, array $args ): bool { return \HappyPrime\ThemeImageBlock\Registry::register( $slug, $args ); } +/** + * Register a theme image style. + * + * This function can be used by themes and plugins to register styles that + * control the dimensions of images in the Theme Image block. + * + * @since 0.1.0 + * + * @param string $slug Unique identifier for the style. + * @param array $args { + * Style configuration arguments. + * + * @type string $name Display name for the style (required). + * @type string $width CSS width value (optional). + * @type string $height CSS height value (optional). + * } + * + * @return bool True if registered successfully, false otherwise. + */ +function register_theme_image_style( string $slug, array $args ): bool { + return \HappyPrime\ThemeImageBlock\StyleRegistry::register( $slug, $args ); +} + add_action( 'plugins_loaded', [ Init::class, 'init' ] );