Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Footnotes (stored inline) #49797

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d9dde03
Rich text: add dynamic data API
ellatrix Apr 4, 2023
5b2aaf7
Try with shortcode
ellatrix Feb 1, 2023
9d6df69
Try rendering below content again.
ellatrix Feb 2, 2023
2f7f6ad
wip
ellatrix Mar 28, 2023
642794b
Add UI
ellatrix Mar 30, 2023
aa602c7
Simplify UI
ellatrix Mar 30, 2023
64f1c81
PHP: use shortcode regex and make accessible
ellatrix Mar 30, 2023
97fd91d
Fix PHP linting errors
ellatrix Mar 30, 2023
4c6d1ec
Fix PHP linting errors
ellatrix Mar 30, 2023
7ce7305
Reset fn array
ellatrix Mar 30, 2023
b9bbf5d
Fix e2e tests
ellatrix Mar 30, 2023
e5c7a38
Fix PHP docs
ellatrix Mar 30, 2023
6622fb0
More PHP lint fixes
ellatrix Mar 30, 2023
08784f0
Reorg
ellatrix Mar 31, 2023
0372f1f
Render footnotes with portal
ellatrix Apr 3, 2023
20feb5c
wip
ellatrix Apr 3, 2023
5d191d8
wip
ellatrix Apr 4, 2023
f77f5f5
Use replaceDataByType
ellatrix Apr 4, 2023
034549e
Remove other formats
ellatrix Apr 13, 2023
f0c0a0e
WIP: Explore replacing RichText footnotes on the server
dmsnell Apr 12, 2023
3d2ce82
Remove regex replace
ellatrix Apr 13, 2023
1b28cfa
Move footnote file, address some linting concerns, switch to <sup>
dmsnell Apr 13, 2023
3ec2a67
Remove the enjoyment and clarity in this coding endeavor.
dmsnell Apr 13, 2023
293901c
Add file doc
dmsnell Apr 13, 2023
aeea637
Things
dmsnell Apr 13, 2023
96a464b
Refactor server rendering to extract incidental from semantic parts
dmsnell Apr 13, 2023
fff247a
Collect and dump footnotes on footnote-list block and at end of post.
dmsnell Apr 14, 2023
1efc92c
Waightspeas
dmsnell Apr 14, 2023
1a1a897
Make PHP linter happy
ellatrix Apr 27, 2023
69dcb68
Add fn block
ellatrix Apr 27, 2023
35f599f
Update package lock
ellatrix Apr 27, 2023
15df4f6
Make links interactive
ellatrix Apr 27, 2023
fe5ab99
PHP: check class instead of value attribute
ellatrix Apr 27, 2023
b1d23af
wip
ellatrix May 3, 2023
40fde71
Reorder
ellatrix May 5, 2023
356feff
Fix rebase errors
ellatrix May 9, 2023
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
9 changes: 9 additions & 0 deletions docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ Add a link to a downloadable file. ([Source](https://github.com/WordPress/gutenb
- **Supports:** align, anchor, color (background, gradients, link, ~~text~~)
- **Attributes:** displayPreview, downloadButtonText, fileId, fileName, href, id, previewHeight, showDownloadButton, textLinkHref, textLinkTarget

## Footnotes

([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/footnotes))

- **Name:** core/footnotes
- **Category:** text
- **Supports:** ~~html~~
- **Attributes:**

## Classic

Use the classic WordPress editor. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/freeform))
Expand Down
242 changes: 242 additions & 0 deletions lib/experimental/class-wp-footnote-processor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php
/**
* This is a silly file that replaces inline footnotes with links to
* footnotes and a trailing footer with a list of footnote items.
*
* @package gutenberg
*
* @since 6.3.0
*/

