From 76e54ec76067e0e617c81e1366f3e5cb9a504dad Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Thu, 14 Sep 2023 17:24:12 +0100
Subject: [PATCH 1/8] (Prelude) Factor out function updateFootnotes
---
packages/core-data/src/entity-provider.js | 136 +--------------------
packages/core-data/src/footnotes/index.js | 137 ++++++++++++++++++++++
2 files changed, 139 insertions(+), 134 deletions(-)
create mode 100644 packages/core-data/src/footnotes/index.js
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/index.js b/packages/core-data/src/footnotes/index.js
new file mode 100644
index 00000000000000..0d58d090d9d6a4
--- /dev/null
+++ b/packages/core-data/src/footnotes/index.js
@@ -0,0 +1,137 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../private-apis';
+
+const { getRichTextValues } = unlock( blockEditorPrivateApis );
+
+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 _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,
+ };
+}
From 6c29f63f13a9f3c9c155dc129f1f7d69e99f14fd Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Wed, 12 Jul 2023 16:24:53 +0100
Subject: [PATCH 2/8] Add per-block caching when extracting rich-text values
---
packages/core-data/src/footnotes/index.js | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js
index 0d58d090d9d6a4..79b1170ab8ed91 100644
--- a/packages/core-data/src/footnotes/index.js
+++ b/packages/core-data/src/footnotes/index.js
@@ -12,13 +12,25 @@ const { getRichTextValues } = unlock( blockEditorPrivateApis );
let oldFootnotes = {};
+const cache = new WeakMap();
+
+function getRichTextValuesCached( block ) {
+ if ( ! cache.has( block ) ) {
+ const values = getRichTextValues( [ block ] );
+ cache.set( block, values );
+ }
+ return cache.get( block );
+}
+
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 _content = getRichTextValues( blocks ).join( '' ) || '';
+ const _content = blocks.map( getRichTextValuesCached ).join( '' );
+
const newOrder = [];
// This can be avoided when
From 5bf0bef0b522b6161a9c79f504dbd61e1b28359c Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Wed, 12 Jul 2023 16:33:25 +0100
Subject: [PATCH 3/8] Extract getRichTextValuesCached
---
.../footnotes/get-rich-text-values-cached.js | 21 +++++++++++++++++++
packages/core-data/src/footnotes/index.js | 19 +----------------
2 files changed, 22 insertions(+), 18 deletions(-)
create mode 100644 packages/core-data/src/footnotes/get-rich-text-values-cached.js
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..bb0dc2eafe540f
--- /dev/null
+++ b/packages/core-data/src/footnotes/get-rich-text-values-cached.js
@@ -0,0 +1,21 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../private-apis';
+
+const { getRichTextValues } = unlock( blockEditorPrivateApis );
+
+const cache = new WeakMap();
+
+export default function getRichTextValuesCached( block ) {
+ if ( ! cache.has( block ) ) {
+ const values = 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
index 79b1170ab8ed91..151061079a2783 100644
--- a/packages/core-data/src/footnotes/index.js
+++ b/packages/core-data/src/footnotes/index.js
@@ -1,27 +1,10 @@
-/**
- * WordPress dependencies
- */
-import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
-
/**
* Internal dependencies
*/
-import { unlock } from '../private-apis';
-
-const { getRichTextValues } = unlock( blockEditorPrivateApis );
+import getRichTextValuesCached from './get-rich-text-values-cached';
let oldFootnotes = {};
-const cache = new WeakMap();
-
-function getRichTextValuesCached( block ) {
- if ( ! cache.has( block ) ) {
- const values = getRichTextValues( [ block ] );
- cache.set( block, values );
- }
- return cache.get( block );
-}
-
export function updateFootnotesFromMeta( blocks, meta ) {
const output = { blocks };
if ( ! meta ) return output;
From 6f99986d782a44769602046a4f7828eb27883c60 Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Wed, 12 Jul 2023 16:41:43 +0100
Subject: [PATCH 4/8] Extract getFootnotesOrder
---
.../src/footnotes/get-footnotes-order.js | 24 +++++++++++++++++++
packages/core-data/src/footnotes/index.js | 17 ++-----------
2 files changed, 26 insertions(+), 15 deletions(-)
create mode 100644 packages/core-data/src/footnotes/get-footnotes-order.js
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..f65930145cd17e
--- /dev/null
+++ b/packages/core-data/src/footnotes/get-footnotes-order.js
@@ -0,0 +1,24 @@
+/**
+ * Internal dependencies
+ */
+import getRichTextValuesCached from './get-rich-text-values-cached';
+
+export default function getFootnotesOrder( blocks ) {
+ const values = blocks.map( getRichTextValuesCached );
+ const content = values.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 ] );
+ }
+ }
+
+ return newOrder;
+}
diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js
index 151061079a2783..b5c075b372e31e 100644
--- a/packages/core-data/src/footnotes/index.js
+++ b/packages/core-data/src/footnotes/index.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import getRichTextValuesCached from './get-rich-text-values-cached';
+import getFootnotesOrder from './get-footnotes-order';
let oldFootnotes = {};
@@ -12,20 +12,7 @@ export function updateFootnotesFromMeta( blocks, meta ) {
// If meta.footnotes is empty, it means the meta is not registered.
if ( meta.footnotes === undefined ) return output;
- const _content = blocks.map( getRichTextValuesCached ).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 newOrder = getFootnotesOrder( blocks );
const footnotes = meta.footnotes ? JSON.parse( meta.footnotes ) : [];
const currentOrder = footnotes.map( ( fn ) => fn.id );
From badef7cc1bd1c6ef6a0589c77093ef7db5896e79 Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Wed, 12 Jul 2023 16:59:31 +0100
Subject: [PATCH 5/8] Move getFootnotesOrder computation to the block level
---
.../core-data/src/footnotes/get-footnotes-order.js | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/packages/core-data/src/footnotes/get-footnotes-order.js b/packages/core-data/src/footnotes/get-footnotes-order.js
index f65930145cd17e..bffc43fce8ada7 100644
--- a/packages/core-data/src/footnotes/get-footnotes-order.js
+++ b/packages/core-data/src/footnotes/get-footnotes-order.js
@@ -3,13 +3,10 @@
*/
import getRichTextValuesCached from './get-rich-text-values-cached';
-export default function getFootnotesOrder( blocks ) {
- const values = blocks.map( getRichTextValuesCached );
- const content = values.join( '' );
-
+function getBlockFootnotesOrder( block ) {
+ const content = getRichTextValuesCached( block ).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 ) {
@@ -22,3 +19,7 @@ export default function getFootnotesOrder( blocks ) {
return newOrder;
}
+
+export default function getFootnotesOrder( blocks ) {
+ return blocks.flatMap( getBlockFootnotesOrder );
+}
From 62f34aef3b4d28e8c555cba60ef703ce4bb8854d Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Wed, 12 Jul 2023 17:00:09 +0100
Subject: [PATCH 6/8] Add per-block caching to getFootnotesOrder
---
.../src/footnotes/get-footnotes-order.js | 25 +++++++++++--------
1 file changed, 15 insertions(+), 10 deletions(-)
diff --git a/packages/core-data/src/footnotes/get-footnotes-order.js b/packages/core-data/src/footnotes/get-footnotes-order.js
index bffc43fce8ada7..e974c4a6e11893 100644
--- a/packages/core-data/src/footnotes/get-footnotes-order.js
+++ b/packages/core-data/src/footnotes/get-footnotes-order.js
@@ -3,21 +3,26 @@
*/
import getRichTextValuesCached from './get-rich-text-values-cached';
+const cache = new WeakMap();
+
function getBlockFootnotesOrder( block ) {
- const content = getRichTextValuesCached( block ).join( '' );
- const newOrder = [];
+ 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 ] );
+ // 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 newOrder;
+ return cache.get( block );
}
export default function getFootnotesOrder( blocks ) {
From 919d6c5c57414e9ef660c287af4f7b58726bb168 Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Thu, 13 Jul 2023 08:49:51 +0100
Subject: [PATCH 7/8] Don't call `unlock` at module level
Fixes unit tests failing at
packages/editor/src/components/document-outline
---
.../src/footnotes/get-rich-text-values-cached.js | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
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
index bb0dc2eafe540f..85308f4130f3ef 100644
--- a/packages/core-data/src/footnotes/get-rich-text-values-cached.js
+++ b/packages/core-data/src/footnotes/get-rich-text-values-cached.js
@@ -8,13 +8,19 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
*/
import { unlock } from '../private-apis';
-const { getRichTextValues } = unlock( blockEditorPrivateApis );
+// Avoid calling `unlock` at the module level, deferring the call until needed
+// in `getRichTextValuesCached`.
+let unlockedApis;
const cache = new WeakMap();
export default function getRichTextValuesCached( block ) {
+ if ( ! unlockedApis ) {
+ unlockedApis = unlock( blockEditorPrivateApis );
+ }
+
if ( ! cache.has( block ) ) {
- const values = getRichTextValues( [ block ] );
+ const values = unlockedApis.getRichTextValues( [ block ] );
cache.set( block, values );
}
return cache.get( block );
From 0020e54b7793f1b8feb51e2dd5253c9410c6199d Mon Sep 17 00:00:00 2001
From: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Date: Mon, 17 Jul 2023 16:14:40 +0100
Subject: [PATCH 8/8] Add TODO note about RNMobile's circular dependencies
---
.../src/footnotes/get-rich-text-values-cached.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
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
index 85308f4130f3ef..06a01c5ef63fdd 100644
--- a/packages/core-data/src/footnotes/get-rich-text-values-cached.js
+++ b/packages/core-data/src/footnotes/get-rich-text-values-cached.js
@@ -8,8 +8,16 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
*/
import { unlock } from '../private-apis';
-// Avoid calling `unlock` at the module level, deferring the call until needed
-// in `getRichTextValuesCached`.
+// 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();