-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Footnotes (stored inline) #49797
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 5b2aaf7
Try with shortcode
ellatrix 9d6df69
Try rendering below content again.
ellatrix 2f7f6ad
wip
ellatrix 642794b
Add UI
ellatrix aa602c7
Simplify UI
ellatrix 64f1c81
PHP: use shortcode regex and make accessible
ellatrix 97fd91d
Fix PHP linting errors
ellatrix 4c6d1ec
Fix PHP linting errors
ellatrix 7ce7305
Reset fn array
ellatrix b9bbf5d
Fix e2e tests
ellatrix e5c7a38
Fix PHP docs
ellatrix 6622fb0
More PHP lint fixes
ellatrix 08784f0
Reorg
ellatrix 0372f1f
Render footnotes with portal
ellatrix 20feb5c
wip
ellatrix 5d191d8
wip
ellatrix f77f5f5
Use replaceDataByType
ellatrix 034549e
Remove other formats
ellatrix f0c0a0e
WIP: Explore replacing RichText footnotes on the server
dmsnell 3d2ce82
Remove regex replace
ellatrix 1b28cfa
Move footnote file, address some linting concerns, switch to <sup>
dmsnell 3ec2a67
Remove the enjoyment and clarity in this coding endeavor.
dmsnell 293901c
Add file doc
dmsnell aeea637
Things
dmsnell 96a464b
Refactor server rendering to extract incidental from semantic parts
dmsnell fff247a
Collect and dump footnotes on footnote-list block and at end of post.
dmsnell 1efc92c
Waightspeas
dmsnell 1a1a897
Make PHP linter happy
ellatrix 69dcb68
Add fn block
ellatrix 35f599f
Update package lock
ellatrix 15df4f6
Make links interactive
ellatrix fe5ab99
PHP: check class instead of value attribute
ellatrix b1d23af
wip
ellatrix 40fde71
Reorder
ellatrix 356feff
Fix rebase errors
ellatrix File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' ) ) { | ||
$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; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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