diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js
index d32b3853627b32..e2274629006ee2 100644
--- a/packages/core-data/src/entity-provider.js
+++ b/packages/core-data/src/entity-provider.js
@@ -9,20 +9,17 @@ import {
} from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { STORE_NAME } from './name';
-import { unlock } from './private-apis';
+import { updateFootnotesFromMeta } from './footnotes';
/** @typedef {import('@wordpress/blocks').WPBlock} WPBlock */
const EMPTY_ARRAY = [];
-let oldFootnotes = {};
-
/**
* Internal dependencies
*/
@@ -182,136 +179,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
}, [ editedBlocks, content ] );
const updateFootnotes = useCallback(
- ( _blocks ) => {
- const output = { blocks: _blocks };
- if ( ! meta ) return output;
- // If meta.footnotes is empty, it means the meta is not registered.
- if ( meta.footnotes === undefined ) return output;
-
- const { getRichTextValues } = unlock( blockEditorPrivateApis );
- const _content = getRichTextValues( _blocks ).join( '' ) || '';
- const newOrder = [];
-
- // This can be avoided when
- // https://github.com/WordPress/gutenberg/pull/43204 lands. We can then
- // get the order directly from the rich text values.
- if ( _content.indexOf( 'data-fn' ) !== -1 ) {
- const regex = /data-fn="([^"]+)"/g;
- let match;
- while ( ( match = regex.exec( _content ) ) !== null ) {
- newOrder.push( match[ 1 ] );
- }
- }
-
- const footnotes = meta.footnotes
- ? JSON.parse( meta.footnotes )
- : [];
- const currentOrder = footnotes.map( ( fn ) => fn.id );
-
- if ( currentOrder.join( '' ) === newOrder.join( '' ) )
- return output;
-
- const newFootnotes = newOrder.map(
- ( fnId ) =>
- footnotes.find( ( fn ) => fn.id === fnId ) ||
- oldFootnotes[ fnId ] || {
- id: fnId,
- content: '',
- }
- );
-
- function updateAttributes( attributes ) {
- // Only attempt to update attributes, if attributes is an object.
- if (
- ! attributes ||
- Array.isArray( attributes ) ||
- typeof attributes !== 'object'
- ) {
- return attributes;
- }
-
- attributes = { ...attributes };
-
- for ( const key in attributes ) {
- const value = attributes[ key ];
-
- if ( Array.isArray( value ) ) {
- attributes[ key ] = value.map( updateAttributes );
- continue;
- }
-
- if ( typeof value !== 'string' ) {
- continue;
- }
-
- if ( value.indexOf( 'data-fn' ) === -1 ) {
- continue;
- }
-
- // When we store rich text values, this would no longer
- // require a regex.
- const regex =
- /(]+data-fn="([^"]+)"[^>]*>]*>)[\d*]*<\/a><\/sup>/g;
-
- attributes[ key ] = value.replace(
- regex,
- ( match, opening, fnId ) => {
- const index = newOrder.indexOf( fnId );
- return `${ opening }${ index + 1 }`;
- }
- );
-
- const compatRegex =
- /]+data-fn="([^"]+)"[^>]*>\*<\/a>/g;
-
- attributes[ key ] = attributes[ key ].replace(
- compatRegex,
- ( match, fnId ) => {
- const index = newOrder.indexOf( fnId );
- return `${
- index + 1
- }`;
- }
- );
- }
-
- return attributes;
- }
-
- function updateBlocksAttributes( __blocks ) {
- return __blocks.map( ( block ) => {
- return {
- ...block,
- attributes: updateAttributes( block.attributes ),
- innerBlocks: updateBlocksAttributes(
- block.innerBlocks
- ),
- };
- } );
- }
-
- // We need to go through all block attributes deeply and update the
- // footnote anchor numbering (textContent) to match the new order.
- const newBlocks = updateBlocksAttributes( _blocks );
-
- oldFootnotes = {
- ...oldFootnotes,
- ...footnotes.reduce( ( acc, fn ) => {
- if ( ! newOrder.includes( fn.id ) ) {
- acc[ fn.id ] = fn;
- }
- return acc;
- }, {} ),
- };
-
- return {
- meta: {
- ...meta,
- footnotes: JSON.stringify( newFootnotes ),
- },
- blocks: newBlocks,
- };
- },
+ ( _blocks ) => updateFootnotesFromMeta( _blocks, meta ),
[ meta ]
);
diff --git a/packages/core-data/src/footnotes/get-footnotes-order.js b/packages/core-data/src/footnotes/get-footnotes-order.js
new file mode 100644
index 00000000000000..e974c4a6e11893
--- /dev/null
+++ b/packages/core-data/src/footnotes/get-footnotes-order.js
@@ -0,0 +1,30 @@
+/**
+ * Internal dependencies
+ */
+import getRichTextValuesCached from './get-rich-text-values-cached';
+
+const cache = new WeakMap();
+
+function getBlockFootnotesOrder( block ) {
+ if ( ! cache.has( block ) ) {
+ const content = getRichTextValuesCached( block ).join( '' );
+ const newOrder = [];
+
+ // https://github.com/WordPress/gutenberg/pull/43204 lands. We can then
+ // get the order directly from the rich text values.
+ if ( content.indexOf( 'data-fn' ) !== -1 ) {
+ const regex = /data-fn="([^"]+)"/g;
+ let match;
+ while ( ( match = regex.exec( content ) ) !== null ) {
+ newOrder.push( match[ 1 ] );
+ }
+ }
+ cache.set( block, newOrder );
+ }
+
+ return cache.get( block );
+}
+
+export default function getFootnotesOrder( blocks ) {
+ return blocks.flatMap( getBlockFootnotesOrder );
+}
diff --git a/packages/core-data/src/footnotes/get-rich-text-values-cached.js b/packages/core-data/src/footnotes/get-rich-text-values-cached.js
new file mode 100644
index 00000000000000..06a01c5ef63fdd
--- /dev/null
+++ b/packages/core-data/src/footnotes/get-rich-text-values-cached.js
@@ -0,0 +1,35 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../private-apis';
+
+// TODO: The following line should have been:
+//
+// const unlockedApis = unlock( blockEditorPrivateApis );
+//
+// But there are hidden circular dependencies in RNMobile code, specifically in
+// certain native components in the `components` package that depend on
+// `block-editor`. What follows is a workaround that defers the `unlock` call
+// to prevent native code from failing.
+//
+// Fix once https://github.com/WordPress/gutenberg/issues/52692 is closed.
+let unlockedApis;
+
+const cache = new WeakMap();
+
+export default function getRichTextValuesCached( block ) {
+ if ( ! unlockedApis ) {
+ unlockedApis = unlock( blockEditorPrivateApis );
+ }
+
+ if ( ! cache.has( block ) ) {
+ const values = unlockedApis.getRichTextValues( [ block ] );
+ cache.set( block, values );
+ }
+ return cache.get( block );
+}
diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js
new file mode 100644
index 00000000000000..b5c075b372e31e
--- /dev/null
+++ b/packages/core-data/src/footnotes/index.js
@@ -0,0 +1,119 @@
+/**
+ * Internal dependencies
+ */
+import getFootnotesOrder from './get-footnotes-order';
+
+let oldFootnotes = {};
+
+export function updateFootnotesFromMeta( blocks, meta ) {
+ const output = { blocks };
+ if ( ! meta ) return output;
+
+ // If meta.footnotes is empty, it means the meta is not registered.
+ if ( meta.footnotes === undefined ) return output;
+
+ const newOrder = getFootnotesOrder( blocks );
+
+ const footnotes = meta.footnotes ? JSON.parse( meta.footnotes ) : [];
+ const currentOrder = footnotes.map( ( fn ) => fn.id );
+
+ if ( currentOrder.join( '' ) === newOrder.join( '' ) ) return output;
+
+ const newFootnotes = newOrder.map(
+ ( fnId ) =>
+ footnotes.find( ( fn ) => fn.id === fnId ) ||
+ oldFootnotes[ fnId ] || {
+ id: fnId,
+ content: '',
+ }
+ );
+
+ function updateAttributes( attributes ) {
+ // Only attempt to update attributes, if attributes is an object.
+ if (
+ ! attributes ||
+ Array.isArray( attributes ) ||
+ typeof attributes !== 'object'
+ ) {
+ return attributes;
+ }
+
+ attributes = { ...attributes };
+
+ for ( const key in attributes ) {
+ const value = attributes[ key ];
+
+ if ( Array.isArray( value ) ) {
+ attributes[ key ] = value.map( updateAttributes );
+ continue;
+ }
+
+ if ( typeof value !== 'string' ) {
+ continue;
+ }
+
+ if ( value.indexOf( 'data-fn' ) === -1 ) {
+ continue;
+ }
+
+ // When we store rich text values, this would no longer
+ // require a regex.
+ const regex =
+ /(]+data-fn="([^"]+)"[^>]*>]*>)[\d*]*<\/a><\/sup>/g;
+
+ attributes[ key ] = value.replace(
+ regex,
+ ( match, opening, fnId ) => {
+ const index = newOrder.indexOf( fnId );
+ return `${ opening }${ index + 1 }`;
+ }
+ );
+
+ const compatRegex = /]+data-fn="([^"]+)"[^>]*>\*<\/a>/g;
+
+ attributes[ key ] = attributes[ key ].replace(
+ compatRegex,
+ ( match, fnId ) => {
+ const index = newOrder.indexOf( fnId );
+ return `${
+ index + 1
+ }`;
+ }
+ );
+ }
+
+ return attributes;
+ }
+
+ function updateBlocksAttributes( __blocks ) {
+ return __blocks.map( ( block ) => {
+ return {
+ ...block,
+ attributes: updateAttributes( block.attributes ),
+ innerBlocks: updateBlocksAttributes( block.innerBlocks ),
+ };
+ } );
+ }
+
+ // We need to go through all block attributes deeply and update the
+ // footnote anchor numbering (textContent) to match the new order.
+ const newBlocks = updateBlocksAttributes( blocks );
+
+ oldFootnotes = {
+ ...oldFootnotes,
+ ...footnotes.reduce( ( acc, fn ) => {
+ if ( ! newOrder.includes( fn.id ) ) {
+ acc[ fn.id ] = fn;
+ }
+ return acc;
+ }, {} ),
+ };
+
+ return {
+ meta: {
+ ...meta,
+ footnotes: JSON.stringify( newFootnotes ),
+ },
+ blocks: newBlocks,
+ };
+}