From 8659df6cfe33005b3932e914b351f88ca9d90c7c Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 20 May 2025 12:24:39 +1000 Subject: [PATCH] Add some demo block supports styles to the schema. --- packages/client-features/src/blocks.ts | 41 +++- packages/client-features/src/styles.ts | 252 +++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 packages/client-features/src/styles.ts diff --git a/packages/client-features/src/blocks.ts b/packages/client-features/src/blocks.ts index f828ec0..e0f771d 100644 --- a/packages/client-features/src/blocks.ts +++ b/packages/client-features/src/blocks.ts @@ -15,6 +15,7 @@ import type { Feature } from '@automattic/wp-feature-api'; * Internal dependencies */ import { isInEditor } from './utils'; +import { getStyleSchema } from './styles'; /** * Client-side feature to insert a paragraph block. @@ -36,6 +37,7 @@ export const insertParagraphBlock: Feature = { type: 'string', description: __( 'Text content for the paragraph.' ), }, + style: getStyleSchema( 'core/paragraph' ), }, }, output_schema: { @@ -46,18 +48,25 @@ export const insertParagraphBlock: Feature = { }, required: [ 'success', 'blockType' ], }, - callback: ( args: { content: string } ) => { + callback: ( args: { + content: string; + style?: Record< string, unknown >; + } ) => { if ( typeof args?.content !== 'string' ) { throw new Error( 'Content argument is missing or invalid for paragraph block.' ); } + try { const content = args.content .replace( /\\n/g, '\n' ) // First replace escaped newlines .replace( /\n/g, '
' ); // Then replace actual newlines with
const newBlock = createBlock( 'core/paragraph', { content, + style: { + ...( args?.style || {} ), + }, } ); if ( ! newBlock ) { throw new Error( 'Failed to create paragraph block.' ); @@ -98,6 +107,7 @@ export const insertHeadingBlock: Feature = { type: 'integer', description: __( 'Heading level (intended range 1–6).' ), }, + style: getStyleSchema( 'core/heading' ), }, required: [ 'content' ], }, @@ -110,7 +120,11 @@ export const insertHeadingBlock: Feature = { }, required: [ 'success', 'blockType', 'level' ], }, - callback: ( args: { content: string; level?: number } ) => { + callback: ( args: { + content: string; + level?: number; + style?: Record< string, unknown >; + } ) => { if ( ! args?.content ) { throw new Error( 'Content is required for heading block.' ); } @@ -122,6 +136,9 @@ export const insertHeadingBlock: Feature = { const newBlock = createBlock( 'core/heading', { content: args.content, level: headingLevel, + style: { + ...( args?.style || {} ), + }, } ); if ( ! newBlock ) { throw new Error( 'Failed to create heading block.' ); @@ -166,6 +183,7 @@ export const insertQuoteBlock: Feature = { type: 'string', description: __( 'Optional citation for the quote.' ), }, + style: getStyleSchema( 'core/quote' ), }, required: [ 'value' ], }, @@ -177,7 +195,11 @@ export const insertQuoteBlock: Feature = { }, required: [ 'success', 'clientId' ], }, - callback: ( args: { value: string; citation?: string } ) => { + callback: ( args: { + value: string; + citation?: string; + style?: Record< string, unknown >; + } ) => { if ( typeof args?.value !== 'string' ) { throw new Error( 'Value argument is missing or invalid for quote block.' @@ -193,6 +215,9 @@ export const insertQuoteBlock: Feature = { 'core/quote', { citation: args.citation || '', + style: { + ...( args?.style || {} ), + }, }, [ innerParagraph ] ); @@ -238,6 +263,7 @@ export const insertListBlock: Feature = { 'Whether the list should be ordered (numbered).' ), }, + style: getStyleSchema( 'core/list' ), }, required: [ 'values' ], }, @@ -249,7 +275,11 @@ export const insertListBlock: Feature = { }, required: [ 'success', 'clientId' ], }, - callback: ( args: { values: string[]; ordered?: boolean } ) => { + callback: ( args: { + values: string[]; + ordered?: boolean; + style?: Record< string, unknown >; + } ) => { if ( ! Array.isArray( args?.values ) || args.values.length === 0 ) { throw new Error( 'Values argument must be a non-empty array for list block.' @@ -266,6 +296,9 @@ export const insertListBlock: Feature = { 'core/list', { ordered: !! args.ordered, + style: { + ...( args?.style || {} ), + }, }, innerListItemBlocks ); diff --git a/packages/client-features/src/styles.ts b/packages/client-features/src/styles.ts new file mode 100644 index 0000000..59967fc --- /dev/null +++ b/packages/client-features/src/styles.ts @@ -0,0 +1,252 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Get the style schema for a core block. + * @param blockName - The name of the block to get the style schema for. + * @return The style schema for the block. + */ +export const getStyleSchema = ( blockName: string ) => { + const schemaDescription = blockName + ? sprintf( + /* translators: %s is the name of the block. */ + __( 'Style object for the %s block.' ), + blockName + ) + : __( 'Style object for the block.' ); + + // Could this be parsed from https://github.com/WordPress/gutenberg/blob/trunk/schemas/json/theme.json? + + return { + type: 'object', + description: schemaDescription, + properties: { + typography: { + type: 'object', + description: __( 'Typography object for the block style.' ), + properties: { + fontSize: { + type: 'string', + description: __( 'Font size for the block style.' ), + }, + lineHeight: { + type: 'number', + description: __( 'Line height for the block style.' ), + }, + fontStyle: { + type: 'string', + description: __( 'Font style for the block style.' ), + enum: [ 'normal', 'italic', 'bold', 'bold italic' ], + }, + letterSpacing: { + type: 'string', + description: __( + 'Letter spacing for the block style.' + ), + }, + fontWeight: { + type: 'number', + description: __( 'Font weight for the block style.' ), + pattern: '^[1-9]00$', + }, + textTransform: { + type: 'string', + description: __( + 'Text transform for the block style.' + ), + enum: [ + 'none', + 'uppercase', + 'lowercase', + 'capitalize', + ], + }, + textDecoration: { + type: 'string', + description: __( + 'Text decoration for the block style.' + ), + enum: [ 'none', 'underline', 'line-through' ], + }, + }, + }, + border: { + description: __( 'Border object for the block style.' ), + type: 'object', + properties: { + color: { + description: __( + 'Border color for the block style in rgb, rgba or hex format.' + ), + type: 'string', + }, + radius: { + description: __( + 'Border radius for the block style with px unit.' + ), + type: 'string', + pattern: '^[0-9]+px$', + }, + style: { + description: __( 'Border style for the block style.' ), + type: 'string', + enum: [ 'solid', 'dashed', 'dotted', 'double' ], + }, + width: { + description: __( + 'Border width for the block style with px unit.' + ), + type: 'string', + pattern: '^[0-9]+px$', + }, + }, + additionalProperties: false, + }, + color: { + type: 'object', + description: __( 'Color object for the block style.' ), + properties: { + text: { + type: 'string', + description: __( + 'Text color for the block style in rgb, rgba or hex format.' + ), + }, + background: { + type: 'string', + description: __( + 'Background color for the block style in rgb, rgba or hex format.' + ), + }, + }, + }, + spacing: { + type: 'object', + description: __( 'Spacing object for the block style.' ), + properties: { + padding: { + type: 'string', + description: __( + 'Padding for the block style with px, rem, em or percentage unit.' + ), + }, + margin: { + type: 'string', + description: __( + 'Margin for the block style with px, rem, em or percentage unit.' + ), + }, + }, + }, + }, + }; +}; + +/** + + Get a core block by name. Could rather check the block support for the block. + https://github.com/WordPress/gutenberg/blob/8889f82eda340ea66c83e945098423ed1ae3f5d3/packages/block-editor/src/hooks/supports.js + Or do it from the backend via registry. + + + export const getBlockStyleSchema = ( + blockName: string + ): Record< string, unknown > | null => { + const foundBlock = getBlockTypes().find( + ( block ) => block.name === blockName + ); + console.log( 'foundBlock', blockName, foundBlock ); + if ( ! foundBlock ) { + return {}; + } + const validStyleGroups = { + typography: 'typography', + color: 'color', + spacing: 'spacing', + __experimentalBorder: 'border', + }; + + const validStyleSubkeys = { + typography: { + fontSize: 'fontSize', + lineHeight: 'lineHeight', + __experimentalFontStyle: 'fontStyle', + __experimentalLetterSpacing: 'letterSpacing', + __experimentalFontWeight: 'fontWeight', + __experimentalTextTransform: 'textTransform', + __experimentalTextDecoration: 'textDecoration', + }, + border: { + color: 'color', + radius: 'radius', + style: 'style', + width: 'width', + }, + color: { + background: 'background', + text: 'text', + }, + spacing: { + padding: 'padding', + margin: 'margin', + }, + }; + + const styleSchema = { + type: 'object', + description: __( 'Style object for the paragraph.' ), + properties: {}, + }; + + // Temp ignore type errors + // @ts-ignore + // Check for valid style group keys. + Object.keys( validStyleGroups ).forEach( ( key ) => { + if ( + foundBlock.supports && + typeof foundBlock.supports === 'object' && + key in foundBlock.supports + ) { + if ( ! ( validStyleGroups[ key ] in styleSchema.properties ) ) { + styleSchema.properties[ validStyleGroups[ key ] ] = {}; + + // Check for valid style group subkeys and add the properties to the style schema. + Object.keys( foundBlock.supports[ key ] ).forEach( + ( subkey ) => { + if ( 'color' === key ) { + styleSchema.properties[ validStyleGroups[ key ] ] = + VALID_STYLE_GROUPS[ validStyleGroups[ key ] ] + ?.properties; + } + + const validStyleSubkey = validStyleSubkeys[ key ]?.[ subkey ]; + + if ( + !! validStyleSubkey && + !! VALID_STYLE_GROUPS[ validStyleGroups[ key ] ] + ?.properties?.[ validStyleSubkey ] + ) { + styleSchema.properties[ validStyleGroups[ key ] ][ + validStyleSubkey + ] = + VALID_STYLE_GROUPS[ validStyleGroups[ key ] ] + ?.properties?.[ validStyleSubkey ]; + } + } + ); + } + } + } ); + console.log( 'styleSchema', blockName, styleSchema ); + if ( Object.keys( styleSchema.properties ).length === 0 ) { + return {}; + } + + return styleSchema; + }; + + + + */