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();