Skip to content
This repository was archived by the owner on Nov 21, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions packages/client-features/src/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,6 +37,7 @@ export const insertParagraphBlock: Feature = {
type: 'string',
description: __( 'Text content for the paragraph.' ),
},
style: getStyleSchema( 'core/paragraph' ),
},
},
output_schema: {
Expand All @@ -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, '<br>' ); // Then replace actual newlines with <br>
const newBlock = createBlock( 'core/paragraph', {
content,
style: {
...( args?.style || {} ),
},
} );
if ( ! newBlock ) {
throw new Error( 'Failed to create paragraph block.' );
Expand Down Expand Up @@ -98,6 +107,7 @@ export const insertHeadingBlock: Feature = {
type: 'integer',
description: __( 'Heading level (intended range 1–6).' ),
},
style: getStyleSchema( 'core/heading' ),
},
required: [ 'content' ],
},
Expand All @@ -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.' );
}
Expand All @@ -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.' );
Expand Down Expand Up @@ -166,6 +183,7 @@ export const insertQuoteBlock: Feature = {
type: 'string',
description: __( 'Optional citation for the quote.' ),
},
style: getStyleSchema( 'core/quote' ),
},
required: [ 'value' ],
},
Expand All @@ -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.'
Expand All @@ -193,6 +215,9 @@ export const insertQuoteBlock: Feature = {
'core/quote',
{
citation: args.citation || '',
style: {
...( args?.style || {} ),
},
},
[ innerParagraph ]
);
Expand Down Expand Up @@ -238,6 +263,7 @@ export const insertListBlock: Feature = {
'Whether the list should be ordered (numbered).'
),
},
style: getStyleSchema( 'core/list' ),
},
required: [ 'values' ],
},
Expand All @@ -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.'
Expand All @@ -266,6 +296,9 @@ export const insertListBlock: Feature = {
'core/list',
{
ordered: !! args.ordered,
style: {
...( args?.style || {} ),
},
},
innerListItemBlocks
);
Expand Down
252 changes: 252 additions & 0 deletions packages/client-features/src/styles.ts
Original file line number Diff line number Diff line change
@@ -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;
};



*/