-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Autogenerate heading anchors #30825
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
Autogenerate heading anchors #30825
Changes from 56 commits
5cb5c9a
ca2f664
556f82b
4a6acd4
042a1aa
8c4be85
766ef32
3296651
3e6928c
1e5d3d7
2d20135
69efbab
e5af9c8
15e04db
635eca9
4c03e1b
abeaaaf
9f728c2
4579a72
93b0d4d
a9f6381
762af2c
c3ccf1c
47014dd
cc34994
4b9e037
a1263d9
370409e
951480c
9953f70
694180e
34d1721
50141d7
7e0f1d9
f1ad7c2
c61d9a7
7d8c3ea
00006fe
7b8b783
516c400
842449d
3ec7e1e
171f228
577feb6
5706771
60114f1
0dbefeb
2a021a6
716b5a9
4f5c06e
88cf66c
8c0e353
234abfc
6535526
8f39085
43cc150
db12f78
5bb9302
bbabee8
98d29dc
b5cb1f9
2b1f215
79f49ff
b5b34a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { deburr, trim } from 'lodash'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { useSelect } from '@wordpress/data'; | ||
| import { store as blockEditorStore } from '@wordpress/block-editor'; | ||
|
|
||
| /** | ||
| * Runs a callback over all blocks, including nested blocks. | ||
| * | ||
| * @param {Object[]} blocks The blocks. | ||
| * @param {Function} callback The callback. | ||
| * | ||
| * @return {void} | ||
| */ | ||
| const recurseOverBlocks = ( blocks, callback ) => { | ||
| for ( const block of blocks ) { | ||
| // eslint-disable-next-line callback-return | ||
| callback( block ); | ||
| if ( block.innerBlocks ) { | ||
| recurseOverBlocks( block.innerBlocks, callback ); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the text without markup. | ||
| * | ||
| * @param {string} text The text. | ||
| * | ||
| * @return {string} The text without markup. | ||
| */ | ||
| const getTextWithoutMarkup = ( text ) => { | ||
| const dummyElement = document.createElement( 'div' ); | ||
| dummyElement.innerHTML = text; | ||
| return dummyElement.innerText; | ||
| }; | ||
|
|
||
| /** | ||
| * Get all heading anchors. | ||
| * | ||
| * @param {Object} blockList An object containing all blocks. | ||
| * @param {string} excludeId A block ID we want to exclude. | ||
| * | ||
| * @return {string[]} Return an array of anchors. | ||
| */ | ||
| const getAllHeadingAnchors = ( blockList, excludeId ) => { | ||
| const anchors = []; | ||
|
|
||
| recurseOverBlocks( blockList, ( block ) => { | ||
| if ( | ||
| block.name === 'core/heading' && | ||
| ( ! excludeId || block.clientId !== excludeId ) && | ||
| block.attributes.anchor | ||
| ) { | ||
| anchors.push( block.attributes.anchor ); | ||
| } | ||
| } ); | ||
|
|
||
| return anchors; | ||
| }; | ||
|
|
||
| /** | ||
| * Get the slug from the content. | ||
| * | ||
| * @param {string} content The block content. | ||
| * | ||
| * @return {string} Returns the slug. | ||
| */ | ||
| const getSlug = ( content ) => { | ||
| // Get the slug. | ||
| return trim( | ||
| deburr( getTextWithoutMarkup( content ) ) | ||
| .replace( /[^\p{L}\p{N}]+/gu, '-' ) | ||
| .toLowerCase(), | ||
| '-' | ||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Generate the anchor for a heading. | ||
| * | ||
| * @param {string} content The block content. | ||
| * @param {string[]} allHeadingAnchors An array containing all headings anchors. | ||
| * | ||
| * @return {string|null} Return the heading anchor. | ||
| */ | ||
| export const generateAnchor = ( content, allHeadingAnchors ) => { | ||
| const slug = getSlug( content ); | ||
| // If slug is empty, then return null. | ||
| // Returning null instead of an empty string allows us to check again when the content changes. | ||
| if ( '' === slug ) { | ||
| return null; | ||
| } | ||
|
|
||
| let anchor = slug; | ||
| let i = 0; | ||
|
|
||
| // If the anchor already exists in another heading, append -i. | ||
| while ( allHeadingAnchors.includes( anchor ) ) { | ||
| i += 1; | ||
| anchor = slug + '-' + i; | ||
| } | ||
|
|
||
| return anchor; | ||
| }; | ||
|
||
|
|
||
| /** | ||
| * Retrieves and returns all heading anchors. | ||
| * | ||
| * @param {string} clientId The block's client-ID. | ||
| * | ||
| * @return {string[]} The array of heading anchors. | ||
| */ | ||
| export const useAllHeadingAnchors = ( clientId ) => { | ||
| return useSelect( | ||
| ( select ) => { | ||
| const allBlocks = select( blockEditorStore ).getBlocks(); | ||
| return getAllHeadingAnchors( allBlocks, clientId ); | ||
| }, | ||
| [ clientId ] | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.