Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/element/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- `createInterpolateElement`: Warn and stop interpolating instead of throwing when an unmatched closing tag is present ([#60843](https://github.com/WordPress/gutenberg/issues/60843)).

## 8.3.0 (2026-07-14)

## 8.2.0 (2026-07-01)
Expand Down
13 changes: 13 additions & 0 deletions packages/element/src/create-interpolate-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,19 @@ function proceed( conversionMap: Record< string, ReactElement > ): boolean {
return true;

case 'closer':
// A closing tag with no matching opening tag on the stack is
// invalid. Warn and bail, keeping anything interpolated so far.
if ( 0 === stackDepth ) {
if ( globalThis.SCRIPT_DEBUG ) {
// eslint-disable-next-line no-console
console.warn(
`Unmatched closing tag '</${ name }>' in createInterpolateElement. The rest of the string was not interpolated.`
);
}
addText();
return false;
}

// If we're not nesting then this is easy - close the block.
if ( 1 === stackDepth ) {
closeOuterElement( startOffset );
Expand Down
32 changes: 32 additions & 0 deletions packages/element/src/test/create-interpolate-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ describe( 'createInterpolateElement', () => {
createInterpolateElement( testString, { 'spaced token': <em /> } )
).toEqual( expectedElement );
} );
it( 'warns and stops interpolating when there is an unmatched closing tag', () => {
const testString = 'This is a </item> string';
const expectedElement = <>{ testString }</>;
expect(
createInterpolateElement( testString, { item: <em /> } )
).toEqual( expectedElement );
expect( console ).toHaveWarnedWith(
`Unmatched closing tag '</item>' in createInterpolateElement. The rest of the string was not interpolated.`
);

// Tags matched before the unmatched closing tag stay interpolated, so
// only the remainder of the string is returned as text.
const partialString = 'This is a <em>styled</em> and </strong> string';
const partialExpectedElement = createElement(
Fragment,
null,
'This is a ',
createElement( 'em', null, 'styled' ),
' and </strong> string'
);
expect(
JSON.stringify(
createInterpolateElement( partialString, {
em: <em />,
strong: <strong />,
} )
)
).toEqual( JSON.stringify( partialExpectedElement ) );
expect( console ).toHaveWarnedWith(
`Unmatched closing tag '</strong>' in createInterpolateElement. The rest of the string was not interpolated.`
);
} );
it( 'returns expected react element for non nested components', () => {
const testString = 'This is a string with <a>a link</a>.';
const expectedElement = createElement(
Expand Down
Loading