__NAMESPACE__ . '\render_callback_simpletoc',
)
);
wp_add_inline_script(
'simpletoc-toc-editor-script',
'window.simpletocEditorSettings = ' . wp_json_encode(
array(
'settingsUrl' => admin_url( 'options-general.php?page=simpletoc' ),
)
) . ';',
'before'
);
}
add_action( 'init', __NAMESPACE__ . '\register_simpletoc_block' );
/**
* Adds SimpleTOC-specific block editor settings.
*
* @param array $editor_settings Default editor settings.
* @param \WP_Block_Editor_Context $editor_context Editor context.
*
* @return array
*/
function add_simpletoc_block_editor_settings( $editor_settings, $editor_context ) {
$editor_settings['simpletocSettingsUrl'] = admin_url( 'options-general.php?page=simpletoc' );
return $editor_settings;
}
add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\add_simpletoc_block_editor_settings', 10, 2 );
/**
* Inject potentially missing translations into the block-editor i18n
* collection.
*
* This keeps the plugin backwards compatible, in case the user did not
* update translations on their website (yet).
*
* @param string|false|null $translations JSON-encoded translation data. Default null.
* @param string|false $file Path to the translation file to load. False if there isn't one.
* @param string $handle Name of the script to register a translation domain to.
* @param string $domain The text domain.
*
* @return string|false|null JSON string
*/
add_filter(
'load_script_translations',
function ( $translations, $file, $handle, $domain ) {
if ( 'simpletoc' === $domain && $translations ) {
// List of translations that we inject into the block-editor JS.
$dynamic_translations = array(
'Table of Contents' => __( 'Table of Contents', 'simpletoc' ),
);
$changed = false;
$obj = json_decode( $translations, true );
// Confirm that the translation JSON is valid.
if ( isset( $obj['locale_data'] ) && isset( $obj['locale_data']['messages'] ) ) {
$messages = $obj['locale_data']['messages'];
// Inject dynamic translations, when needed.
foreach ( $dynamic_translations as $key => $locale ) {
if ( empty( $messages[ $key ] )
|| ! is_array( $messages[ $key ] )
|| ! array_key_exists( 0, $messages[ $key ] )
|| $locale !== $messages[ $key ][0]
) {
$messages[ $key ] = array( $locale );
$changed = true;
}
}
// Only modify the translations string when locales did change.
if ( $changed ) {
$obj['locale_data']['messages'] = $messages;
$translations = wp_json_encode( $obj );
}
}
}
return $translations;
},
10,
4
);
/**
* Sets the default value of translatable attributes.
*
* Values inside block.json are static strings that are not translated. This
* filter inserts relevant translations i
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*
* @return array Modified settings array.
*/
add_filter(
'block_type_metadata_settings',
function ( $settings, $metadata ) {
if ( 'simpletoc/toc' === $metadata['name'] ) {
$settings['attributes']['title_text']['default'] = __( 'Table of Contents', 'simpletoc' );
}
return $settings;
},
10,
2
);
/**
* Filter to add plugins to the TOC list for Rank Math plugin
*
* @param array TOC plugins.
*/
add_filter(
'rank_math/researches/toc_plugins',
function ( $toc_plugins ) {
$toc_plugins['simpletoc/plugin.php'] = 'SimpleTOC';
return $toc_plugins;
}
);
/**
* Adds IDs to the headings of the provided post content using a recursive block structure.
*
* @param string $content The content to add IDs to.
* @return string The content with IDs added to its headings
*/
function simpletoc_add_ids_to_content( $content ) {
$blocks = parse_blocks( $content );
$blocks = add_ids_to_blocks_recursive( $blocks );
$content = serialize_blocks( $blocks );
return $content;
}
add_filter( 'the_content', __NAMESPACE__ . '\simpletoc_add_ids_to_content', 1 );
/**
* Recursively adds IDs to the headings of a nested block structure.
*
* @param array $blocks The blocks to add IDs to.
* @return array The blocks with IDs added to their headings
*/
function add_ids_to_blocks_recursive( $blocks ) {
$supported_blocks = array(
'core/heading',
'generateblocks/text',
'generateblocks/headline',
);
/**
* Filter to add supported blocks for IDs.
*
* @param array $supported_blocks The array of supported blocks.
*/
$supported_blocks = apply_filters( 'simpletoc_supported_blocks_for_ids', $supported_blocks );
// Need two separate instances so that IDs aren't double coubnted.
$inner_html_id_instance = new SimpleTOC_Headline_Ids();
$inner_content_id_instance = new SimpleTOC_Headline_Ids();
foreach ( $blocks as &$block ) {
if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $supported_blocks, true ) && isset( $block['innerHTML'] ) && isset( $block['innerContent'] ) && isset( $block['innerContent'][0] ) ) {
$block['innerHTML'] = add_anchor_attribute( $block['innerHTML'], $inner_html_id_instance, $block );
$block['innerContent'][0] = add_anchor_attribute( $block['innerContent'][0], $inner_content_id_instance, $block );
} elseif ( isset( $block['attrs']['ref'] ) ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif
// search in reusable blocks (this is not finished because I ran out of ideas.)
// $reusable_block_id = $block['attrs']['ref'];
// $reusable_block_content = parse_blocks(get_post($reusable_block_id)->post_content);.
} elseif ( ! empty( $block['innerBlocks'] ) ) {
// search in groups.
$block['innerBlocks'] = add_ids_to_blocks_recursive( $block['innerBlocks'] );
}
}
return $blocks;
}
/**
* Renders a Table of Contents block for a post
*
* @param array $attributes An array of attributes for the Table of Contents block.
* @return string The HTML output for the Table of Contents block
*/
function render_callback_simpletoc( $attributes ) {
$is_backend = defined( 'REST_REQUEST' ) && REST_REQUEST && 'edit' === filter_input( INPUT_GET, 'context' );
$title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : __( 'Table of Contents', 'simpletoc' );
$alignclass = ! empty( $attributes['align'] ) ? 'align' . $attributes['align'] : '';
$class_name = ! empty( $attributes['className'] ) ? wp_strip_all_tags( $attributes['className'] ) : '';
$title_level = $attributes['title_level'];
$global_box_style_enabled = apply_filters( 'simpletoc_box_style_enabled', false ) || true === (bool) get_option( 'simpletoc_box_style_enabled', false );
$box_style_enabled = $global_box_style_enabled || ! empty( $attributes['box_style'] );
$wrapper_classes = array( 'simpletoc' );
$wrapper_style = '';
if ( $box_style_enabled ) {
$wrapper_classes[] = 'has-simpletoc-box-style';
if ( $global_box_style_enabled ) {
$wrapper_classes[] = 'has-background';
$wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
} elseif ( ! empty( $attributes['box_color'] ) ) {
$wrapper_classes[] = 'has-background';
$wrapper_style = safecss_filter_attr( 'background-color:' . $attributes['box_color'] . ';' );
} else {
$wrapper_classes[] = 'has-background';
$wrapper_style = safecss_filter_attr( 'background-color:' . DEFAULT_BOX_COLOR . ';' );
}
}
$wrapper_enabled = apply_filters( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_wrapper_enabled', false ) || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
$wrapper_attrs = get_block_wrapper_attributes(
array(
'class' => implode( ' ', $wrapper_classes ),
'style' => $wrapper_style,
)
);
$has_wrapper = ! empty( $class_name ) || $wrapper_enabled || $attributes['accordion'] || $attributes['wrapper'] || $box_style_enabled;
$pre_html = $has_wrapper ? '
' : '';
$post_html = $has_wrapper ? '
' : '';
$post = get_post();
$blocks = ! is_null( $post ) && ! is_null( $post->post_content ) ? parse_blocks( $post->post_content ) : '';
$headings = array_reverse( filter_headings_recursive( $blocks ) );
$headings = simpletoc_add_pagenumber( $blocks, $headings );
$headings_clean = array_map( 'trim', $headings );
$toc_html = generate_toc( $headings_clean, $attributes );
if ( empty( $blocks ) ) {
return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No blocks found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
}
if ( empty( $headings_clean ) ) {
return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Save or update post first.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
}
if ( empty( $toc_html ) ) {
return get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, __( 'No headings found.', 'simpletoc' ), __( 'Check minimal and maximum level block settings.', 'simpletoc' ), $wrapper_attrs, $has_wrapper );
}
return $pre_html . $toc_html . $post_html;
}
/**
* Generates an HTML message for empty blocks cases in the Table of Contents.
*
* @param bool $is_backend Indicates if the request is from the backend (i.e., the WordPress editor).
* @param array $attributes An array of attributes for the Table of Contents block.
* @param int $title_level The heading level for the Table of Contents title.
* @param string $alignclass The CSS class for alignment of the Table of Contents block.
* @param string $title_text The text for the Table of Contents title.
* @param string $warning_text1 The first part of the warning message to be displayed.
* @param string $warning_text2 The second part of the warning message to be displayed.
* @param string $wrapper_attrs Wrapper attributes for the optional block wrapper.
* @param bool $has_wrapper Indicates if the wrapper should be rendered.
*
* @return string The HTML output for the empty blocks message.
*/
function get_empty_blocks_message( $is_backend, $attributes, $title_level, $alignclass, $title_text, $warning_text1, $warning_text2, $wrapper_attrs = '', $has_wrapper = false ) {
$html = '';
if ( $is_backend ) {
if ( $has_wrapper ) {
$html .= '
\n" . $list_type . ">\n";
}
$list .= '';
}
return $list;
}
/**
* The open_list function appends a new list item to the global $list variable, adding necessary opening tags if needed to maintain the correct nesting of the list.
*
* @param string &$list_to_append_to The global list variable to append the new list item to.
* @param string $list_type The type of list to be created, either "ul" (unordered list) or "ol" (ordered list).
* @param int &$min_depth The minimum depth of headings that should be included in the table of contents.
* @param int $this_depth The depth of the current heading being processed.
* @return void The function modifies the input $list_to_append_to variable directly.
*/
function open_list( &$list_to_append_to, $list_type, &$min_depth, $this_depth ) {
if ( $this_depth === $min_depth ) {
$list_to_append_to .= '
\n";
}
}
}
/**
* Closes an HTML list tag and updates the list string and minimum depth variable as necessary.
*
* @param string $list_to_append_to A reference to the list string being built.
* @param string $list_type The type of list tag being used (ul or ol).
* @param int $min_depth A reference to the minimum depth variable.
* @param int $min_level The minimum depth level of the headings.
* @param int $max_level Maximum depth setting, which is a high number like 6.
* @param int|null $next_depth The depth of the next list item, or null if this is the last item.
* @param int $line The index of the current list item.
* @param int $last_line The index of the last list item.
* @param int $initial_depth The initial depth of the list.
* @param int $this_depth The depth of the current list item.
* @return void
*/
function close_list( &$list_to_append_to, $list_type, &$min_depth, $min_level, $max_level, $next_depth, $line, $last_line, $initial_depth, $this_depth ) {
if ( $line !== $last_line ) {
$list_to_append_to .= PHP_EOL;
if ( $next_depth < $this_depth ) {
// Next heading goes back shallower in the ToC!
if ( $next_depth >= $min_level ) {
// Next heading is within min depth bounds and WILL get ToC'd
// Close this item and step back shallower in the ToC.
for ( $min_depth; $min_depth > $next_depth; $min_depth-- ) {
$list_to_append_to .= "
\n" . $list_type . ">\n";
}
} else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
// SKIP CLOSING! Next heading won't be included in the ToC at all.
}
} elseif ( $next_depth === $this_depth ) {
// Next heading is exactly as deep. Not going shallower or deeper in the ToC hierarchy.
// E.g. this is h3, next is h3.
if ( $next_depth < $min_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
// E.g. this is h3, next is h3, min is h2
// This heading didn't open a ToC item. Nothing to close.
} else {
// SKIP CLOSING! Next heading will open a new sub-list in the ToC.
$list_to_append_to .= "\n";
}
} else { // phpcs:ignore.
// Next heading is deeper in the ToC.
if ( $next_depth <= $max_level ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
// Next deeper heading is within bounds and will open a new sub-list. Leave this one open.
// E.g. this is h3, next is h4, min is h2, max is h5.
} else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
// Next heading is too deep and will be ignored. We'll close out coming up or finishing the ToC.
// E.g. this is h3, next is h4, max is h3.
}
}
} else {
// This is the last line of the ToC. Close out the whole thing.
// IMPORTANT NOTE: The overall ToC list will be wrapped in a list element and closed out.
for ( $initial_depth; $initial_depth < $this_depth; $initial_depth++ ) {
$list_to_append_to .= "\n" . $list_type . ">\n";
}
}
}
/**
* Adds smooth scrolling styles to the output HTML, if enabled by global option or block attribute.
*
* @param string $html The HTML string to which the styles will be added.
* @param array $attributes An array of block attributes.
* @return string The modified HTML string with the added smooth scrolling styles.
*/
function add_smooth( $html, $attributes ) {
// Add smooth scrolling styles, if enabled by global option or block attribute.
$is_smooth_enabled = $attributes['add_smooth'] || true === (bool) get_option( 'simpletoc_smooth_enabled', false );
$html .= $is_smooth_enabled ? '' : '';
return $html;
}
/**
* Enqueues the necessary CSS and JS files for the accordion functionality on the frontend.
*/
function enqueue_accordion_frontend() {
wp_enqueue_script(
'simpletoc-accordion',
plugin_dir_url( __FILE__ ) . 'assets/accordion.js',
array(),
'6.9.0',
true
);
wp_enqueue_style(
'simpletoc-accordion',
plugin_dir_url( __FILE__ ) . 'assets/accordion.css',
array(),
'6.9.0'
);
}
/**
* Adds the opening HTML tag(s) for the hidden markup element and the table of contents title, if applicable.
*
* @param string $html The HTML string to add the opening tag(s) to.
* @param array $attributes The attributes of the table of contents block.
* @param int $itemcount The number of items in the table of contents.
* @param string $alignclass The alignment class for the table of contents block.
*/
function add_hidden_markup_start( $html, $attributes, $itemcount, $alignclass ) { // phpcs:ignore.
$is_hidden_enabled = $attributes['hidden'];
if ( $is_hidden_enabled ) {
$title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
$hidden_start = '' . $title_text . '';
$html .= $hidden_start;
}
// If there are no items in the table of contents, return an empty string.
if ( $itemcount < 1 ) {
return '';
}
return $html;
}
/**
* Adds the closing HTML tag(s) for the hidden markup element if the hidden markup is enabled.
*
* @param string $html The HTML string to add the closing tag(s) to.
* @param array $attributes The attributes of the table of contents block.
* @return string The modified HTML string with the closing tag(s) added.
*/
function add_hidden_markup_end( $html, $attributes ) {
$is_hidden_enabled = $attributes['hidden'];
if ( $is_hidden_enabled ) {
$html .= '';
}
return $html;
}
/**
* Adds the opening HTML tag(s) for the accordion element and the table of contents title, if applicable.
*
* @param string $html The HTML string to add the opening tag(s) to.
* @param array $attributes The attributes of the table of contents block.
* @param int $itemcount The number of items in the table of contents.
* @param string $alignclass The alignment class for the table of contents block.
*/
function add_accordion_start( $html, $attributes, $itemcount, $alignclass ) {
// Check if accordion is enabled either through the function arguments or the options.
$is_accordion_enabled = $attributes['accordion'] || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
$is_hidden_enabled = $attributes['hidden'];
$title_text = $attributes['title_text'] ? esc_html( trim( $attributes['title_text'] ) ) : esc_html__( 'Table of Contents', 'simpletoc' );
// Start and end HTML for accordion, if enabled.
$accordion_start = '';
if ( $is_accordion_enabled ) {
enqueue_accordion_frontend();
$accordion_start = '
';
}
// Add the accordion start HTML to the output.
$html .= $accordion_start;
// Add the table of contents title, if not hidden and not in accordion mode.
$show_title = ! $attributes['no_title'] && ! $is_accordion_enabled && ! $is_hidden_enabled;
if ( $show_title ) {
$title_tag = $attributes['title_level'] > 0 ? "h{$attributes['title_level']}" : 'p';
$title_tag = wp_strip_all_tags( $title_tag );
$html_class = 'simpletoc-title';
if ( ! empty( $alignclass ) ) {
$html_class .= " $alignclass";
}
$html = "<$title_tag class=\"$html_class\">$title_text$title_tag>\n";
}
// If there are no items in the table of contents, return an empty string.
if ( $itemcount < 1 ) {
return '';
}
return $html;
}
/**
* Adds the closing HTML tag(s) for the accordion element if the accordion is enabled.
*
* @param string $html The HTML string to add the closing tag(s) to.
* @param array $attributes The attributes of the table of contents block.
* @return string The modified HTML string with the closing tag(s) added
*/
function add_accordion_end( $html, $attributes ) {
// Check if accordion is enabled either through the function arguments or the options.
$is_accordion_enabled = $attributes['accordion'] || true === (bool) get_option( 'simpletoc_accordion_enabled', false );
if ( $is_accordion_enabled ) {
$html .= '
';
}
return $html;
}
/**
* Extracts the ID value from the provided heading HTML string.
*
* @param string $headline The heading HTML string to extract the ID value from.
* @return mixed Returns the extracted ID value, or false if no ID value is found.
*/
function extract_id( $headline ) {
$pattern = '/id="([^"]*)"/';
preg_match( $pattern, $headline, $matches );
$id_value = $matches[1] ?? false;
if ( false !== $id_value ) {
return $id_value;
}
return false;
}
/**
* Gets the page number from a headline string.
*
* @param string $headline The headline string.
* @return string The page number (in the format "X/") if it exists and is greater than 1, or an empty string otherwise.
*/
function get_page_number_from_headline( $headline ) {
$dom = new \DOMDocument();
try {
$dom->loadHTML( '' . "\n" . $headline, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
} catch ( \Exception $e ) {
return '';
}
$xpath = new \DOMXPath( $dom );
$nodes = $xpath->query( '//*/@data-page' );
if ( isset( $nodes[0] ) && $nodes[0]->nodeValue > 1 ) {
$page_number = $nodes[0]->nodeValue . '/';
return esc_html( $page_number );
} else {
return '';
}
}