/**
* Replaces inline footnotes with inline links and a footer list.
*
* So many things about this are fragile, broken, ad-hoc, and
* coupling to internal details of the Tag Processor. This is
* meant to unlock footnote rendering on the server and should
* not be followed as an example.
*
* @since 6.3.0
*/
class WP_Footnote_Processor extends WP_HTML_Tag_Processor {
/**
* Stores referenced footnotes, hash, and count of links referring to them.
*
* @var array[]
*/
public $notes = array();

/**
* Replaces inline footnotes in a given HTML document with
* links with anchors produced by `$this->get_footer()`.
*
* @return string Transformed HTML with links instead of inline footnotes.
*/
public function replace_footnotes() {
while ( $this->find_opener() ) {
if ( ! $this->find_balanced_closer() ) {
return $this->get_updated_html();
}

$note = substr( $this->get_note_content(), 2, -1 );
$id = md5( $note );

if ( isset( $this->notes[ $id ] ) ) {
$this->notes[ $id ]['count'] += 1;
} else {
$this->notes[ $id ] = array(
'note' => $note,
'count' => 1,
);
}

// List starts at 1. If the note already exists, use the existing index.
$index = 1 + array_search( $id, array_keys( $this->notes ), true );
$count = $this->notes[ $id ]['count'];

$footnote_content = sprintf(
'<sup title="%s"><a class="note-link" href="#%s" id="%s-link-%d">[%d]</a></sup>',
esc_attr( $note ),
$id,
$id,
$count,
$index
);

$this->replace_footnote( $footnote_content );
}

return $this->get_updated_html();
}

/**
* Generates a list of footnote items that can be linked to in the post.
*
* @return string The list of footnote items, if any, otherwise an empty string.
*/
public function get_footer() {
if ( empty( $this->notes ) ) {
return '';
}

$output = '<ol>';
foreach ( $this->notes as $id => $info ) {
$note = $info['note'];
$count = $info['count'];
$output .= sprintf( '<li id="%s">', $id );
$output .= $note;
$label = $count > 1 ?
/* translators: %s: footnote occurrence */
__( 'Back to content (%s)', 'gutenberg' ) :
__( 'Back to content', 'gutenberg' );
$links = '';
while ( $count ) {
$links .= sprintf(
'<a href="#%s-link-%d" aria-label="%s">↩︎</a>',
$id,
$count,
sprintf( $label, $count )
);
$count--;
}
$output .= ' ' . $links;
$output .= '</li>';
}
$output .= '</ol>';

return $output;
}

/**
* Finds the start of the next footnote.
*
* Looks for a superscript tag with the `value=footnote` attribute.
*
* @return bool
*/
private function find_opener() {
while ( $this->next_tag( array( 'tag_name' => 'sup' ) ) ) {
if ( 'fn' === $this->get_attribute( 'class' ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can combine this into the query. as a bonus, it won't fail if there is more than the single fn class

if ( $this->next_tag( array( 'tag_name' => 'sup', 'class_name' => 'fn' ) ) ) {
	$this->set_bookmark( 'start' );
	return true;
}

return false;

$this->set_bookmark( 'start' );
return true;
}
}

return false;
}

/**
* Naively finds the end of the current footnote.
*
* @return bool Whether the end of the current footnote was found.
*/
private function find_balanced_closer() {
$depth = 1;
$query = array(
'tag_name' => 'sup',
'tag_closers' => 'visit',
);

while ( $this->next_tag( $query ) ) {
if ( ! $this->is_tag_closer() ) {
$depth++;
} else {
$depth--;
}

if ( 0 <= $depth ) {
$this->set_bookmark( 'end' );
return true;
}
}

return false;
}

/**
* Returns the content inside footnote superscript tags.
*
* @return string The content found inside footnote superscript tags.
*/
private function get_note_content() {
$open = $this->bookmarks['start'];
$close = $this->bookmarks['end'];

return substr( $this->html, $open->end, $close->start - $open->end );
}

/**
* Replaces the footnote entirely with new HTML.
*
* @param string $new_content Content to store in place of the existing footnote.
*
* @return void
*/
private function replace_footnote( $new_content ) {
$start = $this->bookmarks['start']->start;
$end = $this->bookmarks['end']->end + 1;
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_content );
$this->get_updated_html();

$this->bookmarks['start']->start = $start;
$this->bookmarks['start']->end = $end;
$this->seek( 'start' );
}
}

add_filter(
'block_parser_class',
/**
* Hack to inject a per-render singleton footnote processor.
*/
function ( $parser_class ) {
$notes = array();

add_filter(
'render_block',
function ( $html, $block ) use ( &$notes ) {
if ( 'core/footnotes' === $block['blockName'] ) {
if ( 0 === count( $notes ) ) {
return $html;
}

$p = new WP_Footnote_Processor( $html );
$p->notes = $notes;
$list = $p->get_footer();
$notes = array();

return $html . $list;
}

$p = new WP_Footnote_Processor( $html );
$p->notes = $notes;
$p->replace_footnotes();
$notes = $p->notes;

return $p->get_updated_html();
},
1000,
2
);

add_filter(
'the_content',
function ( $html ) use ( &$notes ) {
if ( 0 === count( $notes ) ) {
return $html;
}

$p = new WP_Footnote_Processor( $html );
$p->notes = $notes;
$list = $p->get_footer();
$notes = array();

return $html . $list;
},
1000,
1
);

return $parser_class;
}
);
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
require __DIR__ . '/experimental/block-editor-settings.php';
require __DIR__ . '/experimental/blocks.php';
require __DIR__ . '/experimental/class-wp-footnote-processor.php';
require __DIR__ . '/experimental/navigation-theme-opt-in.php';
require __DIR__ . '/experimental/kses.php';
require __DIR__ . '/experimental/l10n.php';
Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useCallback,
forwardRef,
createContext,
createPortal,
} from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { children as childrenSource } from '@wordpress/blocks';
Expand Down Expand Up @@ -280,6 +281,7 @@ function RichTextWrapper(
getValue,
onChange,
ref: richTextRef,
replacementRefs,
} = useRichText( {
value: adjustedValue,
onChange( html, { __unstableFormats, __unstableText } ) {
Expand Down Expand Up @@ -413,6 +415,41 @@ function RichTextWrapper(
'rich-text'
) }
/>
{ Object.keys( replacementRefs ).map( ( index ) => {
const ref = replacementRefs[ index ];
const { render: Render } = formatTypes.find(
( { name } ) => name === ref.value
);
const i = parseInt( index, 10 );
function setAttributes( attributes ) {
const newReplacements = value.replacements.slice();
const currentObject = newReplacements[ i ];
newReplacements[ i ] = {
...currentObject,
attributes: {
...currentObject.attributes,
...attributes,
},
};
onChange( {
...value,
replacements: newReplacements,
} );
}
return (
ref &&
createPortal(
<Render
attributes={ ref.dataset }
setAttributes={ setAttributes }
isSelected={
value.start === i && value.end === i + 1
}
/>,
ref
)
);
} ) }
</>
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/block-library/src/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
@import "./query-pagination-numbers/editor.scss";
@import "./post-featured-image/editor.scss";
@import "./post-comments-form/editor.scss";
@import "./footnotes/editor.scss";

@import "./editor-elements.scss";

Expand Down
15 changes: 15 additions & 0 deletions packages/block-library/src/footnotes/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "core/footnotes",
"title": "Footnotes",
"category": "text",
"description": "",
"keywords": [ "references" ],
"textdomain": "default",
"supports": {
"html": false
},
"editorStyle": "wp-block-footnotes-editor",
"style": "wp-block-footnotes"
}
Loading