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;
+ };
+
+
+
+ */