See Building a Block with Advanced Custom Fields for more information.
<?php
/**
* Table of Contents Block
*
* @package Cultivate_Pro
* @author CultivateWP
* @since 1.0.0
* @license GPL-2.0+
**/
namespace Cultivate_Pro\Blocks;
class Table_of_Contents {
/**
* Refers to a single instance of this class
*
* @var null
*/
private static $instance = null;
/**
* Current recipe
*
* @var int
*/
private $current_recipe = 0;
/**
* Headings to include in table of contents
*
* @var array
*/
private $headings = [];
/**
* Creates or returns an instance of this class.
*
* @since 0.1.8
* @return object BE_Table_of_Contents, a single instance of this class.
*/
public static function instance() {
if ( null == self::$instance ) {
self::$instance = new self;
}
return self::$instance;
}
/**
* Construct
*
* @since 0.1.0
*/
function __construct() {
// Setup headings
$this->setup_headings();
// Add ids to headings
add_filter( 'the_content', [ $this, 'the_content' ] );
add_filter( 'cultivate_pro/toc/content', [ $this, 'the_content' ] );
add_filter( 'cultivate_pro/landing/the_content', [ $this, 'the_content' ] );
// Create block
add_action('acf/init', [ $this, 'register_block' ], 4 );
}
/**
* Setup headings
*
*/
function setup_headings() {
$headings = [ 'h2' ];
$headings = apply_filters( 'cultivate_pro/toc/setup_headings', $headings );
$this->headings = $headings;
}
/**
* Using DOMDocument, parse the content and add anchors to headers
*
* @since 0.1.0
*
* @param string $content The content
* @return string the content, updated if the content has H1-H6
*/
function the_content( $content ) {
if ( '' == $content ) {
return $content;
}
if( is_admin() )
return $content;
global $post;
$include_anchors = apply_filters( 'cultivate_pro/toc/include_anchors', false !== strpos( $post->post_content, 'wp:acf/table-of-contents' ) );
if( ! $include_anchors )
return $content;
$anchors = array();
$doc = new \DOMDocument();
// START LibXML error management.
// Modify state
$libxml_previous_state = libxml_use_internal_errors( true );
$doc->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
// handle errors
libxml_clear_errors();
// restore
libxml_use_internal_errors( $libxml_previous_state );
// END LibXML error management.
foreach ( $this->headings as $h ) {
$headings = $doc->getElementsByTagName( $h );
foreach ( $headings as $heading ) {
$slug = $heading->getAttribute( 'id' );
if( empty( $slug ) ) {
$slug = $tmpslug = sanitize_title( $heading->nodeValue );
// @codingStandardsIgnoreEnd
$i = 2;
while ( false !== in_array( $slug, $anchors ) ) {
$slug = sprintf( '%s-%d', $tmpslug, $i++ );
}
$heading->setAttribute( 'id', $slug );
}
$anchors[] = $slug;
}
}
return $doc->saveHTML();
}
/**
* Register table of contents block
*
*/
function register_block() {
acf_register_block_type( apply_filters( 'cultivate_pro/toc/block_type_args', array(
'name' => 'table-of-contents',
'title' => __( 'Table of Contents', 'cultivate-pro' ),
'render_callback' => [ $this, 'render_block' ],
'category' => 'cultivatewp',
'icon' => cultivate_pro()->icon( [ 'icon' => 'cultivatewp' ] ),
'mode' => 'preview',
'supports' => [ 'anchor' => false, 'align' => false ],
'enqueue_assets' => cultivate_pro()->blocks->enqueue_assets( 'table-of-contents', [ 'css' ] ),
)));
}
/**
* Render block
*
*/
function render_block( $block ) {
$classes = ['block-toc'];
if( !empty( $args['className'] ) )
$classes = array_merge( $classes, explode( ' ', $args['className'] ) );
$title = apply_filters( 'cultivate_pro/toc/title', __( 'Table of Contents', 'cultivate-pro' ) );
$preview = apply_filters( 'cultivate_pro/toc/preview', __( 'Table of Contents will appear here.', 'cultivate-pro' ) );
$open = apply_filters( 'cultivate_pro/toc/open', false ) ? ' open' : '';
echo '<details class="' . join( ' ', $classes ) . '"' . $open . '>';
echo '<summary>' . $title . '</summary>';
if( is_admin() && !empty( $preview ) ) {
echo $preview;
} else {
echo cultivate_pro()->blocks->table_of_contents->output();
}
echo '</details>';
}
/**
* Table of Contents
*
*/
function output() {
global $post;
$content = apply_filters( 'cultivate_pro/toc/content', $post->post_content );
$doc = new \DOMDocument();
// START LibXML error management.
// Modify state
$libxml_previous_state = libxml_use_internal_errors( true );
$doc->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
// handle errors
libxml_clear_errors();
// restore
libxml_use_internal_errors( $libxml_previous_state );
// END LibXML error management.
$query = join( '|', array_map(
function($h) {
return '//' . $h;
},
$this->headings
) );
$domxpath = new \DOMXPath($doc);
$headings = $domxpath->query($query);
if( empty( $headings ) )
return;
$output = '';
$depth = 1;
foreach( $headings as $heading ) {
$nodeDepth = intval( str_replace('h', '', $heading->nodeName ) );
if( $nodeDepth > $depth ) {
$output .= '<ol>';
} elseif( $nodeDepth < $depth ) {
$output .= '</ol>';
}
$title = $heading->nodeValue;
$class = $heading->getAttribute( 'class' );
$id = $heading->getAttribute('id');
if( empty( $id ) )
$id = sanitize_title( $heading->nodeValue );
if( 'wprm-fallback-recipe-name' === $class ) {
$append = apply_filters( 'cultivate_pro/toc/append_recipe', ' Recipe' );
if( $append )
$title .= $append;
$id = 'wprm-recipe-container-' . $this->recipe_id();
}
$output .= '<li><a href="#' . $id . '">' . $title . '</a></li>';
$depth = $nodeDepth;
}
$output .= '</ol>';
return $output;
}
/**
* Get Recipe ID
*
*/
function recipe_id() {
if( ! class_exists( 'WPRM_Recipe_Manager' ) )
return false;
$recipe_id = false;
global $post;
$recipes = \WPRM_Recipe_Manager::get_recipe_ids_from_content( $post->post_content );
if( empty( $recipes ) )
return $recipe_id;
if( !empty( $recipes[ $this->current_recipe ] ) ) {
$recipe_id = $recipes[ $this->current_recipe ];
}
$this->current_recipe++;
return $recipe_id;
}
}
cultivate_pro()->blocks->table_of_contents = new Table_of_Contents;