Footnote numbers are incorrect when paragraphs are deleted and undone.
Description
Footnote numbers are incorrect when paragraphs are deleted and undone.
Step-by-step reproduction instructions
- Set footnotes for each of several paragraphs.
- Delete the first paragraph.
- Undo.
Screenshots, screen recording, code snippet
https://github.com/WordPress/gutenberg/assets/1908815/360284a9-aa75-4a85-a13f-26faeb7f04a2
Environment info
- WordPress 6.4.3
- Enable and disable Gutenberg 17.9.0
Please confirm that you have searched existing issues in the repo.
Yes
Please confirm that you have tested with all plugins deactivated except Gutenberg.
Yes
Hi, This issue has gone 30 days without any activity. This means it is time for a check-in to make sure it is still relevant. If you are still experiencing this issue with the latest versions, you can help the project by responding to confirm the problem and by providing any updated reproduction steps. Thanks for helping out.
I was able to reproduce this issue using the described steps on WordPress 6.5.5 with Gutenberg 18.7.1.
I noticed that once you've reproduced the issue by deleting and then undoing the delete of the first paragraph, if you add another footnote then all footnotes are updated to the correct numbers.
Hi,
I tested this issue on the Gutenberg version (20.9.0) and can confirm that the incorrect footnote numbering still persists after deleting and undoing paragraphs.
To verify:
- I added footnotes to multiple paragraphs.
- Deleted the first paragraph.
- Used the undo to restore the deleted paragraph.
After these steps, the footnote numbers did not restore correctly.
Video demonstration
https://github.com/user-attachments/assets/715a9fbf-13d1-45eb-ad2c-1391d7e26036
I'm unable to reproduce this bug with the latest Gutenberg.
Update: I was able to reproduce it, but the bug isn't consistent. More details - https://github.com/WordPress/gutenberg/pull/70911#issuecomment-3126015081.
Screencast
https://github.com/user-attachments/assets/32635380-1d06-44b4-89b0-b7500aac1ff4
Hi so after some researching and debugging, I was able to pinpoint the location of where the issue was arising.
Testing
When we are editing the post, the editedBlock being defined is returned directly. This is where the issue occurs. When removing and undoing the first footnote, the editedBlock is returned as it is hence and the when exploring the value of the RichText I found that the innerHtml of core/footnotes with sup tag has it's value set to 1 and not the updated value 2.
For the Adding footnotes to saved para case the OriginalHTML of block doesn't have footnotes.
For deleting and undo when adding a new block to post having existing footnotes case This still happens but things gets a bit confusing for me. The OriginalHTML is correct in this case ( showing text as 2 ) but the innerHTML in RichTextData is set to 1 aka it didn't update in both case.
Tried Approaches
My first approach on this was to use the existing updateFootnotesFromMeta on editedBlocks but when tried, i found out that if statement for checking order changes will always return true for our case as ids order is correct from both footnotes block and meta.
My second approach was to remove that if statement. This approach does work but I worry about the performance as this prevents early exits due to which footnotes tags are being update every edit.
I think the second approach would work but we need to see a way to still allow early exits so footnotes tags are not updated when changes are not made to footnotes.
Update
So I tried to solve this by filtering all para blocks that have footnotes superscript and then compare the html string generated from richTextData with originalContent.
If there is a diff then we continue with normal update else we early exit.
Sample Code
// regex to match <sup data-fn="..." class="fn"><a href="..." id="...">...</a></sup>"
const footnotesBlocks = blocks
.filter(
( block ) =>
block.originalContent &&
/<sup\s+data-fn="[^"]+"\s+class="fn">\s*<a\s+href="[^"]+"\s+id="[^"]+">.*?<\/a>\s*<\/sup>/.test(
block.originalContent
)
)
.map( ( block ) => {
return {
block,
dataFn: create( {
html: block.originalContent,
} ).replacements.filter(
( replacement ) => replacement.type === 'core/footnote'
)[ 0 ].attributes[ 'data-fn' ],
};
} )
.reduce( ( acc, { block, dataFn } ) => {
acc[ dataFn ] = block;
return acc;
}, {} );
if ( ! Object.keys( footnotesBlocks ).length ) {
return output;
}
const newOrder = getFootnotesOrder( blocks );
const currentOrder = footnotes.map( ( fn ) => fn.id );
if ( currentOrder.join( ' ' ) === newOrder.join( ' ' ) ) {
let isDiffFootnotes = false;
for ( const fn of footnotes ) {
const block = footnotesBlocks[ fn.id ];
const oldHtml = toHTMLString( { value: block.attributes.content } );
const originalHTML = ( block.originalContent || '' )
.replace( '<p>', '' )
.replace( '</p>', '' );
if ( oldHtml !== originalHTML ) {
isDiffFootnotes = true;
break;
}
}
if ( ! isDiffFootnotes ) {
return output;
}
}
cc: @Mamaduka
cc @mcsf, @ellatrix